diff --git a/src/aacs/decrypt.rs b/src/aacs/decrypt.rs index e28090f..8694d9f 100644 --- a/src/aacs/decrypt.rs +++ b/src/aacs/decrypt.rs @@ -5,12 +5,12 @@ use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArr // ── 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] = [ 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; /// An AACS aligned unit spans this many 2048-byte sectors (3). @@ -49,7 +49,7 @@ const TS_SYNC: u8 = 0x47; // ── 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] { let cipher = Aes128::new(GenericArray::from_slice(key)); 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 } -/// 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] { let cipher = Aes128::new(GenericArray::from_slice(key)); 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 } -/// 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 /// 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 -/// 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 /// in the clear (the first 16 bytes of every unit are the unencrypted SEED). So /// 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 - // 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]; header.copy_from_slice(&unit[..16]); 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 { 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]); // Verify content packets; zero out padding packets (their decrypted bytes are diff --git a/src/aacs/keys.rs b/src/aacs/keys.rs index 5eb43fe..6b34e81 100644 --- a/src/aacs/keys.rs +++ b/src/aacs/keys.rs @@ -63,7 +63,8 @@ impl AacsVersion { // ── 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 pub fn derive_vuk(media_key: &[u8; 16], volume_id: &[u8; 16]) -> [u8; 16] { 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 } -/// 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] { aes_ecb_decrypt(vuk, encrypted_uk) } @@ -316,11 +318,11 @@ fn try_pk_against_tables( /// Validate a processing key against a cvalue/UV pair. /// Returns the Media Key if valid. /// -/// Steps: -/// 1. `mk = AES-128D(pk, cvalue)` -/// 2. `mk[12..16] ^= uv` (4 bytes XOR into the last 4 bytes only) -/// 3. `dec_vd = AES-128D(mk, mk_dv)` -/// 4. If `dec_vd[0..8] == 01 23 45 67 89 AB CD EF` → valid. +/// Steps (media key: [C] §3.2.4; verify relation: [C] §3.2.5.1.4): +/// 1. `mk = AES-128D(pk, cvalue)` [C] §3.2.4 +/// 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)` [C] §3.2.5.1.4 +/// 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( pk: &[u8; 16], 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. +/// 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. @@ -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> { 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 /// 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 -/// 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 { // Type-and-Version record (0x10): the 32-bit MKBType is bytes 4-7 (body // offset 0). Needs rec_len >= 8 (4 header + 4 type). @@ -597,12 +600,13 @@ pub fn mkb_is_uhd(mkb: &[u8]) -> Option { // ── 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] = [ 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. /// /// 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 } -/// 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 { let mut v_mask: u32 = 0xFFFF_FFFF; 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. -/// 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( dk: &[u8; 16], uv: u32, @@ -731,9 +735,11 @@ pub fn derive_media_key_and_pk_from_dk( 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 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)) && ((device_number & v_mask) != (uv & v_mask)) { diff --git a/src/aacs/mod.rs b/src/aacs/mod.rs index 67a36e7..556ecfb 100644 --- a/src/aacs/mod.rs +++ b/src/aacs/mod.rs @@ -13,6 +13,17 @@ //! //! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc. //! 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 decrypt; diff --git a/src/aacs/variants.rs b/src/aacs/variants.rs index 0360ddf..802806e 100644 --- a/src/aacs/variants.rs +++ b/src/aacs/variants.rs @@ -41,6 +41,20 @@ //! 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 //! (Soft Correction and Online Challenge). The chain refuses to run in //! 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(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 /// 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 /// 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 +/// because the variant chain also reads `0x2d`/`0x2f`); the /// classical walk operates on raw MKB bytes. Same framing, different /// input type. /// @@ -723,9 +737,9 @@ mod tests { /// 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 - /// and Variant Key Data; satisfies the parser heuristics). - /// - record 0x83 with a 16-byte Nonce. + /// - record 0x2d (Encrypted Media Key Variant Data): a 32-byte body + /// carrying C in the head 16 bytes and a 16-byte Nonce in the tail. + /// - record 0x2f (Variant Key Data): one 16-byte entry. /// /// Returns (records, dk, planted_kp, planted_kmp). fn synthetic_variant_setup(kmp15: u8) -> (Vec, DeviceKey, [u8; 16], [u8; 16]) {