From f55f11d0432a1aa500bee92bf97c4eeaf967d2a1 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:55:06 -0700 Subject: [PATCH] aacs: extract mkb.rs (MKB record format, MkbType, finders) Relocate the shared MKB machinery into a single mkb module: the record framing walker + MkbRecord view (from variant), the MkbType/AacsVersion classification, the MKB-file utilities, and the record-body finders (from keys). Fixes the inversion where the MKB parser lived in the 2.1-only variant module. variant.rs keeps its local MkbRecord-based mkb_find_mk_dv (name collision with the raw one; unified in the dedup follow-up). Relocation only. Proven byte-identical to the pre-refactor state via the function-body logic hash (imports normalized out); 2210 tests green. --- src/aacs/boil.rs | 3 +- src/aacs/keys.rs | 252 +--------------------------------- src/aacs/mkb.rs | 325 ++++++++++++++++++++++++++++++++++++++++++++ src/aacs/mod.rs | 22 +-- src/aacs/variant.rs | 67 +-------- 5 files changed, 344 insertions(+), 325 deletions(-) create mode 100644 src/aacs/mkb.rs diff --git a/src/aacs/boil.rs b/src/aacs/boil.rs index f1d2372..15b05a6 100644 --- a/src/aacs/boil.rs +++ b/src/aacs/boil.rs @@ -204,7 +204,8 @@ pub fn resolve_candidate( unit_key_ro: &[u8], vid: Option, ) -> Option { - use super::keys::{AacsVersion, derive_media_key_and_pk_from_dk, mkb_type, parse_unit_key_ro}; + use super::keys::{derive_media_key_and_pk_from_dk, parse_unit_key_ro}; + use super::mkb::{AacsVersion, mkb_type}; // Boil a VUK → all unit keys, each paired with its declared CPS-unit number. // `.inf` parsing lives here: derive the stride version from the disc's own diff --git a/src/aacs/keys.rs b/src/aacs/keys.rs index 9e02eb8..0dd76c6 100644 --- a/src/aacs/keys.rs +++ b/src/aacs/keys.rs @@ -1,66 +1,11 @@ //! AACS key resolution — VUK derivation, MKB processing, disc hash, unit key parsing. use super::crypto::{aes_ecb_decrypt, aesg3}; +use super::mkb::*; use super::types::DeviceKey; // ── AACS version ──────────────────────────────────────────────────────────── -/// AACS protection generation a disc carries. -/// -/// The content cert byte distinguishes V10 (`0x00`) from V20 (`0x01`). V21 -/// cannot be detected from the cert alone — a V21 disc carries a V20 cert -/// and is upgraded to `V21` only after the MKB walk turns up the real Variant -/// records `0x2d` / `0x2f` (Encrypted Media Key Variant Data and the Variant -/// Key Data table). -/// -/// Key-storage stride in `Unit_Key_RO.inf` is 48 bytes for V10 and 64 -/// bytes for V20 / V21. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AacsVersion { - /// AACS 1.0 — original BD-ROM. - V10, - /// AACS 2.0 — UHD-BD, classical Media Key derivation. - V20, - /// AACS 2.1 — UHD-BD with Media Key Variant chain on top of V20. - V21, -} - -/// AACS major version as the small integer threaded through the scan / key -/// paths (`AacsState.version`, `DiscInputs.version`, `DiscInputsCtx::new`): -/// 1 = AACS 1.0 (BD), 2 = AACS 2.x (UHD). Centralised so the bare `1`/`2` — and -/// the V10-vs-else stride choice it drives — lives in exactly one place. -pub const AACS_MAJOR_BD: u8 = 1; -pub const AACS_MAJOR_UHD: u8 = 2; - -impl AacsVersion { - /// Stride (in bytes) between successive encrypted unit keys in - /// `Unit_Key_RO.inf`. - fn unit_key_stride(self) -> usize { - match self { - AacsVersion::V10 => 48, - AacsVersion::V20 | AacsVersion::V21 => 64, - } - } - - /// This version as the major integer ([`AACS_MAJOR_BD`] / [`AACS_MAJOR_UHD`]). - pub fn major(self) -> u8 { - match self { - AacsVersion::V10 => AACS_MAJOR_BD, - AacsVersion::V20 | AacsVersion::V21 => AACS_MAJOR_UHD, - } - } - - /// The version a bare major integer selects for stride purposes: only the - /// BD major is V10; every other value takes the V20/V21 64-byte stride. - pub fn from_major(major: u8) -> Self { - if major == AACS_MAJOR_BD { - AacsVersion::V10 - } else { - AacsVersion::V20 - } - } -} - // ── VUK derivation ────────────────────────────────────────────────────────── /// Derive VUK from Media Key and Volume ID. [PR] §3.3 / [BD] §3.3 @@ -401,203 +346,12 @@ pub mod probe { } } -/// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB. -/// 0x81: [C] §3.2.5.1.4. 0x86 (AACS 2.x): [libaacs] `mkb.c` — not in the public spec. -fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> { - // Verify-Media-Key record (0x81 for AACS 1.0, 0x86 for AACS 2.x): mk_dv is - // the 16 bytes at record offset 4 (body offset 0). Needs rec_len >= 20. - let found = crate::aacs::variant::mkb_records(mkb) - .find(|&(_, rt, len)| (rt == 0x81 || rt == 0x86) && len >= 20); - match found { - Some((o, rec_type, rec_len)) => { - let mut dv = [0u8; 16]; - dv.copy_from_slice(&mkb[o + 4..o + 20]); - tracing::debug!( - target: "freemkv::disc", - phase = "mkb_mk_dv_found", - rec_type, - pos = o, - rec_len, - "mk_dv extracted from MKB" - ); - Some(dv) - } - None => { - tracing::warn!( - target: "freemkv::disc", - phase = "mkb_mk_dv_not_found", - "no 0x81/0x86 record with rec_len>=20 found" - ); - None - } - } -} - -/// Find Subset-Difference records (type 0x04) in MKB. [C] §3.2.5.1.5. -fn mkb_find_subdiff_records(mkb: &[u8]) -> Option> { - find_record_body(mkb, 0x04) -} - -/// Find the Media Key Data Record (cvalues table) in an MKB. [C] §3.2.4 / §3.2.5.1.7. -/// -/// The cvalue table is record type `0x05` (Media Key Data) on BOTH AACS -/// 1.0 and AACS 2.x MKBs — its 16-byte cvalue entries are 1:1 with the -/// 5-byte Subset-Difference index entries in record `0x04`. This matches -/// libaacs, whose `mkb_cvalues()` reads `0x05` and `mkb_subdiff_records()` -/// reads `0x04`. -/// -/// On AACS 2.x in-drive UHD MKBs the `0x05` table is large (the full -/// subset-difference cvalue set: ~181k entries on a retail MKB, 1:1 with -/// the giant `0x04` index), while record `0x07` (Explicit -/// Subset-Difference Record) is a much smaller structure (~96 entries) and -/// is NOT the cvalue table. An earlier version of this function preferred -/// `0x07`, which under-tested the Subset-Difference walk on UHD discs and -/// prevented the DK→walk path from ever finding the matching uv. The -/// selection MUST therefore be `0x05`-first; `0x07` is only a fallback for -/// malformed/legacy MKBs that somehow lack a `0x05` record. -fn mkb_find_cvalues(mkb: &[u8]) -> Option> { - if let Some(body) = find_record_body(mkb, 0x05) { - return Some(body); - } - find_record_body(mkb, 0x07) -} - -/// Walk an MKB and return the payload (header stripped) of the first -/// record matching `rec_type`. Returns `None` if no such record exists or -/// the record is empty. -fn find_record_body(mkb: &[u8], rec_type_wanted: u8) -> Option> { - crate::aacs::variant::mkb_records(mkb) - .find(|&(_, rt, len)| rt == rec_type_wanted && len > 4) - .map(|(o, _, len)| mkb[o + 4..o + len].to_vec()) -} - -/// Real content length of an MKB: the byte offset where the record stream -/// ends. MKB files (especially `MKB_RW.inf`, but `MKB_RO.inf` too on some -/// discs) are allocated to a fixed size — often ~128 MiB — with the records at -/// the front and the rest zero padding. Walking records (type+len) and stopping -/// at the first padding byte (`type == 0` / zero-length / overrun) gives the -/// actual size so callers can trim off megabytes of zeros before sending or -/// archiving. Returns `mkb.len()` only if the whole buffer parsed as records. -pub fn mkb_content_len(mkb: &[u8]) -> usize { - // End of the last framed record = where the fixed-region zero padding begins. - // (The `00 000000` terminator / overrun stops the walk; real MKBs pad with - // zeros, so this matches the prior "stop at the first padding byte".) - crate::aacs::variant::mkb_records(mkb) - .last() - .map(|(o, _, len)| o + len) - .unwrap_or(0) -} - -/// Trim an MKB's trailing fixed-region padding to its real content length — -/// but ONLY when [`mkb_content_len`] actually found one. It returns 0 for an -/// MKB whose first record cannot be parsed; truncating to 0 in that case would -/// hand downstream consumers (and the online key service) an EMPTY MKB that can -/// never resolve. So a 0 (or a length that isn't strictly inside the buffer) -/// leaves the MKB untouched. A 0.31.0 regression dropped this guard and -/// `truncate`-d unconditionally, zeroing unrecognised MKBs. -pub fn trim_mkb(mut mkb: Vec) -> Vec { - let n = mkb_content_len(&mkb); - if n > 0 && n < mkb.len() { - mkb.truncate(n); - } - mkb -} - -/// Get MKB version from Type and Version Record (type 0x10). -/// Layout: 4-byte record header at `pos` (type + BE24 length), then the -/// record body starts at `pos + 4`. The body holds the BE u32 Type field at -/// body offset 0 (`pos + 4`), then the BE u32 version at body offset 4 -/// (`pos + 8`). -pub fn mkb_version(mkb: &[u8]) -> Option { - // Type-and-Version record (0x10): version is the BE u32 at body offset 4 - // (record offset 8). Needs rec_len >= 12 (4 header + 4 type + 4 version). - crate::aacs::variant::mkb_records(mkb) - .find(|&(_, rt, len)| rt == 0x10 && len >= 12) - .map(|(o, _, _)| u32::from_be_bytes([mkb[o + 8], mkb[o + 9], mkb[o + 10], mkb[o + 11]])) -} - // ── MKB Type field (Type-and-Version record 0x10, bytes 4-7) ──────────────── // // Canonical form is `1003` (low 16 bits `0x1003` is a fixed marker). // Types 3/4/10 are from the AACS Common Cryptographic Elements spec (0.953, // §3.2.5.1.1); the Category-C 2.0/2.1 values match libaacs `mkb.h` constants. -/// `0x00031003` — recordable media MKB (Class I & II compute Km directly). -pub const MKB_TYPE_3_RECORDABLE: u32 = 0x0003_1003; -/// `0x00041003` — AACS 1.0 pre-recorded content MKB (KCD-based). Standard BD. -pub const MKB_TYPE_4_PRERECORDED: u32 = 0x0004_1003; -/// `0x000A1003` — Class II / Unified MKB (Sequence-Key-Block functionality). -pub const MKB_TYPE_10_CLASS_II: u32 = 0x000A_1003; -/// `0x48141003` — AACS 2.0 Category C (UHD content). libaacs `MKB_20_CATEGORY_C`. -pub const MKB_20_CATEGORY_C: u32 = 0x4814_1003; -/// `0x48151003` — AACS 2.1 Category C (UHD content). libaacs `MKB_21_CATEGORY_C`. -pub const MKB_21_CATEGORY_C: u32 = 0x4815_1003; - -/// The AACS MKB Type field, decoded. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MkbType { - /// Type 3 — recordable media. - Recordable, - /// Type 4 — AACS 1.0 pre-recorded content (KCD). Standard Blu-ray. - Prerecorded, - /// Type 10 — Class II / Unified (SKB). - ClassII, - /// AACS 2.0 Category C — UHD content. - CategoryC20, - /// AACS 2.1 Category C — UHD content. - CategoryC21, - /// Unrecognized MKBType value (raw field preserved). - Other(u32), -} - -impl MkbType { - fn from_raw(raw: u32) -> Self { - match raw { - MKB_TYPE_3_RECORDABLE => MkbType::Recordable, - MKB_TYPE_4_PRERECORDED => MkbType::Prerecorded, - MKB_TYPE_10_CLASS_II => MkbType::ClassII, - MKB_20_CATEGORY_C => MkbType::CategoryC20, - MKB_21_CATEGORY_C => MkbType::CategoryC21, - other => MkbType::Other(other), - } - } - - /// AACS generation this MKB belongs to (Category C → 2.0/2.1, else 1.0). - pub fn generation(self) -> AacsVersion { - match self { - MkbType::CategoryC21 => AacsVersion::V21, - MkbType::CategoryC20 => AacsVersion::V20, - _ => AacsVersion::V10, - } - } - - /// `true` for UHD (AACS 2.x Category C); `false` for Blu-ray (AACS 1.x). - pub fn is_uhd(self) -> bool { - matches!(self, MkbType::CategoryC20 | MkbType::CategoryC21) - } -} - -/// The raw 32-bit MKBType field from the Type-and-Version record (0x10), bytes -/// 4-7. `None` if no 0x10 record is present. [C] §3.2.5.1.1 Table 3-2. -pub fn mkb_type_raw(mkb: &[u8]) -> Option { - // Type-and-Version record (0x10): the 32-bit MKBType is bytes 4-7 (body - // offset 0). Needs rec_len >= 8 (4 header + 4 type). - crate::aacs::variant::mkb_records(mkb) - .find(|&(_, rt, len)| rt == 0x10 && len >= 8) - .map(|(o, _, _)| u32::from_be_bytes([mkb[o + 4], mkb[o + 5], mkb[o + 6], mkb[o + 7]])) -} - -/// Decode an MKB's Type field. `None` if no Type-and-Version record is present. -pub fn mkb_type(mkb: &[u8]) -> Option { - mkb_type_raw(mkb).map(MkbType::from_raw) -} - -/// `Some(true)` if this MKB is a UHD (AACS 2.x Category C) block, `Some(false)` -/// for Blu-ray (AACS 1.x), `None` if the Type record is absent. -pub fn mkb_is_uhd(mkb: &[u8]) -> Option { - mkb_type(mkb).map(MkbType::is_uhd) -} - // ── AACS-G3 key derivation (subset-difference tree) ───────────────────────── /// Compute v_mask from a UV value. [C] §3.2.3. Shared with [`super::variants`]. @@ -1096,7 +850,7 @@ pub fn resolve_keys_v1(ctx: &ResolveContext<'_>) -> Option { pub fn resolve_keys_v2(ctx: &ResolveContext<'_>) -> Option { let mut resolved = resolve_keys_classical(ctx, AacsVersion::V20)?; if let Some(mkb) = ctx.mkb { - let recs = super::variant::walk_mkb(mkb); + let recs = super::mkb::walk_mkb(mkb); if super::variant::is_variant_mkb(&recs) { resolved.version = AacsVersion::V21; } @@ -1160,7 +914,7 @@ pub fn resolve_keys_v21(ctx: &ResolveContext<'_>) -> Option { // Path 1: Variant chain (V21's analogue of classical Path 1's // DK derivation). Placeholder until KCD constant is supplied. if let Some(mkb) = ctx.mkb { - let recs = super::variant::walk_mkb(mkb); + let recs = super::mkb::walk_mkb(mkb); let all_dks = providers.device_keys(); match super::variant::derive_media_key_variant( &recs, diff --git a/src/aacs/mkb.rs b/src/aacs/mkb.rs new file mode 100644 index 0000000..e067104 --- /dev/null +++ b/src/aacs/mkb.rs @@ -0,0 +1,325 @@ +//! AACS Media Key Block — [C] Chapter 3. +//! +//! The MKB record format (framing walker, the `MkbRecord` view, record-body +//! finders), the MKBType / AACS-generation classification, and MKB-file +//! utilities (content length, trimming, version). Consolidated here from the +//! former `keys.rs` / `variant.rs` so the one place that understands MKB bytes +//! is `mkb`. A follow-up collapses the remaining duplicate finders (see the +//! private refactor notes) — for now both dialects live here side by side. + +/// A single MKB record produced by [`walk_mkb`]. +#[derive(Debug, Clone)] +pub struct MkbRecord { + /// Byte offset of the record within the MKB. + pub offset: usize, + /// Record type byte. + pub rec_type: u8, + /// Record length in bytes (includes the 4-byte header). + pub rec_len: usize, + /// Record body (the bytes after the 4-byte header). + pub body: Vec, +} + +/// Walk an MKB into a flat list of records. +/// +/// MKB record framing per AACS: 1 byte type, 3 bytes BE length +/// INCLUDING the 4-byte header, followed by payload. The walker stops +/// at the first `(type=0, len=0)` end marker or at end of buffer. +pub fn walk_mkb(mkb: &[u8]) -> Vec { + mkb_records(mkb) + .map(|(offset, rec_type, rec_len)| MkbRecord { + offset, + rec_type, + rec_len, + body: mkb[offset + 4..offset + rec_len].to_vec(), + }) + .collect() +} + +/// THE single MKB record-framing walker: yields `(offset, rec_type, rec_len)` +/// for each record — a 4-byte header (type byte + big-endian 24-bit length) +/// then the body — stopping at the `00 000000` end marker or a +/// malformed/out-of-bounds length. Lazy (no body clone), so a find-one-record +/// caller never materialises the multi-MB cvalue table. [`walk_mkb`] and every +/// MKB record walk in `aacs::keys` are built on this, so the framing rules — and +/// any future fix to them — live in exactly one place (they had drifted across +/// six hand-rolled copies). +pub(crate) fn mkb_records(mkb: &[u8]) -> impl Iterator + '_ { + let mut pos = 0usize; + std::iter::from_fn(move || { + if pos + 4 > mkb.len() { + return None; + } + let rec_type = mkb[pos]; + let rec_len = ((mkb[pos + 1] as usize) << 16) + | ((mkb[pos + 2] as usize) << 8) + | (mkb[pos + 3] as usize); + if rec_type == 0 && rec_len == 0 { + return None; + } + if rec_len < 4 || pos + rec_len > mkb.len() { + return None; + } + let here = pos; + pos += rec_len; + Some((here, rec_type, rec_len)) + }) +} + +pub(crate) fn mkb_find_body(records: &[MkbRecord], rec_type: u8) -> Option<&[u8]> { + records + .iter() + .find(|r| r.rec_type == rec_type && !r.body.is_empty()) + .map(|r| r.body.as_slice()) +} + +/// AACS protection generation a disc carries. +/// +/// The content cert byte distinguishes V10 (`0x00`) from V20 (`0x01`). V21 +/// cannot be detected from the cert alone — a V21 disc carries a V20 cert +/// and is upgraded to `V21` only after the MKB walk turns up the real Variant +/// records `0x2d` / `0x2f` (Encrypted Media Key Variant Data and the Variant +/// Key Data table). +/// +/// Key-storage stride in `Unit_Key_RO.inf` is 48 bytes for V10 and 64 +/// bytes for V20 / V21. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AacsVersion { + /// AACS 1.0 — original BD-ROM. + V10, + /// AACS 2.0 — UHD-BD, classical Media Key derivation. + V20, + /// AACS 2.1 — UHD-BD with Media Key Variant chain on top of V20. + V21, +} + +/// AACS major version as the small integer threaded through the scan / key +/// paths (`AacsState.version`, `DiscInputs.version`, `DiscInputsCtx::new`): +/// 1 = AACS 1.0 (BD), 2 = AACS 2.x (UHD). Centralised so the bare `1`/`2` — and +/// the V10-vs-else stride choice it drives — lives in exactly one place. +pub const AACS_MAJOR_BD: u8 = 1; + +pub const AACS_MAJOR_UHD: u8 = 2; + +impl AacsVersion { + /// Stride (in bytes) between successive encrypted unit keys in + /// `Unit_Key_RO.inf`. + pub(crate) fn unit_key_stride(self) -> usize { + match self { + AacsVersion::V10 => 48, + AacsVersion::V20 | AacsVersion::V21 => 64, + } + } + + /// This version as the major integer ([`AACS_MAJOR_BD`] / [`AACS_MAJOR_UHD`]). + pub fn major(self) -> u8 { + match self { + AacsVersion::V10 => AACS_MAJOR_BD, + AacsVersion::V20 | AacsVersion::V21 => AACS_MAJOR_UHD, + } + } + + /// The version a bare major integer selects for stride purposes: only the + /// BD major is V10; every other value takes the V20/V21 64-byte stride. + pub fn from_major(major: u8) -> Self { + if major == AACS_MAJOR_BD { + AacsVersion::V10 + } else { + AacsVersion::V20 + } + } +} + +/// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB. +/// 0x81: [C] §3.2.5.1.4. 0x86 (AACS 2.x): [libaacs] `mkb.c` — not in the public spec. +pub(crate) fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> { + // Verify-Media-Key record (0x81 for AACS 1.0, 0x86 for AACS 2.x): mk_dv is + // the 16 bytes at record offset 4 (body offset 0). Needs rec_len >= 20. + let found = mkb_records(mkb).find(|&(_, rt, len)| (rt == 0x81 || rt == 0x86) && len >= 20); + match found { + Some((o, rec_type, rec_len)) => { + let mut dv = [0u8; 16]; + dv.copy_from_slice(&mkb[o + 4..o + 20]); + tracing::debug!( + target: "freemkv::disc", + phase = "mkb_mk_dv_found", + rec_type, + pos = o, + rec_len, + "mk_dv extracted from MKB" + ); + Some(dv) + } + None => { + tracing::warn!( + target: "freemkv::disc", + phase = "mkb_mk_dv_not_found", + "no 0x81/0x86 record with rec_len>=20 found" + ); + None + } + } +} + +/// Find Subset-Difference records (type 0x04) in MKB. [C] §3.2.5.1.5. +pub(crate) fn mkb_find_subdiff_records(mkb: &[u8]) -> Option> { + find_record_body(mkb, 0x04) +} + +/// Find the Media Key Data Record (cvalues table) in an MKB. [C] §3.2.4 / §3.2.5.1.7. +/// +/// The cvalue table is record type `0x05` (Media Key Data) on BOTH AACS +/// 1.0 and AACS 2.x MKBs — its 16-byte cvalue entries are 1:1 with the +/// 5-byte Subset-Difference index entries in record `0x04`. This matches +/// libaacs, whose `mkb_cvalues()` reads `0x05` and `mkb_subdiff_records()` +/// reads `0x04`. +/// +/// On AACS 2.x in-drive UHD MKBs the `0x05` table is large (the full +/// subset-difference cvalue set: ~181k entries on a retail MKB, 1:1 with +/// the giant `0x04` index), while record `0x07` (Explicit +/// Subset-Difference Record) is a much smaller structure (~96 entries) and +/// is NOT the cvalue table. An earlier version of this function preferred +/// `0x07`, which under-tested the Subset-Difference walk on UHD discs and +/// prevented the DK→walk path from ever finding the matching uv. The +/// selection MUST therefore be `0x05`-first; `0x07` is only a fallback for +/// malformed/legacy MKBs that somehow lack a `0x05` record. +pub(crate) fn mkb_find_cvalues(mkb: &[u8]) -> Option> { + if let Some(body) = find_record_body(mkb, 0x05) { + return Some(body); + } + find_record_body(mkb, 0x07) +} + +/// Walk an MKB and return the payload (header stripped) of the first +/// record matching `rec_type`. Returns `None` if no such record exists or +/// the record is empty. +pub(crate) fn find_record_body(mkb: &[u8], rec_type_wanted: u8) -> Option> { + mkb_records(mkb) + .find(|&(_, rt, len)| rt == rec_type_wanted && len > 4) + .map(|(o, _, len)| mkb[o + 4..o + len].to_vec()) +} + +/// Real content length of an MKB: the byte offset where the record stream +/// ends. MKB files (especially `MKB_RW.inf`, but `MKB_RO.inf` too on some +/// discs) are allocated to a fixed size — often ~128 MiB — with the records at +/// the front and the rest zero padding. Walking records (type+len) and stopping +/// at the first padding byte (`type == 0` / zero-length / overrun) gives the +/// actual size so callers can trim off megabytes of zeros before sending or +/// archiving. Returns `mkb.len()` only if the whole buffer parsed as records. +pub fn mkb_content_len(mkb: &[u8]) -> usize { + // End of the last framed record = where the fixed-region zero padding begins. + // (The `00 000000` terminator / overrun stops the walk; real MKBs pad with + // zeros, so this matches the prior "stop at the first padding byte".) + mkb_records(mkb) + .last() + .map(|(o, _, len)| o + len) + .unwrap_or(0) +} + +/// Trim an MKB's trailing fixed-region padding to its real content length — +/// but ONLY when [`mkb_content_len`] actually found one. It returns 0 for an +/// MKB whose first record cannot be parsed; truncating to 0 in that case would +/// hand downstream consumers (and the online key service) an EMPTY MKB that can +/// never resolve. So a 0 (or a length that isn't strictly inside the buffer) +/// leaves the MKB untouched. A 0.31.0 regression dropped this guard and +/// `truncate`-d unconditionally, zeroing unrecognised MKBs. +pub fn trim_mkb(mut mkb: Vec) -> Vec { + let n = mkb_content_len(&mkb); + if n > 0 && n < mkb.len() { + mkb.truncate(n); + } + mkb +} + +/// Get MKB version from Type and Version Record (type 0x10). +/// Layout: 4-byte record header at `pos` (type + BE24 length), then the +/// record body starts at `pos + 4`. The body holds the BE u32 Type field at +/// body offset 0 (`pos + 4`), then the BE u32 version at body offset 4 +/// (`pos + 8`). +pub fn mkb_version(mkb: &[u8]) -> Option { + // Type-and-Version record (0x10): version is the BE u32 at body offset 4 + // (record offset 8). Needs rec_len >= 12 (4 header + 4 type + 4 version). + mkb_records(mkb) + .find(|&(_, rt, len)| rt == 0x10 && len >= 12) + .map(|(o, _, _)| u32::from_be_bytes([mkb[o + 8], mkb[o + 9], mkb[o + 10], mkb[o + 11]])) +} + +/// `0x00031003` — recordable media MKB (Class I & II compute Km directly). +pub const MKB_TYPE_3_RECORDABLE: u32 = 0x0003_1003; + +/// `0x00041003` — AACS 1.0 pre-recorded content MKB (KCD-based). Standard BD. +pub const MKB_TYPE_4_PRERECORDED: u32 = 0x0004_1003; + +/// `0x000A1003` — Class II / Unified MKB (Sequence-Key-Block functionality). +pub const MKB_TYPE_10_CLASS_II: u32 = 0x000A_1003; + +/// `0x48141003` — AACS 2.0 Category C (UHD content). libaacs `MKB_20_CATEGORY_C`. +pub const MKB_20_CATEGORY_C: u32 = 0x4814_1003; + +/// `0x48151003` — AACS 2.1 Category C (UHD content). libaacs `MKB_21_CATEGORY_C`. +pub const MKB_21_CATEGORY_C: u32 = 0x4815_1003; + +/// The AACS MKB Type field, decoded. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MkbType { + /// Type 3 — recordable media. + Recordable, + /// Type 4 — AACS 1.0 pre-recorded content (KCD). Standard Blu-ray. + Prerecorded, + /// Type 10 — Class II / Unified (SKB). + ClassII, + /// AACS 2.0 Category C — UHD content. + CategoryC20, + /// AACS 2.1 Category C — UHD content. + CategoryC21, + /// Unrecognized MKBType value (raw field preserved). + Other(u32), +} + +impl MkbType { + pub(crate) fn from_raw(raw: u32) -> Self { + match raw { + MKB_TYPE_3_RECORDABLE => MkbType::Recordable, + MKB_TYPE_4_PRERECORDED => MkbType::Prerecorded, + MKB_TYPE_10_CLASS_II => MkbType::ClassII, + MKB_20_CATEGORY_C => MkbType::CategoryC20, + MKB_21_CATEGORY_C => MkbType::CategoryC21, + other => MkbType::Other(other), + } + } + + /// AACS generation this MKB belongs to (Category C → 2.0/2.1, else 1.0). + pub fn generation(self) -> AacsVersion { + match self { + MkbType::CategoryC21 => AacsVersion::V21, + MkbType::CategoryC20 => AacsVersion::V20, + _ => AacsVersion::V10, + } + } + + /// `true` for UHD (AACS 2.x Category C); `false` for Blu-ray (AACS 1.x). + pub fn is_uhd(self) -> bool { + matches!(self, MkbType::CategoryC20 | MkbType::CategoryC21) + } +} + +/// The raw 32-bit MKBType field from the Type-and-Version record (0x10), bytes +/// 4-7. `None` if no 0x10 record is present. [C] §3.2.5.1.1 Table 3-2. +pub fn mkb_type_raw(mkb: &[u8]) -> Option { + // Type-and-Version record (0x10): the 32-bit MKBType is bytes 4-7 (body + // offset 0). Needs rec_len >= 8 (4 header + 4 type). + mkb_records(mkb) + .find(|&(_, rt, len)| rt == 0x10 && len >= 8) + .map(|(o, _, _)| u32::from_be_bytes([mkb[o + 4], mkb[o + 5], mkb[o + 6], mkb[o + 7]])) +} + +/// Decode an MKB's Type field. `None` if no Type-and-Version record is present. +pub fn mkb_type(mkb: &[u8]) -> Option { + mkb_type_raw(mkb).map(MkbType::from_raw) +} + +/// `Some(true)` if this MKB is a UHD (AACS 2.x Category C) block, `Some(false)` +/// for Blu-ray (AACS 1.x), `None` if the Type record is absent. +pub fn mkb_is_uhd(mkb: &[u8]) -> Option { + mkb_type(mkb).map(MkbType::is_uhd) +} diff --git a/src/aacs/mod.rs b/src/aacs/mod.rs index 4422e4f..91d3883 100644 --- a/src/aacs/mod.rs +++ b/src/aacs/mod.rs @@ -30,6 +30,7 @@ pub mod content; pub mod crypto; pub mod host_certs; pub mod keys; +pub mod mkb; pub mod provider; pub mod trace; pub mod types; @@ -70,19 +71,22 @@ pub use content::{ #[doc(hidden)] pub use keys::probe; pub use keys::{ - AACS_MAJOR_BD, AACS_MAJOR_UHD, AacsVersion, ContentCert, MKB_20_CATEGORY_C, MKB_21_CATEGORY_C, - MKB_TYPE_3_RECORDABLE, MKB_TYPE_4_PRERECORDED, MKB_TYPE_10_CLASS_II, MkbType, ResolveContext, - ResolveFailure, ResolvedKeys, UnitKeyFile, decrypt_unit_key, derive_media_key_and_pk_from_dk, - derive_media_key_from_dk, derive_media_key_from_pk, derive_vuk, disc_hash, disc_hash_hex, - mkb_content_len, mkb_is_uhd, mkb_type, mkb_type_raw, mkb_version, parse_content_cert, - parse_unit_key_ro, read_mkb_from_drive, recover_dk_position, resolve_keys_v1, resolve_keys_v2, - resolve_keys_v21, resolve_keys_with_reason, trim_mkb, + ContentCert, ResolveContext, ResolveFailure, ResolvedKeys, UnitKeyFile, decrypt_unit_key, + derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk, + derive_vuk, disc_hash, disc_hash_hex, parse_content_cert, parse_unit_key_ro, + read_mkb_from_drive, recover_dk_position, resolve_keys_v1, resolve_keys_v2, resolve_keys_v21, + resolve_keys_with_reason, +}; +pub use mkb::{ + AACS_MAJOR_BD, AACS_MAJOR_UHD, AacsVersion, MKB_20_CATEGORY_C, MKB_21_CATEGORY_C, + MKB_TYPE_3_RECORDABLE, MKB_TYPE_4_PRERECORDED, MKB_TYPE_10_CLASS_II, MkbRecord, MkbType, + mkb_content_len, mkb_is_uhd, mkb_type, mkb_type_raw, mkb_version, trim_mkb, walk_mkb, }; pub use provider::KeyProvider; pub use types::{DeviceKey, DiscEntry, HostCert}; pub use variant::{ - KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, MkbRecord, ProcessingKeyMatch, - derive_media_key_variant, is_variant_mkb, variant_nonce, walk_mkb, walk_processing_key, + KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, ProcessingKeyMatch, + derive_media_key_variant, is_variant_mkb, variant_nonce, walk_processing_key, }; #[cfg(test)] diff --git a/src/aacs/variant.rs b/src/aacs/variant.rs index cbabbd9..a8b09cf 100644 --- a/src/aacs/variant.rs +++ b/src/aacs/variant.rs @@ -73,6 +73,7 @@ //! variant path. use super::crypto::{aes_ecb_decrypt, aes_g}; +use super::mkb::*; use super::types::DeviceKey; // ── Public constants ────────────────────────────────────────────────────── @@ -86,65 +87,6 @@ pub const KEY_CORRECTION_DATA_PLACEHOLDER: [u8; 16] = [0u8; 16]; // ── MKB record walking ──────────────────────────────────────────────────── -/// A single MKB record produced by [`walk_mkb`]. -#[derive(Debug, Clone)] -pub struct MkbRecord { - /// Byte offset of the record within the MKB. - pub offset: usize, - /// Record type byte. - pub rec_type: u8, - /// Record length in bytes (includes the 4-byte header). - pub rec_len: usize, - /// Record body (the bytes after the 4-byte header). - pub body: Vec, -} - -/// Walk an MKB into a flat list of records. -/// -/// MKB record framing per AACS: 1 byte type, 3 bytes BE length -/// INCLUDING the 4-byte header, followed by payload. The walker stops -/// at the first `(type=0, len=0)` end marker or at end of buffer. -pub fn walk_mkb(mkb: &[u8]) -> Vec { - mkb_records(mkb) - .map(|(offset, rec_type, rec_len)| MkbRecord { - offset, - rec_type, - rec_len, - body: mkb[offset + 4..offset + rec_len].to_vec(), - }) - .collect() -} - -/// THE single MKB record-framing walker: yields `(offset, rec_type, rec_len)` -/// for each record — a 4-byte header (type byte + big-endian 24-bit length) -/// then the body — stopping at the `00 000000` end marker or a -/// malformed/out-of-bounds length. Lazy (no body clone), so a find-one-record -/// caller never materialises the multi-MB cvalue table. [`walk_mkb`] and every -/// MKB record walk in `aacs::keys` are built on this, so the framing rules — and -/// any future fix to them — live in exactly one place (they had drifted across -/// six hand-rolled copies). -pub(crate) fn mkb_records(mkb: &[u8]) -> impl Iterator + '_ { - let mut pos = 0usize; - std::iter::from_fn(move || { - if pos + 4 > mkb.len() { - return None; - } - let rec_type = mkb[pos]; - let rec_len = ((mkb[pos + 1] as usize) << 16) - | ((mkb[pos + 2] as usize) << 8) - | (mkb[pos + 3] as usize); - if rec_type == 0 && rec_len == 0 { - return None; - } - if rec_len < 4 || pos + rec_len > mkb.len() { - return None; - } - let here = pos; - pos += rec_len; - Some((here, rec_type, rec_len)) - }) -} - /// True iff `records` contains at least one Media Key Variant record. /// /// The real AACS 2.1 Variant markers — confirmed against a live variant MKB — @@ -226,13 +168,6 @@ pub struct ProcessingKeyMatch { pub cvalue_index: usize, } -fn mkb_find_body(records: &[MkbRecord], rec_type: u8) -> Option<&[u8]> { - records - .iter() - .find(|r| r.rec_type == rec_type && !r.body.is_empty()) - .map(|r| r.body.as_slice()) -} - fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> { let r = records .iter()