From 95b26d34b72cf0b88ea803a41f6c81596a2397f6 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:51:24 -0700 Subject: [PATCH] keysources: split KeySource into get_unit_keys + get_fmts_indexes Base per-CPS-unit keys and the AACS 2.1 forensic index set are now two explicit trait operations instead of one overloaded get_uk whose return count was implicit. keydb serves unit keys (forensic opts out via the default); online implements both; MultiSource drives both in order. Teed up for 1.4.5. Local WIP baseline. --- src/keydb.rs | 10 +++++++--- src/lib.rs | 37 +++++++++++++++++++++++++++---------- src/online.rs | 16 +++++++++++++++- tests/key_sources.rs | 10 +++++----- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/keydb.rs b/src/keydb.rs index 6cd3f4f..9008a58 100644 --- a/src/keydb.rs +++ b/src/keydb.rs @@ -328,10 +328,14 @@ fn write_atomic(path: &Path, text: &str) -> Result<(), Error> { } impl KeySource for KeydbSource { - /// Resolve this disc's terminal Unit Keys from the keydb. A missing / + /// Resolve this disc's base per-CPS-unit Unit Keys from the keydb. A missing / /// unreadable keydb is not an error — it simply yields no keys (another /// source may have them), the same as the library's own loader. - fn get_uk(&self, ctx: &dyn ResolveCtx) -> Result, Error> { + /// + /// The keydb carries no AACS 2.1 forensic index keys today, so it does not + /// override `get_fmts_indexes` — the default (empty) opts it out, and an FMTS + /// disc's forensic set comes from the online source. + fn get_unit_keys(&self, ctx: &dyn ResolveCtx) -> Result, Error> { match KeyDb::load(&self.path) { Ok(db) => Ok(Self::unit_keys_from(&db, ctx)), Err(_) => Ok(Vec::new()), @@ -748,7 +752,7 @@ mod tests { fn get_uk_missing_keydb_is_ok_empty() { let src = KeydbSource::new("/nonexistent/path/keydb.cfg"); let got = src - .get_uk(&ctx(HASH, Vec::new(), None)) + .get_unit_keys(&ctx(HASH, Vec::new(), None)) .expect("missing keydb is not an error"); assert!(got.is_empty()); } diff --git a/src/lib.rs b/src/lib.rs index 3bbfd48..b26efc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ //! in — then resolve and hand the resulting key to `Disc::decrypt_with`. //! //! Each source resolves a disc's terminal **Unit Keys** in one shot via -//! [`KeySource::get_uk`], driving libfreemkv's boil-down crypto primitives for +//! [`KeySource::get_unit_keys`], driving libfreemkv's boil-down crypto primitives for //! whatever level of material it holds. Compose several with [`MultiSource`] in //! the caller's chosen order. Reading the encrypted content-sample units a key //! server validates on, and applying the resolved keys against a disc, is @@ -51,9 +51,11 @@ pub(crate) fn uks_from_vuk(vuk: &[u8; 16], enc_title_keys: &[[u8; 16]]) -> Vec Result, libfreemkv::Error> { + /// Try each inner source in order; the FIRST to return a non-empty base Unit + /// Key set wins. An inner source that returns empty OR errors is treated as + /// "no key here" and the next is tried (a single source failure never blocks + /// the chain). All sources exhausted → empty. + fn get_unit_keys(&self, ctx: &dyn ResolveCtx) -> Result, libfreemkv::Error> { for s in &self.sources { - if let Ok(uks) = s.get_uk(ctx) { + if let Ok(uks) = s.get_unit_keys(ctx) { + if !uks.is_empty() { + return Ok(uks); + } + } + } + Ok(Vec::new()) + } + + /// Forensic-index counterpart: try each inner source's `get_fmts_indexes` in + /// the same order and return the first non-empty set. A source with no + /// forensic material (the keydb, via the trait default) contributes empty and + /// is skipped; on today's discs the online source answers. + fn get_fmts_indexes(&self, ctx: &dyn ResolveCtx) -> Result, libfreemkv::Error> { + for s in &self.sources { + if let Ok(uks) = s.get_fmts_indexes(ctx) { if !uks.is_empty() { return Ok(uks); } diff --git a/src/online.rs b/src/online.rs index fada4f0..53e30b3 100644 --- a/src/online.rs +++ b/src/online.rs @@ -365,7 +365,21 @@ impl OnlineSource { } impl KeySource for OnlineSource { - fn get_uk(&self, ctx: &dyn ResolveCtx) -> Result, Error> { + /// Base per-CPS-unit Unit Keys: submit the ctx's content samples and take the + /// service's reply (a terminal `UK`, or a `VUK` derived locally). One network + /// round-trip; any failure yields empty (the resolver tries the next source). + fn get_unit_keys(&self, ctx: &dyn ResolveCtx) -> Result, Error> { + Ok(self.query(ctx)) + } + + /// AACS 2.1 forensic index set: the mux injects an index-1 single-phase anchor + /// batch as the ctx's samples; the service maps it to the full ordered set of + /// forensic index keys, tagged by array position (element `i` → forensic index + /// `i + 1`). Same one round-trip as [`get_unit_keys`](Self::get_unit_keys) — + /// the difference is purely which samples the mux gathered and how the caller + /// reads the reply. The count is whatever the service returns; the mux trusts + /// any non-empty result as the complete set and never assumes 32. + fn get_fmts_indexes(&self, ctx: &dyn ResolveCtx) -> Result, Error> { Ok(self.query(ctx)) } diff --git a/tests/key_sources.rs b/tests/key_sources.rs index 72f68a2..d8c509d 100644 --- a/tests/key_sources.rs +++ b/tests/key_sources.rs @@ -2,7 +2,7 @@ //! //! These exercise the *public* surface of `freemkv-keysources` end-to-end — //! real files on disk, the real `KeyDb`/`Mapfile` parsers from libfreemkv, and -//! the `KeySource` trait (`get_uk` over a `ResolveCtx`) the applications drive. +//! the `KeySource` trait (`get_unit_keys` over a `ResolveCtx`) the applications drive. //! //! Covered: //! - `KeydbSource`: terminal unit-key lookup by disc hash through a real @@ -71,8 +71,8 @@ fn inputs(hash: &str) -> DiscInputs { /// Resolve a source through the public trait over a `DiscInputsCtx`. fn resolve(src: &dyn KeySource, inp: &DiscInputs) -> Vec { let ctx = DiscInputsCtx::new(inp); - src.get_uk(&ctx) - .expect("get_uk must not error for these fixtures") + src.get_unit_keys(&ctx) + .expect("get_unit_keys must not error for these fixtures") } // ── KeydbSource: real-file lookup by disc hash ────────────────────────────── @@ -130,7 +130,7 @@ fn keydb_source_missing_file_is_silent_ok_empty() { let inp = inputs(DISC_HASH); let ctx = DiscInputsCtx::new(&inp); assert!( - src.get_uk(&ctx) + src.get_unit_keys(&ctx) .expect("missing keydb is Ok, not Err") .is_empty() ); @@ -258,7 +258,7 @@ impl ScriptedSource { } impl KeySource for ScriptedSource { - fn get_uk(&self, _ctx: &dyn ResolveCtx) -> Result, libfreemkv::Error> { + fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result, libfreemkv::Error> { Ok(self.keys.clone()) } fn label(&self) -> &'static str {