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.
This commit is contained in:
Matthew Jackson
2026-07-04 14:14:56 -07:00
parent cdee9739fd
commit 84aaceceb7
6 changed files with 49 additions and 54 deletions
+5 -5
View File
@@ -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<Vid>,
) -> Option<ResolvedChain> {
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.
@@ -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()
}
+8 -9
View File
@@ -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 {
+2 -4
View File
@@ -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 `<category>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
+2 -2
View File
@@ -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() {
-34
View File
@@ -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()
}