Library-wide review-and-fix pass: tightened AACS keydb/handshake/variant handling and trailing-partial-unit policy, corrected MPLS mark offset and added UDF allocation bounds, hardened the mux/codec framing and M2TS paths, guarded SCSI READ CAPACITY short transfers and unified error mapping, added overflow guards on untrusted disc input, and made prefetch shutdown deterministic. Release profile now builds with thin LTO + single codegen unit.
361 lines
13 KiB
Rust
361 lines
13 KiB
Rust
//! CSS cipher implementation based on the Stevenson 1999 analysis.
|
|
//!
|
|
//! The CSS cipher uses two table-driven feedback circuits:
|
|
//! - LFSR1: 17-bit state (9-bit lo + 8-bit hi register, seeded from
|
|
//! key[0..2]), driven by TAB2/TAB3
|
|
//! - LFSR0: 32-bit state, driven by a feedback polynomial through TAB4
|
|
//!
|
|
//! The keystream is the bytewise sum (with carry) of both LFSR outputs.
|
|
//! Content descrambling XORs this keystream with the encrypted sector data.
|
|
//!
|
|
//! Algorithm: Frank A. Stevenson's divide-and-conquer attack (1999).
|
|
//! Tables: CSS specification constants.
|
|
|
|
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.
|
|
///
|
|
/// The scramble flag at byte 0x14 (bits 4-5) indicates encryption.
|
|
/// After descrambling, the flag is cleared.
|
|
///
|
|
/// No-op (returns without modifying `sector`) in two cases:
|
|
/// - `sector.len() < 2048`: the encrypted region (0x80..0x800) is not
|
|
/// fully present. Callers chunk by 2048, so a trailing partial chunk is
|
|
/// 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.
|
|
pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
|
|
debug_assert!(
|
|
sector.len() >= 2048,
|
|
"descramble_sector: buffer shorter than one 2048-byte sector"
|
|
);
|
|
if sector.len() < 2048 {
|
|
return;
|
|
}
|
|
|
|
let flags = (sector[0x14] >> 4) & 0x03;
|
|
if flags == 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],
|
|
];
|
|
|
|
// 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);
|
|
|
|
// 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 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;
|
|
}
|
|
|
|
// Clear scramble flags
|
|
sector[0x14] &= 0xCF;
|
|
}
|
|
|
|
/// CSS key decryption / mangling function.
|
|
///
|
|
/// 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,
|
|
);
|
|
}
|
|
|
|
// 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];
|
|
|
|
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];
|
|
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn descramble_skips_unscrambled() {
|
|
let key = [0x01, 0x02, 0x03, 0x04, 0x05];
|
|
let mut sector = vec![0xAA; 2048];
|
|
sector[0x14] = 0x00;
|
|
let original = sector.clone();
|
|
descramble_sector(&key, &mut sector);
|
|
assert_eq!(sector, original);
|
|
}
|
|
|
|
#[test]
|
|
fn descramble_modifies_scrambled() {
|
|
let key = [0x01, 0x02, 0x03, 0x04, 0x05];
|
|
let mut sector = vec![0xAA; 2048];
|
|
sector[0x14] = 0x30; // scramble flag set
|
|
// Set a sector seed
|
|
sector[0x54..0x59].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55]);
|
|
let original = sector.clone();
|
|
descramble_sector(&key, &mut sector);
|
|
// Header (0..128) unchanged except byte 0x14 (flag cleared)
|
|
for i in 0..128 {
|
|
if i == 0x14 {
|
|
continue;
|
|
}
|
|
assert_eq!(sector[i], original[i], "header byte {} changed", i);
|
|
}
|
|
// Encrypted region should be different
|
|
assert_ne!(§or[128..256], &original[128..256]);
|
|
}
|
|
|
|
#[test]
|
|
fn descramble_clears_flags() {
|
|
let key = [0x01, 0x02, 0x03, 0x04, 0x05];
|
|
let mut sector = vec![0x00; 2048];
|
|
sector[0x14] = 0x30;
|
|
sector[0x54..0x59].copy_from_slice(&[0x00; 5]);
|
|
descramble_sector(&key, &mut sector);
|
|
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
|
|
///
|
|
/// 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: 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() {
|
|
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
|
|
|
let mut sector = vec![0xAAu8; 2048];
|
|
sector[0x14] = 0x30; // scramble flag
|
|
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
|
|
for i in 0..128 {
|
|
if i == 0x14 {
|
|
continue;
|
|
}
|
|
assert_eq!(sector[i], original[i], "header byte {} changed", i);
|
|
}
|
|
// 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_sector(&title_key, &mut sector);
|
|
assert_eq!(
|
|
§or[128..2048],
|
|
&original[128..2048],
|
|
"double descramble did not restore the encrypted region"
|
|
);
|
|
}
|
|
|
|
/// 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 tab1_val in &TAB1 {
|
|
let v = *tab1_val 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;
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
);
|
|
}
|
|
}
|
|
}
|