AACS pipeline reshape + TrueHD metadata + central consts + clippy/fmt clean

- AACS: delete in-lib keydb parser (Step 3); boil-down primitives
  (mk_from_dk/vuk_from_mk/uk_from_vuk) + newtypes; KeySource->get_uk(ctx)+
  ResolveCtx; Unlocker->unlock()->Result<Vid,UnlockError> + AacsCertUnlocker;
  OEM bus-key gate (AacsBusKeyUnavailable); structured ResolutionTrace (Step 4).
- TrueHD: sample-rate from major-sync, Atmos label, 44.1k AU duration.
- consts: central media/format constants module; 17 duplicate const-defs
  centralized (sector/TS-packet/source-packet); mpls stream-entry + category
  codes named.
- clippy --all-targets -D warnings clean (1.86); fmt clean; 2199 lib tests.
This commit is contained in:
Matthew Jackson
2026-06-26 12:19:24 -07:00
parent 05729f5dfe
commit decb87a250
48 changed files with 2208 additions and 2031 deletions
+107 -307
View File
@@ -1,7 +1,7 @@
//! AACS key resolution — VUK derivation, MKB processing, disc hash, unit key parsing.
use super::decrypt::aes_ecb_decrypt;
use super::keydb::DeviceKey;
use super::types::DeviceKey;
// ── AACS version ────────────────────────────────────────────────────────────
@@ -1529,16 +1529,10 @@ fn match_keydb_unit_keys(
#[cfg(test)]
mod tests {
use super::super::decrypt::{ALIGNED_UNIT_LEN, aes_ecb_encrypt};
use super::super::keydb::{DiscEntry, KeyDb};
use super::super::provider::SuppliedKey;
use super::super::types::DiscEntry;
use super::*;
/// Get KEYDB path from KEYDB_PATH environment variable. Returns None if not set or not found.
fn keydb_path() -> Option<std::path::PathBuf> {
let path = std::path::PathBuf::from(std::env::var("KEYDB_PATH").ok()?);
if path.exists() { Some(path) } else { None }
}
/// Finding #5 regression: parse_unit_key_ro must REJECT a Unit_Key_RO.inf
/// whose declared `num_unit_keys` exceeds the keys actually present in the
/// buffer, instead of silently returning a short list. A truncated list
@@ -1604,135 +1598,24 @@ mod tests {
let _ = derive_media_key_from_dk(&mkb, &[dk]);
}
#[test]
fn test_vuk_derivation() {
// Pick any UHD entry with a known MK, VID, and VUK from KEYDB.
// VUK = AES-DEC(MK, VID) XOR VID
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
// Find a disc with both MK, disc_id, and VUK so we can verify derivation
let entry = db
.disc_entries
.values()
.find(|e| e.media_key.is_some() && e.disc_id.is_some() && e.vuk.is_some())
.expect("No disc with MK + VID + VUK");
let mk = entry.media_key.unwrap();
let vid = entry.disc_id.unwrap();
let expected_vuk = entry.vuk.unwrap();
let derived = derive_vuk(&mk, &vid);
assert_eq!(
derived, expected_vuk,
"VUK derivation failed for disc: {} (hash {})",
entry.title, entry.disc_hash
);
eprintln!("VUK derivation verified for: {}", entry.title);
}
#[test]
fn test_decrypt_unit_key_from_vuk() {
// Test the full chain: VUK → decrypt encrypted unit key → unit key
// Use a known disc from KEYDB that has both VUK and unit keys
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
// Find a disc with VUK and unit keys
let entry = db
.disc_entries
.values()
.find(|e| e.vuk.is_some() && !e.unit_keys.is_empty())
.expect("No disc with VUK + unit keys");
eprintln!(
"Testing unit key decrypt for: {} ({})",
entry.title, entry.disc_hash
);
eprintln!(" VUK: {:02X?}", entry.vuk.unwrap());
for (num, key) in &entry.unit_keys {
eprintln!(" Unit key {}: {:02X?}", num, key);
}
// The unit keys in KEYDB are already decrypted — we can verify the chain
// by encrypting with VUK and then decrypting
let vuk = entry.vuk.unwrap();
for (num, expected_uk) in &entry.unit_keys {
let encrypted = aes_ecb_encrypt(&vuk, expected_uk);
// VUK → encrypted unit key → unit key roundtrip. The keydb-sourced
// variant of this test (which scanned a real KEYDB for VUK + unit
// keys) moved to freemkv-keysources; this rebuilt version exercises
// the same AES-G primitive (decrypt_unit_key ∘ aes_ecb_encrypt under a
// VUK) with directly-constructed material, so it needs no parser and
// keeps the crypto covered in libfreemkv. `aes_ecb_encrypt` is
// pub(crate), reachable here but not from keysources — the reason this
// half stays.
use super::super::decrypt::aes_ecb_encrypt;
let vuk = [0x5Au8; 16];
// A few representative "decrypted" unit keys.
for expected_uk in [[0x11u8; 16], [0x22u8; 16], [0xCDu8; 16]] {
let encrypted = aes_ecb_encrypt(&vuk, &expected_uk);
let decrypted = decrypt_unit_key(&vuk, &encrypted);
assert_eq!(
&decrypted, expected_uk,
"Unit key {} roundtrip failed for {}",
num, entry.title
);
assert_eq!(decrypted, expected_uk, "unit key roundtrip under VUK");
}
eprintln!(" All {} unit key roundtrips passed", entry.unit_keys.len());
}
#[test]
fn test_decrypt_real_unit() {
// Try decrypting a real encrypted aligned unit from a UHD sample.
// This disc is AACS 2.0 (BEE) so unit key alone won't work —
// we need bus decryption first. But this verifies the pipeline.
// Path comes from ENCRYPTED_UNIT_PATH (same env-driven pattern as the
// KEYDB_PATH / MKB_SAMPLE_DIR fixtures); no-ops in CI when unset.
let unit_path = match std::env::var("ENCRYPTED_UNIT_PATH").ok() {
Some(p) => std::path::PathBuf::from(p),
None => return,
};
if !unit_path.exists() {
return;
}
let original = std::fs::read(&unit_path).unwrap();
assert_eq!(original.len(), ALIGNED_UNIT_LEN);
assert!(
super::super::decrypt::is_aacs_scrambled(&original),
"Unit should be encrypted"
);
let kp = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&kp).unwrap();
// Candidate entries: any UHD entry that carries unit keys.
let candidate_entries: Vec<&DiscEntry> = db
.disc_entries
.values()
.filter(|e| !e.unit_keys.is_empty())
.collect();
eprintln!("Found {} entries with unit keys", candidate_entries.len());
// Try each entry's unit keys
for entry in &candidate_entries {
let keys: Vec<[u8; 16]> = entry.unit_keys.iter().map(|(_, k)| *k).collect();
let mut unit = original.clone();
if let Some(res) = super::super::decrypt::decrypt_unit_try_keys(&mut unit, &keys) {
eprintln!(
"SUCCESS: Decrypted with entry {} ({res:?})",
entry.disc_hash
);
// Count TS sync bytes
let ts = (0..32).filter(|&i| unit[4 + i * 192] == 0x47).count();
eprintln!(" TS sync bytes: {}/32", ts);
return;
}
}
// Expected: none work because this is AACS 2.0 and needs bus decryption first
eprintln!("No unit key worked (expected for AACS 2.0 BEE disc — needs read_data_key)");
}
#[test]
@@ -1834,7 +1717,7 @@ mod tests {
mkb.extend_from_slice(&[0xAB; 16]);
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
let records_len = mkb.len();
mkb.extend(std::iter::repeat(0u8).take(128 * 1024)); // padding
mkb.extend(std::iter::repeat_n(0u8, 128 * 1024)); // padding
assert_eq!(mkb_content_len(&mkb), records_len);
// No padding → returns the full length.
assert_eq!(mkb_content_len(&mkb[..records_len]), records_len);
@@ -1869,7 +1752,7 @@ mod tests {
mkb.extend_from_slice(&[0xAB; 16]);
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
let records_len = mkb.len();
mkb.extend(std::iter::repeat(0u8).take(1024));
mkb.extend(std::iter::repeat_n(0u8, 1024));
assert_eq!(
trim_mkb(mkb).len(),
records_len,
@@ -2197,44 +2080,6 @@ mod tests {
assert_eq!(mkb_find_mk_dv(&mkb), Some(expected));
}
#[test]
fn test_resolve_keys_vuk_path() {
// Test the full resolve chain using VUK path
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
// Find any BD entry that carries a VUK and unit keys, then exercise
// the lookup-by-hash + VUK-derivation chain against it.
let entry = db
.disc_entries
.values()
.find(|e| e.vuk.is_some() && !e.unit_keys.is_empty() && e.disc_id.is_some());
if entry.is_none() {
return;
}
let entry = entry.unwrap();
let vuk = entry.vuk.unwrap();
let vid = entry.disc_id.unwrap();
let hash_hex = format!("0x{}", entry.disc_hash.trim_start_matches("0x"));
// We need the actual Unit_Key_RO.inf from the disc to compute disc hash.
// Since we don't have it, we can at least test that the KEYDB lookup
// works with a known hash.
let found = db.find_disc(&hash_hex);
assert!(found.is_some());
assert_eq!(found.unwrap().vuk, Some(vuk));
// Verify VUK derivation if we have MK + VID
if let Some(mk) = entry.media_key {
let derived = derive_vuk(&mk, &vid);
assert_eq!(derived, vuk, "VUK derivation mismatch");
eprintln!("VUK derivation verified");
}
}
/// Build a minimal Unit_Key_RO.inf with `num_unit_keys = 1`. The
/// disc hash won't be in any synthetic keydb so path 1 misses,
/// which lets us isolate the path-2/3/4 short-circuit behavior.
@@ -2262,23 +2107,18 @@ mod tests {
let uk_ro = minimal_unit_key_ro();
let zero_vid = [0u8; 16];
// Populate keydb with a non-matching VID entry (path 2 would
// miss anyway) plus dummy processing/device keys (paths 3/4
// would also miss, but the short-circuit means they're never
// attempted).
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
"0xDEADBEEF".to_string(),
DiscEntry {
disc_hash: "0xDEADBEEF".to_string(),
title: "fixture".to_string(),
media_key: Some([0x11u8; 16]),
disc_id: Some([0x22u8; 16]),
vuk: None,
unit_keys: Vec::new(),
},
);
keydb.processing_keys.push([0u8; 16]);
// A provider carrying a dummy processing key but NO disc entry that
// matches this disc. `disc_entry: None` preserves the negative-miss
// the test asserts: with VID=0, paths 1/2/3 are skipped and the
// path-4/5 hash lookup must MISS (a SuppliedKey returns its
// disc_entry unconditionally, so the planted entry would WRONGLY hit
// path 4 — None keeps the miss).
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: vec![[0u8; 16]],
media_keys: Vec::new(),
disc_entry: None,
};
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
@@ -2307,19 +2147,20 @@ mod tests {
// keyed lowercase too, so we have to lowercase here.
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let mut keydb = KeyDb::empty();
let known_vuk = [0xABu8; 16];
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: hash_hex,
title: "fixture".to_string(),
media_key: None,
disc_id: None,
vuk: Some(known_vuk),
unit_keys: Vec::new(),
},
);
}),
};
let vid = [0u8; 16];
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
@@ -2349,18 +2190,19 @@ mod tests {
// `minimal_unit_key_ro` declares CPS unit 1; supply a matching
// pre-decrypted unit key in the KEYDB entry.
let known_uk = [0xCDu8; 16];
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: hash_hex,
title: "fixture".to_string(),
media_key: None,
disc_id: None,
vuk: None,
unit_keys: vec![(1, known_uk)],
},
);
}),
};
let vid = [0u8; 16];
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
@@ -2377,7 +2219,6 @@ mod tests {
assert_eq!(resolved.key_source, 5);
assert_eq!(resolved.unit_keys, vec![(1, known_uk)]);
}
#[test]
fn resolve_keys_path5_rejects_partial_unit_key_coverage() {
// If the disc declares a CPS unit that's not in the KEYDB
@@ -2388,20 +2229,20 @@ mod tests {
let uk_ro = minimal_unit_key_ro();
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
// KEYDB has a key for CPS unit 99, but the disc declares unit 1.
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: hash_hex,
title: "fixture".to_string(),
media_key: None,
disc_id: None,
vuk: None,
unit_keys: vec![(99, [0xEEu8; 16])],
},
);
}),
};
let vid = [0u8; 16];
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
@@ -2417,17 +2258,14 @@ mod tests {
"partial CPS-unit coverage must not produce a half-decrypted result"
);
}
#[test]
fn resolve_keys_path2_5_mk_pool_brute_resolves_unkeyed_disc() {
// The keyless-disc case: this disc's own hash/VID are NOT in keydb, but its
// Media Key IS — filed under a sibling disc that shares its MKB. Path
// 2.5 must km_verifies that MK against the MKB and resolve.
use super::super::decrypt::aes_ecb_encrypt as enc;
let km = [0x11u8; 16];
let vid = [0x22u8; 16];
// MKB: 0x10 type/version + 0x86 verify record whose mk_dv decrypts under
// km to the AACS verify magic, so km_verifies(mkb, km) == true.
let mut vd = [0u8; 16];
@@ -2439,26 +2277,21 @@ mod tests {
probe::km_verifies(&mkb, &km),
"fixture: km must verify the MKB"
);
// This disc's inf (its hash will NOT be in keydb).
let uk_ro = minimal_unit_key_ro();
// keydb: a SIBLING disc carries our km, keyed by the sibling's own
// hash + VID (neither matches THIS disc) — so only the MK-pool brute
// (km_verifies) can find it.
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
"0xsibling".to_string(),
DiscEntry {
disc_hash: "0xsibling".to_string(),
title: "sibling".to_string(),
media_key: Some(km),
disc_id: Some([0x99u8; 16]),
vuk: None,
unit_keys: Vec::new(),
},
);
// The sibling's MK is lifted directly into the MK pool: a KeyDb
// aggregated per-disc media_keys into media_keys(), but SuppliedKey
// does NOT harvest its disc_entry's media_key — it has an explicit
// media_keys field. `disc_entry: None` preserves the miss on this
// disc's own hash/VID (the sibling matches neither), so ONLY the
// MK-pool brute (km_verifies) can resolve it — exactly the path under
// test.
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: vec![km],
disc_entry: None,
};
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
@@ -2479,7 +2312,6 @@ mod tests {
"VUK must derive from the verified Km + this disc's VID"
);
}
#[test]
fn test_content_cert_parse() {
// AACS 1.0 cert
@@ -2489,7 +2321,6 @@ mod tests {
let cc = parse_content_cert(&data).unwrap();
assert_eq!(cc.version, AacsVersion::V10);
assert!(!cc.bus_encryption);
// AACS 2.0 with bus encryption
data[0] = 0x01; // AACS 2.0
data[1] = 0x01; // bus encryption enabled
@@ -2497,13 +2328,10 @@ mod tests {
assert_eq!(cc.version, AacsVersion::V20);
assert!(cc.bus_encryption);
}
// ════════════════════════════════════════════════════════════════════
// Hardening additions
// ════════════════════════════════════════════════════════════════════
// ── VUK derivation: spec relation VUK = AES-D(MK, VID) XOR VID ─────────
#[test]
fn derive_vuk_matches_spec_relation_explicitly() {
// Independently compute AES-ECB-D(mk, vid) XOR vid and confirm
@@ -2524,7 +2352,6 @@ mod tests {
}
assert_eq!(derive_vuk(&mk, &vid), expected);
}
#[test]
fn decrypt_unit_key_is_plain_aes_ecb_decrypt_under_vuk() {
// The encrypted unit key in Unit_Key_RO.inf is AES-ECB-E(VUK, uk);
@@ -2536,9 +2363,7 @@ mod tests {
let enc_uk = enc(&vuk, &uk);
assert_eq!(decrypt_unit_key(&vuk, &enc_uk), uk);
}
// ── Unit_Key_RO stride: 48 (V10) vs 64 (V20/V21) ──────────────────────
/// Build a Unit_Key_RO.inf carrying `num_uk` keys at a given stride,
/// where key `i` is filled with byte `0x10 + i`. uk_pos = 0x60.
fn build_unit_key_ro(num_uk: usize, stride: usize) -> Vec<u8> {
@@ -2561,7 +2386,6 @@ mod tests {
}
data
}
#[test]
fn stride_v10_is_48_v20_is_64_and_picks_distinct_keys() {
// AACS 1.0 stride = 48, AACS 2.0/2.1 stride = 64 (keys.rs:30-35).
@@ -2574,7 +2398,6 @@ mod tests {
assert_eq!(v20.encrypted_keys.len(), 2);
assert_eq!(v20.encrypted_keys[0].1, [0x10; 16]);
assert_eq!(v20.encrypted_keys[1].1, [0x11; 16]);
// Same buffer, V10 stride: key 1 still lands at uk_pos+48, but key 2
// is read at +48 (not +64) so it is NOT the planted 0x11 block.
let v10 = parse_unit_key_ro(&data, AacsVersion::V10).unwrap();
@@ -2584,7 +2407,6 @@ mod tests {
"48-byte stride must read different bytes than 64-byte stride"
);
}
#[test]
fn v21_uses_same_64_byte_stride_as_v20() {
// V21 shares V20's 64-byte stride (the enum match groups V20|V21).
@@ -2594,15 +2416,12 @@ mod tests {
assert_eq!(v20.encrypted_keys, v21.encrypted_keys);
assert_eq!(v21.version, AacsVersion::V21);
}
// ── parse_unit_key_ro: early returns / boundaries ──────────────────────
#[test]
fn parse_unit_key_ro_rejects_too_short_header() {
// < 20 bytes → None (header fields at 16-18 would index OOB).
assert!(parse_unit_key_ro(&[0u8; 19], AacsVersion::V10).is_none());
}
#[test]
fn parse_unit_key_ro_rejects_uk_pos_past_end() {
// uk_pos points past the buffer → the `uk_pos + 2 > len` guard
@@ -2611,7 +2430,6 @@ mod tests {
data[0..4].copy_from_slice(&1000u32.to_be_bytes()); // uk_pos = 1000
assert!(parse_unit_key_ro(&data, AacsVersion::V10).is_none());
}
#[test]
fn parse_unit_key_ro_zero_keys_returns_empty_set() {
// num_unit_keys == 0 → a valid file with no encrypted keys (early
@@ -2625,7 +2443,6 @@ mod tests {
assert!(parsed.encrypted_keys.is_empty());
assert_eq!(parsed.app_type, 1);
}
#[test]
fn parse_unit_key_ro_truncated_key_region_returns_none() {
// keys_start + 16 > len → None (the first key can't fit).
@@ -2635,7 +2452,6 @@ mod tests {
data[uk_pos + 1] = 1; // 1 key declared
assert!(parse_unit_key_ro(&data, AacsVersion::V10).is_none());
}
#[test]
fn parse_unit_key_ro_rejects_when_keys_run_off_end() {
// Finding #5: 3 keys declared but the buffer holds only 2 strides plus
@@ -2656,7 +2472,6 @@ mod tests {
"a buffer declaring more keys than it contains must be rejected"
);
}
#[test]
fn parse_unit_key_ro_app_type_and_skb_flag() {
// app_type at [16], num_bdmv_dir at [17], use_skb_mkb = bit 7 of [18].
@@ -2673,7 +2488,6 @@ mod tests {
let p2 = parse_unit_key_ro(&data, AacsVersion::V10).unwrap();
assert!(!p2.use_skb_mkb);
}
#[test]
fn parse_unit_key_ro_cps_unit_numbers_are_1_based() {
// The disc's CPS unit numbers are emitted as (i+1) — keys.rs:162.
@@ -2684,7 +2498,6 @@ mod tests {
vec![1, 2, 3]
);
}
#[test]
fn parse_unit_key_ro_title_cps_mapping_first_play_top_menu_then_titles() {
// [20..22] first_play, [22..24] top_menu, [24..26] num_titles, then
@@ -2698,9 +2511,7 @@ mod tests {
let p = parse_unit_key_ro(&data, AacsVersion::V20).unwrap();
assert_eq!(p.title_cps_unit, vec![7, 9, 3, 4]);
}
// ── MKB record framing: rec_len is BE24 incl. 4-byte header ────────────
#[test]
fn mkb_version_uses_be24_length_and_reads_offset_8() {
// Type 0x10, BE24 length 0x0C (12). Body starts at pos+4: Type field
@@ -2712,7 +2523,6 @@ mod tests {
// version = 0x01020304.
assert_eq!(mkb_version(&mkb), Some(0x0102_0304));
}
#[test]
fn mkb_type_category_c_20_is_uhd() {
// Type 0x10 record, BE24 length 0x0C (12). MKBType field (body
@@ -2729,7 +2539,6 @@ mod tests {
assert_eq!(MkbType::from_raw(MKB_21_CATEGORY_C), MkbType::CategoryC21);
assert_eq!(MkbType::CategoryC21.generation(), AacsVersion::V21);
}
#[test]
fn mkb_type_prerecorded_is_bluray_v10() {
// Type 0x10 record with MKB_TYPE_4_PRERECORDED (0x00041003) — a
@@ -2742,7 +2551,6 @@ mod tests {
assert!(!MkbType::Prerecorded.is_uhd());
assert_eq!(MkbType::Prerecorded.generation(), AacsVersion::V10);
}
#[test]
fn mkb_type_none_when_no_0x10_record() {
// A buffer whose only record is a 0x81 (verify-media-key) record and
@@ -2752,7 +2560,6 @@ mod tests {
assert_eq!(mkb_type(&mkb), None);
assert_eq!(mkb_is_uhd(&mkb), None);
}
#[test]
fn mkb_find_mk_dv_skips_short_verify_record() {
// A 0x81 record with rec_len < 20 carries no full mk_dv; the finder
@@ -2765,7 +2572,6 @@ mod tests {
mkb.extend_from_slice(&[0x00; 4]);
assert_eq!(mkb_find_mk_dv(&mkb), Some(expected));
}
#[test]
fn mkb_find_mk_dv_stops_on_overrun_length() {
// A rec_len that runs past the buffer ends the walk (break), so no
@@ -2773,7 +2579,6 @@ mod tests {
let mkb = [0x81, 0x00, 0xFF, 0xFF, 0x00, 0x00]; // claims 65535 bytes
assert_eq!(mkb_find_mk_dv(&mkb), None);
}
#[test]
fn mkb_find_mk_dv_stops_on_zero_length_record() {
// rec_len < 4 (here 0) breaks the walk — guards against an infinite
@@ -2781,9 +2586,7 @@ mod tests {
let mkb = [0x81, 0x00, 0x00, 0x00, 0x99];
assert_eq!(mkb_find_mk_dv(&mkb), None);
}
// ── mkb_content_len / trim_mkb ─────────────────────────────────────────
#[test]
fn mkb_content_len_stops_at_zero_type_padding_byte() {
// A type==0 byte marks the start of padding (records done). Two real
@@ -2794,14 +2597,12 @@ mod tests {
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x08]); // padding starts (type 0)
assert_eq!(mkb_content_len(&mkb), content);
}
#[test]
fn mkb_content_len_returns_full_len_when_no_padding() {
let mut mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x08, 9, 9, 9, 9]);
assert_eq!(mkb_content_len(&mkb), mkb.len());
}
#[test]
fn trim_mkb_leaves_exactly_sized_buffer_untouched() {
// n == mkb.len() (no padding) → the `n < mkb.len()` guard is false,
@@ -2809,15 +2610,12 @@ mod tests {
let mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
assert_eq!(trim_mkb(mkb.clone()), mkb);
}
// ── Content Certificate parsing ────────────────────────────────────────
#[test]
fn parse_content_cert_rejects_short_buffer() {
// < 8 bytes → None (cc_id slice [2..8] would index OOB).
assert!(parse_content_cert(&[0x00; 7]).is_none());
}
#[test]
fn parse_content_cert_extracts_cc_id_and_nonzero_type_is_v20() {
// [0]=type, [1]=bus-enc bit0, [2..8]=cc_id. Any non-0x00 type → V20.
@@ -2830,7 +2628,6 @@ mod tests {
assert_eq!(cc.cc_id, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
assert!(!cc.bus_encryption);
}
#[test]
fn parse_content_cert_bus_encryption_only_reads_bit0() {
// bus_encryption = (data[1] & 0x01) != 0. A high bit set (0x02) with
@@ -2841,9 +2638,7 @@ mod tests {
data[1] = 0x03; // bit 0 set
assert!(parse_content_cert(&data).unwrap().bus_encryption);
}
// ── resolve: version → stride wiring + V21 upgrade on variant MKB ──────
#[test]
fn resolve_keys_v2_upgrades_to_v21_on_variant_mkb() {
// resolve_keys_v2 parses with the V20 64-byte stride but upgrades the
@@ -2852,19 +2647,19 @@ mod tests {
let uk_ro = build_unit_key_ro(1, 64);
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: hash_hex,
title: "fixture".to_string(),
media_key: None,
disc_id: None,
vuk: Some([0x5Au8; 16]),
unit_keys: Vec::new(),
},
);
}),
};
// MKB with a 0x83 variant record makes is_variant_mkb true.
let mut mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
@@ -2893,18 +2688,19 @@ mod tests {
let uk_ro = build_unit_key_ro(1, 64);
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: hash_hex,
title: "f".to_string(),
media_key: None,
disc_id: None,
vuk: Some([0x5Au8; 16]),
unit_keys: Vec::new(),
},
);
}),
};
let mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
@@ -2923,18 +2719,19 @@ mod tests {
let uk_ro = build_unit_key_ro(1, 48);
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: hash_hex,
title: "f".to_string(),
media_key: None,
disc_id: None,
vuk: Some([1u8; 16]),
unit_keys: Vec::new(),
},
);
}),
};
// Content cert: AACS2 + bus encryption enabled.
let mut cc = vec![0u8; 8];
cc[0] = 0x01;
@@ -2960,18 +2757,19 @@ mod tests {
let hash = disc_hash(&data);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let vuk = [0x77u8; 16];
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: hash_hex,
title: "f".to_string(),
media_key: None,
disc_id: None,
vuk: Some(vuk),
unit_keys: Vec::new(),
},
);
}),
};
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &data,
@@ -2997,18 +2795,19 @@ mod tests {
let uk_ro = minimal_unit_key_ro();
let vid = [0x42u8; 16];
let mk = [0x24u8; 16];
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
"0xnotthishash".to_string(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: "0xnotthishash".to_string(),
title: "sibling".to_string(),
media_key: Some(mk),
disc_id: Some(vid),
vuk: None,
unit_keys: Vec::new(),
},
);
}),
};
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
@@ -3282,18 +3081,19 @@ mod tests {
let uk_ro = build_unit_key_ro(1, 64);
let hash_hex = disc_hash_hex(&disc_hash(&uk_ro)).to_lowercase();
let vuk = [0x77u8; 16];
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
let keydb = SuppliedKey {
device_keys: Vec::new(),
processing_keys: Vec::new(),
media_keys: Vec::new(),
disc_entry: Some(DiscEntry {
disc_hash: hash_hex,
title: "f".to_string(),
media_key: None,
disc_id: None,
vuk: Some(vuk),
unit_keys: Vec::new(),
},
);
}),
};
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,