diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 4ae059d..7d9c7bf 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -1508,9 +1508,12 @@ pub enum Key { /// Processing key(s) (AACS PK). libfreemkv applies each against the MKB /// → media key → VUK → unit keys. Processing(Vec<[u8; 16]>), - /// Media key (Km). libfreemkv derives the VUK from it via the Volume ID, - /// then the per-CPS-unit keys. - Media([u8; 16]), + /// Media key candidate(s) (Km). A source hands its full pool because an MK + /// is MKB-scoped (shared across a pressing/MKB family) — picking the one + /// that applies is `km_verifies` against this disc's MKB, which is + /// derivation, so it lives here. libfreemkv verifies, then derives the VUK + /// via the Volume ID and the per-CPS-unit keys. + Media(Vec<[u8; 16]>), /// Volume Unique Key (VK / VUK). libfreemkv decrypts `Unit_Key_RO.inf` /// into the per-CPS-unit keys. NOT terminal — the chain continues to the /// unit keys. @@ -1589,6 +1592,19 @@ impl Disc { } } + /// The public AACS inputs for this disc, for a [`crate::KeySource`] to look + /// a key up. `None` when the disc carries no AACS state (unencrypted, CSS, + /// or AACS inputs not captured at scan). Contains no secrets — just disc + /// identity plus the on-disc AACS structures. + pub fn inputs(&self) -> Option { + self.aacs.as_ref().map(|a| crate::keysource::DiscInputs { + disc_hash: a.disc_hash.clone(), + volume_id: a.volume_id, + mkb: a.mkb.clone(), + unit_key_ro: a.uk_ro.clone(), + }) + } + /// Apply a caller-resolved [`Key`] so [`Self::decrypt_keys`] yields usable /// decryption state. **Lookup-free**: no keydb, no network — the caller /// (an application, via a key source) does all resolution and hands the key @@ -1624,7 +1640,7 @@ impl Disc { match key { Key::Device(dks) => supplied.device_keys = dks, Key::Processing(pks) => supplied.processing_keys = pks, - Key::Media(mk) => supplied.media_keys = vec![mk], + Key::Media(mks) => supplied.media_keys = mks, Key::Volume(vuk) => { supplied.disc_entry = Some(crate::aacs::DiscEntry { disc_hash: aacs.disc_hash.clone(), @@ -3040,7 +3056,9 @@ mod tests { let mut disc2 = make_test_disc(1000, "UHD"); disc2.encrypted = true; assert!(matches!( - disc2.decrypt_with(Key::Media([0x22u8; 16])).unwrap_err(), + disc2 + .decrypt_with(Key::Media(vec![[0x22u8; 16]])) + .unwrap_err(), crate::error::Error::AacsNoKeys )); } diff --git a/src/keysource.rs b/src/keysource.rs new file mode 100644 index 0000000..28f0a6c --- /dev/null +++ b/src/keysource.rs @@ -0,0 +1,49 @@ +//! Key sources — the lookup layer that hands libfreemkv a [`Key`]. +//! +//! libfreemkv performs NO key lookup. An application resolves a key for a disc +//! through one or more [`KeySource`]s, each a dumb adapter over a backing store +//! (a keydb file, a key server, the mapfile cache): given the disc's +//! [`DiscInputs`] it returns the raw [`Key`] at whatever level it holds. The +//! library then derives down and decrypts via `Disc::decrypt_with`. +//! +//! Source implementations are published in the companion `freemkv-keysources` +//! crate — keeping all key *policy* (which store, which order, online vs local) +//! out of the library while all key *mechanism* (the AACS derivation chain) +//! stays in it. + +use crate::disc::Key; +use crate::error::Result; + +/// The public AACS inputs a key source needs to look a disc up. Captured at +/// scan; contains no secrets — only the disc identity and the on-disc AACS +/// structures a source or key server may key on. +#[derive(Debug, Clone)] +pub struct DiscInputs { + /// SHA-1 of `Unit_Key_RO.inf`, `0x`-prefixed hex. The value a keydb keys + /// its per-disc entries by, and a key server identifies the disc with. + pub disc_hash: String, + /// Volume ID (16 bytes). `[0u8; 16]` when no authenticated handshake ran + /// (e.g. an ISO/mapfile flow), which disables VID-keyed lookups. + pub volume_id: [u8; 16], + /// Raw MKB bytes. Empty when not captured. + pub mkb: Vec, + /// Raw `Unit_Key_RO.inf` bytes. Empty when not captured. + pub unit_key_ro: Vec, +} + +/// A key source: given a disc's [`DiscInputs`], look up a [`Key`]. +/// +/// Dumb by contract — a source queries its backing store and returns the raw +/// key at whatever level it has (device / processing / media / volume / unit). +/// It performs NO AACS derivation; `Disc::decrypt_with` derives down. That +/// keeps every derivation step in one place (the library) across AACS +/// 1.0 / 2.0 / 2.1 / 2.x. +pub trait KeySource { + /// Look up a key for this disc. + /// + /// - `Ok(Some(key))` — a key was found; the caller hands it to + /// `Disc::decrypt_with`. + /// - `Ok(None)` — this source has nothing for the disc; try the next one. + /// - `Err(_)` — the source itself failed (I/O, network, parse). + fn resolve(&self, inputs: &DiscInputs) -> Result>; +} diff --git a/src/lib.rs b/src/lib.rs index 81041b3..5878dc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ pub(crate) mod identity; pub(crate) mod ifo; pub mod io; pub mod keydb; +pub mod keysource; pub mod labels; pub(crate) mod mpls; pub mod mux; @@ -174,6 +175,7 @@ pub use disc::{ LabelPurpose, LabelQualifier, PatchOptions, PatchOutcome, Resolution, SampleRate, ScanOptions, Stream, SubtitleStream, SweepOptions, VideoStream, classify_damage, }; +pub use keysource::{DiscInputs, KeySource}; // ─── Streams ──────────────────────────────────────────────────────────────── //