libfreemkv 0.31.4: prune 144 vacuous tests (keep spec-grounded subset)
This commit is contained in:
@@ -593,42 +593,6 @@ mod tests {
|
||||
|
||||
// ── CSS constant-table integrity ───────────────────────────────────────
|
||||
|
||||
/// The CSSCryptKey lookup tables are each a full 256-entry byte table and
|
||||
/// the variant tables are 32 entries (one per CSS variant). The cipher
|
||||
/// indexes CRYPT_TAB0..3 with arbitrary bytes (0..256) and indexes
|
||||
/// VARIANTS / PERM_VARIANT with the css_variant (0..32). A short table
|
||||
/// would index out of bounds.
|
||||
///
|
||||
/// Grounding: crypt_key indexes `CRYPT_TABx[idx]` where idx is a u8 cast
|
||||
/// to usize (0..256); `VARIANTS[css_variant]` and `PERM_VARIANT[k][variant]`
|
||||
/// with variant 0..32.
|
||||
/// Mutation: drop the last entry of CRYPT_TAB0 (make it [u8;255]) ->
|
||||
/// compile error / length assert fails.
|
||||
#[test]
|
||||
fn crypt_tables_have_spec_lengths() {
|
||||
assert_eq!(CRYPT_TAB0.len(), 256);
|
||||
assert_eq!(CRYPT_TAB1.len(), 256);
|
||||
assert_eq!(CRYPT_TAB2.len(), 256);
|
||||
assert_eq!(CRYPT_TAB3.len(), 256);
|
||||
assert_eq!(VARIANTS.len(), 32, "one CSS variant byte per variant 0..32");
|
||||
assert_eq!(PERM_VARIANT.len(), 2);
|
||||
assert_eq!(PERM_VARIANT[0].len(), 32);
|
||||
assert_eq!(PERM_VARIANT[1].len(), 32);
|
||||
assert_eq!(
|
||||
PERM_CHALLENGE.len(),
|
||||
3,
|
||||
"one challenge perm per key_type 0..3"
|
||||
);
|
||||
for p in &PERM_CHALLENGE {
|
||||
assert_eq!(
|
||||
p.len(),
|
||||
10,
|
||||
"challenge permutation covers all 10 challenge bytes"
|
||||
);
|
||||
}
|
||||
assert_eq!(SECRET.len(), 5);
|
||||
}
|
||||
|
||||
/// Each PERM_CHALLENGE row is a permutation of indices 0..10 (it reorders
|
||||
/// the 10 challenge bytes). A non-permutation would drop/duplicate
|
||||
/// challenge bytes, weakening or corrupting the bus key derivation.
|
||||
|
||||
@@ -378,31 +378,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// recover_title_key with exactly 10 plaintext bytes is accepted at the
|
||||
/// length guard (it may still return None from the attack, but must not be
|
||||
/// rejected by the `plain.len() < 10` check). Pins the boundary at the
|
||||
/// inclusive value 10.
|
||||
///
|
||||
/// Grounding: `if sector.len() < SECTOR_SIZE || plain.len() < 10 { None }`
|
||||
/// — 10 is the minimum accepted length.
|
||||
/// Mutation: change `< 10` to `< 11` -> a 10-byte plaintext would be
|
||||
/// rejected. We detect acceptance by observing the function runs the
|
||||
/// attack (it returns None for this synthetic data, but a 9-byte plain
|
||||
/// returns None *at the guard*; to distinguish, we assert a 9-byte input
|
||||
/// is rejected and a 10-byte input is not panicking and consistent).
|
||||
#[test]
|
||||
fn recover_accepts_exactly_10_plain_bytes() {
|
||||
let mut sector = vec![0x00u8; SECTOR_SIZE];
|
||||
sector[FLAG_BYTE] = 0x30;
|
||||
sector[SEED_OFFSET..SEED_OFFSET + 5].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55]);
|
||||
let plain9 = [0u8; 9];
|
||||
let plain10 = [0u8; 10];
|
||||
// 9 bytes: rejected at the guard.
|
||||
assert!(recover_title_key(§or, &plain9).is_none());
|
||||
// 10 bytes: passes the guard and runs to completion without panic.
|
||||
let _ = recover_title_key(§or, &plain10);
|
||||
}
|
||||
|
||||
// ── crack_title_key early-return guards (flag uses bits 4-5) ────────────
|
||||
|
||||
/// crack_title_key uses the same bits-4-5 scramble field. A sector with
|
||||
@@ -466,27 +441,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// recover_title_key, when it DOES return a key, XORs the sector seed into
|
||||
/// the recovered raw key (the final step `result_key[i] ^= seed[i]`).
|
||||
/// We cannot easily force a hit on this crate's (non-functional) attack,
|
||||
/// so instead pin the structural guard that the seed is read from the
|
||||
/// documented offset 0x54..0x59 and that a None result is returned for an
|
||||
/// all-zero scrambled sector (the search exhausts without a match rather
|
||||
/// than panicking on the seed XOR).
|
||||
///
|
||||
/// Grounding: SEED_OFFSET == 0x54; seed slice is `sector[0x54..0x59]`.
|
||||
/// Mutation: change SEED_OFFSET to 0x55 -> the seed slice shifts; the
|
||||
/// search still completes (None) but on a functional path the recovered
|
||||
/// key would be wrong. This test pins the no-panic completion only.
|
||||
#[test]
|
||||
fn recover_all_zero_scrambled_completes_none() {
|
||||
let mut sector = vec![0x00u8; SECTOR_SIZE];
|
||||
sector[FLAG_BYTE] = 0x30;
|
||||
let plain = [0x00u8, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
|
||||
// All-zero body: the textbook attack finds no consistent state.
|
||||
assert!(recover_title_key(§or, &plain).is_none());
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -595,60 +595,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// seed_lfsr0 applies the per-byte TAB4 bit-reversal to the 4 bytes of the
|
||||
/// packed LFSR0 seed value. The seeding expression for the all-zero key is
|
||||
/// `(0<<17)|(0<<9)|((0<<1)+8-(0&7)) == 8`, so the raw lfsr0 = 0x00000008.
|
||||
/// Each byte is then TAB4-reversed and re-packed big-endian-ish per the
|
||||
/// code. Byte (lfsr0 & 0xFF) == 0x08 -> TAB4[0x08] == 0x10 placed in the
|
||||
/// top byte (<<24). The other three source bytes are 0 -> TAB4[0]=0. So
|
||||
/// the seed for an all-zero key must be 0x10 << 24 == 0x10000000.
|
||||
///
|
||||
/// Grounding: seed_lfsr0 body + TAB4[0x08] = bit-reverse(0x08=0b00001000)
|
||||
/// = 0b00010000 = 0x10.
|
||||
/// Mutation: change the `<< 24` on the first TAB4 term to `<< 16` -> the
|
||||
/// expected seed changes and the round-trip-anchored value below fails.
|
||||
#[test]
|
||||
fn seed_lfsr0_zero_key_matches_spec_packing() {
|
||||
// We cannot call seed_lfsr0 directly (private), but decrypt_key seeds
|
||||
// LFSR0 with it. Instead pin the documented TAB4 anchor the seed
|
||||
// relies on, plus the algebraic seed value, so a regression in either
|
||||
// the packing constant or TAB4 is caught.
|
||||
assert_eq!(
|
||||
TAB4[0x08], 0x10,
|
||||
"bit-reverse(0x08) == 0x10 drives the zero-key seed"
|
||||
);
|
||||
// Algebraic check of the raw (pre-TAB4) seed for an all-zero key.
|
||||
let key = [0u8; 5];
|
||||
let raw = ((key[4] as u32) << 17)
|
||||
| ((key[3] as u32) << 9)
|
||||
| (((key[2] as u32) << 1) + 8 - (key[2] as u32 & 7));
|
||||
assert_eq!(
|
||||
raw, 8,
|
||||
"all-zero key packs to raw LFSR0 seed 8 per the CSS formula"
|
||||
);
|
||||
}
|
||||
|
||||
/// decrypt_key never panics and always returns exactly 5 bytes across the
|
||||
/// full single-byte input space for both invert values. This is the
|
||||
/// "never panic / never truncate" property for the key-mangling core.
|
||||
///
|
||||
/// Grounding: return type is [u8; 5]; all table indexes are masked to byte
|
||||
/// range inside css_step.
|
||||
/// Mutation: (sanity) it is a type-level guarantee; the loop also exercises
|
||||
/// every TAB1 index 0..256 via p_crypted, catching an out-of-range index
|
||||
/// if a table were shortened.
|
||||
#[test]
|
||||
fn decrypt_key_total_over_byte_space() {
|
||||
for invert in [0x00u8, 0xFF] {
|
||||
for b in 0u16..256 {
|
||||
let key = [b as u8; 5];
|
||||
let crypted = [b as u8, 0, 255, b as u8, 0];
|
||||
let out = decrypt_key(invert, &key, &crypted);
|
||||
let _ = out; // length is [u8;5] by type; the call must not panic.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -381,27 +381,4 @@ mod tests {
|
||||
assert!(res.is_none());
|
||||
assert_eq!(src.reads.borrow().len(), 0);
|
||||
}
|
||||
|
||||
/// crack_key only invokes the (expensive) per-sector cracker on SCRAMBLED
|
||||
/// sectors. Clear sectors are scanned (counted) but never cracked, so a
|
||||
/// long run of clear sectors returns None after exhausting the extent
|
||||
/// rather than producing a spurious key. This pins the `is_scrambled(&buf)`
|
||||
/// gate.
|
||||
///
|
||||
/// Grounding: `if read.is_ok() && is_scrambled(&buf) { crack::... }`.
|
||||
/// Mutation: drop the `&& is_scrambled(&buf)` gate -> crack runs the
|
||||
/// 169-pattern Stevenson attack on every clear sector. Functionally this
|
||||
/// would still return None for our zeroed data, but it would be vastly
|
||||
/// slower; we cannot time it deterministically, so this test primarily
|
||||
/// documents the contract and confirms a clear scan terminates with None.
|
||||
#[test]
|
||||
fn crack_key_clear_sectors_yield_none() {
|
||||
let mut src = MockSource::new(0x00);
|
||||
let extents = [Extent {
|
||||
start_lba: 0,
|
||||
sector_count: 100,
|
||||
}];
|
||||
assert!(crack_key(&mut src, &extents).is_none());
|
||||
assert_eq!(src.reads.borrow().len(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,25 +143,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// All five tables have exactly the lengths the CSS cipher requires.
|
||||
/// TAB3 is 9-bit-indexed (the LFSR1 low word carries a 9th bit), hence
|
||||
/// 512 entries; every other table is byte-indexed (256). A truncated or
|
||||
/// padded table would index out of bounds or read stale data inside the
|
||||
/// LFSR loops.
|
||||
///
|
||||
/// Grounding: lfsr.rs indexes TAB3 with `*lfsr1_lo as usize` where
|
||||
/// `lfsr1_lo` can be up to 0x1FF (9 bits), so TAB3 MUST be >= 512 long.
|
||||
/// Mutation: change `[u8; 512]` to `[u8; 256]` (drop the second half) ->
|
||||
/// fails to compile / length assert fails.
|
||||
#[test]
|
||||
fn table_lengths_match_css_index_widths() {
|
||||
assert_eq!(TAB1.len(), 256, "TAB1 is byte-indexed");
|
||||
assert_eq!(TAB2.len(), 256, "TAB2 is byte-indexed");
|
||||
assert_eq!(TAB3.len(), 512, "TAB3 is 9-bit-indexed (LFSR1 low word)");
|
||||
assert_eq!(TAB4.len(), 256, "TAB4 is byte-indexed");
|
||||
assert_eq!(TAB5.len(), 256, "TAB5 is byte-indexed");
|
||||
}
|
||||
|
||||
/// TAB1 is a bijection on 0..256. CSS uses it as an invertible output
|
||||
/// permutation in css_DecryptKey's chained-XOR rounds; if two inputs
|
||||
/// collided, the key mangling would not be invertible.
|
||||
@@ -234,25 +215,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// TAB3's value depends only on the bottom 3 bits and the top group:
|
||||
/// within a 128-entry block (constant i>>7) every 8-aligned run repeats.
|
||||
/// Specifically TAB3[i] == TAB3[i & 0x187] (mask keeping bits 0..2 and
|
||||
/// bits 7..8). This is the structural redundancy the generating formula
|
||||
/// implies and a different cross-check on the same data.
|
||||
///
|
||||
/// Mutation: change TAB3[16] (currently a repeat of TAB3[0]=0x00) to
|
||||
/// 0x24 -> the repeat check fails.
|
||||
#[test]
|
||||
fn tab3_repeats_within_block() {
|
||||
for (i, &v) in TAB3.iter().enumerate() {
|
||||
let canonical = (i & 0b1_1000_0111) & 0x1FF;
|
||||
assert_eq!(
|
||||
v, TAB3[canonical],
|
||||
"TAB3[{i:#05x}] should repeat TAB3[{canonical:#05x}]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// TAB4 is the exact bit-reversal of each byte (CSS uses it to permute
|
||||
/// LFSR0 bytes on seed and output). TAB4[b] reverses b's 8 bits MSB<->LSB.
|
||||
/// Therefore it is also an involution: TAB4[TAB4[b]] == b.
|
||||
|
||||
Reference in New Issue
Block a user