libfreemkv 0.31.4: prune 144 vacuous tests (keep spec-grounded subset)

This commit is contained in:
Matthew Jackson
2026-06-08 07:28:55 -07:00
parent d181362460
commit f79c2a0aa9
51 changed files with 7 additions and 2142 deletions
-36
View File
@@ -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.
-46
View File
@@ -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(&sector, &plain9).is_none());
// 10 bytes: passes the guard and runs to completion without panic.
let _ = recover_title_key(&sector, &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(&sector, &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.
-54
View File
@@ -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
-23
View File
@@ -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);
}
}
-38
View File
@@ -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.