From 89af9876aed1e60673a8d6768bf002a72a4f9121 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:43:02 -0700 Subject: [PATCH] =?UTF-8?q?keysource:=20add=20DecodeSampleSet=20=E2=80=94?= =?UTF-8?q?=20a=20>=3DMIN=5FSAMPLE=5FUNITS-by-construction=20sample=20set?= =?UTF-8?q?=20so=20an=20online=20request=20can't=20be=20built=20under-size?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/keysource.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/keysource.rs b/src/keysource.rs index 7ba7e2c..f8947f9 100644 --- a/src/keysource.rs +++ b/src/keysource.rs @@ -39,6 +39,48 @@ use crate::error::Error; /// value lives at the lower layer both share. pub const MIN_SAMPLE_UNITS: usize = 8; +/// A set of encrypted content-unit samples PROVEN to carry at least +/// [`MIN_SAMPLE_UNITS`] units — the online `/decode` request's proof-of-ownership. +/// +/// "Parse, don't validate": the only constructor, [`DecodeSampleSet::new`], returns +/// `None` for an under-sized slice, so an online key request simply *cannot be built* +/// from too few samples. The runtime `len() < MIN_SAMPLE_UNITS` check that used to +/// live at the request site (and was silently forgotten by an under-sampling caller, +/// reading as "key service down") becomes a compile-time obligation: a request builder +/// that takes `&DecodeSampleSet` can never receive an unchecked `Vec`. +/// +/// The *count* enforced here is a runtime property of the disc (how many encrypted +/// units it yields); the *requested* count is a caller-side compile-time constant that +/// callers pin to `MIN_SAMPLE_UNITS` (see e.g. autorip's `SAMPLE_UNITS`). Together the +/// two make under-sampling unrepresentable at the request boundary. +#[derive(Debug, Clone)] +pub struct DecodeSampleSet(Vec>); + +impl DecodeSampleSet { + /// Wrap `units` iff it carries at least [`MIN_SAMPLE_UNITS`] samples; `None` + /// otherwise (the caller then skips the online source rather than sending an + /// ambiguous request). This is the sole way to obtain a `DecodeSampleSet`. + pub fn new(units: Vec>) -> Option { + (units.len() >= MIN_SAMPLE_UNITS).then_some(Self(units)) + } + + /// The proven-sufficient samples. Guaranteed `>= MIN_SAMPLE_UNITS` in length. + pub fn units(&self) -> &[Vec] { + &self.0 + } + + /// Number of samples — always `>= MIN_SAMPLE_UNITS`. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Always `false` (a `DecodeSampleSet` never holds fewer than `MIN_SAMPLE_UNITS`); + /// provided so the type satisfies the usual `len`/`is_empty` pairing. + pub fn is_empty(&self) -> bool { + false + } +} + /// 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. @@ -467,6 +509,46 @@ mod tests { use crate::aacs::types::UnitKey; use std::sync::{Arc, Mutex}; + fn units(n: usize) -> Vec> { + (0..n).map(|i| vec![i as u8; 4]).collect() + } + + // ── DecodeSampleSet: the online request can't be built under-sized ───────── + + /// Fewer than MIN_SAMPLE_UNITS → no set. Mutation: accepting a short slice + /// resurrects the exact autorip bug (a 4-sample request silently skipped / + /// read as "service down"). + #[test] + fn decode_sample_set_rejects_under_min() { + for n in 0..MIN_SAMPLE_UNITS { + assert!( + DecodeSampleSet::new(units(n)).is_none(), + "{n} samples (< {MIN_SAMPLE_UNITS}) must not build a DecodeSampleSet" + ); + } + } + + /// Exactly the minimum, and above it, construct — and expose all samples. + #[test] + fn decode_sample_set_accepts_min_and_above() { + let exact = DecodeSampleSet::new(units(MIN_SAMPLE_UNITS)).expect("min builds"); + assert_eq!(exact.len(), MIN_SAMPLE_UNITS); + assert_eq!(exact.units().len(), MIN_SAMPLE_UNITS); + assert!(!exact.is_empty()); + + let more = DecodeSampleSet::new(units(MIN_SAMPLE_UNITS + 5)).expect("above min builds"); + assert_eq!(more.len(), MIN_SAMPLE_UNITS + 5); + } + + /// The wrapped units round-trip byte-for-byte (the request carries exactly what + /// was gathered — no reordering/truncation). + #[test] + fn decode_sample_set_preserves_units() { + let raw = units(MIN_SAMPLE_UNITS); + let set = DecodeSampleSet::new(raw.clone()).unwrap(); + assert_eq!(set.units(), raw.as_slice()); + } + // ── KeySource default-method behaviour ──────────────────────────────────── /// KeySource::host_certs() defaults to empty regardless of the MKB argument.