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:
+11
-7
@@ -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
|
||||
|
||||
+20
-14
@@ -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<Vec<u8>> {
|
||||
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<u32> {
|
||||
// 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<bool> {
|
||||
|
||||
// ── 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))
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
+19
-5
@@ -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<MkbRecord>, DeviceKey, [u8; 16], [u8; 16]) {
|
||||
|
||||
Reference in New Issue
Block a user