diff --git a/src/lib.rs b/src/lib.rs index 1309412..6ede751 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,10 +15,11 @@ //! Sources are dumb and stateful: each hands its candidate keys out one at a //! time via [`KeySource::next_key`], in its own best order, and reports //! exhaustion. Compose several with [`MultiSource`] in the caller's chosen -//! order; [`resolve_and_apply`] drives the loop — handing each key to -//! `Disc::decrypt_with` (which validates against the disc's content samples) and -//! stopping at the first that decrypts, or reporting a genuine "no key" when -//! every source is spent. +//! order. Resolving those candidates against a disc, and reading the encrypted +//! content-sample units a key server validates on, is decryption *mechanism* — +//! it lives in the library (`libfreemkv::resolve_and_apply`, +//! `libfreemkv::read_encrypted_units`), not here. A source only ever looks a key +//! up and hands it back; what's done with the key is not its concern. mod keydb; mod mapfile; @@ -34,8 +35,6 @@ pub use paths::{default_keydb_path, existing_keydb_path, keydb_search_paths}; // for the source-side types. pub use libfreemkv::{DiscInputs, Key, KeySource}; -use libfreemkv::{Disc, DiscTitle, SectorSource}; - /// An ordered composition of key sources, driven as one. `next_key` exhausts /// the first source (one candidate per call), then the next, … then `None`. /// **The caller supplies the list AND the order** — local-first `[Keydb, @@ -74,89 +73,3 @@ impl KeySource for MultiSource { self.sources.iter().any(|s| s.errored()) } } - -/// Drive `sources` until one key decrypts `disc`. Loops `next_key` and hands -/// each candidate to [`Disc::decrypt_with`] (which validates it against -/// `inputs.samples` and only mutates the disc on success), returning `true` at -/// the first key that decrypts and `false` once every source is exhausted — the -/// genuine "no key for this disc". THE shared key-resolution loop: every -/// application (the `freemkv` CLI, autorip) uses it instead of re-rolling the -/// candidate/retry logic, so the "no key" verdict is identical everywhere. -pub fn resolve_and_apply( - sources: &mut dyn KeySource, - inputs: &DiscInputs, - disc: &mut Disc, -) -> bool { - while let Some(key) = sources.next_key(inputs) { - if disc.decrypt_with(key, &inputs.samples).is_ok() { - return true; - } - } - false -} - -/// Read up to `n` ENCRYPTED 6144-byte aligned units from `title`'s body, raw (no -/// decrypt) — the content samples a caller hands to [`resolve_and_apply`] (for -/// `Disc::decrypt_with` to validate a key against) and that a sample-needing -/// source (an online key service) byte-validates against. -/// -/// "Encrypted" is decided by `libfreemkv::aacs::is_aacs_scrambled` — the SAME -/// predicate the library's decrypt gate and a key service use — so all sides -/// agree. A clip opens with clear navigation units (PAT/PMT, menus); only the -/// feature body is scrambled, and a clear unit proves nothing, so this collects -/// only scrambled ones, sampling the largest extent at its midpoint forward. -pub fn read_sample_units( - reader: &mut dyn SectorSource, - title: &DiscTitle, - n: usize, -) -> Vec> { - const UNIT_LEN: usize = 6144; - const UNIT_SECTORS: u32 = 3; // 6144 / 2048 - const CHUNK_UNITS: u32 = 15; // 45 sectors/read — under the drive transfer cap - const MAX_CHUNKS_PER_EXTENT: u32 = 4; // ~60 units scanned at each extent's midpoint - - let mut out: Vec> = Vec::new(); - for ext in &title.extents { - let total_units = ext.sector_count / UNIT_SECTORS; - if total_units == 0 { - continue; - } - let mut unit = total_units / 2; // midpoint (past the clear nav at the head) - for _ in 0..MAX_CHUNKS_PER_EXTENT { - if unit >= total_units { - break; - } - let units_this = CHUNK_UNITS.min(total_units - unit); - // Saturate: start_lba comes from attacker-controlled UDF/MPLS - // extents; a malformed extent near u32::MAX would otherwise panic - // (debug) or wrap to a wrong LBA (release). Matches the hardened - // pattern in mux/disc.rs and verify.rs; an over-capacity LBA then - // fails cleanly via the read_sectors().is_err() break below. - let lba = ext - .start_lba - .saturating_add(unit.saturating_mul(UNIT_SECTORS)); - let count = (units_this * UNIT_SECTORS) as u16; - let mut buf = vec![0u8; count as usize * 2048]; - // `false` = no recovery retries; the reader is the raw drive/file - // (no decrypt decorator), so these are the on-disc encrypted bytes. - if reader.read_sectors(lba, count, &mut buf, false).is_err() { - break; - } - for i in 0..units_this as usize { - let o = i * UNIT_LEN; - if o + UNIT_LEN > buf.len() { - break; - } - let u = &buf[o..o + UNIT_LEN]; - if libfreemkv::aacs::is_aacs_scrambled(u) { - out.push(u.to_vec()); - if out.len() >= n { - return out; - } - } - } - unit += units_this; - } - } - out -}