Compare commits

..
10 Commits
Author SHA1 Message Date
Matthew Jackson 3bc8969c5f 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.
2026-07-05 12:09:50 -07:00
Matthew Jackson ff63a4dfa9 v1.2.2: bump version (unified release) 2026-07-03 20:50:20 -07:00
Matthew Jackson f93dddf40c keydb: adopt corrected mk_from_dk (2-arg), reviving the DK->MK fallback
libfreemkv's AACS work changes mk_from_dk from (dk, mkb, vid) to (dk, mkb) —
the VID enters at the VUK step, not the MK step. Adopt the 2-arg call so the DK
pool is tried unconditionally (no longer gated on a VID being present), matching
libfreemkv. Without this keysources fails to compile against the new libfreemkv.
(Equivalent to the stranded b80d4cb; applied directly since that branch had
diverged surrounding context.)
2026-07-03 20:36:50 -07:00
Matthew Jackson c07467c84e v1.2.1: bump version (unified release) 2026-07-01 19:08:59 -07:00
Matthew Jackson 290fc6aafb v1.2.1: bump version 2026-07-01 17:30:25 -07:00
Matthew Jackson 2cc7c30fa3 Cargo: publish = false + git-pin libfreemkv (off crates.io)
keysources deps libfreemkv, now git-only, so keysources follows it off
crates.io. A committed [patch.crates-io] redirects the bare libfreemkv version
req to the git tag (local dev overrides to a path via the gitignored config).
2026-06-29 21:04:05 -07:00
Matthew Jackson a5c492f4c2 docs: 1.2.0 changelog date → 2026-06-29 (release prep) 2026-06-29 09:57:29 -07:00
Matthew Jackson 408ebc493c docs: 1.2.0 changelog — drop pre-1.1.0 carryover entries
The 'KeydbSource owns save+update' (Added) and 'Processing-Key
decryption restored' (Fixed) entries were stale carryover from the
never-released 1.1.0-beta.1; that code shipped in v1.1.0 (zero diff in
v1.1.0..HEAD). The 1.2.0 entry now lists only the real 1.2.0 changes
(hex unification, DiscInputs version stride, MKB cap).
2026-06-29 01:21:46 -07:00
Matthew Jackson f32f0bc282 v1.2.0: bump version + reconcile CHANGELOG to [1.2.0] 2026-06-28 23:34:19 -07:00
Matthew Jackson 34e2d3a0e8 1.2.0: route online/keydb hex parsing through libfreemkv::hex (one parser) 2026-06-28 22:12:07 -07:00
7 changed files with 93 additions and 100 deletions
+11 -13
View File
@@ -1,17 +1,15 @@
# Changelog # Changelog
## [1.1.0-beta.1] — UNRELEASED ## [1.2.0] — 2026-06-29
### Added ### Changed
- `KeydbSource` now owns keydb save + update (atomic write to the source's own - **One hex parser across the toolchain.** Online and keydb hex inputs now parse
path); honors the caller-supplied location. through `libfreemkv::hex`, the same parser the library uses — no separate
decoder with its own length/nibble rules.
### Fixed - **`DiscInputs` carries the disc's AACS version**, and the tests derive the
`Unit_Key_RO` stride from `inputs.version` instead of hardcoding it, so an
- **Processing-Key decryption restored.** A keydb Processing Key is again driven AACS-1.0 (V10, 48-byte) and AACS-2.x (V20/V21, 64-byte) disc are each handled
through the full AACS chain — PK → Media Key (against this disc's own MKB) → at their own stride.
Volume Unique Key (with the disc Volume ID) → unit keys — so discs that ship - **Online MKB read cap aligned with libfreemkv (64 MiB)**, and an over-cap MKB
only a Processing Key decrypt again. Stored Media Keys and Volume Unique Keys is logged rather than silently truncated.
are still honored directly. (Cross-disc Media-Key reuse remains intentionally
disabled.)
+16 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "freemkv-keysources" name = "freemkv-keysources"
version = "1.1.0" version = "1.2.2"
edition = "2024" edition = "2024"
rust-version = "1.86" rust-version = "1.86"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
@@ -8,10 +8,16 @@ description = "Pluggable AACS key sources (keydb, online key service) for libfre
repository = "https://github.com/freemkv/freemkv-keysources" repository = "https://github.com/freemkv/freemkv-keysources"
keywords = ["aacs", "blu-ray", "uhd", "decryption", "keydb"] keywords = ["aacs", "blu-ray", "uhd", "decryption", "keydb"]
categories = ["multimedia"] categories = ["multimedia"]
# OFF crates.io: depends on libfreemkv, which is now git-only (it git-deps the
# firmware crate). A crates.io crate can't carry a git dep, so keysources is
# consumed by git tag too. Clients git-tag-pin it.
publish = false
[dependencies] [dependencies]
# The crate provides the `KeySource` trait + `Key`/`DiscInputs` types these impls fill. # The crate provides the `KeySource` trait + `Key`/`DiscInputs` types these impls fill.
libfreemkv = "1.1" # libfreemkv is git-only now; the committed [patch.crates-io] below redirects this
# bare version req to the libfreemkv git tag (local dev overrides it to a path).
libfreemkv = "1.2"
# OnlineSource: POST disc inputs + samples to a key service over HTTP. # OnlineSource: POST disc inputs + samples to a key service over HTTP.
ureq = { version = "2", features = ["json"] } ureq = { version = "2", features = ["json"] }
serde_json = "1" serde_json = "1"
@@ -27,3 +33,11 @@ tracing = "0.1"
[profile.release] [profile.release]
lto = "thin" lto = "thin"
codegen-units = 1 codegen-units = 1
# libfreemkv is git-only (off crates.io). Redirect the bare `libfreemkv = "1.2"`
# req above to the git tag — committed and CI-visible. The release script
# rewrites this tag to the new version before regenerating Cargo.lock. Local dev
# overrides it with a path patch via the gitignored .cargo/config.toml (a
# config-level [patch.crates-io] wins over this manifest one for the same crate).
[patch.crates-io]
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "v1.2.2" }
+32 -34
View File
@@ -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,17 +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))
.or_else(|| vid.and_then(|v| mk_from_dk(&db.device_keys, mkb, v).ok())); // 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(|| 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(),
} }
@@ -351,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
@@ -468,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
@@ -505,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)"
@@ -536,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"
@@ -564,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"
@@ -665,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"
@@ -708,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).
+10 -32
View File
@@ -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]);
@@ -95,18 +95,8 @@ pub struct DiscEntry {
/// codepoint) must not panic on a mid-codepoint slice. Any non-hex /// codepoint) must not panic on a mid-codepoint slice. Any non-hex
/// byte yields `None`. /// byte yields `None`.
pub(crate) fn parse_hex(s: &str) -> Option<Vec<u8>> { pub(crate) fn parse_hex(s: &str) -> Option<Vec<u8>> {
let s = s.trim().trim_start_matches("0x").trim_start_matches("0X"); // The one workspace hex parser (strips an optional 0x/0X, byte-based).
let bytes = s.as_bytes(); libfreemkv::hex::parse_hex_bytes(s)
if bytes.len() % 2 != 0 {
return None;
}
let mut out = Vec::with_capacity(bytes.len() / 2);
for pair in bytes.chunks_exact(2) {
let hi = (pair[0] as char).to_digit(16)?;
let lo = (pair[1] as char).to_digit(16)?;
out.push((hi * 16 + lo) as u8);
}
Some(out)
} }
/// Read the run of consecutive ASCII decimal digits immediately following the /// Read the run of consecutive ASCII decimal digits immediately following the
@@ -147,23 +137,11 @@ fn parse_revoked_at_mkb(line: &str) -> Option<u32> {
/// Parse hex into a fixed-size array. /// Parse hex into a fixed-size array.
pub(crate) fn parse_hex16(s: &str) -> Option<[u8; 16]> { pub(crate) fn parse_hex16(s: &str) -> Option<[u8; 16]> {
let v = parse_hex(s)?; libfreemkv::hex::parse_hex_fixed::<16>(s)
if v.len() != 16 {
return None;
}
let mut out = [0u8; 16];
out.copy_from_slice(&v);
Some(out)
} }
pub(crate) fn parse_hex20(s: &str) -> Option<[u8; 20]> { pub(crate) fn parse_hex20(s: &str) -> Option<[u8; 20]> {
let v = parse_hex(s)?; libfreemkv::hex::parse_hex_fixed::<20>(s)
if v.len() != 20 {
return None;
}
let mut out = [0u8; 20];
out.copy_from_slice(&v);
Some(out)
} }
impl KeyDb { impl KeyDb {
@@ -1449,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 {})",
@@ -1474,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"
); );
@@ -1500,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
@@ -1548,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
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 // 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))
+6 -16
View File
@@ -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()
@@ -346,20 +347,9 @@ fn bearer_header(secret: &str) -> Option<String> {
} }
fn parse_uk(hex: &str) -> Option<[u8; 16]> { fn parse_uk(hex: &str) -> Option<[u8; 16]> {
if hex.len() != 32 { // The one workspace hex parser: byte-based (rejects sign chars / multi-byte),
return None; // 32 hex digits → [u8; 16], with an optional 0x/0X prefix tolerated.
} libfreemkv::hex::parse_hex_fixed::<16>(hex)
// Reject any non-hex byte up front. `u8::from_str_radix` on a 2-char
// window otherwise accepts sign prefixes (e.g. "+5", "-A"), letting a
// signed/whitespace-tainted string slip through as a valid key.
if !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let mut out = [0u8; 16];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(hex.get(i * 2..i * 2 + 2)?, 16).ok()?;
}
Some(out)
} }
#[cfg(test)] #[cfg(test)]
+1 -1
View File
@@ -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(),