freemkv-keysources: pure key lookup

Move the encrypted sample reader and the key-resolution loop into libfreemkv
(they read the disc and validate keys — decryption mechanism, not lookup). A
key source now only looks a key up and hands it back.
This commit is contained in:
Matthew Jackson
2026-06-24 15:40:50 -07:00
parent a4d94d4ce3
commit 91b95e15cf
+5 -92
View File
@@ -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<Vec<u8>> {
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<u8>> = 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
}