0.31.0: hardening and correctness pass across mux, codec, AACS/CSS, UDF/MPLS/CLPI, recovery, drive/SCSI, labels, and I/O
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.
This commit is contained in:
+108
-54
@@ -1,7 +1,8 @@
|
||||
//! CSS cipher implementation based on the Stevenson 1999 analysis.
|
||||
//!
|
||||
//! The CSS cipher uses two table-driven feedback circuits:
|
||||
//! - LFSR1: 9-bit state (two halves), driven by TAB2/TAB3
|
||||
//! - 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.
|
||||
@@ -12,6 +13,45 @@
|
||||
|
||||
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
|
||||
@@ -20,7 +60,18 @@ use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5};
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
@@ -39,36 +90,40 @@ pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
|
||||
title_key[4] ^ sector[0x58],
|
||||
];
|
||||
|
||||
// Decrypt the key through the CSS mangling function to get the working key
|
||||
let working_key = decrypt_key(0xFF, &key, §or[0x54..0x59]);
|
||||
// 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 = ((working_key[4] as u32) << 17)
|
||||
| ((working_key[3] as u32) << 9)
|
||||
| (((working_key[2] as u32) << 1) + 8 - (working_key[2] as u32 & 7));
|
||||
lfsr0 = (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;
|
||||
let mut lfsr0: u32 = seed_lfsr0(&working_key);
|
||||
|
||||
let mut combined: u32 = 0;
|
||||
|
||||
// Generate 1920 keystream bytes (for sector bytes 128..2048)
|
||||
// Per libdvdcss css_unscramble: TAB1 permutation on ciphertext, no invert on LFSR0
|
||||
// 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 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 as usize] as u32;
|
||||
*byte ^= (combined & 0xFF) as u8;
|
||||
combined >>= 8;
|
||||
let ks = css_step(
|
||||
&mut lfsr1_lo,
|
||||
&mut lfsr1_hi,
|
||||
&mut lfsr0,
|
||||
&mut combined,
|
||||
0x00,
|
||||
);
|
||||
*byte ^= ks;
|
||||
}
|
||||
|
||||
// Clear scramble flags
|
||||
@@ -80,37 +135,23 @@ pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
|
||||
/// 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]) -> [u8; 5] {
|
||||
if p_crypted.len() < 5 {
|
||||
return *p_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 = ((p_key[4] as u32) << 17)
|
||||
| ((p_key[3] as u32) << 9)
|
||||
| (((p_key[2] as u32) << 1) + 8 - (p_key[2] as u32 & 7));
|
||||
lfsr0 = (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;
|
||||
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 {
|
||||
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);
|
||||
|
||||
// TAB5 for LFSR1 output, TAB4 for LFSR0^invert (per libdvdcss css_DecryptKey)
|
||||
combined += TAB5[o_lfsr1 as usize] as u32 + TAB4[(o_lfsr0 ^ invert) as usize] as u32;
|
||||
*byte = (combined & 0xFF) as u8;
|
||||
combined >>= 8;
|
||||
*byte = css_step(
|
||||
&mut lfsr1_lo,
|
||||
&mut lfsr1_hi,
|
||||
&mut lfsr0,
|
||||
&mut combined,
|
||||
invert,
|
||||
);
|
||||
}
|
||||
|
||||
// Two rounds of chained XOR through TAB1
|
||||
@@ -184,7 +225,7 @@ mod tests {
|
||||
assert_ne!(result, [0u8; 5]);
|
||||
}
|
||||
|
||||
/// Test 1: css_decrypt_key_roundtrip
|
||||
/// 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
|
||||
@@ -228,11 +269,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test 2: css_descramble_produces_valid_mpeg2
|
||||
/// Test 2: descramble_modifies_encrypted_region
|
||||
///
|
||||
/// 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.
|
||||
/// 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];
|
||||
@@ -255,9 +298,20 @@ 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_sector(&title_key, &mut sector);
|
||||
assert_eq!(
|
||||
§or[128..2048],
|
||||
&original[128..2048],
|
||||
"double descramble did not restore the encrypted region"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 4: css_tab1_relationship
|
||||
/// 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
|
||||
@@ -281,7 +335,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test 5: css_tab4_is_bit_reversal
|
||||
/// css_tab4_is_bit_reversal
|
||||
///
|
||||
/// TAB4 reverses the bits of each byte: TAB4[0x01] = 0x80, TAB4[0x80] = 0x01, etc.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user