aacs: annotate crypto with AACS spec section citations

Add [C]/[PR]/[BD]/[libaacs] §x.y provenance markers across the AACS
crypto so each primitive links to the spec section it implements, with a
source-tag legend in mod.rs. Doc-comments only — no logic, constant, or
signature changes.

Also: correct two stale record-type comments in variants.rs (0x82/0x83 →
the real 0x2d/0x2f) and document the Variant Number width (spec lsb_10 vs
the 2.1 chain's lsb_16, driven by the 65,535-entry VKD table).
This commit is contained in:
Matthew Jackson
2026-07-04 13:15:53 -07:00
parent bce11a2de0
commit 188baced39
4 changed files with 61 additions and 26 deletions
+11 -7
View File
@@ -5,12 +5,12 @@ use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArr
// ── AACS constants ────────────────────────────────────────────────────────── // ── AACS constants ──────────────────────────────────────────────────────────
/// Fixed IV used by AACS for all AES-CBC operations. /// Fixed IV used by AACS for all AES-CBC operations. [C] §2.1.2 (default CBC IV, `iv0`).
pub(crate) const AACS_IV: [u8; 16] = [ pub(crate) const AACS_IV: [u8; 16] = [
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78, 0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
]; ];
/// Size of an AACS aligned unit (3 × 2048-byte sectors). /// Size of an AACS aligned unit (3 × 2048-byte sectors). [BD] §3.10.1.
pub const ALIGNED_UNIT_LEN: usize = 6144; pub const ALIGNED_UNIT_LEN: usize = 6144;
/// An AACS aligned unit spans this many 2048-byte sectors (3). /// An AACS aligned unit spans this many 2048-byte sectors (3).
@@ -49,7 +49,7 @@ const TS_SYNC: u8 = 0x47;
// ── AES primitives ────────────────────────────────────────────────────────── // ── AES primitives ──────────────────────────────────────────────────────────
/// AES-128-ECB encrypt a single 16-byte block. /// AES-128-ECB encrypt a single 16-byte block. [C] §2.1.1 (`AES-128E`).
pub(crate) fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { pub(crate) fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
let cipher = Aes128::new(GenericArray::from_slice(key)); let cipher = Aes128::new(GenericArray::from_slice(key));
let mut block = GenericArray::clone_from_slice(data); let mut block = GenericArray::clone_from_slice(data);
@@ -59,7 +59,7 @@ pub(crate) fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
out out
} }
/// AES-128-ECB decrypt a single 16-byte block. /// AES-128-ECB decrypt a single 16-byte block. [C] §2.1.1 (`AES-128D`).
pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
let cipher = Aes128::new(GenericArray::from_slice(key)); let cipher = Aes128::new(GenericArray::from_slice(key));
let mut block = GenericArray::clone_from_slice(data); let mut block = GenericArray::clone_from_slice(data);
@@ -69,7 +69,7 @@ pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
out out
} }
/// AES-128-CBC decrypt in-place with the fixed AACS IV. /// AES-128-CBC decrypt in-place with the fixed AACS IV. [C] §2.1.2 (`AES-128CBCD`).
/// ///
/// Precondition: `data.len()` is a multiple of 16. Any trailing partial /// Precondition: `data.len()` is a multiple of 16. Any trailing partial
/// block is silently ignored; all callers pass aligned regions (6128 and /// block is silently ignored; all callers pass aligned regions (6128 and
@@ -127,7 +127,7 @@ pub fn ts_sync_destroyed(unit: &[u8]) -> bool {
} }
/// The AUTHORITATIVE AACS "is this aligned unit encrypted?" signal — the Copy /// The AUTHORITATIVE AACS "is this aligned unit encrypted?" signal — the Copy
/// Permission Indicator (CPI) in the top 2 bits of byte 0. Byte 0 is the first /// Permission Indicator (CPI) in the top 2 bits of byte 0. [BD] §3.10.2. Byte 0 is the first
/// byte of the first source packet's `TP_extra_header`, which AACS always leaves /// byte of the first source packet's `TP_extra_header`, which AACS always leaves
/// in the clear (the first 16 bytes of every unit are the unencrypted SEED). So /// in the clear (the first 16 bytes of every unit are the unencrypted SEED). So
/// this is readable WITHOUT a key: /// this is readable WITHOUT a key:
@@ -385,7 +385,10 @@ pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
} }
// Save original first 16 bytes (the plaintext seed / header) and derive the // Save original first 16 bytes (the plaintext seed / header) and derive the
// per-unit decrypt key (identical to `decrypt_unit_checked`). // per-unit Block Key (identical to `decrypt_unit_checked`).
// Block Key = AES-128E(Kcu, seed) ⊕ seed ([BD] §3.10.1 Fig 3-8, two-node
// construction: encrypt the clear seed under the CPS Unit Key, then XOR the
// seed back in — the trailing ⊕seed is load-bearing).
let mut header = [0u8; 16]; let mut header = [0u8; 16];
header.copy_from_slice(&unit[..16]); header.copy_from_slice(&unit[..16]);
let derived = aes_ecb_encrypt(unit_key, &header); let derived = aes_ecb_encrypt(unit_key, &header);
@@ -393,6 +396,7 @@ pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
for i in 0..16 { for i in 0..16 {
decrypt_key[i] = derived[i] ^ header[i]; decrypt_key[i] = derived[i] ^ header[i];
} }
// Final 6128 bytes of the aligned unit under the Block Key; first 16 = clear seed. [BD] §3.10.1.
aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]); aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]);
// Verify content packets; zero out padding packets (their decrypted bytes are // Verify content packets; zero out padding packets (their decrypted bytes are
+20 -14
View File
@@ -63,7 +63,8 @@ impl AacsVersion {
// ── VUK derivation ────────────────────────────────────────────────────────── // ── VUK derivation ──────────────────────────────────────────────────────────
/// Derive VUK from Media Key and Volume ID. /// Derive VUK from Media Key and Volume ID. [PR] §3.3 / [BD] §3.3
/// (`Kvu = AES-G(Km, IDv)`; AES-G uses AES-128D):
/// VUK = AES-128-ECB-DECRYPT(media_key, volume_id) XOR volume_id /// VUK = AES-128-ECB-DECRYPT(media_key, volume_id) XOR volume_id
pub fn derive_vuk(media_key: &[u8; 16], volume_id: &[u8; 16]) -> [u8; 16] { pub fn derive_vuk(media_key: &[u8; 16], volume_id: &[u8; 16]) -> [u8; 16] {
let mut vuk = aes_ecb_decrypt(media_key, volume_id); let mut vuk = aes_ecb_decrypt(media_key, volume_id);
@@ -73,7 +74,8 @@ pub fn derive_vuk(media_key: &[u8; 16], volume_id: &[u8; 16]) -> [u8; 16] {
vuk vuk
} }
/// Decrypt an encrypted unit key using the VUK (AES-128-ECB). /// Decrypt an encrypted unit key using the VUK (AES-128-ECB). [PR] §3.5
/// (Title Key unwrap `Kt = AES-128D(Ku, Kte)`); the BD "CPS Unit Key" synonym is [BD] §3.9.3.
pub fn decrypt_unit_key(vuk: &[u8; 16], encrypted_uk: &[u8; 16]) -> [u8; 16] { pub fn decrypt_unit_key(vuk: &[u8; 16], encrypted_uk: &[u8; 16]) -> [u8; 16] {
aes_ecb_decrypt(vuk, encrypted_uk) aes_ecb_decrypt(vuk, encrypted_uk)
} }
@@ -316,11 +318,11 @@ fn try_pk_against_tables(
/// Validate a processing key against a cvalue/UV pair. /// Validate a processing key against a cvalue/UV pair.
/// Returns the Media Key if valid. /// Returns the Media Key if valid.
/// ///
/// Steps: /// Steps (media key: [C] §3.2.4; verify relation: [C] §3.2.5.1.4):
/// 1. `mk = AES-128D(pk, cvalue)` /// 1. `mk = AES-128D(pk, cvalue)` [C] §3.2.4
/// 2. `mk[12..16] ^= uv` (4 bytes XOR into the last 4 bytes only) /// 2. `mk[12..16] ^= uv` (4 bytes XOR into the last 4 bytes only) [C] §3.2.4
/// 3. `dec_vd = AES-128D(mk, mk_dv)` /// 3. `dec_vd = AES-128D(mk, mk_dv)` [C] §3.2.5.1.4
/// 4. If `dec_vd[0..8] == 01 23 45 67 89 AB CD EF` → valid. /// 4. If `dec_vd[0..8] == 01 23 45 67 89 AB CD EF` → valid. [C] §3.2.5.1.4
fn validate_processing_key( fn validate_processing_key(
pk: &[u8; 16], pk: &[u8; 16],
cvalue: &[u8], cvalue: &[u8],
@@ -400,6 +402,7 @@ pub mod probe {
} }
/// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB. /// 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]> { 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.
@@ -430,12 +433,12 @@ fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
} }
} }
/// Find Subset-Difference records (type 0x04) in MKB. /// Find Subset-Difference records (type 0x04) in MKB. [C] §3.2.5.1.5.
fn mkb_find_subdiff_records(mkb: &[u8]) -> Option<Vec<u8>> { fn mkb_find_subdiff_records(mkb: &[u8]) -> Option<Vec<u8>> {
find_record_body(mkb, 0x04) find_record_body(mkb, 0x04)
} }
/// Find the Media Key Data Record (cvalues table) in an MKB. /// 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 /// 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 /// 1.0 and AACS 2.x MKBs — its 16-byte cvalue entries are 1:1 with the
@@ -575,7 +578,7 @@ impl MkbType {
} }
/// The raw 32-bit MKBType field from the Type-and-Version record (0x10), bytes /// The raw 32-bit MKBType field from the Type-and-Version record (0x10), bytes
/// 4-7. `None` if no 0x10 record is present. /// 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<u32> { 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).
@@ -597,12 +600,13 @@ pub fn mkb_is_uhd(mkb: &[u8]) -> Option<bool> {
// ── AACS-G3 key derivation (subset-difference tree) ───────────────────────── // ── AACS-G3 key derivation (subset-difference tree) ─────────────────────────
/// AACS-G3 seed constant. /// AACS-G3 seed constant (`s0`). [C] §3.2.2.
const AESG3_SEED: [u8; 16] = [ const AESG3_SEED: [u8; 16] = [
0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9, 0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9,
]; ];
/// AACS-G3: derive a subkey from a parent key. /// AACS-G3: derive a subkey from a parent key. [C] §3.2.2 (Triple AES Generator:
/// left=`D(k,s0)⊕s0` inc 0, pk=`D(k,s0+1)⊕(s0+1)` inc 1, right=`D(k,s0+2)⊕(s0+2)` inc 2).
/// seed[15] += inc, then AES-DEC(key, seed) XOR seed. /// seed[15] += inc, then AES-DEC(key, seed) XOR seed.
/// ///
/// Shared with [`super::variants`] (its variant chain runs the same SD /// Shared with [`super::variants`] (its variant chain runs the same SD
@@ -617,7 +621,7 @@ pub(super) fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
out out
} }
/// Compute v_mask from a UV value. Shared with [`super::variants`]. /// Compute v_mask from a UV value. [C] §3.2.3. Shared with [`super::variants`].
pub(super) fn calc_v_mask(uv: u32) -> u32 { pub(super) fn calc_v_mask(uv: u32) -> u32 {
let mut v_mask: u32 = 0xFFFF_FFFF; let mut v_mask: u32 = 0xFFFF_FFFF;
while (uv & !v_mask) == 0 && v_mask != 0 { while (uv & !v_mask) == 0 && v_mask != 0 {
@@ -627,7 +631,7 @@ pub(super) fn calc_v_mask(uv: u32) -> u32 {
} }
/// Derive processing key from device key using subset-difference tree traversal. /// Derive processing key from device key using subset-difference tree traversal.
/// Shared with [`super::variants`]. /// [C] §3.2.4 (device-tree descent, MSB-branch, terminal PK). Shared with [`super::variants`].
pub(super) fn calc_pk_from_dk( pub(super) fn calc_pk_from_dk(
dk: &[u8; 16], dk: &[u8; 16],
uv: u32, uv: u32,
@@ -731,9 +735,11 @@ pub fn derive_media_key_and_pk_from_dk(
continue; continue;
} }
// u-mask = shift count of low-order 0 bits ([C] §3.2.5.1.5); v-mask [C] §3.2.3.
let u_mask: u32 = 0xFFFF_FFFF << u_mask_shift; let u_mask: u32 = 0xFFFF_FFFF << u_mask_shift;
let v_mask = calc_v_mask(uv); let v_mask = calc_v_mask(uv);
// Subset-difference applies iff (d&mu)==(uv&mu) && (d&mv)!=(uv&mv). [C] §3.2.4.
if ((device_number & u_mask) == (uv & u_mask)) if ((device_number & u_mask) == (uv & u_mask))
&& ((device_number & v_mask) != (uv & v_mask)) && ((device_number & v_mask) != (uv & v_mask))
{ {
+11
View File
@@ -13,6 +13,17 @@
//! //!
//! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc. //! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc.
//! Title keys decrypt m2ts stream content (AES-128-CBC). //! Title keys decrypt m2ts stream content (AES-128-CBC).
//!
//! ## Spec provenance
//!
//! The crypto below carries `[TAG] §x.y` citations back to the published AACS
//! specification (Final Rev 0.953), so each primitive links to the section it
//! implements:
//! - `[C]` — AACS Introduction and Common Cryptographic Elements Book (primitives, MKB/key-management).
//! - `[PR]` — AACS Pre-recorded Video Book (Volume/Title Key layer).
//! - `[BD]` — AACS Blu-ray Disc Pre-recorded Book (CPS Unit Key, Aligned Unit, Block Key).
//! - `[libaacs]` — the libaacs reference implementation, cited only where the spec
//! is silent (the `0x86` verify record and the Category-C MKBType names).
pub mod boil; pub mod boil;
pub mod decrypt; pub mod decrypt;
+19 -5
View File
@@ -41,6 +41,20 @@
//! Km = AES-128D(Kpnew, VKD) XOR uv //! Km = AES-128D(Kpnew, VKD) XOR uv
//! ``` //! ```
//! //!
//! **Spec note — Variant Number width (`Kvn`).** The published AACS
//! Sequence-Key Variant Number (Introduction and Common Cryptographic
//! Elements book, Rev 0.953, §3.2.5.2.2, record `0x0D`) is the **low 10
//! bits** of `AES-G(Kp, Nonce)` — a range of ≤1024 variants. This 2.1
//! chain instead takes the **low 16 bits** (`& 0xFFFF`), because it
//! indexes the 2.1 VKD table (`0x2f`), which carries up to 65,535 entries:
//! the wider index is demanded by the larger table, not a mis-transcription
//! of the 10-bit spec value. Both the 16-bit width and the Nonce source
//! (tail of `0x2d`) are RE-derived from a single live variant MKB and
//! remain UNCONFIRMED — the spec's `0x0D` "Variant Number" record does not
//! appear on a real 2.1 MKB. If a covering key ever lets the chain run
//! end-to-end against the `0x86` verify, this width is the first thing to
//! confirm.
//!
//! Two condition bits on `Kmp[15]` route off the hardcoded-KCD path //! Two condition bits on `Kmp[15]` route off the hardcoded-KCD path
//! (Soft Correction and Online Challenge). The chain refuses to run in //! (Soft Correction and Online Challenge). The chain refuses to run in
//! either case — callers must handle those modes out of band. //! either case — callers must handle those modes out of band.
@@ -191,7 +205,7 @@ pub(crate) fn variant_key_data(records: &[MkbRecord]) -> Option<&[u8]> {
// ── AES-G ──────────────────────────────────────────────────────────────── // ── AES-G ────────────────────────────────────────────────────────────────
/// AES-G(x1, x2) = AES-128D(x1, x2) XOR x2. /// AES-G(x1, x2) = AES-128D(x1, x2) XOR x2. [C] §2.1.3 (note: uses AES-128**D**).
/// ///
/// The Media Key Variant chain uses AES-G to derive both the variant /// The Media Key Variant chain uses AES-G to derive both the variant
/// number (`Kvn = AES-G(Kp, Nonce)`) and the Volume Unique Key /// number (`Kvn = AES-G(Kp, Nonce)`) and the Volume Unique Key
@@ -261,7 +275,7 @@ fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
/// note on [`super::keys::probe::mkb_cvalues`]). They must NOT be /// note on [`super::keys::probe::mkb_cvalues`]). They must NOT be
/// unified to one order — each is correct for its own MKB shape. /// unified to one order — each is correct for its own MKB shape.
/// - finders: this walk operates on parsed [`MkbRecord`]s (needed /// - finders: this walk operates on parsed [`MkbRecord`]s (needed
/// because the variant chain also reads `0x82`/`0x83`); the /// because the variant chain also reads `0x2d`/`0x2f`); the
/// classical walk operates on raw MKB bytes. Same framing, different /// classical walk operates on raw MKB bytes. Same framing, different
/// input type. /// input type.
/// ///
@@ -723,9 +737,9 @@ mod tests {
/// times — Kp = aesg3(dk, 1). /// times — Kp = aesg3(dk, 1).
/// - one cvalue in record 0x07 chosen so AES-D(Kp, C) ⊕ uv produces a /// - one cvalue in record 0x07 chosen so AES-D(Kp, C) ⊕ uv produces a
/// Kmp whose byte-15 is exactly `kmp15`. /// Kmp whose byte-15 is exactly `kmp15`.
/// - record 0x82 with a 16-byte body (acts as both Variant Data /// - record 0x2d (Encrypted Media Key Variant Data): a 32-byte body
/// and Variant Key Data; satisfies the parser heuristics). /// carrying C in the head 16 bytes and a 16-byte Nonce in the tail.
/// - record 0x83 with a 16-byte Nonce. /// - record 0x2f (Variant Key Data): one 16-byte entry.
/// ///
/// Returns (records, dk, planted_kp, planted_kmp). /// Returns (records, dk, planted_kp, planted_kmp).
fn synthetic_variant_setup(kmp15: u8) -> (Vec<MkbRecord>, DeviceKey, [u8; 16], [u8; 16]) { fn synthetic_variant_setup(kmp15: u8) -> (Vec<MkbRecord>, DeviceKey, [u8; 16], [u8; 16]) {