From 3bc8969c5fd90f155c570a6010b36e90ae607930 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:09:50 -0700 Subject: [PATCH] keydb/online: resolve off aacs::derive + types, drop boil wrappers libfreemkv deleted the aacs::boil veneer, so switch the resolve path to the raw primitives: derive_media_key_from_{pk,dk}, derive_vuk, decrypt_unit_key from aacs::derive, and the newtypes from aacs::types. Add a local uks_from_vuk helper composing decrypt_unit_key. No behaviour change; fmt/clippy/test green on Rust 1.86. --- src/keydb.rs | 64 +++++++++++++++++++++----------------------- src/keydb_format.rs | 12 ++++----- src/lib.rs | 19 +++++++++++-- src/online.rs | 5 ++-- tests/key_sources.rs | 2 +- 5 files changed, 57 insertions(+), 45 deletions(-) diff --git a/src/keydb.rs b/src/keydb.rs index 32ccce9..980ca39 100644 --- a/src/keydb.rs +++ b/src/keydb.rs @@ -1,20 +1,21 @@ //! `keydb.cfg` key source (source #1). //! //! Parses a local `keydb.cfg`, looks the disc up by hash, and derives the -//! disc's terminal **Unit Keys** itself by driving libfreemkv's boil-down -//! primitives ([`uk_from_vuk`] / [`vuk_from_mk`] / [`mk_from_pk`] / -//! [`mk_from_dk`]) — never re-implementing AES. The path it picks mirrors the -//! OLD candidate order (which libfreemkv's resolver used to walk) EXACTLY, -//! cheapest-first: +//! disc's terminal **Unit Keys** itself by composing libfreemkv's raw +//! `aacs::derive` primitives (`derive_vuk` / `decrypt_unit_key` / +//! `derive_media_key_from_pk` / `derive_media_key_from_dk`) — never +//! re-implementing AES. The path it picks mirrors the OLD candidate order +//! (which libfreemkv's resolver used to walk) EXACTLY, cheapest-first: //! //! 1. per-disc **Unit Keys** (hash hit) → returned terminal, no derivation. -//! 2. per-disc **VUK** (hash hit) → [`uk_from_vuk`] over the disc's +//! 2. per-disc **VUK** (hash hit) → `uks_from_vuk` over the disc's //! encrypted title keys. -//! 3. a **Media Key**, then [`vuk_from_mk`] → [`uk_from_vuk`]. The MK comes +//! 3. a **Media Key**, then `derive_vuk` → `uks_from_vuk`. The MK comes //! from, in order: the disc's stored MK (hash hit); the keydb's -//! **Processing Key** pool walked against THIS disc's MKB via [`mk_from_pk`]; -//! or the device-key pool via [`mk_from_dk`]. The PK and DK pools resolve the -//! Media Key WITHOUT a VID; the final [`vuk_from_mk`] still needs one. The +//! **Processing Key** pool walked against THIS disc's MKB via +//! `derive_media_key_from_pk`; or the device-key pool via +//! `derive_media_key_from_dk`. The PK and DK pools resolve the +//! Media Key WITHOUT a VID; the final `derive_vuk` still needs one. The //! VID is the unlocker's physical VID ([`ResolveCtx::vid`]) when present, else //! the keydb entry's OWN stored VID (the `I` field, `vid`) for the //! non-physical / ISO path. With no VID from either source the MK path cannot @@ -31,9 +32,9 @@ use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use libfreemkv::aacs::{ - HostCert, MediaKey, UnitKey, Vid, Vuk, mk_from_dk, mk_from_pk, uk_from_vuk, vuk_from_mk, -}; +use crate::uks_from_vuk; +use libfreemkv::aacs::derive::{derive_media_key_from_dk, derive_media_key_from_pk, derive_vuk}; +use libfreemkv::aacs::types::{HostCert, MediaKey, UnitKey, Vid}; use libfreemkv::keysource::ResolveCtx; use libfreemkv::{Error, KeySource}; @@ -199,19 +200,23 @@ impl KeydbSource { // Either branch yields the COMPLETE declared set, so we take the // first that resolves (VUK preferred — cheapest). let derived = if let Some(vuk) = entry.vuk { - uk_from_vuk(Vuk(vuk), enc_title_keys) + uks_from_vuk(&vuk, enc_title_keys) } else { let vid = ctx.vid().or_else(|| entry.vid.map(Vid)); let mkb = ctx.mkb().unwrap_or(&[]); let mk: Option = entry .media_key .map(MediaKey) - .or_else(|| mk_from_pk(&db.processing_keys, mkb).ok()) + .or_else(|| derive_media_key_from_pk(mkb, &db.processing_keys).map(MediaKey)) // DK pool: the real Subset-Difference MKB walk. No VID at the MK // step (it enters at the VUK step below); the VID guard follows. - .or_else(|| mk_from_dk(&db.device_keys, mkb).ok()); + .or_else(|| derive_media_key_from_dk(mkb, &db.device_keys).map(MediaKey)); match (mk, vid) { - (Some(mk), Some(vid)) => uk_from_vuk(vuk_from_mk(mk, vid), enc_title_keys), + // VUK = derive_vuk(MK, VID), then boil the disc's encrypted + // title keys to the terminal Unit Keys. + (Some(mk), Some(vid)) => { + uks_from_vuk(&derive_vuk(&mk.0, &vid.0), enc_title_keys) + } // Locked VID-per-path rule: an MK with no VID cannot derive. _ => Vec::new(), } @@ -353,7 +358,8 @@ impl KeySource for KeydbSource { mod tests { use super::*; use crate::keydb_format::DiscEntry; - use libfreemkv::aacs::{DeviceKey, derive_vuk}; + use libfreemkv::aacs::derive::derive_vuk; + use libfreemkv::aacs::types::DeviceKey; use std::collections::HashMap; // ── A test ResolveCtx, so get_uk's path selection can be exercised without @@ -470,7 +476,7 @@ mod tests { fn union_partial_stored_plus_vuk_yields_all_declared_units() { let vuk = [0x5Au8; 16]; let enc = vec![[0x31u8; 16], [0xCDu8; 16]]; // two declared CPS units - let derived = uk_from_vuk(Vuk(vuk), &enc); // [d0, d1] + let derived = crate::uks_from_vuk(&vuk, &enc); // [d0, d1] let mut e = blank_entry(HASH); e.unit_keys = vec![(1, [0xA0u8; 16])]; // PARTIAL: only uk1 stored @@ -507,7 +513,7 @@ mod tests { let got = KeydbSource::unit_keys_from(&db, &ctx(HASH, enc.clone(), None)); // Reference: the boil primitive directly — the OLD derivation. - let expect = uk_from_vuk(Vuk(vuk), &enc); + let expect = crate::uks_from_vuk(&vuk, &enc); assert_eq!( got, expect, "VUK path must equal uk_from_vuk(vuk, enc_title_keys)" @@ -538,11 +544,11 @@ mod tests { let got = KeydbSource::unit_keys_from(&db, &ctx(HASH, enc.clone(), Some(Vid(vid_phys)))); // Reference uses the PHYSICAL VID. - let expect = uk_from_vuk(vuk_from_mk(MediaKey(mk), Vid(vid_phys)), &enc); + let expect = crate::uks_from_vuk(&derive_vuk(&mk, &vid_phys), &enc); assert_eq!(got, expect, "MK path must use the physical (unlock) VID"); // Sanity: it must NOT match the keydb-VID derivation (different VID → // different VUK → different keys), proving the right VID was selected. - let wrong = uk_from_vuk(vuk_from_mk(MediaKey(mk), Vid(vid_keydb)), &enc); + let wrong = crate::uks_from_vuk(&derive_vuk(&mk, &vid_keydb), &enc); assert_ne!( got, wrong, "must not derive with the keydb VID when a physical VID exists" @@ -566,7 +572,7 @@ mod tests { // ctx.vid() == None → ISO path. let got = KeydbSource::unit_keys_from(&db, &ctx(HASH, enc.clone(), None)); - let expect = uk_from_vuk(vuk_from_mk(MediaKey(mk), Vid(vid_keydb)), &enc); + let expect = crate::uks_from_vuk(&derive_vuk(&mk, &vid_keydb), &enc); assert_eq!( got, expect, "MK path must use the keydb VID when no physical VID is present" @@ -667,7 +673,7 @@ mod tests { let got = KeydbSource::unit_keys_from(&db, &ctx); assert!(!got.is_empty(), "PK pool must yield Unit Keys for the disc"); // Byte-identical to deriving from the recovered MK via the public chain. - let expect = uk_from_vuk(vuk_from_mk(MediaKey(mk), Vid(vid_phys)), &enc); + let expect = crate::uks_from_vuk(&derive_vuk(&mk, &vid_phys), &enc); assert_eq!( got, expect, "PK path must equal MK → VUK → UK from the recovered Media Key" @@ -710,16 +716,6 @@ mod tests { ); } - /// `vuk_from_mk` anchor: the VUK the MK path derives equals the library's own - /// `derive_vuk(mk, vid)` (the pre-boil primitive) — pinning that the boil - /// chain this source drives is the audited math, not a re-implementation. - #[test] - fn mk_path_vuk_matches_library_derive_vuk() { - let mk = [0x3Cu8; 16]; - let vid = [0xA5u8; 16]; - assert_eq!(vuk_from_mk(MediaKey(mk), Vid(vid)).0, derive_vuk(&mk, &vid)); - } - /// No per-disc entry → no key, even with a universal device-key pool present. /// Without a matched entry there is no per-disc anchor, so the global pools /// are never consulted (the cross-disc MK-pool brute stays retired). diff --git a/src/keydb_format.rs b/src/keydb_format.rs index a06a477..d29fab8 100644 --- a/src/keydb_format.rs +++ b/src/keydb_format.rs @@ -17,7 +17,7 @@ use std::collections::HashMap; -use libfreemkv::aacs::{DeviceKey, HostCert}; +use libfreemkv::aacs::types::{DeviceKey, HostCert}; /// A keydb per-disc unit key: the CPS-unit number paired with its 16-byte key. pub type NumberedUnitKey = (u32, [u8; 16]); @@ -1427,7 +1427,7 @@ mod tests { let vid = entry.vid.unwrap(); let expected_vuk = entry.vuk.unwrap(); - let derived = libfreemkv::aacs::derive_vuk(&mk, &vid); + let derived = libfreemkv::aacs::derive::derive_vuk(&mk, &vid); assert_eq!( derived, expected_vuk, "VUK derivation failed for disc: {} (hash {})", @@ -1452,9 +1452,9 @@ mod tests { } let original = std::fs::read(&unit_path).unwrap(); - assert_eq!(original.len(), libfreemkv::aacs::ALIGNED_UNIT_LEN); + assert_eq!(original.len(), libfreemkv::aacs::content::ALIGNED_UNIT_LEN); assert!( - libfreemkv::aacs::ts_sync_destroyed(&original), + libfreemkv::aacs::content::ts_sync_destroyed(&original), "Unit should be encrypted" ); @@ -1478,7 +1478,7 @@ mod tests { let keys: Vec<[u8; 16]> = entry.unit_keys.iter().map(|(_, k)| *k).collect(); let mut unit = original.clone(); - if let Some(res) = libfreemkv::aacs::decrypt_unit_try_keys(&mut unit, &keys) { + if let Some(res) = libfreemkv::aacs::content::decrypt_unit_try_keys(&mut unit, &keys) { eprintln!( "SUCCESS: Decrypted with entry {} ({res:?})", entry.disc_hash @@ -1526,7 +1526,7 @@ mod tests { // Verify VUK derivation if we have MK + VID if let Some(mk) = entry.media_key { - let derived = libfreemkv::aacs::derive_vuk(&mk, &vid); + let derived = libfreemkv::aacs::derive::derive_vuk(&mk, &vid); assert_eq!(derived, vuk, "VUK derivation mismatch"); eprintln!("VUK derivation verified"); } diff --git a/src/lib.rs b/src/lib.rs index 31f68c0..84b2464 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,10 +35,25 @@ pub use paths::{default_keydb_path, existing_keydb_path, keydb_search_paths}; // Re-exported for downstream convenience so apps need only depend on this crate // for the source-side types. -pub use libfreemkv::aacs::UnitKey; +pub use libfreemkv::aacs::types::UnitKey; pub use libfreemkv::keysource::ResolveCtx; pub use libfreemkv::{DiscInputs, KeySource}; +/// VUK → the disc's terminal Unit Keys (positional index), one AES-ECB-decrypt +/// per encrypted title key. Composes the raw `aacs::derive::decrypt_unit_key` +/// primitive directly — replaces the removed libfreemkv `aacs::boil::uk_from_vuk` +/// wrapper (that veneer is gone; libfreemkv owns only the AES). +pub(crate) fn uks_from_vuk(vuk: &[u8; 16], enc_title_keys: &[[u8; 16]]) -> Vec { + enc_title_keys + .iter() + .enumerate() + .map(|(i, e)| UnitKey { + idx: i as u32, + key: libfreemkv::aacs::derive::decrypt_unit_key(vuk, e), + }) + .collect() +} + /// An ordered composition of key sources, driven as one. [`MultiSource::get_uk`] /// tries each inner source in order and returns the first non-empty Unit Key /// set. **The caller supplies the list AND the order** — local-first `[Keydb, @@ -76,7 +91,7 @@ impl KeySource for MultiSource { /// UNION every inner source's host certs (filtered at the given MKB /// generation). Without this a composed source would hide an inner source's /// cert from the OEM cert-auth route — the gap this fixes. - fn host_certs(&self, mkb: Option) -> Vec { + fn host_certs(&self, mkb: Option) -> Vec { self.sources .iter() .flat_map(|s| s.host_certs(mkb)) diff --git a/src/online.rs b/src/online.rs index 62e5b4f..26c9e19 100644 --- a/src/online.rs +++ b/src/online.rs @@ -4,8 +4,9 @@ use std::io::Read; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::time::Duration; +use crate::uks_from_vuk; use base64::Engine; -use libfreemkv::aacs::{UnitKey, Vuk, uk_from_vuk}; +use libfreemkv::aacs::types::UnitKey; use libfreemkv::keysource::ResolveCtx; use libfreemkv::{Error, KeySource}; @@ -311,7 +312,7 @@ impl OnlineSource { // encrypted title keys from the context — the library owns the crypto. if let Some(vuk) = json.get("VUK").and_then(|u| u.as_str()).and_then(parse_uk) { if let Ok(enc) = ctx.enc_title_keys() { - return uk_from_vuk(Vuk(vuk), enc); + return uks_from_vuk(&vuk, enc); } } Vec::new() diff --git a/tests/key_sources.rs b/tests/key_sources.rs index a9bdd2d..a3db556 100644 --- a/tests/key_sources.rs +++ b/tests/key_sources.rs @@ -60,7 +60,7 @@ fn inputs(hash: &str) -> DiscInputs { DiscInputs { disc_hash: hash.into(), volume_id: [0u8; 16], - version: libfreemkv::aacs::AACS_MAJOR_UHD, + version: libfreemkv::aacs::mkb::AACS_MAJOR_UHD, mkb: Vec::new(), unit_key_ro: Vec::new(), samples: Vec::new(),