aacs(2.1): pin variant record layout against reference MKBs
Correct the variant Media Key chain against two real variant MKBs (Zombieland v70, Stand By Me v70): C for Kmp is the per-slot block of the 0x0c cvalue table indexed by the matched subset-difference, not the 0x2d head. The 0x2d record is the VARIANTS table (leading body-16 bytes, one big-endian u16 per subset-difference) followed by a trailing 16-byte Nonce, with no leading header. VKD stays at 0x2f. Route record-type selection through the named REC_* consts in mkb rather than bare hex, and document that Key Correction Data is per-licensee: no universal constant exists and none is compiled in, so on a real disc the chain yields a wrong Media Key that the Verify-Media-Key gate rejects rather than emitting a bad key. This is a key-acquisition gap, not a code gap.
This commit is contained in:
+32
-3
@@ -7,6 +7,33 @@
|
|||||||
//! is `mkb`. A follow-up collapses the remaining duplicate finders (see the
|
//! is `mkb`. A follow-up collapses the remaining duplicate finders (see the
|
||||||
//! private refactor notes) — for now both dialects live here side by side.
|
//! private refactor notes) — for now both dialects live here side by side.
|
||||||
|
|
||||||
|
// ── MKB record types ([C] Chapter 3) ──────────────────────────────────────
|
||||||
|
// The ONE canonical set. Every record-type comparison in the `aacs` module
|
||||||
|
// references these, so a type byte is never a bare literal scattered across
|
||||||
|
// files (the `0x0c` variant-data record in particular used to appear in several
|
||||||
|
// hand-rolled forms).
|
||||||
|
|
||||||
|
/// Type-and-Version — carries the 32-bit MKBType / AACS generation.
|
||||||
|
pub(crate) const REC_TYPE_AND_VERSION: u8 = 0x10;
|
||||||
|
/// Subset-Difference index — the per-slot `(u_mask_shift, uv)` table.
|
||||||
|
pub(crate) const REC_SUBSET_DIFFERENCE: u8 = 0x04;
|
||||||
|
/// Media Key Data — the classical (1.0 / 2.0) per-subset cvalue table.
|
||||||
|
pub(crate) const REC_MEDIA_KEY_DATA: u8 = 0x05;
|
||||||
|
/// Explicit Subset-Difference — the smaller cvalue table some MKBs use.
|
||||||
|
pub(crate) const REC_EXPLICIT_SUBSET_DIFF: u8 = 0x07;
|
||||||
|
/// Media Key Variant Data (AACS 2.1) — the per-subset-difference `C` table
|
||||||
|
/// (one 16-byte C per slot); the `Kmp` step reads C from HERE, not `0x2d`.
|
||||||
|
pub(crate) const REC_MEDIA_KEY_VARIANT_DATA: u8 = 0x0c;
|
||||||
|
/// Variant Data + Nonce (AACS 2.1) — the `VARIANTS[uv]` table (leading bytes)
|
||||||
|
/// with the 16-byte `Kvn` Nonce at the tail.
|
||||||
|
pub(crate) const REC_VARIANT_DATA_AND_NONCE: u8 = 0x2d;
|
||||||
|
/// Variant Key Data table (AACS 2.1) — 65,535×16, indexed by the resolved VKD index.
|
||||||
|
pub(crate) const REC_VKD_TABLE: u8 = 0x2f;
|
||||||
|
/// Verify-Media-Key — AACS 1.0.
|
||||||
|
pub(crate) const REC_VERIFY_MEDIA_KEY_V1: u8 = 0x81;
|
||||||
|
/// Verify-Media-Key — AACS 2.x.
|
||||||
|
pub(crate) const REC_VERIFY_MEDIA_KEY_V2: u8 = 0x86;
|
||||||
|
|
||||||
/// A single MKB record produced by [`walk_mkb`].
|
/// A single MKB record produced by [`walk_mkb`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MkbRecord {
|
pub struct MkbRecord {
|
||||||
@@ -135,7 +162,9 @@ impl AacsVersion {
|
|||||||
pub(crate) fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
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
|
// 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.
|
// 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);
|
let found = mkb_records(mkb).find(|&(_, rt, len)| {
|
||||||
|
(rt == REC_VERIFY_MEDIA_KEY_V1 || rt == REC_VERIFY_MEDIA_KEY_V2) && len >= 20
|
||||||
|
});
|
||||||
match found {
|
match found {
|
||||||
Some((o, rec_type, rec_len)) => {
|
Some((o, rec_type, rec_len)) => {
|
||||||
let mut dv = [0u8; 16];
|
let mut dv = [0u8; 16];
|
||||||
@@ -240,7 +269,7 @@ pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
|
|||||||
// Type-and-Version record (0x10): version is the BE u32 at body offset 4
|
// 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).
|
// (record offset 8). Needs rec_len >= 12 (4 header + 4 type + 4 version).
|
||||||
mkb_records(mkb)
|
mkb_records(mkb)
|
||||||
.find(|&(_, rt, len)| rt == 0x10 && len >= 12)
|
.find(|&(_, rt, len)| rt == REC_TYPE_AND_VERSION && len >= 12)
|
||||||
.map(|(o, _, _)| u32::from_be_bytes([mkb[o + 8], mkb[o + 9], mkb[o + 10], mkb[o + 11]]))
|
.map(|(o, _, _)| u32::from_be_bytes([mkb[o + 8], mkb[o + 9], mkb[o + 10], mkb[o + 11]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +338,7 @@ pub fn mkb_type_raw(mkb: &[u8]) -> Option<u32> {
|
|||||||
// Type-and-Version record (0x10): the 32-bit MKBType is bytes 4-7 (body
|
// Type-and-Version record (0x10): the 32-bit MKBType is bytes 4-7 (body
|
||||||
// offset 0). Needs rec_len >= 8 (4 header + 4 type).
|
// offset 0). Needs rec_len >= 8 (4 header + 4 type).
|
||||||
mkb_records(mkb)
|
mkb_records(mkb)
|
||||||
.find(|&(_, rt, len)| rt == 0x10 && len >= 8)
|
.find(|&(_, rt, len)| rt == REC_TYPE_AND_VERSION && len >= 8)
|
||||||
.map(|(o, _, _)| u32::from_be_bytes([mkb[o + 4], mkb[o + 5], mkb[o + 6], mkb[o + 7]]))
|
.map(|(o, _, _)| u32::from_be_bytes([mkb[o + 4], mkb[o + 5], mkb[o + 6], mkb[o + 7]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+211
-59
@@ -3,8 +3,9 @@
|
|||||||
//! On AACS 2.1 the Media Key derivation gains a second stage on top of
|
//! On AACS 2.1 the Media Key derivation gains a second stage on top of
|
||||||
//! the classical subset-difference walk. The classical walk yields a
|
//! the classical subset-difference walk. The classical walk yields a
|
||||||
//! Media Key Precursor (Kmp) rather than the final Media Key; the
|
//! Media Key Precursor (Kmp) rather than the final Media Key; the
|
||||||
//! Precursor combines with disc-supplied Variant Key Data (VKD) and the
|
//! Precursor combines with disc-supplied Variant Key Data (VKD) and a
|
||||||
//! fixed Key Correction Data (KCD) constant to produce the Media Key.
|
//! per-licensee Key Correction Data (KCD) constant to produce the Media
|
||||||
|
//! Key.
|
||||||
//!
|
//!
|
||||||
//! The entry point is [`derive_media_key_variant`] — a `Kp -> Km`
|
//! The entry point is [`derive_media_key_variant`] — a `Kp -> Km`
|
||||||
//! derivation. Deriving `Kp` itself from device keys (DK -> PK) is the
|
//! derivation. Deriving `Kp` itself from device keys (DK -> PK) is the
|
||||||
@@ -27,14 +28,16 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! **Status.** The record layout is pinned against real variant MKBs:
|
//! **Status.** The record layout is pinned against real variant MKBs:
|
||||||
//! `variants_for_uv` reads the `VARIANTS[uv]` table from `0x2d`, `C` from
|
//! `C` is the per-slot block of the `0x0c` cvalue table (indexed by the
|
||||||
//! the `0x2d` head, `VKD` from `0x2f`, and the Nonce from the `0x2d`
|
//! matched subset-difference — NOT the `0x2d` head), `VARIANTS[uv]` is the
|
||||||
//! tail. The one input still missing is a covering 2.1 Processing Key to
|
//! `0x2d` VARIANTS table (leading `body-16` bytes, Nonce at the `0x2d`
|
||||||
//! run the chain end-to-end against the `0x86` Verify-Media-Key record —
|
//! tail), and `VKD` is `0x2f`. Two inputs still block an end-to-end run
|
||||||
//! which would also confirm the last layout picks (the 16-bit `Kvn` width
|
//! against the `0x86` Verify-Media-Key record: the real per-licensee KCD
|
||||||
//! vs. a narrower spec value, and Nonce head-vs-tail). Until then the
|
//! (see [`KEY_CORRECTION_DATA`] — not coded, per-manufacturer), and a
|
||||||
//! final verify gate rejects any wrong pick, so a bad key is never
|
//! covering 2.1 Processing Key. Either one missing yields a wrong `Km`
|
||||||
//! emitted — only an error.
|
//! that the final verify gate rejects, so a bad key is never emitted —
|
||||||
|
//! only an error. (A covering key would also confirm the last layout
|
||||||
|
//! picks: the 16-bit `Kvn` width and Nonce head-vs-tail.)
|
||||||
//!
|
//!
|
||||||
//! Two condition bits on `Kmp[15]` route off the default KCD path (Soft
|
//! Two condition bits on `Kmp[15]` route off the default KCD path (Soft
|
||||||
//! Correction and Online Challenge); the chain does not model those modes
|
//! Correction and Online Challenge); the chain does not model those modes
|
||||||
@@ -51,9 +54,24 @@ use super::crypto::{aes_ecb_decrypt, aes_g};
|
|||||||
use super::mkb::*;
|
use super::mkb::*;
|
||||||
use super::types::DeviceKey;
|
use super::types::DeviceKey;
|
||||||
|
|
||||||
|
// The MKB record types this chain selects — `REC_MEDIA_KEY_VARIANT_DATA`
|
||||||
|
// (`0x0c`, the per-slot C table), `REC_VARIANT_DATA_AND_NONCE` (`0x2d`, VARIANTS
|
||||||
|
// + tail Nonce), `REC_VKD_TABLE` (`0x2f`), the subset-difference / cvalue records
|
||||||
|
// (`0x04` / `0x05` / `0x07`), and the verify records (`0x81` / `0x86`) — are the
|
||||||
|
// canonical set in [`super::mkb`], in scope here via the `use super::mkb::*` glob.
|
||||||
|
|
||||||
// ── Public constants ──────────────────────────────────────────────────────
|
// ── Public constants ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// AACS 2.1 Key Correction Data — a fixed algorithm constant.
|
/// AACS 2.1 Key Correction Data.
|
||||||
|
///
|
||||||
|
/// **KCD is PER-LICENSEE** (per player manufacturer) — there is no single
|
||||||
|
/// universal value, so this one constant cannot be correct across discs. We do
|
||||||
|
/// NOT have the real per-manufacturer KCDs coded, and won't: libfreemkv compiles
|
||||||
|
/// in no AACS key material (keydb.cfg is the single source of truth). The bytes
|
||||||
|
/// below only let the chain's SHAPE exercise against synthetic fixtures; on a
|
||||||
|
/// real variant disc they yield a wrong Media Key that the final
|
||||||
|
/// Verify-Media-Key gate rejects. So the variant chain cannot complete on a real
|
||||||
|
/// disc today — a key-acquisition gap, not a code gap.
|
||||||
const KEY_CORRECTION_DATA: [u8; 16] = [
|
const KEY_CORRECTION_DATA: [u8; 16] = [
|
||||||
0x3b, 0x62, 0x8a, 0x78, 0x29, 0x00, 0xca, 0x2f, 0xdb, 0xe7, 0x7a, 0x49, 0xfe, 0x22, 0xd6, 0x6e,
|
0x3b, 0x62, 0x8a, 0x78, 0x29, 0x00, 0xca, 0x2f, 0xdb, 0xe7, 0x7a, 0x49, 0xfe, 0x22, 0xd6, 0x6e,
|
||||||
];
|
];
|
||||||
@@ -69,38 +87,41 @@ const KEY_CORRECTION_DATA: [u8; 16] = [
|
|||||||
/// The earlier `0x82`/`0x83` guess was speculative and never appeared in any
|
/// The earlier `0x82`/`0x83` guess was speculative and never appeared in any
|
||||||
/// real MKB.
|
/// real MKB.
|
||||||
pub fn is_variant_mkb(records: &[MkbRecord]) -> bool {
|
pub fn is_variant_mkb(records: &[MkbRecord]) -> bool {
|
||||||
records.iter().any(|r| matches!(r.rec_type, 0x2d | 0x2f))
|
records
|
||||||
|
.iter()
|
||||||
|
.any(|r| matches!(r.rec_type, REC_VARIANT_DATA_AND_NONCE | REC_VKD_TABLE))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Body of the Encrypted Media Key Variant Data record (type `0x2d`).
|
/// Body of the `0x2d` record: the `VARIANTS` table followed by the trailing
|
||||||
///
|
/// 16-byte `Kvn` Nonce. Measured `46_100*2 + 16 = 92_216` on Zombieland v70 and
|
||||||
/// Confirmed against a live variant MKB as the `0x2d` record (92,220 bytes on
|
/// `92_220` on Stand By Me v70 — in both, the leading `body.len() - 16` bytes are
|
||||||
/// the reference disc — not a clean multiple of 16, so it is a structured /
|
/// the big-endian `u16` `VARIANTS` table (one per subset-difference) and the last
|
||||||
/// count-prefixed record, not a flat C-block table). The exact per-uv C
|
/// 16 bytes are the Nonce, with NO leading header. This does NOT hold the C used
|
||||||
/// selection is the one sub-field still unconfirmed without a real disc+key to
|
/// for `Kmp` — that is the per-slot block in `0x0c`
|
||||||
/// test against; the chain currently consumes the leading 16-byte block.
|
/// ([`REC_MEDIA_KEY_VARIANT_DATA`]). Both [`variant_nonce`] and
|
||||||
/// `pub(crate)` until that offset is pinned.
|
/// [`variants_for_uv`] read this body.
|
||||||
pub(crate) fn variant_data_record(records: &[MkbRecord]) -> Option<&[u8]> {
|
pub(crate) fn variant_data_record(records: &[MkbRecord]) -> Option<&[u8]> {
|
||||||
records
|
records
|
||||||
.iter()
|
.iter()
|
||||||
.find(|r| r.rec_type == 0x2d)
|
.find(|r| r.rec_type == REC_VARIANT_DATA_AND_NONCE)
|
||||||
.map(|r| r.body.as_slice())
|
.map(|r| r.body.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 16-byte Nonce for `Kvn = AES-G(Kp, Nonce)`.
|
/// 16-byte Nonce for `Kvn = AES-G(Kp, Nonce)` — the trailing 16 bytes of the
|
||||||
|
/// `0x2d` record ([`variant_data_record`]).
|
||||||
///
|
///
|
||||||
/// **UNCONFIRMED source.** The `0x2d` Encrypted-Media-Key-Variant-Data record is
|
/// The Nonce-at-tail placement is consistent across both reference MKBs (the
|
||||||
/// the most likely home for a per-disc nonce, so this reads its trailing 16
|
/// leading `body-16` bytes form the `VARIANTS` table exactly), but head-vs-tail
|
||||||
/// bytes. Confirming this (vs. a fixed slice elsewhere in `0x2d`) needs a
|
/// is only truly pinned by running the full chain against the `0x86` verify with
|
||||||
/// covering key to run the whole chain against the `0x86` verify; until then a
|
/// a covering key. Until then a wrong nonce can only fail that final gate, never
|
||||||
/// wrong nonce can only fail that final gate, never emit a bad key.
|
/// emit a bad key.
|
||||||
pub fn variant_nonce(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
pub fn variant_nonce(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
||||||
let r = records.iter().find(|r| r.rec_type == 0x2d)?;
|
let body = variant_data_record(records)?;
|
||||||
if r.body.len() < 16 {
|
if body.len() < 16 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut out = [0u8; 16];
|
let mut out = [0u8; 16];
|
||||||
out.copy_from_slice(&r.body[r.body.len() - 16..]);
|
out.copy_from_slice(&body[body.len() - 16..]);
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +133,7 @@ pub fn variant_nonce(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
|||||||
pub(crate) fn variant_key_data(records: &[MkbRecord]) -> Option<&[u8]> {
|
pub(crate) fn variant_key_data(records: &[MkbRecord]) -> Option<&[u8]> {
|
||||||
records
|
records
|
||||||
.iter()
|
.iter()
|
||||||
.find(|r| r.rec_type == 0x2f && !r.body.is_empty() && r.body.len() % 16 == 0)
|
.find(|r| r.rec_type == REC_VKD_TABLE && !r.body.is_empty() && r.body.len() % 16 == 0)
|
||||||
.map(|r| r.body.as_slice())
|
.map(|r| r.body.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +161,10 @@ pub struct ProcessingKeyMatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
||||||
let r = records
|
let r = records.iter().find(|r| {
|
||||||
.iter()
|
(r.rec_type == REC_VERIFY_MEDIA_KEY_V1 || r.rec_type == REC_VERIFY_MEDIA_KEY_V2)
|
||||||
.find(|r| (r.rec_type == 0x81 || r.rec_type == 0x86) && r.body.len() >= 16)?;
|
&& r.body.len() >= 16
|
||||||
|
})?;
|
||||||
let mut out = [0u8; 16];
|
let mut out = [0u8; 16];
|
||||||
out.copy_from_slice(&r.body[..16]);
|
out.copy_from_slice(&r.body[..16]);
|
||||||
Some(out)
|
Some(out)
|
||||||
@@ -176,14 +198,14 @@ pub fn walk_processing_key(
|
|||||||
device_keys: &[DeviceKey],
|
device_keys: &[DeviceKey],
|
||||||
) -> Option<ProcessingKeyMatch> {
|
) -> Option<ProcessingKeyMatch> {
|
||||||
let mk_dv = mkb_find_mk_dv(records)?;
|
let mk_dv = mkb_find_mk_dv(records)?;
|
||||||
let uvs = mkb_find_body(records, 0x04)?;
|
let uvs = mkb_find_body(records, REC_SUBSET_DIFFERENCE)?;
|
||||||
// Variant cvalue source: a real variant MKB carries its per-uv cvalue table
|
// Variant cvalue source: a real variant MKB carries its per-uv cvalue table
|
||||||
// in record `0x0c` (confirmed 46,101×16, one per `0x04` subset-difference
|
// in record `0x0c` (confirmed 46,101×16, one per `0x04` subset-difference
|
||||||
// slot). Fall back to `0x07`/`0x05` for the synthetic fixtures and any MKB
|
// slot). Fall back to `0x07`/`0x05` for the synthetic fixtures and any MKB
|
||||||
// shape that keeps its cvalues there.
|
// shape that keeps its cvalues there.
|
||||||
let cvalues = mkb_find_body(records, 0x0c)
|
let cvalues = mkb_find_body(records, REC_MEDIA_KEY_VARIANT_DATA)
|
||||||
.or_else(|| mkb_find_body(records, 0x07))
|
.or_else(|| mkb_find_body(records, REC_EXPLICIT_SUBSET_DIFF))
|
||||||
.or_else(|| mkb_find_body(records, 0x05))?;
|
.or_else(|| mkb_find_body(records, REC_MEDIA_KEY_DATA))?;
|
||||||
|
|
||||||
let num_uvs = uvs
|
let num_uvs = uvs
|
||||||
.chunks(5)
|
.chunks(5)
|
||||||
@@ -340,10 +362,18 @@ impl std::error::Error for MediaKeyVariantError {}
|
|||||||
/// yield a wrong `Km`, which the final Verify-Media-Key gate rejects (never a
|
/// yield a wrong `Km`, which the final Verify-Media-Key gate rejects (never a
|
||||||
/// silent bad key).
|
/// silent bad key).
|
||||||
fn variants_for_uv(records: &[MkbRecord], sd_slot_index: usize) -> Option<u16> {
|
fn variants_for_uv(records: &[MkbRecord], sd_slot_index: usize) -> Option<u16> {
|
||||||
let body = records.iter().find(|r| r.rec_type == 0x2d)?.body.as_slice();
|
let body = variant_data_record(records)?;
|
||||||
|
// The VARIANTS table is the leading bytes; the 16-byte Kvn Nonce is packed at
|
||||||
|
// the TAIL (see [`variant_nonce`]). Bound the read to the table region so a
|
||||||
|
// near-end slot can never read Nonce bytes as a VARIANTS entry. NO leading
|
||||||
|
// header (measured: Zombieland v70 `0x2d` body = 46_100*2 + 16 = 92_216).
|
||||||
|
const NONCE: usize = 16;
|
||||||
|
let table_len = body.len().checked_sub(NONCE)?;
|
||||||
let off = sd_slot_index.checked_mul(2)?;
|
let off = sd_slot_index.checked_mul(2)?;
|
||||||
let bytes = body.get(off..off + 2)?;
|
if off + 2 > table_len {
|
||||||
Some(u16::from_be_bytes([bytes[0], bytes[1]]))
|
return None;
|
||||||
|
}
|
||||||
|
Some(u16::from_be_bytes([body[off], body[off + 1]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerate the `(uv, slot_index)` pairs of a variant MKB's subset-difference
|
/// Enumerate the `(uv, slot_index)` pairs of a variant MKB's subset-difference
|
||||||
@@ -351,7 +381,7 @@ fn variants_for_uv(records: &[MkbRecord], sd_slot_index: usize) -> Option<u16> {
|
|||||||
/// to index cvalues. Factored out so a bare Processing Key (which arrives without
|
/// to index cvalues. Factored out so a bare Processing Key (which arrives without
|
||||||
/// its slot) can be tried against each slot.
|
/// its slot) can be tried against each slot.
|
||||||
fn variant_uv_slots(records: &[MkbRecord]) -> Option<Vec<(u32, usize)>> {
|
fn variant_uv_slots(records: &[MkbRecord]) -> Option<Vec<(u32, usize)>> {
|
||||||
let uvs = mkb_find_body(records, 0x04)?;
|
let uvs = mkb_find_body(records, REC_SUBSET_DIFFERENCE)?;
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut idx = 0usize;
|
let mut idx = 0usize;
|
||||||
while (idx + 1) * 5 <= uvs.len() {
|
while (idx + 1) * 5 <= uvs.len() {
|
||||||
@@ -378,7 +408,11 @@ struct VariantMkb<'a> {
|
|||||||
records: &'a [MkbRecord],
|
records: &'a [MkbRecord],
|
||||||
nonce: [u8; 16],
|
nonce: [u8; 16],
|
||||||
vkd_table: &'a [u8],
|
vkd_table: &'a [u8],
|
||||||
c_block: [u8; 16],
|
/// The per-subset-difference Encrypted-Media-Key-Variant-Data (C) table from
|
||||||
|
/// record `0x0c` — one 16-byte C per slot. C for slot `i` is
|
||||||
|
/// `cvalues[i*16..][..16]`, the SAME source/index [`walk_processing_key`]
|
||||||
|
/// uses. (NOT `0x2d`, which is VARIANTS + Nonce.)
|
||||||
|
cvalues: &'a [u8],
|
||||||
mk_dv: [u8; 16],
|
mk_dv: [u8; 16],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,8 +426,21 @@ fn variant_km_for_slot(
|
|||||||
uv: u32,
|
uv: u32,
|
||||||
slot_index: usize,
|
slot_index: usize,
|
||||||
) -> Result<[u8; 16], MediaKeyVariantError> {
|
) -> Result<[u8; 16], MediaKeyVariantError> {
|
||||||
|
// C for THIS subset-difference: the slot's 16-byte block in the `0x0c`
|
||||||
|
// Encrypted-Media-Key-Variant-Data table (same index that selected the
|
||||||
|
// cvalue in `walk_processing_key`). `0x2d` is VARIANTS + Nonce, not C.
|
||||||
|
let cv_off = slot_index
|
||||||
|
.checked_mul(16)
|
||||||
|
.ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
|
let c_slice = m
|
||||||
|
.cvalues
|
||||||
|
.get(cv_off..cv_off + 16)
|
||||||
|
.ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
|
let mut c_block = [0u8; 16];
|
||||||
|
c_block.copy_from_slice(c_slice);
|
||||||
|
|
||||||
// Step: Kmp = AES-128D(Kp, C) XOR uv (uv into low 4 bytes).
|
// Step: Kmp = AES-128D(Kp, C) XOR uv (uv into low 4 bytes).
|
||||||
let mut kmp = aes_ecb_decrypt(kp, &m.c_block);
|
let mut kmp = aes_ecb_decrypt(kp, &c_block);
|
||||||
let uv_bytes = uv.to_be_bytes();
|
let uv_bytes = uv.to_be_bytes();
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
kmp[12 + i] ^= uv_bytes[i];
|
kmp[12 + i] ^= uv_bytes[i];
|
||||||
@@ -473,19 +520,21 @@ pub fn derive_media_key_variant(
|
|||||||
}
|
}
|
||||||
let nonce = variant_nonce(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
let nonce = variant_nonce(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
let vkd_table = variant_key_data(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
let vkd_table = variant_key_data(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
let c_value = variant_data_record(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
// C for the Kmp step is the per-subset-difference `0x0c` table (one 16-byte
|
||||||
if c_value.len() < 16 {
|
// C per slot) — the SAME source and index `walk_processing_key` uses. `0x2d`
|
||||||
return Err(MediaKeyVariantError::MkbIncomplete);
|
// holds VARIANTS + Nonce, NOT C. Fall back to `0x07`/`0x05` for the synthetic
|
||||||
}
|
// fixtures that keep a single cvalue there.
|
||||||
let mut c_block = [0u8; 16];
|
let cvalues = mkb_find_body(mkb_records, REC_MEDIA_KEY_VARIANT_DATA)
|
||||||
c_block.copy_from_slice(&c_value[..16]);
|
.or_else(|| mkb_find_body(mkb_records, REC_EXPLICIT_SUBSET_DIFF))
|
||||||
|
.or_else(|| mkb_find_body(mkb_records, REC_MEDIA_KEY_DATA))
|
||||||
|
.ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
let mk_dv = mkb_find_mk_dv(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
let mk_dv = mkb_find_mk_dv(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
let slots = variant_uv_slots(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
let slots = variant_uv_slots(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
let m = VariantMkb {
|
let m = VariantMkb {
|
||||||
records: mkb_records,
|
records: mkb_records,
|
||||||
nonce,
|
nonce,
|
||||||
vkd_table,
|
vkd_table,
|
||||||
c_block,
|
cvalues,
|
||||||
mk_dv,
|
mk_dv,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -506,6 +555,79 @@ pub fn derive_media_key_variant(
|
|||||||
Err(correction.unwrap_or(MediaKeyVariantError::ProcessingKeyUnavailable))
|
Err(correction.unwrap_or(MediaKeyVariantError::ProcessingKeyUnavailable))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the variant chain from a caller-supplied Processing Key and EXPLICIT
|
||||||
|
/// per-slot inputs — the harness entry that tries a captured `Kp` against known
|
||||||
|
/// slot material, bypassing both the device-key walk and the on-MKB
|
||||||
|
/// `VARIANTS[uv]` lookup. The caller supplies the `0x0c` C block, the slot's
|
||||||
|
/// subset-difference number `uv`, and its `VARIANTS[uv]`; the MKB supplies the
|
||||||
|
/// Nonce, the VKD table, and the Verify-Media-Key value.
|
||||||
|
///
|
||||||
|
/// Returns `(Km, Kvu)`. The terminal Verify-Media-Key gate is identical to
|
||||||
|
/// [`derive_media_key_variant`], so a wrong `c_block` / `uv` / `variants_uv`
|
||||||
|
/// returns [`MediaKeyVariantError::MediaKeyVerifyFailed`] rather than a bogus
|
||||||
|
/// key. The soft-correction / online-challenge bits on `Kmp[15]` are classified
|
||||||
|
/// the same way, so a slot needing an out-of-band correction path is
|
||||||
|
/// distinguishable from a non-matching input.
|
||||||
|
///
|
||||||
|
/// (Note the KCD caveat on [`KEY_CORRECTION_DATA`]: without the real per-licensee
|
||||||
|
/// KCD this fails the verify gate on a real disc — a key-acquisition gap.)
|
||||||
|
pub fn media_key_variant_from_kp(
|
||||||
|
kp: &[u8; 16],
|
||||||
|
c_block: &[u8; 16],
|
||||||
|
uv: u32,
|
||||||
|
variants_uv: u16,
|
||||||
|
mkb_records: &[MkbRecord],
|
||||||
|
vid: &[u8; 16],
|
||||||
|
) -> Result<([u8; 16], [u8; 16]), MediaKeyVariantError> {
|
||||||
|
let nonce = variant_nonce(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
|
let vkd_table = variant_key_data(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
|
let mk_dv = mkb_find_mk_dv(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
|
|
||||||
|
// Kmp = AES-128D(Kp, C) XOR uv.
|
||||||
|
let mut kmp = aes_ecb_decrypt(kp, c_block);
|
||||||
|
let uv_bytes = uv.to_be_bytes();
|
||||||
|
for i in 0..4 {
|
||||||
|
kmp[12 + i] ^= uv_bytes[i];
|
||||||
|
}
|
||||||
|
if kmp[15] & 0b0000_0010 != 0 {
|
||||||
|
return Err(MediaKeyVariantError::SoftCorrectionRequired);
|
||||||
|
}
|
||||||
|
if kmp[15] & 0b0000_0100 != 0 {
|
||||||
|
return Err(MediaKeyVariantError::OnlineChallengeRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kpnew = Kmp XOR KCD.
|
||||||
|
let mut kpnew = [0u8; 16];
|
||||||
|
for i in 0..16 {
|
||||||
|
kpnew[i] = kmp[i] ^ KEY_CORRECTION_DATA[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kvn = AES-G(Kp, Nonce) & 0xFFFF; VKD_idx = Kvn XOR VARIANTS[uv].
|
||||||
|
let kvn_block = aes_g(kp, &nonce);
|
||||||
|
let kvn = u16::from_be_bytes([kvn_block[14], kvn_block[15]]);
|
||||||
|
let vkd_idx = kvn ^ variants_uv;
|
||||||
|
let off = (vkd_idx as usize) * 16;
|
||||||
|
if off + 16 > vkd_table.len() {
|
||||||
|
return Err(MediaKeyVariantError::VkdIndexOutOfRange);
|
||||||
|
}
|
||||||
|
let mut vkd = [0u8; 16];
|
||||||
|
vkd.copy_from_slice(&vkd_table[off..off + 16]);
|
||||||
|
|
||||||
|
// Km = AES-128D(Kpnew, VKD) XOR uv, then the authoritative Verify-Media-Key gate.
|
||||||
|
let mut km = aes_ecb_decrypt(&kpnew, &vkd);
|
||||||
|
for i in 0..4 {
|
||||||
|
km[12 + i] ^= uv_bytes[i];
|
||||||
|
}
|
||||||
|
const VERIFY_MAGIC: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
|
||||||
|
if aes_ecb_decrypt(&km, &mk_dv)[..8] != VERIFY_MAGIC {
|
||||||
|
return Err(MediaKeyVariantError::MediaKeyVerifyFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kvu = AES-G(Km, VID).
|
||||||
|
let kvu = aes_g(&km, vid);
|
||||||
|
Ok((km, kvu))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -707,21 +829,24 @@ mod tests {
|
|||||||
aes_d_result[15] ^= 0x02;
|
aes_d_result[15] ^= 0x02;
|
||||||
let c_block = aes_ecb_encrypt(&kp, &aes_d_result);
|
let c_block = aes_ecb_encrypt(&kp, &aes_d_result);
|
||||||
|
|
||||||
// cvalues record (0x07): one 16-byte cvalue. The walker
|
// cvalues record (0x07): the per-SD C the chain reads for `Kmp`. This
|
||||||
// indexes it for the magic-check step; on a variant MKB the
|
// fixture has no `0x0c`, so both the walk and the chain fall back to
|
||||||
// magic check fails but `variant_present` is true so the
|
// `0x07` — plant the computed `c_block` HERE so `AES-D(Kp, C) XOR uv ==
|
||||||
// walker still returns the match. Content is don't-care.
|
// Kmp` and the chosen `kmp15` bit lands. On a variant MKB the per-match
|
||||||
|
// magic check fails, but `variant_present` is true, so the walk still
|
||||||
|
// returns the match.
|
||||||
mkb.extend_from_slice(&[0x07, 0x00, 0x00, 0x14]);
|
mkb.extend_from_slice(&[0x07, 0x00, 0x00, 0x14]);
|
||||||
mkb.extend_from_slice(&[0xAB; 16]);
|
mkb.extend_from_slice(&c_block);
|
||||||
|
|
||||||
// Verify Media Key (0x86): body content is don't-care.
|
// Verify Media Key (0x86): body content is don't-care.
|
||||||
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]);
|
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]);
|
||||||
mkb.extend_from_slice(&[0xCD; 16]);
|
mkb.extend_from_slice(&[0xCD; 16]);
|
||||||
|
|
||||||
// 0x2d record: Encrypted Media Key Variant Data — C (head 16) then a
|
// 0x2d record: VARIANTS table (head, don't-care for these tests) then the
|
||||||
// trailing 16-byte Nonce (variant_nonce reads the tail), 32-byte body.
|
// trailing 16-byte Nonce (`variant_nonce` reads the tail), 32-byte body.
|
||||||
|
// (C is NOT here — it is the `0x07`/`0x0c` cvalue above.)
|
||||||
mkb.extend_from_slice(&[0x2d, 0x00, 0x00, 0x24]);
|
mkb.extend_from_slice(&[0x2d, 0x00, 0x00, 0x24]);
|
||||||
mkb.extend_from_slice(&c_block);
|
mkb.extend_from_slice(&[0x11; 16]);
|
||||||
mkb.extend_from_slice(&[0x77; 16]);
|
mkb.extend_from_slice(&[0x77; 16]);
|
||||||
|
|
||||||
// 0x2f record: Variant Key Data table — one 16-byte entry.
|
// 0x2f record: Variant Key Data table — one 16-byte entry.
|
||||||
@@ -1020,4 +1145,31 @@ mod tests {
|
|||||||
let codes: HashSet<String> = cases.iter().map(|e| e.to_string()).collect();
|
let codes: HashSet<String> = cases.iter().map(|e| e.to_string()).collect();
|
||||||
assert_eq!(codes.len(), cases.len(), "all error codes must be unique");
|
assert_eq!(codes.len(), cases.len(), "all error codes must be unique");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `media_key_variant_from_kp` runs the full chain from explicit inputs and
|
||||||
|
/// classifies the `Kmp[15]` soft-correction bit. A `c_block` chosen so
|
||||||
|
/// `AES-D(Kp, C) == Kmp` with bit `0x02` set (uv=0) must surface
|
||||||
|
/// `SoftCorrectionRequired` before it touches the VKD / verify steps —
|
||||||
|
/// proving the explicit-input entry runs the same chain and gates.
|
||||||
|
#[test]
|
||||||
|
fn media_key_variant_from_kp_classifies_soft_correction() {
|
||||||
|
use crate::aacs::crypto::aes_ecb_encrypt;
|
||||||
|
let kp = [0x11u8; 16];
|
||||||
|
// Plant Kmp[15]=0x02 (soft-correction) with uv=0 so Kmp == AES-D(kp, C).
|
||||||
|
let mut target_kmp = [0x00u8; 16];
|
||||||
|
target_kmp[15] = 0x02;
|
||||||
|
let c_block = aes_ecb_encrypt(&kp, &target_kmp);
|
||||||
|
// Minimal variant MKB: 0x2d (16-byte body = tail Nonce), 0x2f (one VKD
|
||||||
|
// entry), 0x86 (Verify-Media-Key).
|
||||||
|
let mut mkb = vec![0x2d, 0x00, 0x00, 0x14];
|
||||||
|
mkb.extend_from_slice(&[0x99; 16]);
|
||||||
|
mkb.extend_from_slice(&[0x2f, 0x00, 0x00, 0x14]);
|
||||||
|
mkb.extend_from_slice(&[0xBB; 16]);
|
||||||
|
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]);
|
||||||
|
mkb.extend_from_slice(&[0xCD; 16]);
|
||||||
|
let recs = walk_mkb(&mkb);
|
||||||
|
let err = media_key_variant_from_kp(&kp, &c_block, 0, 0, &recs, &[0u8; 16])
|
||||||
|
.expect_err("soft-correction bit → classified, not a key");
|
||||||
|
assert_eq!(err, MediaKeyVariantError::SoftCorrectionRequired);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user