CSS crypto tests + DVD pipeline fully wired
- CSS roundtrip tests: decrypt_key determinism, descramble XOR roundtrip - CSS table verification: TAB1 is permutation, TAB4 is bit-reversal involution - DVD scan pipeline confirmed: scan_dvd_titles, CSS crack, ContentReader descramble - 229 tests, all passing
This commit is contained in:
+138
@@ -194,4 +194,142 @@ mod tests {
|
||||
assert_ne!(result, key);
|
||||
assert_ne!(result, [0u8; 5]);
|
||||
}
|
||||
|
||||
/// Test 1: css_decrypt_key_roundtrip
|
||||
///
|
||||
/// decrypt_key is not a simple encrypt/decrypt pair — it is a one-way mangling
|
||||
/// function. However, we can verify consistency: calling it twice with the same
|
||||
/// parameters produces the same output, and varying the invert byte changes
|
||||
/// the LFSR0 contribution predictably.
|
||||
#[test]
|
||||
fn css_decrypt_key_roundtrip() {
|
||||
let keys: &[[u8; 5]] = &[
|
||||
[0x12, 0x34, 0x56, 0x78, 0x9A],
|
||||
[0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
|
||||
[0xAB, 0xCD, 0xEF, 0x01, 0x23],
|
||||
];
|
||||
let crypted_inputs: &[[u8; 5]] = &[
|
||||
[0x11, 0x22, 0x33, 0x44, 0x55],
|
||||
[0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
|
||||
[0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
];
|
||||
|
||||
for key in keys {
|
||||
for crypted in crypted_inputs {
|
||||
// decrypt_key with invert=0x00 and invert=0xFF should give different results
|
||||
let r0 = decrypt_key(0x00, key, crypted);
|
||||
let rff = decrypt_key(0xFF, key, crypted);
|
||||
|
||||
// The two results differ because the invert byte XORs the LFSR0 output
|
||||
// They should not be equal (except by extreme coincidence)
|
||||
// More importantly, both should be deterministic
|
||||
let r0_again = decrypt_key(0x00, key, crypted);
|
||||
let rff_again = decrypt_key(0xFF, key, crypted);
|
||||
assert_eq!(r0, r0_again, "decrypt_key(0x00) not deterministic");
|
||||
assert_eq!(rff, rff_again, "decrypt_key(0xFF) not deterministic");
|
||||
|
||||
// With different invert values, the keystream differs
|
||||
assert_ne!(r0, rff, "invert=0x00 and 0xFF gave same result for key {:?}", key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test 2: css_descramble_produces_valid_mpeg2
|
||||
///
|
||||
/// descramble_sector XORs a keystream into bytes 128..2048. Calling it
|
||||
/// twice with the same key and restored scramble flag should roundtrip,
|
||||
/// since XOR is its own inverse.
|
||||
#[test]
|
||||
fn css_descramble_produces_valid_mpeg2() {
|
||||
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
||||
|
||||
// Build a sector with MPEG-2 pack header and PES header
|
||||
let mut sector = vec![0x00u8; 2048];
|
||||
// Pack header at byte 0
|
||||
sector[0] = 0x00;
|
||||
sector[1] = 0x00;
|
||||
sector[2] = 0x01;
|
||||
sector[3] = 0xBA;
|
||||
// Scramble flag at byte 0x14
|
||||
sector[0x14] = 0x30;
|
||||
// Sector seed at bytes 0x54-0x58
|
||||
sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
|
||||
// PES header at byte 128
|
||||
sector[0x80] = 0x00;
|
||||
sector[0x81] = 0x00;
|
||||
sector[0x82] = 0x01;
|
||||
sector[0x83] = 0xE0;
|
||||
// Fill some content in the encrypted region
|
||||
for i in 0x84..2048 {
|
||||
sector[i] = (i & 0xFF) as u8;
|
||||
}
|
||||
|
||||
let original = sector.clone();
|
||||
|
||||
// First descramble: "encrypts" by XORing keystream
|
||||
descramble_sector(&title_key, &mut sector);
|
||||
// Flag should be cleared
|
||||
assert_eq!(sector[0x14] & 0x30, 0x00, "scramble flag not cleared after first descramble");
|
||||
// Encrypted region should differ
|
||||
assert_ne!(§or[0x80..0x84], &original[0x80..0x84],
|
||||
"encrypted region unchanged after descramble");
|
||||
|
||||
// Restore the scramble flag and sector seed for second pass
|
||||
sector[0x14] = 0x30;
|
||||
|
||||
// Second descramble: XOR again = roundtrip
|
||||
descramble_sector(&title_key, &mut sector);
|
||||
// Now the encrypted region should match original
|
||||
assert_eq!(§or[0x80..2048], &original[0x80..2048],
|
||||
"double descramble did not roundtrip");
|
||||
}
|
||||
|
||||
/// Test 4: css_tab1_relationship
|
||||
///
|
||||
/// Verify the structure of TAB1: it is a substitution table used in
|
||||
/// key mangling. Check that no two inputs map to the same output
|
||||
/// (TAB1 is a permutation of 0..255).
|
||||
#[test]
|
||||
fn css_tab1_is_permutation() {
|
||||
let mut seen = [false; 256];
|
||||
for i in 0..256 {
|
||||
let v = TAB1[i] as usize;
|
||||
assert!(!seen[v], "TAB1 maps two inputs to {:#04x}", v);
|
||||
seen[v] = true;
|
||||
}
|
||||
// Check involution property: TAB1[TAB1[x]] should map back predictably
|
||||
// TAB1 is not necessarily a strict involution, but we verify the
|
||||
// composition TAB1[TAB1[x]] is also a permutation
|
||||
let mut seen2 = [false; 256];
|
||||
for i in 0..256 {
|
||||
let v = TAB1[TAB1[i] as usize] as usize;
|
||||
assert!(!seen2[v], "TAB1[TAB1[x]] maps two inputs to {:#04x}", v);
|
||||
seen2[v] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Test 5: css_tab4_is_bit_reversal
|
||||
///
|
||||
/// TAB4 reverses the bits of each byte: TAB4[0x01] = 0x80, TAB4[0x80] = 0x01, etc.
|
||||
#[test]
|
||||
fn css_tab4_is_bit_reversal() {
|
||||
for i in 0u16..256 {
|
||||
let expected = (0..8).fold(0u8, |acc, bit| {
|
||||
acc | (((i as u8 >> bit) & 1) << (7 - bit))
|
||||
});
|
||||
assert_eq!(
|
||||
TAB4[i as usize], expected,
|
||||
"TAB4[{:#04x}] = {:#04x}, expected {:#04x} (bit reversal)",
|
||||
i, TAB4[i as usize], expected
|
||||
);
|
||||
}
|
||||
// Also verify TAB4 is an involution: TAB4[TAB4[x]] == x
|
||||
for i in 0..256 {
|
||||
assert_eq!(
|
||||
TAB4[TAB4[i] as usize], i as u8,
|
||||
"TAB4 is not an involution at {:#04x}", i
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user