aacs: sample only index-1 forensic segments for the online key query; hoist MIN_SAMPLE_UNITS into the base crate

This commit is contained in:
Matthew Jackson
2026-07-16 21:01:16 -07:00
parent edc60582ec
commit add9d8e0cd
2 changed files with 40 additions and 4 deletions
+16
View File
@@ -23,6 +23,22 @@ use crate::aacs::types::{UnitKey, Vid};
use crate::disc::Key;
use crate::error::Error;
/// Minimum encrypted-content unit samples a single online key request must carry.
///
/// The key service identifies a key by which of the submitted units it decrypts,
/// so too few samples — especially on FMTS, where a segment interleaves several
/// variants at the unit level — can return a key that matches an incidental unit
/// rather than the one asked about (a false positive). This many distinct units
/// make the request unambiguous.
///
/// Canonical here (the base crate) so BOTH consumers agree on one value: the
/// online source in `freemkv-keysources` (which refuses to send an under-sampled
/// request) re-exports it, and libfreemkv's own FMTS forensic query
/// ([`crate::mux`]) sizes its per-segment batch by it. Layering forbids the
/// reverse import (keysources depends on libfreemkv, not vice versa), so the
/// value lives at the lower layer both share.
pub const MIN_SAMPLE_UNITS: usize = 8;
/// 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.
+24 -4
View File
@@ -691,17 +691,37 @@ fn resolve_fmts_key_map(
// decrypt-and-check: the array position IS the index. The first readable
// segment whose batch yields the full set wins; a short (e.g. 1-key,
// base-UK-shaped) response means that batch wasn't forensic (a wrong
// feature-title mapping), so try the next segment. ─────────────────────────
// feature-title mapping), so try the next segment.
//
// ANCHOR RULE: the query MUST sample an INDEX-1 segment. The key service only
// returns the full set for the canonical anchor sample (a unit that decrypts
// under the index-1 key); a batch from any other forensic index is rejected
// (a base-UK-shaped miss). The `index == 1` filter guarantees every batch we
// send is an anchor — and the forensic tag cycles 1..32 in file order, so
// ~1-in-32 segments qualify (~25 across the feature), leaving ample read-fault
// fallback within the `MAX_ANCHOR_ATTEMPTS` budget. ─────────────────────────
const N_INDEX: usize = 32;
// Each forensic batch carries the server's minimum-samples count (the same
// disambiguation floor the online source enforces), drawn as even-phase units
// to land one clean variant half.
const BATCH_UNITS: usize = crate::keysource::MIN_SAMPLE_UNITS;
// Read-fault fallback budget: how many INDEX-1 (anchor) segments to attempt
// before giving up. Only matters when the leading anchor segments are
// unreadable; each attempt is one server round-trip, so it is bounded.
const MAX_ANCHOR_ATTEMPTS: usize = 16;
let mut index_keys: Vec<[u8; 16]> = Vec::new();
for seg in segments.iter().take(16) {
for seg in segments
.iter()
.filter(|s| s.index == 1)
.take(MAX_ANCHOR_ATTEMPTS)
{
let mut batch: Vec<Vec<u8>> = Vec::new();
for p in 0..8usize {
for p in 0..BATCH_UNITS {
if let Some(c) = read_unit(reader, seg, p * 2) {
batch.push(c);
}
}
if batch.len() < 8 {
if batch.len() < BATCH_UNITS {
continue; // read fault / short tail
}
let fresh = fetch(&batch);