v1.0.0-rc.1
CSS keyless decrypt (Stevenson), AACS 1.0/2.0/2.1, MPEG-2 DVD, multi-OS SCSI, multipass recovery, mux highway, audit hardening
This commit is contained in:
+153
-261
@@ -13,53 +13,21 @@
|
||||
|
||||
use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5};
|
||||
|
||||
/// Seed the 32-bit LFSR0 register from the 5-byte working key, applying
|
||||
/// the per-byte TAB4 bit-reversal. Shared by [`descramble_sector`] and
|
||||
/// [`decrypt_key`] so the seeding lives in one place.
|
||||
#[inline]
|
||||
fn seed_lfsr0(key: &[u8; 5]) -> u32 {
|
||||
let lfsr0: u32 = ((key[4] as u32) << 17)
|
||||
| ((key[3] as u32) << 9)
|
||||
| (((key[2] as u32) << 1) + 8 - (key[2] as u32 & 7));
|
||||
(TAB4[(lfsr0 & 0xFF) as usize] as u32) << 24
|
||||
| (TAB4[((lfsr0 >> 8) & 0xFF) as usize] as u32) << 16
|
||||
| (TAB4[((lfsr0 >> 16) & 0xFF) as usize] as u32) << 8
|
||||
| TAB4[((lfsr0 >> 24) & 0xFF) as usize] as u32
|
||||
}
|
||||
|
||||
/// One CSS keystream step. Advances both LFSRs, folds their permuted
|
||||
/// outputs into `combined` (carry kept across calls), and returns the
|
||||
/// low keystream byte. `invert` XORs the LFSR0 output index (0x00 on the
|
||||
/// descramble path, 0xFF on the key-decrypt path).
|
||||
#[inline]
|
||||
fn css_step(
|
||||
lfsr1_lo: &mut u32,
|
||||
lfsr1_hi: &mut u32,
|
||||
lfsr0: &mut u32,
|
||||
combined: &mut u32,
|
||||
invert: u8,
|
||||
) -> u8 {
|
||||
let o_lfsr1 = TAB2[*lfsr1_hi as usize] ^ TAB3[*lfsr1_lo as usize];
|
||||
*lfsr1_hi = *lfsr1_lo >> 1;
|
||||
*lfsr1_lo = ((*lfsr1_lo & 1) << 8) ^ o_lfsr1 as u32;
|
||||
|
||||
let o_lfsr0 = (((((((*lfsr0 >> 8) ^ *lfsr0) >> 1) ^ *lfsr0) >> 3) ^ *lfsr0) >> 7) as u8;
|
||||
*lfsr0 = (*lfsr0 >> 8) | ((o_lfsr0 as u32) << 24);
|
||||
|
||||
*combined += TAB5[o_lfsr1 as usize] as u32 + TAB4[(o_lfsr0 ^ invert) as usize] as u32;
|
||||
let out = (*combined & 0xFF) as u8;
|
||||
*combined >>= 8;
|
||||
out
|
||||
}
|
||||
|
||||
/// Descramble a CSS-encrypted DVD sector in place.
|
||||
///
|
||||
/// The sector seed (bytes 0x54-0x58) is XORed with the title key to produce
|
||||
/// the per-sector key. Bytes 0x80..0x800 (128..2048) are then decrypted
|
||||
/// using the two-LFSR keystream.
|
||||
/// Exact port of libdvdcss `dvdcss_unscramble` (css.c). The two content
|
||||
/// LFSRs are seeded **directly** from `title_key XOR sector_seed` — there is
|
||||
/// no `decrypt_key` mangling on this path (that is the disc/title-key
|
||||
/// hierarchy, not the content cipher). Bytes 0x80..0x800 are recovered with
|
||||
/// `*p = TAB1[*p] ^ (i_t5 & 0xff)`.
|
||||
///
|
||||
/// The scramble flag at byte 0x14 (bits 4-5) indicates encryption.
|
||||
/// After descrambling, the flag is cleared.
|
||||
/// The scramble flag at byte 0x14 (bits 4-5) indicates encryption. Like
|
||||
/// libdvdcss, the flag byte is NOT modified here — the caller treats a
|
||||
/// nonzero `sector[0x14] & 0x30` as "needs unscrambling" and the descramble
|
||||
/// is its own inverse, so re-running it on plaintext would re-scramble.
|
||||
/// (freemkv historically cleared the flag; we keep clearing it so callers
|
||||
/// and the existing tests can distinguish a descrambled sector. This does
|
||||
/// not affect the recovered body.)
|
||||
///
|
||||
/// No-op (returns without modifying `sector`) in two cases:
|
||||
/// - `sector.len() < 2048`: the encrypted region (0x80..0x800) is not
|
||||
@@ -67,6 +35,21 @@ fn css_step(
|
||||
/// left untouched. The `debug_assert!` flags this misuse in debug/test
|
||||
/// builds; a DVD sector is always exactly 2048 bytes.
|
||||
/// - scramble flags are zero: the sector is not CSS-encrypted.
|
||||
///
|
||||
/// Design reference: libdvdcss `dvdcss_unscramble`. The combiner mirrors
|
||||
/// `css.c` line-for-line:
|
||||
/// ```text
|
||||
/// i_t1 = (key[0] ^ sec[0x54]) | 0x100;
|
||||
/// i_t2 = key[1] ^ sec[0x55];
|
||||
/// i_t3 = (key[2]|key[3]<<8|key[4]<<16) ^ (sec[0x56]|sec[0x57]<<8|sec[0x58]<<16);
|
||||
/// i_t4 = i_t3 & 7; i_t3 = i_t3*2 + 8 - i_t4;
|
||||
/// // per byte over 0x80..0x800:
|
||||
/// i_t4 = TAB2[i_t2] ^ TAB3[i_t1];
|
||||
/// i_t2 = i_t1 >> 1; i_t1 = ((i_t1 & 1) << 8) ^ i_t4; i_t4 = TAB5[i_t4];
|
||||
/// i_t6 = (((((((i_t3>>3)^i_t3)>>1)^i_t3)>>8)^i_t3)>>5) & 0xff;
|
||||
/// i_t3 = (i_t3 << 8) | i_t6; i_t6 = TAB4[i_t6];
|
||||
/// i_t5 += i_t6 + i_t4; *p = TAB1[*p] ^ (i_t5 & 0xff); i_t5 >>= 8;
|
||||
/// ```
|
||||
pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
|
||||
debug_assert!(
|
||||
sector.len() >= 2048,
|
||||
@@ -76,101 +59,111 @@ pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let flags = (sector[0x14] >> 4) & 0x03;
|
||||
if flags == 0 {
|
||||
// libdvdcss: `if( !(p_sec[0x14] & 0x30) ) return;`
|
||||
if sector[0x14] & 0x30 == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-sector key = title_key XOR sector_seed (bytes 0x54-0x58)
|
||||
let key = [
|
||||
title_key[0] ^ sector[0x54],
|
||||
title_key[1] ^ sector[0x55],
|
||||
title_key[2] ^ sector[0x56],
|
||||
title_key[3] ^ sector[0x57],
|
||||
title_key[4] ^ sector[0x58],
|
||||
];
|
||||
// LFSR1: seeded directly from (key ^ seed) — NO decrypt_key.
|
||||
let mut i_t1: u32 = ((title_key[0] ^ sector[0x54]) as u32) | 0x100;
|
||||
let mut i_t2: u32 = (title_key[1] ^ sector[0x55]) as u32;
|
||||
|
||||
// Decrypt the key through the CSS mangling function to get the working key.
|
||||
// The sector seed is bytes 0x54..0x59 (5 bytes).
|
||||
let seed: [u8; 5] = [
|
||||
sector[0x54],
|
||||
sector[0x55],
|
||||
sector[0x56],
|
||||
sector[0x57],
|
||||
sector[0x58],
|
||||
];
|
||||
let working_key = decrypt_key(0xFF, &key, &seed);
|
||||
// LFSR0 (i_t3): 24-bit feedback register seeded from the remaining three
|
||||
// key/seed bytes, then transformed `i_t3 = i_t3*2 + 8 - (i_t3 & 7)`.
|
||||
let mut i_t3: u32 = (((title_key[2] as u32)
|
||||
| ((title_key[3] as u32) << 8)
|
||||
| ((title_key[4] as u32) << 16))
|
||||
^ ((sector[0x56] as u32) | ((sector[0x57] as u32) << 8) | ((sector[0x58] as u32) << 16)))
|
||||
& 0xFF_FFFF;
|
||||
let i_t4_seed = i_t3 & 7;
|
||||
i_t3 = i_t3 * 2 + 8 - i_t4_seed;
|
||||
|
||||
// Generate keystream and XOR with encrypted region
|
||||
let mut lfsr1_lo: u32 = working_key[0] as u32 | 0x100;
|
||||
let mut lfsr1_hi: u32 = working_key[1] as u32;
|
||||
let mut lfsr0: u32 = seed_lfsr0(&working_key);
|
||||
let mut i_t5: u32 = 0;
|
||||
|
||||
let mut combined: u32 = 0;
|
||||
|
||||
// Generate 1920 keystream bytes (for sector bytes 128..2048) and XOR them
|
||||
// into the encrypted region. Each keystream byte is the carrying sum of the
|
||||
// TAB5-permuted LFSR1 output and the TAB4-permuted LFSR0 output. No TAB1
|
||||
// permutation is applied to the ciphertext here (TAB1 is only used inside
|
||||
// decrypt_key); the working key was already produced by decrypt_key above,
|
||||
// so this keystream is paired with that mangling step, not a plain
|
||||
// direct-seed unscramble. No invert is applied on the LFSR0 output.
|
||||
for byte in sector.iter_mut().take(2048).skip(128) {
|
||||
let ks = css_step(
|
||||
&mut lfsr1_lo,
|
||||
&mut lfsr1_hi,
|
||||
&mut lfsr0,
|
||||
&mut combined,
|
||||
0x00,
|
||||
);
|
||||
*byte ^= ks;
|
||||
// Advance LFSR1.
|
||||
let mut i_t4 = (TAB2[i_t2 as usize] ^ TAB3[i_t1 as usize]) as u32;
|
||||
i_t2 = i_t1 >> 1;
|
||||
i_t1 = ((i_t1 & 1) << 8) ^ i_t4;
|
||||
i_t4 = TAB5[i_t4 as usize] as u32;
|
||||
|
||||
// Advance LFSR0 (i_t3) and fold both outputs into i_t5.
|
||||
let mut i_t6 = (((((((i_t3 >> 3) ^ i_t3) >> 1) ^ i_t3) >> 8) ^ i_t3) >> 5) & 0xFF;
|
||||
i_t3 = (i_t3 << 8) | i_t6;
|
||||
i_t6 = TAB4[i_t6 as usize] as u32;
|
||||
i_t5 += i_t6 + i_t4;
|
||||
|
||||
*byte = TAB1[*byte as usize] ^ (i_t5 & 0xFF) as u8;
|
||||
i_t5 >>= 8;
|
||||
}
|
||||
|
||||
// Clear scramble flags
|
||||
// libdvdcss leaves byte 0x14 untouched; freemkv clears the scramble bits
|
||||
// so downstream code and tests can tell a sector was descrambled.
|
||||
sector[0x14] &= 0xCF;
|
||||
}
|
||||
|
||||
/// CSS key decryption / mangling function.
|
||||
/// Exact inverse of [`descramble_sector`]: turn a plaintext sector body into
|
||||
/// CSS ciphertext under `title_key`.
|
||||
///
|
||||
/// Decrypts `p_crypted` using `p_key` with the CSS two-LFSR cipher.
|
||||
/// The `invert` parameter controls the XOR applied to LFSR0 output
|
||||
/// (0x00 for disc key decryption, 0xFF for title key / sector key).
|
||||
pub(crate) fn decrypt_key(invert: u8, p_key: &[u8; 5], p_crypted: &[u8; 5]) -> [u8; 5] {
|
||||
let mut lfsr1_lo: u32 = p_key[0] as u32 | 0x100;
|
||||
let mut lfsr1_hi: u32 = p_key[1] as u32;
|
||||
let mut lfsr0: u32 = seed_lfsr0(p_key);
|
||||
|
||||
let mut combined: u32 = 0;
|
||||
let mut k = [0u8; 5];
|
||||
|
||||
// TAB5 for LFSR1 output, TAB4 for LFSR0^invert (per libdvdcss css_DecryptKey).
|
||||
for byte in &mut k {
|
||||
*byte = css_step(
|
||||
&mut lfsr1_lo,
|
||||
&mut lfsr1_hi,
|
||||
&mut lfsr0,
|
||||
&mut combined,
|
||||
invert,
|
||||
);
|
||||
/// Descramble computes `plain = TAB1[cipher] ^ (i_t5 & 0xff)`, so the
|
||||
/// inverse is `cipher = TAB1_INV[plain ^ (i_t5 & 0xff)]` with the identical
|
||||
/// LFSR keystream. The keystream derivation is byte-for-byte the same as
|
||||
/// `descramble_sector` (libdvdcss `dvdcss_unscramble`); only the final
|
||||
/// substitution differs. Bytes 0x80..0x800 are rewritten in place; the
|
||||
/// scramble flag is set to 0x10 so a subsequent descramble runs.
|
||||
///
|
||||
/// Not on any production read path — it exists so the key-recovery tests
|
||||
/// (and any caller that needs to produce a known CSS-encrypted sector) can
|
||||
/// build genuine ciphertext rather than approximating it.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn scramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
|
||||
if sector.len() < 2048 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Two rounds of chained XOR through TAB1
|
||||
let mut result = [0u8; 5];
|
||||
result[4] = k[4] ^ TAB1[p_crypted[4] as usize] ^ p_crypted[3];
|
||||
result[3] = k[3] ^ TAB1[p_crypted[3] as usize] ^ p_crypted[2];
|
||||
result[2] = k[2] ^ TAB1[p_crypted[2] as usize] ^ p_crypted[1];
|
||||
result[1] = k[1] ^ TAB1[p_crypted[1] as usize] ^ p_crypted[0];
|
||||
result[0] = k[0] ^ TAB1[p_crypted[0] as usize] ^ result[4];
|
||||
let mut i_t1: u32 = ((title_key[0] ^ sector[0x54]) as u32) | 0x100;
|
||||
let mut i_t2: u32 = (title_key[1] ^ sector[0x55]) as u32;
|
||||
let mut i_t3: u32 = (((title_key[2] as u32)
|
||||
| ((title_key[3] as u32) << 8)
|
||||
| ((title_key[4] as u32) << 16))
|
||||
^ ((sector[0x56] as u32) | ((sector[0x57] as u32) << 8) | ((sector[0x58] as u32) << 16)))
|
||||
& 0xFF_FFFF;
|
||||
let i_t4_seed = i_t3 & 7;
|
||||
i_t3 = i_t3 * 2 + 8 - i_t4_seed;
|
||||
|
||||
result[4] = k[4] ^ TAB1[result[4] as usize] ^ result[3];
|
||||
result[3] = k[3] ^ TAB1[result[3] as usize] ^ result[2];
|
||||
result[2] = k[2] ^ TAB1[result[2] as usize] ^ result[1];
|
||||
result[1] = k[1] ^ TAB1[result[1] as usize] ^ result[0];
|
||||
result[0] = k[0] ^ TAB1[result[0] as usize];
|
||||
let mut i_t5: u32 = 0;
|
||||
|
||||
result
|
||||
for byte in sector.iter_mut().take(2048).skip(128) {
|
||||
let mut i_t4 = (TAB2[i_t2 as usize] ^ TAB3[i_t1 as usize]) as u32;
|
||||
i_t2 = i_t1 >> 1;
|
||||
i_t1 = ((i_t1 & 1) << 8) ^ i_t4;
|
||||
i_t4 = TAB5[i_t4 as usize] as u32;
|
||||
|
||||
let mut i_t6 = (((((((i_t3 >> 3) ^ i_t3) >> 1) ^ i_t3) >> 8) ^ i_t3) >> 5) & 0xFF;
|
||||
i_t3 = (i_t3 << 8) | i_t6;
|
||||
i_t6 = TAB4[i_t6 as usize] as u32;
|
||||
i_t5 += i_t6 + i_t4;
|
||||
|
||||
// Inverse of `*p = TAB1[*p] ^ ks`: apply ks then TAB1's inverse.
|
||||
*byte = (*TAB1_INV)[(*byte ^ (i_t5 & 0xFF) as u8) as usize];
|
||||
i_t5 >>= 8;
|
||||
}
|
||||
|
||||
// Mark the sector scrambled so the descrambler will process it.
|
||||
sector[0x14] = (sector[0x14] & 0xCF) | 0x10;
|
||||
}
|
||||
|
||||
/// Inverse permutation of [`TAB1`], built at first use. `TAB1` is a
|
||||
/// bijection on 0..256, so `TAB1_INV[TAB1[x]] == x`.
|
||||
#[cfg(test)]
|
||||
static TAB1_INV: std::sync::LazyLock<[u8; 256]> = std::sync::LazyLock::new(|| {
|
||||
let mut inv = [0u8; 256];
|
||||
for (i, &v) in TAB1.iter().enumerate() {
|
||||
inv[v as usize] = i as u8;
|
||||
}
|
||||
inv
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -185,6 +178,34 @@ mod tests {
|
||||
assert_eq!(sector, original);
|
||||
}
|
||||
|
||||
/// Cross-check `descramble_sector` against the EXACT output of libdvdcss
|
||||
/// `dvdcss_unscramble` (css.c) for a fixed sector, computed from the
|
||||
/// reference C semantics with the reference tables. Pins the content
|
||||
/// cipher to libdvdcss byte-for-byte.
|
||||
///
|
||||
/// key = 42 13 37 BE EF, seed (0x54..0x59) = DE AD BE EF 42, body = 0xAA.
|
||||
#[test]
|
||||
fn descramble_matches_libdvdcss_unscramble_vector() {
|
||||
let key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
||||
let mut sector = vec![0xAAu8; 2048];
|
||||
sector[0x14] = 0x30;
|
||||
sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
|
||||
descramble_sector(&key, &mut sector);
|
||||
assert_eq!(
|
||||
§or[0x80..0x90],
|
||||
&[
|
||||
0x81, 0x92, 0x24, 0xA2, 0x46, 0x70, 0x3C, 0x64, 0xA6, 0x91, 0x84, 0xF5, 0x1F, 0x98,
|
||||
0xA0, 0x31
|
||||
],
|
||||
"descramble body head must match libdvdcss dvdcss_unscramble"
|
||||
);
|
||||
assert_eq!(
|
||||
§or[0x7F8..0x800],
|
||||
&[0x46, 0x94, 0x80, 0x0E, 0x67, 0x36, 0x65, 0xBC],
|
||||
"descramble body tail must match libdvdcss dvdcss_unscramble"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descramble_modifies_scrambled() {
|
||||
let key = [0x01, 0x02, 0x03, 0x04, 0x05];
|
||||
@@ -215,69 +236,14 @@ mod tests {
|
||||
assert_eq!(sector[0x14] & 0x30, 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_key_produces_output() {
|
||||
let key = [0x12, 0x34, 0x56, 0x78, 0x9A];
|
||||
let crypted = [0xAB, 0xCD, 0xEF, 0x01, 0x23];
|
||||
let result = decrypt_key(0xFF, &key, &crypted);
|
||||
// Should produce a 5-byte result different from input
|
||||
assert_ne!(result, key);
|
||||
assert_ne!(result, [0u8; 5]);
|
||||
}
|
||||
|
||||
/// css_decrypt_key_roundtrip
|
||||
/// Test 2: descramble inverts scramble over the body.
|
||||
///
|
||||
/// 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.
|
||||
/// The content cipher is NOT a plain XOR involution (it applies TAB1 to
|
||||
/// the ciphertext: `plain = TAB1[cipher] ^ ks`). The true inverse is
|
||||
/// [`scramble_sector`]. Scrambling a plaintext body and then descrambling
|
||||
/// with the same key must reproduce the original body exactly.
|
||||
#[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: descramble_modifies_encrypted_region
|
||||
///
|
||||
/// descramble_sector XORs a keystream into bytes 128..2048. The keystream
|
||||
/// depends only on (title_key, sector_seed), so applying descramble twice
|
||||
/// with the scramble flag restored between calls re-XORs the same keystream
|
||||
/// and restores the original encrypted region — the keystream XOR is its
|
||||
/// own inverse. This pins the cipher's involution property over the body.
|
||||
#[test]
|
||||
fn css_descramble_modifies_encrypted_region() {
|
||||
fn css_descramble_inverts_scramble_over_body() {
|
||||
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
||||
|
||||
let mut sector = vec![0xAAu8; 2048];
|
||||
@@ -285,11 +251,10 @@ mod tests {
|
||||
sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
|
||||
|
||||
let original = sector.clone();
|
||||
descramble_sector(&title_key, &mut sector);
|
||||
|
||||
// Flag cleared
|
||||
assert_eq!(sector[0x14] & 0x30, 0x00);
|
||||
// Header (0..128) unchanged except flag byte
|
||||
// Scramble the plaintext body into ciphertext.
|
||||
scramble_sector(&title_key, &mut sector);
|
||||
// Header (0..128) unchanged except the flag byte (set by scramble).
|
||||
for i in 0..128 {
|
||||
if i == 0x14 {
|
||||
continue;
|
||||
@@ -299,15 +264,13 @@ mod tests {
|
||||
// Encrypted region modified
|
||||
assert_ne!(§or[128..256], &original[128..256]);
|
||||
|
||||
// Round-trip: restore the scramble flag and descramble again. The same
|
||||
// keystream is regenerated (it depends only on title_key + seed, both
|
||||
// unchanged), so the body is restored to its original bytes.
|
||||
sector[0x14] = 0x30;
|
||||
// Descramble restores the plaintext body byte-for-byte.
|
||||
descramble_sector(&title_key, &mut sector);
|
||||
assert_eq!(sector[0x14] & 0x30, 0x00, "flag cleared after descramble");
|
||||
assert_eq!(
|
||||
§or[128..2048],
|
||||
&original[128..2048],
|
||||
"double descramble did not restore the encrypted region"
|
||||
"descramble(scramble(body)) did not restore the body"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -542,75 +505,4 @@ mod tests {
|
||||
"different seeds must descramble differently"
|
||||
);
|
||||
}
|
||||
|
||||
// ── decrypt_key chained-XOR dependency structure ───────────────────────
|
||||
|
||||
/// css_DecryptKey's two TAB1 rounds form a fixed dependency chain. After
|
||||
/// both rounds, `result[0]` is the last value computed and depends on the
|
||||
/// full key/crypted state; but the FIRST-round seed for `result[4]` is
|
||||
/// `k[4] ^ TAB1[p_crypted[4]] ^ p_crypted[3]`. Changing ONLY p_crypted[4]
|
||||
/// must change the output (p_crypted[4] feeds result[4] which propagates).
|
||||
///
|
||||
/// Grounding: lines computing result[4] use p_crypted[4] and p_crypted[3].
|
||||
/// Mutation: in `result[4] = k[4] ^ TAB1[p_crypted[4]] ^ p_crypted[3]`
|
||||
/// drop the `TAB1[p_crypted[4]]` term -> output stops depending on
|
||||
/// p_crypted[4], this assert fires.
|
||||
#[test]
|
||||
fn decrypt_key_depends_on_every_crypted_byte() {
|
||||
let key = [0x12, 0x34, 0x56, 0x78, 0x9A];
|
||||
let base = [0xAB, 0xCD, 0xEF, 0x01, 0x23];
|
||||
let base_out = decrypt_key(0xFF, &key, &base);
|
||||
for i in 0..5 {
|
||||
let mut c = base;
|
||||
c[i] ^= 0x01;
|
||||
assert_ne!(
|
||||
decrypt_key(0xFF, &key, &c),
|
||||
base_out,
|
||||
"flipping crypted byte {i} did not change the decrypted key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Likewise every key byte feeds the LFSR seeding (key[0],key[1] seed
|
||||
/// LFSR1; key[2..5] seed LFSR0 via seed_lfsr0). Flipping any single key
|
||||
/// byte must change the output.
|
||||
///
|
||||
/// Grounding: lfsr1_lo=key[0]|0x100, lfsr1_hi=key[1], seed_lfsr0(key) uses
|
||||
/// key[2],key[3],key[4].
|
||||
/// Mutation: in seed_lfsr0 drop the `(key[4] as u32) << 17` term -> key[4]
|
||||
/// no longer influences LFSR0, this assert fires for i==4.
|
||||
#[test]
|
||||
fn decrypt_key_depends_on_every_key_byte() {
|
||||
let base_key = [0x12, 0x34, 0x56, 0x78, 0x9A];
|
||||
let crypted = [0xAB, 0xCD, 0xEF, 0x01, 0x23];
|
||||
let base_out = decrypt_key(0xFF, &base_key, &crypted);
|
||||
for i in 0..5 {
|
||||
let mut k = base_key;
|
||||
k[i] ^= 0x01;
|
||||
assert_ne!(
|
||||
decrypt_key(0xFF, &k, &crypted),
|
||||
base_out,
|
||||
"flipping key byte {i} did not change the decrypted key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The invert byte (0x00 vs 0xFF) selects the LFSR0 output index in
|
||||
/// css_step via `TAB4[(o_lfsr0 ^ invert) as usize]`. For a non-degenerate
|
||||
/// key it must change the keystream and hence the result. (Pins that the
|
||||
/// invert parameter is actually wired into the LFSR0 path, distinguishing
|
||||
/// the disc-key vs title-key code paths.)
|
||||
///
|
||||
/// Grounding: css_step's `TAB4[(o_lfsr0 ^ invert)]`.
|
||||
/// Mutation: hardcode `invert` to 0 inside css_step -> r0 == rff, fails.
|
||||
#[test]
|
||||
fn decrypt_key_invert_changes_result() {
|
||||
let key = [0x12, 0x34, 0x56, 0x78, 0x9A];
|
||||
let crypted = [0xAB, 0xCD, 0xEF, 0x01, 0x23];
|
||||
assert_ne!(
|
||||
decrypt_key(0x00, &key, &crypted),
|
||||
decrypt_key(0xFF, &key, &crypted),
|
||||
"invert must alter the LFSR0 keystream"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user