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:
+29
-75
@@ -106,7 +106,7 @@ const CRYPT_TAB2: [u8; 256] = [
|
||||
0x45, 0x78, 0xA9, 0xA8, 0xEA, 0xC9, 0x6A, 0xF7, 0x29, 0x91, 0xF0, 0x02, 0x18, 0x3A, 0x4E, 0x7C,
|
||||
];
|
||||
|
||||
const CRYPT_TAB3: [u8; 288] = [
|
||||
const CRYPT_TAB3: [u8; 256] = [
|
||||
0x73, 0x51, 0x95, 0xE1, 0x12, 0xE4, 0xC0, 0x58, 0xEE, 0xF2, 0x08, 0x1B, 0xA9, 0xFA, 0x98, 0x4C,
|
||||
0xA7, 0x33, 0xE2, 0x1B, 0xA7, 0x6D, 0xF5, 0x30, 0x97, 0x1D, 0xF3, 0x02, 0x60, 0x5A, 0x82, 0x0F,
|
||||
0x91, 0xD0, 0x9C, 0x10, 0x39, 0x7A, 0x83, 0x85, 0x3B, 0xB2, 0xB8, 0xAE, 0x0C, 0x09, 0x52, 0xEA,
|
||||
@@ -123,8 +123,6 @@ const CRYPT_TAB3: [u8; 288] = [
|
||||
0xBD, 0xC1, 0x0E, 0x56, 0x54, 0x3E, 0x14, 0x5F, 0x8C, 0x8F, 0x6E, 0x75, 0x1C, 0x07, 0x39, 0x7B,
|
||||
0x4B, 0xDB, 0xD3, 0x4B, 0x1E, 0xC8, 0x7E, 0xFE, 0x3E, 0x72, 0x16, 0x83, 0x7D, 0xEE, 0xF5, 0xCA,
|
||||
0xC5, 0x18, 0xF9, 0xD8, 0x68, 0xAB, 0x38, 0x85, 0xA8, 0xF0, 0xA1, 0x73, 0x9F, 0x5D, 0x19, 0x0B,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x72, 0x39, 0x25, 0x67, 0x26, 0x6D, 0x71,
|
||||
0x36, 0x77, 0x3C, 0x20, 0x62, 0x23, 0x68, 0x74, 0xC3, 0x82, 0xC9, 0x15, 0x57, 0x16, 0x5D, 0x81,
|
||||
];
|
||||
|
||||
const VARIANTS: [u8; 32] = [
|
||||
@@ -153,10 +151,6 @@ const PERM_VARIANT: [[u8; 32]; 2] = [
|
||||
],
|
||||
];
|
||||
|
||||
// ── SCSI constants ────────────────────────────────────────────────────────
|
||||
|
||||
const SCSI_READ_DVD_STRUCTURE: u8 = 0xAD;
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Perform CSS bus authentication only.
|
||||
@@ -307,7 +301,7 @@ fn read_disc_key(drive: &mut Drive, agid: u8, bus_key: &[u8; 5]) -> Result<[u8;
|
||||
// READ DVD STRUCTURE, format 0x02 (disc key), 2048+4 bytes
|
||||
let alloc_len: u16 = 2048 + 4;
|
||||
let mut cdb = [0u8; 12];
|
||||
cdb[0] = SCSI_READ_DVD_STRUCTURE;
|
||||
cdb[0] = crate::scsi::SCSI_READ_DISC_STRUCTURE;
|
||||
// bytes 2-5: address = 0
|
||||
cdb[6] = 0; // layer
|
||||
cdb[7] = 0x02; // format = disc key
|
||||
@@ -333,12 +327,23 @@ fn read_disc_key(drive: &mut Drive, agid: u8, bus_key: &[u8; 5]) -> Result<[u8;
|
||||
}
|
||||
|
||||
// Try each player key against each of 408 disc key entries.
|
||||
// Each entry in the block is the disc key encrypted with a specific player key.
|
||||
// We try all known player keys and verify by checking that two different
|
||||
// entries produce the same disc key.
|
||||
let mut candidates: Vec<([u8; 5], usize, usize)> = Vec::new(); // (disc_key, pk_idx, pos)
|
||||
// Each entry in the block is the disc key encrypted with a specific player
|
||||
// key. We collect every decryption and accept the disc key as soon as two
|
||||
// independent decryptions agree on the same 5-byte value (the agreement may
|
||||
// come from two different player keys or from one player key decrypting two
|
||||
// different entries to the same value).
|
||||
//
|
||||
// NOTE: this is a collision heuristic, not the canonical CSS disc-key
|
||||
// self-verification (which decrypts the verification entry with the
|
||||
// candidate and checks the result equals the candidate). A coincidental
|
||||
// collision among the ~12,648 candidate decryptions could in principle
|
||||
// accept a wrong disc key; in practice a chance collision on 5 bytes is
|
||||
// improbable enough to serve as the validity check, and this path is the
|
||||
// production DVD disc-key recovery. Left as-is to avoid regressing it
|
||||
// without a real disc-key-block test vector to validate against.
|
||||
let mut candidates: Vec<[u8; 5]> = Vec::new();
|
||||
|
||||
for (pk_idx, player_key) in PLAYER_KEYS.iter().enumerate() {
|
||||
for player_key in PLAYER_KEYS.iter() {
|
||||
for pos in 0..408 {
|
||||
let offset = pos * 5;
|
||||
if offset + 5 > disc_key_block.len() {
|
||||
@@ -348,13 +353,11 @@ fn read_disc_key(drive: &mut Drive, agid: u8, bus_key: &[u8; 5]) -> Result<[u8;
|
||||
enc.copy_from_slice(&disc_key_block[offset..offset + 5]);
|
||||
let candidate = super::lfsr::decrypt_key(0x00, player_key, &enc);
|
||||
|
||||
// Check if any previous candidate matches (same disc key from different entry/pk)
|
||||
for (prev, _, _) in &candidates {
|
||||
if *prev == candidate {
|
||||
return Ok(candidate);
|
||||
}
|
||||
// Accept on the first agreement between two independent decryptions.
|
||||
if candidates.contains(&candidate) {
|
||||
return Ok(candidate);
|
||||
}
|
||||
candidates.push((candidate, pk_idx, pos));
|
||||
candidates.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,65 +395,16 @@ fn read_raw_title_key(drive: &mut Drive, agid: u8, lba: u32) -> Result<[u8; 5]>
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn read_title_key(
|
||||
drive: &mut Drive,
|
||||
agid: u8,
|
||||
lba: u32,
|
||||
bus_key: &[u8; 5],
|
||||
disc_key: &[u8; 5],
|
||||
) -> Result<[u8; 5]> {
|
||||
let scsi = drive.scsi_mut();
|
||||
|
||||
let mut cdb = [0u8; 12];
|
||||
cdb[0] = crate::scsi::SCSI_REPORT_KEY;
|
||||
cdb[2] = (lba >> 24) as u8;
|
||||
cdb[3] = (lba >> 16) as u8;
|
||||
cdb[4] = (lba >> 8) as u8;
|
||||
cdb[5] = lba as u8;
|
||||
cdb[8] = 0x00;
|
||||
cdb[9] = 0x0C;
|
||||
cdb[10] = (agid << 6) | 0x04;
|
||||
|
||||
let mut buf = [0u8; 12];
|
||||
let tk_result = scsi.execute(
|
||||
&cdb,
|
||||
crate::scsi::DataDirection::FromDevice,
|
||||
&mut buf,
|
||||
5_000,
|
||||
);
|
||||
tk_result.map_err(|_| Error::CssAuthFailed)?;
|
||||
|
||||
// Title key at bytes 5..10, byte-reversed
|
||||
let mut title_key = [0u8; 5];
|
||||
for i in 0..5 {
|
||||
title_key[i] = buf[5 + (4 - i)];
|
||||
}
|
||||
|
||||
// XOR with reversed bus key (same pattern as disc key block)
|
||||
for i in 0..5 {
|
||||
title_key[i] ^= bus_key[4 - i];
|
||||
}
|
||||
|
||||
// Check for null key (title not encrypted)
|
||||
if title_key == [0u8; 5] {
|
||||
return Ok(title_key);
|
||||
}
|
||||
|
||||
// Decrypt with disc key (invert=0xFF for title keys)
|
||||
let title_key = super::lfsr::decrypt_key(0xFF, disc_key, &title_key);
|
||||
|
||||
Ok(title_key)
|
||||
}
|
||||
|
||||
// ── CSSCryptKey ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Exposed for testing only.
|
||||
pub fn test_crypt_key(key_type: usize, variant: u8, challenge: &[u8; 10]) -> [u8; 5] {
|
||||
crypt_key(key_type, variant, challenge)
|
||||
}
|
||||
|
||||
fn crypt_key(key_type: usize, variant: u8, challenge: &[u8; 10]) -> [u8; 5] {
|
||||
// key_type indexes PERM_CHALLENGE ([_;3]); variant indexes
|
||||
// VARIANTS/PERM_VARIANT ([_;32]). All internal callers pass key_type in
|
||||
// 0..3 and variant in 0..32; the asserts document the contract for the
|
||||
// pub(crate) test entry point test_crypt_key and turn a would-be
|
||||
// out-of-bounds panic into an explicit precondition violation.
|
||||
debug_assert!(key_type < 3, "crypt_key: key_type out of range");
|
||||
debug_assert!((variant as usize) < 32, "crypt_key: variant out of range");
|
||||
let perm = &PERM_CHALLENGE[key_type];
|
||||
let mut scratch = [0u8; 10];
|
||||
for i in 0..10 {
|
||||
|
||||
+120
-115
@@ -1,13 +1,22 @@
|
||||
//! CSS title key recovery — Stevenson's divide-and-conquer attack (1999).
|
||||
//!
|
||||
//! Given a scrambled DVD sector with known plaintext (MPEG-2 PES headers),
|
||||
//! recovers the 5-byte title key by:
|
||||
//! this would recover the 5-byte title key by:
|
||||
//!
|
||||
//! 1. XORing ciphertext with TAB1[ciphertext] to cancel the mangling
|
||||
//! 1. Computing `TAB1[ciphertext] ^ plaintext` to cancel the TAB1 output
|
||||
//! mangling and expose the raw LFSR-combination keystream
|
||||
//! 2. Iterating all 2^16 LFSR1 states
|
||||
//! 3. For each: deducing what LFSR0 must produce, then verifying
|
||||
//!
|
||||
//! Total work: ~65536 iterations with 10-byte validation = instant.
|
||||
//! NOTE: this recovery path is currently non-functional. It models the
|
||||
//! textbook direct-seed CSS cipher, whereas the in-repo descrambler
|
||||
//! ([`super::lfsr::descramble_sector`]) seeds its LFSRs from a key that
|
||||
//! has been run through an additional `decrypt_key` mangling step. The two
|
||||
//! are therefore inconsistent and [`recover_title_key`] never returns a key
|
||||
//! for a sector scrambled by this crate's own descrambler. The production
|
||||
//! DVD path does NOT use this fallback — it derives the title key over SCSI
|
||||
//! ([`super::auth::authenticate_and_read_title_key`]). See the ignored
|
||||
//! regression test below.
|
||||
//!
|
||||
//! Algorithm: Frank A. Stevenson, "Divide and conquer attack" (1999).
|
||||
|
||||
@@ -25,7 +34,12 @@ const FLAG_BYTE: usize = 0x14;
|
||||
/// region (bytes 0x80+). For MPEG-2 sectors, the first bytes are typically
|
||||
/// a PES header: `00 00 01 [stream_id] ...`
|
||||
///
|
||||
/// Returns the recovered 5-byte title key, or None if recovery fails.
|
||||
/// Returns the recovered 5-byte title key, or `None` if recovery fails.
|
||||
///
|
||||
/// NOTE: see the module docs — this attack models the textbook direct-seed
|
||||
/// CSS cipher and is inconsistent with this crate's descrambler, so it
|
||||
/// currently returns `None` even for an exact known plaintext. It is not on
|
||||
/// the production DVD decrypt path.
|
||||
pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
|
||||
if sector.len() < SECTOR_SIZE || plain.len() < 10 {
|
||||
return None;
|
||||
@@ -39,10 +53,11 @@ pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
|
||||
let crypted = §or[ENCRYPTED_START..];
|
||||
let seed = §or[SEED_OFFSET..SEED_OFFSET + 5];
|
||||
|
||||
// Phase 1: Cancel the TAB1 mangling layer
|
||||
// The CSS cipher applies TAB1 as an output permutation.
|
||||
// XORing ciphertext with TAB1[ciphertext] and plaintext removes it,
|
||||
// leaving the raw LFSR combination output.
|
||||
// Phase 1: Cancel the TAB1 mangling layer and subtract the known plaintext.
|
||||
// The CSS cipher applies TAB1 as an output permutation. Computing
|
||||
// `buf[i] = TAB1[crypted[i]] ^ plain[i]` both undoes that permutation and
|
||||
// XORs out the known plaintext, leaving the raw LFSR-combination keystream
|
||||
// bytes for the attack to match against.
|
||||
let mut buf = [0u8; 10];
|
||||
for i in 0..10 {
|
||||
if i >= crypted.len() || i >= plain.len() {
|
||||
@@ -82,8 +97,12 @@ pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
|
||||
t5 += t6 + t4_perm as u32;
|
||||
let t6_inv = TAB4[t6 as usize & 0xFF];
|
||||
|
||||
// Build LFSR0 candidate from deduced output bytes
|
||||
t3 = (t3 << 8) | t6_inv as u32;
|
||||
// Build LFSR0 candidate from deduced output bytes.
|
||||
// wrapping_shl: the accumulator is a rolling 32-bit window;
|
||||
// the top byte is intentionally shifted out. Matches the
|
||||
// release-mode wrap (no behaviour change) without a debug
|
||||
// overflow panic.
|
||||
t3 = t3.wrapping_shl(8) | t6_inv as u32;
|
||||
t5 >>= 8;
|
||||
}
|
||||
|
||||
@@ -97,9 +116,11 @@ pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
|
||||
t1 = ((t1 & 1) << 8) ^ t4 as u32;
|
||||
let t4_perm = TAB5[t4 as usize];
|
||||
|
||||
// Clock LFSR0 forward
|
||||
// Clock LFSR0 forward. wrapping_shl keeps the rolling 32-bit
|
||||
// window semantics (top byte shifted out) identical to the
|
||||
// release build while avoiding a debug overflow panic.
|
||||
let t6 = ((((((t3 >> 8) ^ t3) >> 1) ^ t3) >> 3) ^ t3) >> 7;
|
||||
t3 = (t3 << 8) | (t6 & 0xFF);
|
||||
t3 = t3.wrapping_shl(8) | (t6 & 0xFF);
|
||||
let t6_perm = TAB4[(t6 & 0xFF) as usize];
|
||||
|
||||
t5 += t6_perm as u32 + t4_perm as u32;
|
||||
@@ -143,7 +164,11 @@ pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
|
||||
let t4 = (t3 >> 1).wrapping_sub(4);
|
||||
for t5_off in 0u32..8 {
|
||||
let val = t4.wrapping_add(t5_off);
|
||||
if (val * 2 + 8 - (val & 7)) == t3 {
|
||||
// Reconstruction probe: val can sit near u32::MAX, so the
|
||||
// (val*2 + 8 - (val & 7)) expression must wrap rather than
|
||||
// panic in debug. wrapping_* reproduces the release result
|
||||
// exactly (the comparison against t3 is unaffected).
|
||||
if val.wrapping_mul(2).wrapping_add(8).wrapping_sub(val & 7) == t3 {
|
||||
result_key[0] = (i_try >> 8) as u8;
|
||||
result_key[1] = (i_try & 0xFF) as u8;
|
||||
result_key[2] = (val & 0xFF) as u8;
|
||||
@@ -172,9 +197,15 @@ pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
|
||||
Some(result_key)
|
||||
}
|
||||
|
||||
/// Crack the CSS title key from an encrypted sector using MPEG-2 pattern attack.
|
||||
/// Crack the CSS title key from an encrypted sector using an MPEG-2
|
||||
/// pattern attack.
|
||||
///
|
||||
/// Detects the PES header pattern at byte 0x80 and uses it as known plaintext.
|
||||
/// Detects the PES header pattern at byte 0x80 and uses it as known
|
||||
/// plaintext. This is a best-effort fallback for the SCSI auth path
|
||||
/// (see [`super::resolve`]): it only succeeds on a sector whose
|
||||
/// encrypted region begins with one of the tried PES header patterns,
|
||||
/// and returns `None` otherwise. The production DVD path obtains the
|
||||
/// title key via drive authentication, not cracking.
|
||||
pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
|
||||
if sector.len() < SECTOR_SIZE {
|
||||
return None;
|
||||
@@ -199,7 +230,8 @@ pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
|
||||
|
||||
// Try many PES header patterns at byte 0x80.
|
||||
// Structure: 00 00 01 [stream_id] [len_hi] [len_lo] [flags1] [flags2] [hdr_len] [data]
|
||||
let mut patterns: Vec<[u8; 10]> = Vec::with_capacity(128);
|
||||
// 24 padding-stream + 144 video/audio + 1 navigation = 169 patterns.
|
||||
let mut patterns: Vec<[u8; 10]> = Vec::with_capacity(169);
|
||||
|
||||
// Padding stream (0xBE): payload is 0xFF bytes, various lengths
|
||||
for len_hi in 0u8..8 {
|
||||
@@ -242,23 +274,6 @@ pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Crack CSS key from multiple sectors.
|
||||
pub fn crack_from_sectors(sectors: &[Vec<u8>]) -> Option<[u8; 5]> {
|
||||
for sector in sectors {
|
||||
if sector.len() < SECTOR_SIZE {
|
||||
continue;
|
||||
}
|
||||
let flags = (sector[FLAG_BYTE] >> 4) & 0x03;
|
||||
if flags == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(key) = crack_title_key(sector) {
|
||||
return Some(key);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -269,6 +284,30 @@ mod tests {
|
||||
assert!(crack_title_key(§or).is_none());
|
||||
}
|
||||
|
||||
/// Regression: the LFSR0 reconstruction arithmetic must not overflow
|
||||
/// (panic) in a debug build for scrambled sector content. Exercises
|
||||
/// the full 2^16 Stevenson search via recover_title_key directly (the
|
||||
/// overflow site), with the assertion simply that it does not panic.
|
||||
/// recover_title_key is used rather than crack_title_key to avoid
|
||||
/// re-running the search for all 169 PES patterns.
|
||||
#[test]
|
||||
fn crack_scrambled_sectors_never_overflow() {
|
||||
for seed in 0u32..4 {
|
||||
let mut sector = vec![0u8; SECTOR_SIZE];
|
||||
sector[FLAG_BYTE] = 0x30; // scramble flag set
|
||||
let mut x = seed.wrapping_mul(2_654_435_761).wrapping_add(1);
|
||||
for b in sector.iter_mut().skip(0x80) {
|
||||
x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
|
||||
*b = (x >> 16) as u8;
|
||||
}
|
||||
for (i, b) in sector[SEED_OFFSET..SEED_OFFSET + 5].iter_mut().enumerate() {
|
||||
*b = seed.wrapping_add(i as u32) as u8;
|
||||
}
|
||||
let plain = [0x00u8, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
|
||||
let _ = recover_title_key(§or, &plain);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crack_too_short_returns_none() {
|
||||
let sector = vec![0u8; 100];
|
||||
@@ -282,13 +321,22 @@ mod tests {
|
||||
assert!(recover_title_key(§or, &short_plain).is_none());
|
||||
}
|
||||
|
||||
/// Test 3: css_crack_recovers_key_from_scrambled_sector
|
||||
/// Build a scrambled sector with known plaintext (both an MPEG PES header
|
||||
/// at 0x80 and an exact-plaintext probe), then assert that the Stevenson
|
||||
/// recovery actually recovers a key whose descramble round-trips the body.
|
||||
///
|
||||
/// Build a plaintext sector with known MPEG-2 PES headers, scramble it
|
||||
/// with a known title key, then run crack_title_key() on the scrambled
|
||||
/// sector. If the Stevenson attack succeeds, verify that descrambling
|
||||
/// with the recovered key produces the original plaintext at bytes 128..132.
|
||||
/// This is the regression gate for the CSS crack/recover path. It is
|
||||
/// `#[ignore]`d because that path is currently non-functional: the
|
||||
/// recovery models the textbook direct-seed cipher, whereas this crate's
|
||||
/// [`descramble_sector`] seeds from a `decrypt_key`-mangled key, so the
|
||||
/// two are inconsistent and recovery returns `None`. When the crack
|
||||
/// algorithm is re-derived against this crate's actual descrambler, this
|
||||
/// test must pass with `--ignored` removed. The production DVD path does
|
||||
/// not use crack/recover (it authenticates over SCSI), so the broken
|
||||
/// fallback does not affect shipped behavior.
|
||||
#[test]
|
||||
#[ignore = "CSS crack/recover path is non-functional vs this crate's descrambler; \
|
||||
see module docs. Regression gate for a future fix."]
|
||||
fn css_crack_recovers_key_from_scrambled_sector() {
|
||||
use super::super::lfsr::descramble_sector;
|
||||
|
||||
@@ -309,90 +357,47 @@ mod tests {
|
||||
// Sector seed at bytes 0x54-0x58
|
||||
plaintext[SEED_OFFSET..SEED_OFFSET + 5].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55]);
|
||||
|
||||
// PES header at byte 0x80: 00 00 01 E0 (video stream)
|
||||
// Then typical PES header bytes for a stream with PTS
|
||||
plaintext[0x80] = 0x00;
|
||||
plaintext[0x81] = 0x00;
|
||||
plaintext[0x82] = 0x01;
|
||||
plaintext[0x83] = 0xE0;
|
||||
plaintext[0x84] = 0x00; // PES length hi
|
||||
plaintext[0x85] = 0x00; // PES length lo
|
||||
plaintext[0x86] = 0x80; // flags: data_alignment, copyright
|
||||
plaintext[0x87] = 0x80; // PTS flag
|
||||
plaintext[0x88] = 0x05; // PES header data length
|
||||
plaintext[0x89] = 0x21; // PTS byte 1
|
||||
// PES header at byte 0x80: 00 00 01 E0 (video stream) with PTS.
|
||||
let exact_plain: [u8; 10] = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
|
||||
plaintext[0x80..0x80 + 10].copy_from_slice(&exact_plain);
|
||||
|
||||
let original_plaintext = plaintext.clone();
|
||||
|
||||
// "Scramble" the sector by calling descramble (which XORs the keystream)
|
||||
// on the plaintext. This produces a scrambled sector.
|
||||
// "Scramble" the sector by XORing the keystream over the plaintext.
|
||||
descramble_sector(&title_key, &mut plaintext);
|
||||
|
||||
// The scramble flag was cleared by descramble_sector. Restore it so
|
||||
// the cracker sees it as encrypted.
|
||||
// descramble_sector cleared the flag; restore it so the cracker sees
|
||||
// the sector as encrypted.
|
||||
plaintext[FLAG_BYTE] = 0x30;
|
||||
|
||||
// Now we have a scrambled sector. Try to crack the title key.
|
||||
let cracked_key = crack_title_key(&plaintext);
|
||||
// 1) Pattern-guessing entry point must recover a key.
|
||||
let cracked = crack_title_key(&plaintext);
|
||||
assert!(
|
||||
cracked.is_some(),
|
||||
"crack_title_key returned None for a sector scrambled with a known key"
|
||||
);
|
||||
let cracked = cracked.unwrap();
|
||||
let mut body = plaintext.clone();
|
||||
descramble_sector(&cracked, &mut body);
|
||||
assert_eq!(
|
||||
&body[0x80..SECTOR_SIZE],
|
||||
&original_plaintext[0x80..SECTOR_SIZE],
|
||||
"crack_title_key key did not round-trip the body"
|
||||
);
|
||||
|
||||
match cracked_key {
|
||||
Some(key) => {
|
||||
// Verify: descramble with the cracked key should recover plaintext
|
||||
let mut test = plaintext.clone();
|
||||
descramble_sector(&key, &mut test);
|
||||
|
||||
// Check that the PES header is recovered
|
||||
assert_eq!(test[0x80], 0x00, "PES byte 0 mismatch");
|
||||
assert_eq!(test[0x81], 0x00, "PES byte 1 mismatch");
|
||||
assert_eq!(test[0x82], 0x01, "PES byte 2 mismatch");
|
||||
assert_eq!(test[0x83], 0xE0, "PES byte 3 mismatch");
|
||||
|
||||
// Also verify the rest of the encrypted region matches original
|
||||
assert_eq!(
|
||||
&test[0x80..SECTOR_SIZE],
|
||||
&original_plaintext[0x80..SECTOR_SIZE],
|
||||
"Decrypted content does not match original plaintext"
|
||||
);
|
||||
|
||||
eprintln!(
|
||||
"Stevenson attack succeeded: cracked key = {:02X?}, original = {:02X?}",
|
||||
key, title_key
|
||||
);
|
||||
}
|
||||
None => {
|
||||
// The Stevenson attack may not always find a key for all title keys
|
||||
// and sector seeds. This is expected for some combinations where the
|
||||
// known plaintext pattern doesn't match what crack_title_key tries.
|
||||
eprintln!(
|
||||
"Stevenson attack did not find key for title_key={:02X?} seed={:02X?}. \
|
||||
This can happen when the cipher output doesn't match the tried patterns. \
|
||||
Testing with recover_title_key directly with exact plaintext.",
|
||||
title_key,
|
||||
&[0x11u8, 0x22, 0x33, 0x44, 0x55],
|
||||
);
|
||||
|
||||
// Try with exact known plaintext instead of guessing
|
||||
let exact_plain: [u8; 10] =
|
||||
[0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
|
||||
let recovered = recover_title_key(&plaintext, &exact_plain);
|
||||
if let Some(key) = recovered {
|
||||
let mut test = plaintext.clone();
|
||||
descramble_sector(&key, &mut test);
|
||||
assert_eq!(test[0x80], 0x00);
|
||||
assert_eq!(test[0x81], 0x00);
|
||||
assert_eq!(test[0x82], 0x01);
|
||||
eprintln!(
|
||||
"recover_title_key with exact plaintext succeeded: {:02X?}",
|
||||
key
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"recover_title_key also returned None. The attack may not converge \
|
||||
for this particular key/seed combination. This is a known limitation \
|
||||
of the brute-force LFSR0 recovery phase."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2) Exact known plaintext must also recover a round-tripping key.
|
||||
let recovered = recover_title_key(&plaintext, &exact_plain);
|
||||
assert!(
|
||||
recovered.is_some(),
|
||||
"recover_title_key returned None for exact known plaintext"
|
||||
);
|
||||
let recovered = recovered.unwrap();
|
||||
let mut body2 = plaintext.clone();
|
||||
descramble_sector(&recovered, &mut body2);
|
||||
assert_eq!(
|
||||
&body2[0x80..SECTOR_SIZE],
|
||||
&original_plaintext[0x80..SECTOR_SIZE],
|
||||
"recover_title_key key did not round-trip the body"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+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]
|
||||
|
||||
+28
-12
@@ -1,13 +1,21 @@
|
||||
//! CSS (Content Scramble System) — DVD disc encryption.
|
||||
//!
|
||||
//! CSS uses a weak 40-bit LFSR stream cipher (broken since 1999).
|
||||
//! No keys needed — the title key is cracked from encrypted content
|
||||
//! using a known-plaintext attack on MPEG-2 PES headers.
|
||||
//!
|
||||
//! The production entry point is [`resolve`]. Two title-key acquisition
|
||||
//! paths exist behind it:
|
||||
//! - The SCSI auth path drives bus authentication with the compiled-in CSS
|
||||
//! player keys and reads the title key from the drive (the production DVD
|
||||
//! path on a live drive).
|
||||
//! - The crack fallback ([`crack_key`]) needs no keys — it attempts the
|
||||
//! Stevenson known-plaintext attack on MPEG-2 PES headers. (Currently
|
||||
//! non-functional; see the `crack` module docs.)
|
||||
//!
|
||||
//! Usage:
|
||||
//! ```rust,ignore
|
||||
//! let key = css::crack_key(reader, &extents)?;
|
||||
//! css::descramble_sector(&key, &mut sector);
|
||||
//! if let Some(state) = css::resolve(&mut ctx) {
|
||||
//! css::descramble_sector(&state, &mut sector);
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod auth;
|
||||
@@ -22,7 +30,7 @@ use crate::sector::SectorSource;
|
||||
/// CSS decryption state for a DVD title.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CssState {
|
||||
/// Cracked 5-byte title key
|
||||
/// 5-byte CSS title key (from SCSI auth or the crack fallback).
|
||||
pub title_key: [u8; 5],
|
||||
}
|
||||
|
||||
@@ -37,7 +45,7 @@ pub struct CssState {
|
||||
/// headers; works on disc images and on drives whose CSS auth path
|
||||
/// is unavailable).
|
||||
///
|
||||
/// `live_drive` always wins when both modes are populated.
|
||||
/// The `drive` (auth) path always wins when both modes are populated.
|
||||
pub struct CssContext<'a> {
|
||||
/// Live SCSI drive — when present, [`resolve`] tries the auth path.
|
||||
pub drive: Option<&'a mut Drive>,
|
||||
@@ -70,23 +78,32 @@ pub fn resolve(ctx: &mut CssContext<'_>) -> Option<CssState> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Crack the CSS title key by reading encrypted sectors and applying
|
||||
/// a known-plaintext attack on MPEG-2 headers.
|
||||
///
|
||||
/// Crack the CSS title key by scanning scrambled sectors across extents.
|
||||
/// Crack the CSS title key by scanning scrambled sectors across extents and
|
||||
/// applying a known-plaintext attack on MPEG-2 PES headers.
|
||||
///
|
||||
/// The Stevenson attack needs a sector where a PES header starts at byte
|
||||
/// 0x80 (start of the encrypted region). This only happens when a new PES
|
||||
/// packet begins at exactly sector offset 128. We scan up to 50000
|
||||
/// scrambled sectors sequentially across all extents.
|
||||
///
|
||||
/// NOTE: the underlying recovery ([`crack::recover_title_key`]) is currently
|
||||
/// non-functional against this crate's descrambler (see `crack` module
|
||||
/// docs), so this scan returns `None`. The production DVD path uses the SCSI
|
||||
/// auth path, not this crack fallback.
|
||||
pub fn crack_key(reader: &mut dyn SectorSource, extents: &[Extent]) -> Option<CssState> {
|
||||
let mut tried = 0u32;
|
||||
let max_tries = 50_000;
|
||||
|
||||
// Reused across every scanned sector; read_sectors overwrites all 2048
|
||||
// bytes on success, so no re-zeroing is needed between iterations.
|
||||
let mut buf = vec![0u8; 2048];
|
||||
|
||||
for ext in extents {
|
||||
let mut i = 0;
|
||||
while i < ext.sector_count && tried < max_tries {
|
||||
let mut buf = vec![0u8; 2048];
|
||||
// Every scanned sector counts toward the cap, so a long run
|
||||
// of unscrambled sectors can't read past the budget.
|
||||
tried += 1;
|
||||
if reader
|
||||
.read_sectors(ext.start_lba + i, 1, &mut buf, true)
|
||||
.is_ok()
|
||||
@@ -95,7 +112,6 @@ pub fn crack_key(reader: &mut dyn SectorSource, extents: &[Extent]) -> Option<Cs
|
||||
if let Some(key) = crack::crack_title_key(&buf) {
|
||||
return Some(CssState { title_key: key });
|
||||
}
|
||||
tried += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
+26
-3
@@ -44,7 +44,10 @@ pub const TAB2: [u8; 256] = [
|
||||
0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf6, 0xf7, 0xf4, 0xf5, 0xf2, 0xf3, 0xf0, 0xf1,
|
||||
];
|
||||
|
||||
/// Table 3: LFSR1 low-byte feedback permutation.
|
||||
/// Table 3: LFSR1 9-bit low-word feedback table (512 entries).
|
||||
///
|
||||
/// Indexed by the 9-bit LFSR1 low word (the upper feedback bit makes the
|
||||
/// index 9-bit, hence 512 entries, not 256).
|
||||
pub const TAB3: [u8; 512] = [
|
||||
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||
@@ -100,8 +103,10 @@ pub const TAB4: [u8; 256] = [
|
||||
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff,
|
||||
];
|
||||
|
||||
/// Table 5: LFSR1 output permutation for the Stevenson attack.
|
||||
/// This is the inverse byte-reversal of TAB4.
|
||||
/// Table 5: LFSR1 output permutation used in the keystream combiner.
|
||||
/// `TAB5[i] == TAB4[i] ^ 0xFF` (bitwise complement of the TAB4 bit-reversal
|
||||
/// table). Applied on the normal descramble/recrypt path (lfsr.rs) as well as
|
||||
/// in the key-recovery fallback (crack.rs).
|
||||
pub const TAB5: [u8; 256] = [
|
||||
0xff, 0x7f, 0xbf, 0x3f, 0xdf, 0x5f, 0x9f, 0x1f, 0xef, 0x6f, 0xaf, 0x2f, 0xcf, 0x4f, 0x8f, 0x0f,
|
||||
0xf7, 0x77, 0xb7, 0x37, 0xd7, 0x57, 0x97, 0x17, 0xe7, 0x67, 0xa7, 0x27, 0xc7, 0x47, 0x87, 0x07,
|
||||
@@ -120,3 +125,21 @@ pub const TAB5: [u8; 256] = [
|
||||
0xf8, 0x78, 0xb8, 0x38, 0xd8, 0x58, 0x98, 0x18, 0xe8, 0x68, 0xa8, 0x28, 0xc8, 0x48, 0x88, 0x08,
|
||||
0xf0, 0x70, 0xb0, 0x30, 0xd0, 0x50, 0x90, 0x10, 0xe0, 0x60, 0xa0, 0x20, 0xc0, 0x40, 0x80, 0x00,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Pins the documented relationship `TAB5[i] == TAB4[i] ^ 0xFF` so the
|
||||
/// table doc cannot drift from the data.
|
||||
#[test]
|
||||
fn tab5_is_complement_of_tab4() {
|
||||
for i in 0..256 {
|
||||
assert_eq!(
|
||||
TAB5[i],
|
||||
TAB4[i] ^ 0xFF,
|
||||
"TAB5[{i:#04x}] != TAB4[{i:#04x}] ^ 0xFF"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user