From 84aaceceb7751ff622a4ed05bd41d2f4bdda7bb4 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 4 Jul 2026 14:14:56 -0700 Subject: [PATCH] aacs: merge media_key + volume_key into derive.rs The volume_key module was only 34 lines and is just the tail of the same DK/PK -> MK -> VUK -> UK derivation ladder as media_key. Fold both into one derive module so every aacs module is a substantial, distinct responsibility (crypto/mkb/derive/inf/content/variant/resolve). Relocation only; logic hash identical (95fb9924); 2210 tests green. --- src/aacs/boil.rs | 10 ++++---- src/aacs/{media_key.rs => derive.rs} | 32 ++++++++++++++++++++++++++ src/aacs/mod.rs | 17 +++++++------- src/aacs/resolve.rs | 6 ++--- src/aacs/variant.rs | 4 ++-- src/aacs/volume_key.rs | 34 ---------------------------- 6 files changed, 49 insertions(+), 54 deletions(-) rename src/aacs/{media_key.rs => derive.rs} (92%) delete mode 100644 src/aacs/volume_key.rs diff --git a/src/aacs/boil.rs b/src/aacs/boil.rs index 6023642..5dd8982 100644 --- a/src/aacs/boil.rs +++ b/src/aacs/boil.rs @@ -26,9 +26,9 @@ //! the processing-key path starts from a precomputed PK. Neither needs a VID //! (the VID enters at `vuk_from_mk`). -use super::media_key::{derive_media_key_and_pk_from_dk, derive_media_key_from_pk}; +use super::derive::{decrypt_unit_key, derive_vuk}; +use super::derive::{derive_media_key_and_pk_from_dk, derive_media_key_from_pk}; use super::types::DeviceKey; -use super::volume_key::{decrypt_unit_key, derive_vuk}; /// Volume ID (16 bytes) — read from the disc via the SCSI handshake / OEM path. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -203,8 +203,8 @@ pub fn resolve_candidate( unit_key_ro: &[u8], vid: Option, ) -> Option { + use super::derive::derive_media_key_and_pk_from_dk; use super::inf::parse_unit_key_ro; - use super::media_key::derive_media_key_and_pk_from_dk; use super::mkb::{AacsVersion, mkb_type}; // Boil a VUK → all unit keys, each paired with its declared CPS-unit number. @@ -219,7 +219,7 @@ pub fn resolve_candidate( if ukf.encrypted_keys.is_empty() { return None; } - Some(super::volume_key::derive_unit_keys(&ukf, &vuk.0)) + Some(super::derive::derive_unit_keys(&ukf, &vuk.0)) }; match candidate { @@ -277,7 +277,7 @@ pub fn resolve_candidate( mod tests { use super::*; use crate::aacs::crypto::aes_ecb_encrypt; - use crate::aacs::volume_key::{decrypt_unit_key, derive_vuk}; + use crate::aacs::derive::{decrypt_unit_key, derive_vuk}; /// `vuk_from_mk` must equal the inline `derive_vuk` path bit-for-bit, for /// several known (MK, VID) vectors. diff --git a/src/aacs/media_key.rs b/src/aacs/derive.rs similarity index 92% rename from src/aacs/media_key.rs rename to src/aacs/derive.rs index afc4f78..89fc1bd 100644 --- a/src/aacs/media_key.rs +++ b/src/aacs/derive.rs @@ -2,6 +2,7 @@ //! [C] §3.2.2–§3.2.5. use super::crypto::*; +use super::inf::*; use super::mkb::*; use super::types::*; @@ -409,3 +410,34 @@ pub mod probe { } } } + +// ── Volume key: Media Key + Volume ID → VUK → unit keys ────────────────────── + +/// 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); + for i in 0..16 { + vuk[i] ^= volume_id[i]; + } + vuk +} + +/// 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) +} + +/// Decrypt every encrypted unit key in a parsed `Unit_Key_RO.inf` with a VUK, +/// paired with its declared CPS-unit number. THE single VUK→unit-keys step: +/// both classical/v21 resolvers and `boil::resolve_candidate` call this, so the +/// map cannot drift between the player and harvest paths. +pub(crate) fn derive_unit_keys(uk_file: &UnitKeyFile, vuk: &[u8; 16]) -> Vec<(u32, [u8; 16])> { + uk_file + .encrypted_keys + .iter() + .map(|(num, enc_key)| (*num, decrypt_unit_key(vuk, enc_key))) + .collect() +} diff --git a/src/aacs/mod.rs b/src/aacs/mod.rs index 77dbd02..655a391 100644 --- a/src/aacs/mod.rs +++ b/src/aacs/mod.rs @@ -28,16 +28,15 @@ pub mod boil; pub mod content; pub mod crypto; +pub mod derive; pub mod host_certs; pub mod inf; -pub mod media_key; pub mod mkb; pub mod provider; pub mod resolve; pub mod trace; pub mod types; pub mod variant; -pub mod volume_key; /// On-disc UDF paths to the AACS key-input files (with their fallbacks). /// Centralised so every reader (`resolve_vid_only`, `read_aacs_inputs`, @@ -71,16 +70,17 @@ pub use content::{ // `probe` is a reproduction-harness helper (see keys.rs), not part of the // documented 1.0 surface; keep it reachable but off the rendered docs so we // don't commit semver stability to test primitives. +#[doc(hidden)] +pub use derive::probe; +pub use derive::{decrypt_unit_key, derive_vuk}; +pub use derive::{ + derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk, + recover_dk_position, +}; pub use inf::{ ContentCert, UnitKeyFile, disc_hash, disc_hash_hex, parse_content_cert, parse_unit_key_ro, read_mkb_from_drive, }; -#[doc(hidden)] -pub use media_key::probe; -pub use media_key::{ - derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk, - recover_dk_position, -}; 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, @@ -96,7 +96,6 @@ pub use variant::{ KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, ProcessingKeyMatch, derive_media_key_variant, is_variant_mkb, variant_nonce, walk_processing_key, }; -pub use volume_key::{decrypt_unit_key, derive_vuk}; #[cfg(test)] mod tests { diff --git a/src/aacs/resolve.rs b/src/aacs/resolve.rs index 6c69e92..87cc667 100644 --- a/src/aacs/resolve.rs +++ b/src/aacs/resolve.rs @@ -1,10 +1,9 @@ //! AACS key resolution — VUK derivation, MKB processing, disc hash, unit key parsing. use super::crypto::aes_ecb_decrypt; +use super::derive::*; use super::inf::*; -use super::media_key::*; use super::mkb::*; -use super::volume_key::*; // // Canonical form is `1003` (low 16 bits `0x1003` is a fixed marker). @@ -457,13 +456,12 @@ mod tests { // This suite predates the module split; it white-box-tests items now living // in sibling modules. Pull them all in so the tests keep exercising them. use super::super::crypto::*; + use super::super::derive::*; use super::super::inf::*; - use super::super::media_key::*; use super::super::mkb::*; use super::super::provider::SuppliedKey; use super::super::types::DiscEntry; use super::super::types::*; - use super::super::volume_key::*; use super::*; /// Audit #5: the `major` / `from_major` mapping is load-bearing for the diff --git a/src/aacs/variant.rs b/src/aacs/variant.rs index d16e2f5..a3c8f7f 100644 --- a/src/aacs/variant.rs +++ b/src/aacs/variant.rs @@ -151,7 +151,7 @@ pub(crate) fn variant_key_data(records: &[MkbRecord]) -> Option<&[u8]> { // on) are shared with the classical walk in [`super::keys`] — a single // definition keeps the variant SD tree byte-identical to the classical one. // (`aesg3` itself is imported separately in the test module.) -use super::media_key::{calc_pk_from_dk, calc_v_mask}; +use super::derive::{calc_pk_from_dk, calc_v_mask}; /// Outcome of a subset-difference walk against an MKB. Carries the /// processing key and the matching `uv` slot — both needed as inputs @@ -483,7 +483,7 @@ mod tests { // `use super::*` does not re-export the parent module's private `use` // imports, so pull them in directly for the tests below. use super::super::crypto::aesg3; - use super::super::media_key::calc_pk_from_dk; + use super::super::derive::calc_pk_from_dk; #[test] fn calc_pk_from_dk_terminates_on_nonconvergent_mask() { diff --git a/src/aacs/volume_key.rs b/src/aacs/volume_key.rs deleted file mode 100644 index a0b54da..0000000 --- a/src/aacs/volume_key.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Volume-key layer: derive the Volume Unique Key from the Media Key + Volume -//! ID, unwrap unit keys with it. [PR] §3.3 / §3.5, [BD] §3.3 / §3.9.3. - -use super::crypto::*; -use super::inf::*; - -/// 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); - for i in 0..16 { - vuk[i] ^= volume_id[i]; - } - vuk -} - -/// 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) -} - -/// Decrypt every encrypted unit key in a parsed `Unit_Key_RO.inf` with a VUK, -/// paired with its declared CPS-unit number. THE single VUK→unit-keys step: -/// both classical/v21 resolvers and `boil::resolve_candidate` call this, so the -/// map cannot drift between the player and harvest paths. -pub(crate) fn derive_unit_keys(uk_file: &UnitKeyFile, vuk: &[u8; 16]) -> Vec<(u32, [u8; 16])> { - uk_file - .encrypted_keys - .iter() - .map(|(num, enc_key)| (*num, decrypt_unit_key(vuk, enc_key))) - .collect() -}