diff --git a/src/aacs/keys.rs b/src/aacs/keys.rs index dce080d..3ef7000 100644 --- a/src/aacs/keys.rs +++ b/src/aacs/keys.rs @@ -611,7 +611,10 @@ const AESG3_SEED: [u8; 16] = [ /// AACS-G3: derive a subkey from a parent key. /// seed[15] += inc, then AES-DEC(key, seed) XOR seed. -fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] { +/// +/// Shared with [`super::variants`] (its variant chain runs the same SD +/// tree); a single definition keeps the two walks byte-identical. +pub(super) fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] { let mut seed = AESG3_SEED; seed[15] = seed[15].wrapping_add(inc); let mut out = aes_ecb_decrypt(key, &seed); @@ -621,8 +624,8 @@ fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] { out } -/// Compute v_mask from a UV value. -fn calc_v_mask(uv: u32) -> u32 { +/// Compute v_mask from a UV value. Shared with [`super::variants`]. +pub(super) fn calc_v_mask(uv: u32) -> u32 { let mut v_mask: u32 = 0xFFFF_FFFF; while (uv & !v_mask) == 0 && v_mask != 0 { v_mask <<= 1; @@ -631,7 +634,13 @@ fn calc_v_mask(uv: u32) -> u32 { } /// Derive processing key from device key using subset-difference tree traversal. -fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) -> [u8; 16] { +/// Shared with [`super::variants`]. +pub(super) fn calc_pk_from_dk( + dk: &[u8; 16], + uv: u32, + v_mask: u32, + dev_key_v_mask: u32, +) -> [u8; 16] { // Initial derivation: left_child = aesg3(dk, 0), pk = aesg3(dk, 1), right_child = aesg3(dk, 2) let mut left_child = aesg3(dk, 0); let mut pk = aesg3(dk, 1); @@ -675,7 +684,26 @@ fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) -> } /// Derive Media Key from MKB using device keys (subset-difference tree). +/// +/// Thin wrapper over [`derive_media_key_and_pk_from_dk`] that drops the +/// intermediate Processing Key. Callers that need the PK lineage (e.g. +/// the key service banking DK·PK·MK) should call the `_and_pk_` form. pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option<[u8; 16]> { + derive_media_key_and_pk_from_dk(mkb, device_keys).map(|(mk, _pk)| mk) +} + +/// Derive both the Media Key and the intermediate Processing Key from an +/// MKB using device keys (subset-difference tree). +/// +/// Identical walk to [`derive_media_key_from_dk`]; this form additionally +/// returns the Processing Key `Kp` derived at the matching subset-difference +/// node — the value `calc_pk_from_dk` produces immediately before it +/// validates into the Media Key. Returns `Some((mk, pk))` for the first DK +/// that walks a uv slot whose Processing Key validates against the MKB. +pub fn derive_media_key_and_pk_from_dk( + mkb: &[u8], + device_keys: &[DeviceKey], +) -> Option<([u8; 16], [u8; 16])> { let mk_dv = mkb_find_mk_dv(mkb)?; let uvs = mkb_find_subdiff_records(mkb)?; let cvalues = mkb_find_cvalues(mkb)?; @@ -735,7 +763,7 @@ pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option if let Some(mk) = validate_processing_key(&pk, cv, &uvs[1 + uvs_idx * 5..], &mk_dv) { - return Some(mk); + return Some((mk, pk)); } } } @@ -2741,4 +2769,85 @@ mod tests { let mkb = [0x05, 0x00, 0x00, 0x04]; // type 0x05, no body assert!(probe::mkb_record_body(&mkb, 0x05).is_none()); } + + #[test] + fn derive_media_key_and_pk_from_dk_returns_intermediate_pk() { + // Regression: a classical DK boil must yield the intermediate + // Processing Key, not just the Media Key. The key service banks the + // PK lineage (DK·PK·MK·VUK·UK); before the `_and_pk_` form existed it + // recovered the MK here but lost the PK silently. + // + // Build a minimal classical MKB (no 0x82/0x83) with: + // - 0x04 Subset-Difference: u_mask_shift=3, uv=0x00000002 + // - 0x05 cvalues: one cvalue C planted so AES-D(Kp, C) XOR uv == mk + // - 0x86 Verify Media Key: mk_dv = AES-E(mk, magic || pad) + // and a DK with node=4, uv=2, u_mask_shift=3 so dev_key_v_mask == + // v_mask: the calc_pk_from_dk loop is a no-op and Kp == aesg3(dk, 1). + use super::super::decrypt::aes_ecb_encrypt as enc; + + let dk_bytes: [u8; 16] = [ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, + 0xFF, 0x00, + ]; + // Expected Processing Key for the no-op walk. + let expected_pk = aesg3(&dk_bytes, 1); + + // Plant a known Media Key. + let mk: [u8; 16] = [ + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, + 0xAE, 0xAF, + ]; + // uv (big-endian) = 0x00000002; validate XORs uv into mk[12..16]. + let uv_bytes: [u8; 4] = [0x00, 0x00, 0x00, 0x02]; + + // cvalue C = AES-E(Kp, mk_raw) where mk_raw = mk with the uv XOR + // pre-undone, so the validate step XORs uv back in and recovers mk. + let mut mk_raw = mk; + for a in 0..4 { + mk_raw[12 + a] ^= uv_bytes[a]; + } + let cvalue = enc(&expected_pk, &mk_raw); + + // mk_dv = AES-E(mk, magic || pad); validate decrypts it under mk and + // checks the leading 8 bytes against the verify magic. + let mut plaintext_vd = [0u8; 16]; + plaintext_vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + plaintext_vd[8..].copy_from_slice(&[0x11; 8]); + let mk_dv = enc(&mk, &plaintext_vd); + + // Assemble the MKB. Type/Version (0x10) header first. + let mut mkb = vec![ + 0x10, 0x00, 0x00, 0x0C, 0x48, 0x14, 0x10, 0x03, 0x00, 0x00, 0x00, 0x4D, + ]; + // 0x04 Subset-Difference: body = u_mask_shift(0x03) || uv(4 bytes). + mkb.extend_from_slice(&[0x04, 0x00, 0x00, 0x09]); + mkb.extend_from_slice(&[0x03]); + mkb.extend_from_slice(&uv_bytes); + // 0x05 cvalues: one 16-byte cvalue (mkb_find_cvalues prefers 0x05). + mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x14]); + mkb.extend_from_slice(&cvalue); + // 0x86 Verify Media Key: mk_dv. + mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]); + mkb.extend_from_slice(&mk_dv); + + let dk = DeviceKey { + key: dk_bytes, + node: 4, + uv: 2, + u_mask_shift: 3, + }; + + // The new `_and_pk_` form returns BOTH the MK and the intermediate PK. + let dks = [dk]; + let (got_mk, got_pk) = derive_media_key_and_pk_from_dk(&mkb, &dks) + .expect("classical DK boil must derive (mk, pk)"); + assert_eq!(got_mk, mk, "recovered Media Key must match the planted MK"); + assert_eq!( + got_pk, expected_pk, + "returned Processing Key must equal aesg3(dk, 1) for the no-op walk" + ); + + // And the thin wrapper must still return just the MK. + assert_eq!(derive_media_key_from_dk(&mkb, &dks), Some(mk)); + } } diff --git a/src/aacs/mod.rs b/src/aacs/mod.rs index 1098dfc..9b079a6 100644 --- a/src/aacs/mod.rs +++ b/src/aacs/mod.rs @@ -31,10 +31,10 @@ pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb}; pub use keys::probe; pub use keys::{ AacsVersion, ContentCert, ResolveContext, ResolvedKeys, UnitKeyFile, decrypt_unit_key, - derive_media_key_from_dk, derive_media_key_from_pk, derive_media_key_from_pk_walked, - derive_vuk, disc_hash, disc_hash_hex, mkb_content_len, mkb_version, parse_content_cert, - parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1, resolve_keys_v2, resolve_keys_v21, - trim_mkb, + derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk, + derive_media_key_from_pk_walked, derive_vuk, disc_hash, disc_hash_hex, mkb_content_len, + mkb_version, parse_content_cert, parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1, + resolve_keys_v2, resolve_keys_v21, trim_mkb, }; pub use provider::KeyProvider; pub use variants::{ diff --git a/src/aacs/variants.rs b/src/aacs/variants.rs index cd1d622..4d2f3ed 100644 --- a/src/aacs/variants.rs +++ b/src/aacs/variants.rs @@ -177,66 +177,11 @@ fn aes_g(x1: &[u8; 16], x2: &[u8; 16]) -> [u8; 16] { // ── Subset-difference walk that exposes (Kp, uv) ────────────────────────── -/// AES-G3 seed register initial value. -const AESG3_SEED: [u8; 16] = [ - 0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9, -]; - -/// AES-G3 single step: AES-G against the seed register at offset `inc`. -fn aesg3_step(key: &[u8; 16], inc: u8) -> [u8; 16] { - let mut seed = AESG3_SEED; - seed[15] = seed[15].wrapping_add(inc); - aes_g(key, &seed) -} - -fn calc_v_mask(uv: u32) -> u32 { - let mut v_mask: u32 = 0xFFFF_FFFF; - while (uv & !v_mask) == 0 && v_mask != 0 { - v_mask <<= 1; - } - v_mask -} - -fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) -> [u8; 16] { - let mut left_child = aesg3_step(dk, 0); - let mut pk = aesg3_step(dk, 1); - let mut right_child = aesg3_step(dk, 2); - let mut current_v_mask = dev_key_v_mask; - - // Bound the walk to the 32-level depth of a u32 subset-difference tree. - // `current_v_mask` advances via an arithmetic `>> 1` which sign-extends, so - // a disc-supplied v_mask coarser than dev_key_v_mask would otherwise drive - // current_v_mask up to 0xFFFF_FFFF and spin forever — a crafted MKB must - // not hang the rip thread (this runs before the KCD placeholder gate). - let mut steps = 0u32; - while current_v_mask != v_mask { - if steps >= 32 { - break; - } - steps += 1; - let mut bit_pos: i32 = -1; - for i in (0..32).rev() { - if (current_v_mask & (1u32 << i)) == 0 { - bit_pos = i; - break; - } - } - - let curr_key = if bit_pos < 0 || (uv & (1u32 << bit_pos as u32)) == 0 { - left_child - } else { - right_child - }; - - left_child = aesg3_step(&curr_key, 0); - pk = aesg3_step(&curr_key, 1); - right_child = aesg3_step(&curr_key, 2); - - current_v_mask = ((current_v_mask as i32) >> 1) as u32; - } - - pk -} +// `calc_v_mask` and `calc_pk_from_dk` (and the AES-G3 seed step they ride +// on) are shared with the classical walk in [`super::keys`] — a single +// definition keeps the variant SD tree byte-identical to the classical one. +// (`aesg3` itself is imported separately in the test module.) +use super::keys::{calc_pk_from_dk, calc_v_mask}; /// Outcome of a subset-difference walk against an MKB. Carries the /// processing key and the matching `uv` slot — both needed as inputs @@ -271,6 +216,27 @@ fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> { /// Walk an MKB and return the first `(Kp, uv, cvalue)` that /// `device_keys` covers. Returns `None` if no DK walks any uv. +/// +/// This is the AACS-2.1 **variant** walk; the classical walk lives in +/// [`super::keys::derive_media_key_and_pk_from_dk`]. The two are kept +/// separate on purpose and select MKB records in DELIBERATELY different +/// order: +/// +/// - cvalues: this variant walk tries record `0x07`-then-`0x05`; the +/// classical walk tries `0x05`-then-`0x07`. On a variant MKB the +/// small `0x07` Explicit-Subset-Difference record carries the +/// cvalue the Precursor chain consumes, whereas a classical UHD MKB +/// keeps its 1:1 cvalue table in the large `0x05` record (see the +/// note on [`super::keys::probe::mkb_cvalues`]). They must NOT be +/// unified to one order — each is correct for its own MKB shape. +/// - finders: this walk operates on parsed [`MkbRecord`]s (needed +/// because the variant chain also reads `0x82`/`0x83`); the +/// classical walk operates on raw MKB bytes. Same framing, different +/// input type. +/// +/// Consequence: do NOT route the classical DK path through this function +/// — on a classical MKB the `0x07`-first selection picks the wrong (or +/// missing) cvalue and the magic check fails, so it returns `None`. pub fn walk_processing_key( records: &[MkbRecord], device_keys: &[DeviceKey], @@ -544,6 +510,10 @@ pub fn derive_media_key_variant( #[cfg(test)] mod tests { use super::*; + // These three live in `super::keys` now (consolidated SD-walk helpers); + // `use super::*` does not re-export the parent module's private `use` + // imports, so pull them in directly for the tests below. + use super::super::keys::{aesg3, calc_pk_from_dk}; #[test] fn calc_pk_from_dk_terminates_on_nonconvergent_mask() { @@ -708,7 +678,7 @@ mod tests { /// agreeing with uv on bits 3+ (the u_mask=1 region). dk.uv == /// MKB.uv and dk.u_mask_shift == MKB.u_mask_shift make /// `dev_key_v_mask == v_mask`, so `calc_pk_from_dk` loops zero - /// times — Kp = aesg3_step(dk, 1). + /// times — Kp = aesg3(dk, 1). /// - one cvalue in record 0x07 chosen so AES-D(Kp, C) ⊕ uv produces a /// Kmp whose byte-15 is exactly `kmp15`. /// - record 0x82 with a 16-byte body (acts as both Variant Data @@ -731,12 +701,12 @@ mod tests { // Pick a known DK; with dk.uv == MKB.uv (==2) and // dk.u_mask_shift == MKB.u_mask_shift (==3), dev_key_v_mask // equals the MKB's v_mask and the calc_pk_from_dk loop is a - // no-op — Kp = aesg3_step(dk, 1). + // no-op — Kp = aesg3(dk, 1). let dk_bytes: [u8; 16] = [ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, ]; - let kp = aesg3_step(&dk_bytes, 1); + let kp = aesg3(&dk_bytes, 1); // Plant Kmp with chosen byte-15, then compute C such that // AES-D(Kp, C) ⊕ uv == Kmp. uv=2 → low-4 bytes XOR is 00 00 00 02. @@ -1002,10 +972,7 @@ mod tests { let (recs, dk, planted_kp, _) = synthetic_variant_setup(0x00); let m = walk_processing_key(&recs, &[dk]).expect("variant MKB yields a match"); assert_eq!(m.uv, 2, "matched the planted uv"); - assert_eq!( - m.kp, planted_kp, - "Kp equals aesg3_step(dk,1) for the no-op walk" - ); + assert_eq!(m.kp, planted_kp, "Kp equals aesg3(dk,1) for the no-op walk"); assert_eq!(m.cvalue_index, 0); }