keydb/online: resolve off aacs::derive + types, drop boil wrappers
leak-guard / leak-guard (push) Failing after 5s

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.
This commit is contained in:
Matthew Jackson
2026-07-05 12:09:50 -07:00
parent ff63a4dfa9
commit 3bc8969c5f
5 changed files with 57 additions and 45 deletions
+30 -34
View File
@@ -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<MediaKey> = 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).
+6 -6
View File
@@ -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");
}
+17 -2
View File
@@ -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<UnitKey> {
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<u32>) -> Vec<libfreemkv::aacs::HostCert> {
fn host_certs(&self, mkb: Option<u32>) -> Vec<libfreemkv::aacs::types::HostCert> {
self.sources
.iter()
.flat_map(|s| s.host_certs(mkb))
+3 -2
View File
@@ -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()
+1 -1
View File
@@ -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(),