keydb/online: resolve off aacs::derive + types, drop boil wrappers
leak-guard / leak-guard (push) Failing after 5s
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:
+30
-34
@@ -1,20 +1,21 @@
|
|||||||
//! `keydb.cfg` key source (source #1).
|
//! `keydb.cfg` key source (source #1).
|
||||||
//!
|
//!
|
||||||
//! Parses a local `keydb.cfg`, looks the disc up by hash, and derives the
|
//! 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
|
//! disc's terminal **Unit Keys** itself by composing libfreemkv's raw
|
||||||
//! primitives ([`uk_from_vuk`] / [`vuk_from_mk`] / [`mk_from_pk`] /
|
//! `aacs::derive` primitives (`derive_vuk` / `decrypt_unit_key` /
|
||||||
//! [`mk_from_dk`]) — never re-implementing AES. The path it picks mirrors the
|
//! `derive_media_key_from_pk` / `derive_media_key_from_dk`) — never
|
||||||
//! OLD candidate order (which libfreemkv's resolver used to walk) EXACTLY,
|
//! re-implementing AES. The path it picks mirrors the OLD candidate order
|
||||||
//! cheapest-first:
|
//! (which libfreemkv's resolver used to walk) EXACTLY, cheapest-first:
|
||||||
//!
|
//!
|
||||||
//! 1. per-disc **Unit Keys** (hash hit) → returned terminal, no derivation.
|
//! 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.
|
//! 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
|
//! 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`];
|
//! **Processing Key** pool walked against THIS disc's MKB via
|
||||||
//! or the device-key pool via [`mk_from_dk`]. The PK and DK pools resolve the
|
//! `derive_media_key_from_pk`; or the device-key pool via
|
||||||
//! Media Key WITHOUT a VID; the final [`vuk_from_mk`] still needs one. The
|
//! `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
|
//! 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
|
//! 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
|
//! 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::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use libfreemkv::aacs::{
|
use crate::uks_from_vuk;
|
||||||
HostCert, MediaKey, UnitKey, Vid, Vuk, mk_from_dk, mk_from_pk, uk_from_vuk, vuk_from_mk,
|
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::keysource::ResolveCtx;
|
||||||
use libfreemkv::{Error, KeySource};
|
use libfreemkv::{Error, KeySource};
|
||||||
|
|
||||||
@@ -199,19 +200,23 @@ impl KeydbSource {
|
|||||||
// Either branch yields the COMPLETE declared set, so we take the
|
// Either branch yields the COMPLETE declared set, so we take the
|
||||||
// first that resolves (VUK preferred — cheapest).
|
// first that resolves (VUK preferred — cheapest).
|
||||||
let derived = if let Some(vuk) = entry.vuk {
|
let derived = if let Some(vuk) = entry.vuk {
|
||||||
uk_from_vuk(Vuk(vuk), enc_title_keys)
|
uks_from_vuk(&vuk, enc_title_keys)
|
||||||
} else {
|
} else {
|
||||||
let vid = ctx.vid().or_else(|| entry.vid.map(Vid));
|
let vid = ctx.vid().or_else(|| entry.vid.map(Vid));
|
||||||
let mkb = ctx.mkb().unwrap_or(&[]);
|
let mkb = ctx.mkb().unwrap_or(&[]);
|
||||||
let mk: Option<MediaKey> = entry
|
let mk: Option<MediaKey> = entry
|
||||||
.media_key
|
.media_key
|
||||||
.map(MediaKey)
|
.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
|
// 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.
|
// 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) {
|
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.
|
// Locked VID-per-path rule: an MK with no VID cannot derive.
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
@@ -353,7 +358,8 @@ impl KeySource for KeydbSource {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::keydb_format::DiscEntry;
|
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;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// ── A test ResolveCtx, so get_uk's path selection can be exercised without
|
// ── 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() {
|
fn union_partial_stored_plus_vuk_yields_all_declared_units() {
|
||||||
let vuk = [0x5Au8; 16];
|
let vuk = [0x5Au8; 16];
|
||||||
let enc = vec![[0x31u8; 16], [0xCDu8; 16]]; // two declared CPS units
|
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);
|
let mut e = blank_entry(HASH);
|
||||||
e.unit_keys = vec![(1, [0xA0u8; 16])]; // PARTIAL: only uk1 stored
|
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));
|
let got = KeydbSource::unit_keys_from(&db, &ctx(HASH, enc.clone(), None));
|
||||||
// Reference: the boil primitive directly — the OLD derivation.
|
// 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!(
|
assert_eq!(
|
||||||
got, expect,
|
got, expect,
|
||||||
"VUK path must equal uk_from_vuk(vuk, enc_title_keys)"
|
"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))));
|
let got = KeydbSource::unit_keys_from(&db, &ctx(HASH, enc.clone(), Some(Vid(vid_phys))));
|
||||||
// Reference uses the PHYSICAL VID.
|
// 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");
|
assert_eq!(got, expect, "MK path must use the physical (unlock) VID");
|
||||||
// Sanity: it must NOT match the keydb-VID derivation (different VID →
|
// Sanity: it must NOT match the keydb-VID derivation (different VID →
|
||||||
// different VUK → different keys), proving the right VID was selected.
|
// 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!(
|
assert_ne!(
|
||||||
got, wrong,
|
got, wrong,
|
||||||
"must not derive with the keydb VID when a physical VID exists"
|
"must not derive with the keydb VID when a physical VID exists"
|
||||||
@@ -566,7 +572,7 @@ mod tests {
|
|||||||
|
|
||||||
// ctx.vid() == None → ISO path.
|
// ctx.vid() == None → ISO path.
|
||||||
let got = KeydbSource::unit_keys_from(&db, &ctx(HASH, enc.clone(), None));
|
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!(
|
assert_eq!(
|
||||||
got, expect,
|
got, expect,
|
||||||
"MK path must use the keydb VID when no physical VID is present"
|
"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);
|
let got = KeydbSource::unit_keys_from(&db, &ctx);
|
||||||
assert!(!got.is_empty(), "PK pool must yield Unit Keys for the disc");
|
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.
|
// 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!(
|
assert_eq!(
|
||||||
got, expect,
|
got, expect,
|
||||||
"PK path must equal MK → VUK → UK from the recovered Media Key"
|
"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.
|
/// 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
|
/// 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).
|
/// are never consulted (the cross-disc MK-pool brute stays retired).
|
||||||
|
|||||||
+6
-6
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
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.
|
/// A keydb per-disc unit key: the CPS-unit number paired with its 16-byte key.
|
||||||
pub type NumberedUnitKey = (u32, [u8; 16]);
|
pub type NumberedUnitKey = (u32, [u8; 16]);
|
||||||
@@ -1427,7 +1427,7 @@ mod tests {
|
|||||||
let vid = entry.vid.unwrap();
|
let vid = entry.vid.unwrap();
|
||||||
let expected_vuk = entry.vuk.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!(
|
assert_eq!(
|
||||||
derived, expected_vuk,
|
derived, expected_vuk,
|
||||||
"VUK derivation failed for disc: {} (hash {})",
|
"VUK derivation failed for disc: {} (hash {})",
|
||||||
@@ -1452,9 +1452,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let original = std::fs::read(&unit_path).unwrap();
|
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!(
|
assert!(
|
||||||
libfreemkv::aacs::ts_sync_destroyed(&original),
|
libfreemkv::aacs::content::ts_sync_destroyed(&original),
|
||||||
"Unit should be encrypted"
|
"Unit should be encrypted"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1478,7 +1478,7 @@ mod tests {
|
|||||||
let keys: Vec<[u8; 16]> = entry.unit_keys.iter().map(|(_, k)| *k).collect();
|
let keys: Vec<[u8; 16]> = entry.unit_keys.iter().map(|(_, k)| *k).collect();
|
||||||
let mut unit = original.clone();
|
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!(
|
eprintln!(
|
||||||
"SUCCESS: Decrypted with entry {} ({res:?})",
|
"SUCCESS: Decrypted with entry {} ({res:?})",
|
||||||
entry.disc_hash
|
entry.disc_hash
|
||||||
@@ -1526,7 +1526,7 @@ mod tests {
|
|||||||
|
|
||||||
// Verify VUK derivation if we have MK + VID
|
// Verify VUK derivation if we have MK + VID
|
||||||
if let Some(mk) = entry.media_key {
|
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");
|
assert_eq!(derived, vuk, "VUK derivation mismatch");
|
||||||
eprintln!("VUK derivation verified");
|
eprintln!("VUK derivation verified");
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -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
|
// Re-exported for downstream convenience so apps need only depend on this crate
|
||||||
// for the source-side types.
|
// for the source-side types.
|
||||||
pub use libfreemkv::aacs::UnitKey;
|
pub use libfreemkv::aacs::types::UnitKey;
|
||||||
pub use libfreemkv::keysource::ResolveCtx;
|
pub use libfreemkv::keysource::ResolveCtx;
|
||||||
pub use libfreemkv::{DiscInputs, KeySource};
|
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`]
|
/// 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
|
/// 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,
|
/// 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
|
/// UNION every inner source's host certs (filtered at the given MKB
|
||||||
/// generation). Without this a composed source would hide an inner source's
|
/// generation). Without this a composed source would hide an inner source's
|
||||||
/// cert from the OEM cert-auth route — the gap this fixes.
|
/// 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
|
self.sources
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|s| s.host_certs(mkb))
|
.flat_map(|s| s.host_certs(mkb))
|
||||||
|
|||||||
+3
-2
@@ -4,8 +4,9 @@ use std::io::Read;
|
|||||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::uks_from_vuk;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use libfreemkv::aacs::{UnitKey, Vuk, uk_from_vuk};
|
use libfreemkv::aacs::types::UnitKey;
|
||||||
use libfreemkv::keysource::ResolveCtx;
|
use libfreemkv::keysource::ResolveCtx;
|
||||||
use libfreemkv::{Error, KeySource};
|
use libfreemkv::{Error, KeySource};
|
||||||
|
|
||||||
@@ -311,7 +312,7 @@ impl OnlineSource {
|
|||||||
// encrypted title keys from the context — the library owns the crypto.
|
// 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 Some(vuk) = json.get("VUK").and_then(|u| u.as_str()).and_then(parse_uk) {
|
||||||
if let Ok(enc) = ctx.enc_title_keys() {
|
if let Ok(enc) = ctx.enc_title_keys() {
|
||||||
return uk_from_vuk(Vuk(vuk), enc);
|
return uks_from_vuk(&vuk, enc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vec::new()
|
Vec::new()
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ fn inputs(hash: &str) -> DiscInputs {
|
|||||||
DiscInputs {
|
DiscInputs {
|
||||||
disc_hash: hash.into(),
|
disc_hash: hash.into(),
|
||||||
volume_id: [0u8; 16],
|
volume_id: [0u8; 16],
|
||||||
version: libfreemkv::aacs::AACS_MAJOR_UHD,
|
version: libfreemkv::aacs::mkb::AACS_MAJOR_UHD,
|
||||||
mkb: Vec::new(),
|
mkb: Vec::new(),
|
||||||
unit_key_ro: Vec::new(),
|
unit_key_ro: Vec::new(),
|
||||||
samples: Vec::new(),
|
samples: Vec::new(),
|
||||||
|
|||||||
Reference in New Issue
Block a user