//! `DecryptingSectorSource` — wrap any [`SectorSource`] to apply //! AACS / CSS in-place decryption on every read. //! //! This is the single source of truth for decrypt-on-read: every //! decrypt-on-read caller (e.g. `DiscStream`) wraps its source in this //! decorator. The actual cipher code lives in [`crate::aacs`] and //! [`crate::css`]; we just call the existing //! [`crate::decrypt::decrypt_sectors`] helper that drives both of them //! in-place after each read (a no-op for [`DecryptKeys::None`]). //! //! Composition: `Drive` → `DecryptingSectorSource` → caller sees //! plaintext. For `DecryptKeys::None` discs the decorator is a //! pass-through, so callers can wire it unconditionally and keep //! their pipeline shape uniform regardless of encryption state. use crate::decrypt::{DecryptKeys, decrypt_sectors, decrypt_sectors_in_content}; use crate::error::Result; use std::sync::Arc; use super::SectorSource; /// A closure resolving keys from encrypted-content samples — the shape of both /// [`KeyFetch`] operations. Named so the two constructors (and the struct fields) /// read clearly. pub type KeyFetchFn = std::sync::Arc]) -> Vec<[u8; 16]> + Send + Sync>; /// Application-supplied "fetch a fresh key for THIS data" callback. /// /// Invoked by [`DecryptingSectorSource`] when a read contains scrambled AACS /// units that NONE of the currently-held unit keys could decrypt. The argument /// is those still-scrambled 6144-byte aligned units (real on-disc ciphertext); /// the return is any additional unit keys to add to the pool and retry with — /// empty if the source can't help. Mirrors the DVD model (try the held key, /// then ask the key source for the failing data) generalised to AACS. /// /// The library performs NO key lookup or network I/O itself; this is the seam an /// application uses to call its key source (e.g. an online key service) with the /// exact ciphertext that failed. /// /// TWO explicit operations, so the "one base key vs a whole forensic set" /// contract lives in the type instead of a caller guessing at the return length: /// /// * [`unit_keys`](Self::unit_keys) — the base Unit Key(s) for a CPS unit, from /// real encrypted samples drawn from it. The non-forensic path: one key per CPS /// unit (the pool grows by whatever it returns). Used by the mux's base / /// multi-CPS map resolution AND by the sweep/patch recovery decorator. /// * [`fmts_indexes`](Self::fmts_indexes) — the disc's AACS 2.1 forensic index /// keys, from an index-1 single-phase anchor batch. The source hands back the /// COMPLETE set (ordered index 1..N); the caller sizes the forensic map to /// `len()` and never assumes a fixed N (32 is all we've seen, but the contract /// is "whatever the source returns, ≥ 1, is all of them"). /// /// A **stateless, shared** pair of `Arc` — the decorator owns the only /// mutable state (its call-count cap and spent flag), so one `KeyFetch` is built /// once and cloned cheaply (two `Arc` bumps) into every read path. `Send + Sync` /// so it can ride the mux highway's producer thread. #[derive(Clone)] pub struct KeyFetch { unit: KeyFetchFn, fmts: KeyFetchFn, } impl KeyFetch { /// Build a resolver from its two operations: `unit` resolves base Unit Keys /// from a CPS unit's samples; `fmts` resolves the forensic index set from an /// index-1 anchor batch. pub fn new(unit: KeyFetchFn, fmts: KeyFetchFn) -> Self { Self { unit, fmts } } /// A resolver that serves ONLY base Unit Keys; [`fmts_indexes`](Self::fmts_indexes) /// is always empty. For read paths that never resolve forensic keys — the /// sweep/patch recovery decorator, which handles CPS units only. pub fn unit_only(unit: KeyFetchFn) -> Self { Self::new(unit, std::sync::Arc::new(|_| Vec::new())) } /// Resolve the base Unit Key(s) for a CPS unit from `samples` (real encrypted /// units drawn from it). Normally one key; the caller adds whatever it returns /// to the pool. pub fn unit_keys(&self, samples: &[Vec]) -> Vec<[u8; 16]> { (self.unit)(samples) } /// Resolve the disc's AACS 2.1 forensic index keys from an index-1 single- /// phase `anchor` batch. The source returns the COMPLETE ordered set (index i /// = element i); the caller trusts any non-empty result as all of them. pub fn fmts_indexes(&self, anchor: &[Vec]) -> Vec<[u8; 16]> { (self.fmts)(anchor) } } /// Decorator: read from `inner`, then run the configured /// AACS / CSS decrypt over the bytes that landed in `buf`. /// /// `unit_key_idx` selects the AACS unit key for the disc (0 for /// the vast majority of titles; the rare multi-CPS-unit discs pick /// the index that covers the title being read). For /// [`DecryptKeys::None`] and [`DecryptKeys::Css`] the index is /// ignored. pub struct DecryptingSectorSource { inner: S, keys: DecryptKeys, unit_key_idx: usize, /// Base LBA of the encrypted region currently being read — the clip / /// extent `start_lba` that AACS aligned units are anchored at. The unit- /// alignment gate measures `lba` relative to THIS, not absolute disc LBA 0, /// so a clip whose `start_lba` is not 3-aligned still gates correctly. Set /// per-extent by the mux read paths via [`set_unit_base`]; defaults to 0 /// (absolute alignment) for callers that read from a 3-aligned base. /// /// [`set_unit_base`]: Self::set_unit_base unit_base: u32, /// The miss policy (see [`crate::sector::recovery::Recover`]) — a generic, /// scheme-neutral recovery the input stream (L3) installs and this decorator /// (L2) executes at the one seam when a content unit will not decrypt. `None` /// = no recovery (a miss is loss). Installed via /// [`with_key_fetch`](Self::with_key_fetch). recovery: Option, /// Encrypted-content extent map — the disc's m2ts ranges as sorted/merged /// `(start_lba, sector_count)` (see /// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)). /// When `Some`, a unit whose absolute LBA is OUTSIDE these ranges is clear /// (UDF filesystem / BDMV nav) and is passed through untouched: never /// decrypted, verified, or counted as loss. `None` means "the caller only /// reads encrypted content" (the mux reads title extents only) → every unit /// is treated as content (the legacy behaviour). content_ranges: Option>, /// Reused scratch holding the pre-decrypt on-disc ciphertext for the /// key-fetch retry. Only touched when `recovery` is installed; kept on the /// struct (rather than a per-read `Vec`) so the mux hot path — 16 MiB /// batches at highway speed, now that the mux installs a key-fetch for /// multi-CPS — reuses one allocation instead of alloc/free-ing every read. cipher_scratch: Vec, /// Proactive AACS key map (see [`crate::decrypt::AacsKeyMap`]). When set, the /// mux resolved one key per CPS unit / segment UP FRONT, so this read decrypts /// each aligned unit with its MAPPED key and TRUSTS it — no per-unit /// `is_clean` verdict, no reactive key-fetch, no key-server storm. `None` /// keeps the legacy trial-and-recover path (sweep / patch, or a mux that did /// not build a map). Mutually exclusive with `recovery` in practice: the mux /// installs one or the other. key_map: Option>, } impl DecryptingSectorSource { /// Wrap `inner` with the given keys. The default unit-key /// index is 0; use [`with_unit_key_idx`] for the multi-CPS-unit /// case. /// /// [`with_unit_key_idx`]: Self::with_unit_key_idx pub fn new(inner: S, keys: DecryptKeys) -> Self { Self { inner, keys, unit_key_idx: 0, unit_base: 0, // No recovery by default. CSS self-decrypts in `decrypt_sectors` // (needs no external input); AACS installs a key-fetch via // `with_key_fetch`. recovery: None, content_ranges: None, cipher_scratch: Vec::new(), key_map: None, } } /// Install a proactive [`AacsKeyMap`](crate::decrypt::AacsKeyMap): the mux /// resolved one key per CPS unit / segment up front, so every aligned unit is /// decrypted with its MAPPED key and trusted — no per-unit `is_clean` check, /// no reactive key-fetch. This is the storm-free mux path; it supersedes /// [`with_key_fetch`](Self::with_key_fetch) (do not set both). AACS-only; a /// CSS / clear disc ignores it. pub fn with_key_map(mut self, map: Arc) -> Self { self.key_map = Some(map); self } /// `&mut` counterpart of [`with_key_map`](Self::with_key_map): install the /// proactive map on an already-constructed source (the inline live-drive /// [`DiscStream`](crate::mux::DiscStream) builds the decorator first, then /// installs the map via its own `with_key_map`). pub fn set_key_map(&mut self, map: Arc) { self.key_map = Some(map); } /// Restrict decrypt to the disc's encrypted-content extents /// (sorted/merged `(start_lba, sector_count)` — see /// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)). /// Units outside content (UDF filesystem / BDMV nav) pass through untouched, /// so the TS-sync content check is never consulted /// about non-content bytes. Whole-disc readers (sweep / patch) set this; the /// mux leaves it unset because it only ever reads title extents. pub fn with_content_ranges(mut self, ranges: Arc<[(u32, u32)]>) -> Self { self.content_ranges = Some(ranges); self } /// Override the AACS unit-key index. Only meaningful for /// [`DecryptKeys::Aacs`]; other variants ignore it. pub fn with_unit_key_idx(mut self, idx: usize) -> Self { self.unit_key_idx = idx; self } /// Install a [`KeyFetch`] callback: when a read holds scrambled AACS units /// that no current key decrypts, the decorator hands those units to `cb` and /// adds any keys it returns to the pool, then re-decrypts. Only meaningful /// for [`DecryptKeys::Aacs`]; ignored otherwise. The library makes no network /// call — `cb` is the application's seam to its key source. pub fn with_key_fetch(mut self, cb: KeyFetch) -> Self { self.recovery = Some(crate::sector::recovery::key_fetch(cb)); self } /// Replace the configured keys without unwrapping the decorator. /// Used by `DiscStream::set_raw()` to flip from encrypted-disc /// decryption to a pass-through after the inner reader is already /// owned by the wrapper. For new construction prefer [`new`]. /// /// [`new`]: Self::new pub fn set_keys(&mut self, keys: DecryptKeys) { self.keys = keys; } /// Borrow the inner source. Useful for tests and for adapters /// that want to introspect the underlying drive / file without /// unwrapping the decorator. pub fn inner(&self) -> &S { &self.inner } /// Mutable borrow of the inner source. pub fn inner_mut(&mut self) -> &mut S { &mut self.inner } /// Consume the decorator and return the underlying source. pub fn into_inner(self) -> S { self.inner } /// Decrypt `buf` in place with the active keys, applying the content gate /// when one is installed (whole-disc readers) or running ungated (the mux). /// The single dispatch both the first read and the post-fetch retry share, /// so they agree on which units are content and on the unit-key try order. fn decrypt_buf( buf: &mut [u8], keys: &mut DecryptKeys, unit_key_idx: usize, lba: u32, content: Option<&[(u32, u32)]>, ) -> Result { match content { Some(ranges) => decrypt_sectors_in_content(buf, keys, unit_key_idx, lba, ranges), None => decrypt_sectors(buf, keys, unit_key_idx), } } } impl SectorSource for DecryptingSectorSource { fn capacity_sectors(&self) -> u32 { self.inner.capacity_sectors() } fn read_sectors( &mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool, ) -> Result { // Bulk path: no Force Unit Access (the cache IS the streaming // throughput). FUA is a Pass-N recovery lever threaded through // `read_sectors_fua`. self.read_sectors_fua(lba, count, buf, recovery, false) } fn read_sectors_fua( &mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool, fua: bool, ) -> Result { // Defense-in-depth: AACS aligned units are 3 sectors (6144 bytes) and // `decrypt_sectors` anchors units at buffer offset 0. A read that does // not begin a whole number of units past the encrypted region's base // (`unit_base`, the clip/extent start_lba) would decrypt every unit // under the wrong CBC/unit alignment and silently mis-decrypt. Reject // loud (DecryptFailed) BEFORE reading rather than ever mis-decrypting. // The gate is measured RELATIVE to `unit_base` (set per-extent by the // mux read paths via `set_unit_base`), never absolute `lba % 3` — a clip // whose start_lba is not itself 3-aligned must still gate on its own // units (else its readable units are wrongly rejected → "Decryption // failed" on exactly those titles). if matches!(self.keys, DecryptKeys::Aacs { .. }) && !crate::aacs::content::is_unit_aligned(lba, self.unit_base) { return Err(crate::error::Error::DecryptFailed); } let n = self .inner .read_sectors_fua(lba, count, buf, recovery, fua)?; // PROACTIVE MAP PATH (the storm-free mux): when a key map is installed the // mux resolved one key per CPS unit / segment up front, so decrypt each // aligned unit with its MAPPED key and trust it — no per-unit `is_clean` // verdict, no reactive key-fetch. A resolver gap surfaces loud from // `decrypt_sectors_mapped` (DecryptFailed); authored-bad TS just passes // through for the muxer to drop. if let Some(map) = self.key_map.clone() { crate::decrypt::decrypt_sectors_mapped(&mut buf[..n], &self.keys, lba, &map)?; return Ok(n); } // Decrypt the bytes just read IN PLACE. Scheme-agnostic (None / CSS / AACS). // With a content map installed the `*_in_content` entry skips units OUTSIDE // the encrypted extents (clear filesystem / nav pass through untouched); the // mux installs no map (it reads title extents only). A genuine can't-decrypt // (no key / misaligned unit) surfaces as `Err` and propagates; otherwise // every unit gets its key applied and the bytes pass through — a unit that // decrypts to broken TS is the consumer's concern (the muxer drops it), // never a read failure. (The decrypt-verify read gate was removed: bad // sectors are marked by physical read success, not by TS structure.) let content = self.content_ranges.clone(); // cheap Arc bump; frees the &self borrow let content_ref = content.as_deref(); let unit_key_idx = self.unit_key_idx; let dropped = Self::decrypt_buf( &mut buf[..n], &mut self.keys, self.unit_key_idx, lba, content_ref, )?; // FRESH-KEY-ON-FAILURE: hand a unit no held key opened (as its on-disc // ciphertext) to the application's key source; any returned key is added to // the pool and the read is re-decrypted, caching the key for later units. // If the source is asked for this exact ciphertext and STILL cannot supply a // key (the recovery's residual `dropped > 0`), the unit is genuinely // unresolvable — this decrypting sweep/patch path FAILS LOUD rather than // write the still-encrypted bytes into the output as if they were clear // content (the mux path fails loud the same way via `decrypt_sectors_mapped`). if dropped > 0 && self.recovery.is_some() { // Rare miss only: the in-place decrypt overwrote `buf`, so RE-READ the // on-disc ciphertext for the key-fetch retry. This keeps the happy path // zero-copy — the common single-CPS mux batch (dropped == 0) never // captures or copies; a genuine miss pays one re-read into the reused // `cipher_scratch`. self.cipher_scratch.resize(n, 0); self.inner.read_sectors_fua( lba, count, &mut self.cipher_scratch[..n], recovery, fua, )?; let rctx = crate::sector::recovery::RecoverCtx { unit_key_idx, lba, content: content.clone(), prev_dropped: dropped, }; let r = self .recovery .as_mut() .expect("recovery.is_some() checked above"); let cipher = &self.cipher_scratch[..n]; let outcome = r(&mut buf[..n], cipher, &mut self.keys, &rctx); if outcome.dropped > 0 { return Err(crate::error::Error::DecryptFailed); } } Ok(n) } fn set_speed(&mut self, kbs: u16) { self.inner.set_speed(kbs) } fn set_unit_base(&mut self, lba: u32) { self.unit_base = lba; } } #[cfg(test)] mod tests { use super::*; use crate::error::Result; /// Synthetic SectorSource that yields a deterministic byte /// pattern keyed by LBA. Used to verify the decorator's /// pass-through behaviour for `DecryptKeys::None`. struct PatternedSource { capacity: u32, } impl PatternedSource { fn fill(lba: u32, count: u16, buf: &mut [u8]) { let bytes = count as usize * 2048; for (i, slot) in buf[..bytes].iter_mut().enumerate() { let abs = lba as u64 * 2048 + i as u64; *slot = ((abs.wrapping_mul(2654435761) >> 16) & 0xff) as u8; } } } impl SectorSource for PatternedSource { fn capacity_sectors(&self) -> u32 { self.capacity } fn read_sectors( &mut self, lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { Self::fill(lba, count, buf); Ok(count as usize * 2048) } } #[test] fn passthrough_with_no_keys() { let src = PatternedSource { capacity: 16 }; let mut wrapped = DecryptingSectorSource::new(src, DecryptKeys::None); // capacity_sectors delegates. assert_eq!(wrapped.capacity_sectors(), 16); let mut got = vec![0u8; 4 * 2048]; let n = wrapped.read_sectors(3, 4, &mut got, false).unwrap(); assert_eq!(n, 4 * 2048); let mut expected = vec![0u8; 4 * 2048]; PatternedSource::fill(3, 4, &mut expected); assert_eq!(got, expected); } #[test] fn passthrough_set_speed_delegates() { struct SpeedRecorder { last: Option, } impl SectorSource for SpeedRecorder { fn capacity_sectors(&self) -> u32 { 0 } fn read_sectors( &mut self, _lba: u32, _count: u16, _buf: &mut [u8], _recovery: bool, ) -> Result { Ok(0) } fn set_speed(&mut self, kbs: u16) { self.last = Some(kbs); } } let mut wrapped = DecryptingSectorSource::new(SpeedRecorder { last: None }, DecryptKeys::None); wrapped.set_speed(7200); assert_eq!(wrapped.inner().last, Some(7200)); } // TODO: AACS round-trip test — needs a fixture-encrypted unit // (6144-byte aligned) plus the matching unit key. The cipher // path itself is exercised by `crate::aacs` unit tests; here // we only assert the decorator wires the existing helper, not // that AES-128 is correct. // --------------------------------------------------------------- // Additional coverage. // --------------------------------------------------------------- use std::sync::{Arc, Mutex}; /// Source that fills the FULL requested span with a CSS-scrambled- /// FLAGGED sector pattern (byte 0x14 scramble bits set, non-zero /// data) but reports a SHORTER read (`report_n`). With a CSS key the /// decorator must descramble ONLY `buf[..report_n]`; the bytes /// beyond `report_n` must stay exactly as filled. A whole-`buf` /// decrypt would clear the flagged sector's scramble bits and XOR /// its data region — observable here. struct ShortReportSource { report_n: usize, } impl ShortReportSource { fn fill_one(buf: &mut [u8]) { for (i, b) in buf.iter_mut().enumerate() { *b = (i as u8).wrapping_mul(29).wrapping_add(3); } buf[0x14] = 0x30; // scramble-control bits set → flags == 0x03 } } impl SectorSource for ShortReportSource { fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { for s in 0..count as usize { Self::fill_one(&mut buf[s * 2048..(s + 1) * 2048]); } Ok(self.report_n) } } /// Records the (lba, count, recovery) the decorator forwarded. struct ArgRecorder { calls: Arc>>, } impl SectorSource for ArgRecorder { fn read_sectors( &mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool, ) -> Result { self.calls.lock().unwrap().push((lba, count, recovery)); let bytes = count as usize * 2048; buf[..bytes].fill(0); Ok(bytes) } } /// A source whose read returns an error — the decorator must /// propagate it and NOT call decrypt afterward (decrypt over an /// unwritten buffer would be at best wasted work, at worst a panic /// for a missing AACS key). Grounding: `read_sectors` uses `?` on /// the inner read before `decrypt_sectors`. struct FailingSource; impl SectorSource for FailingSource { fn read_sectors( &mut self, _lba: u32, _count: u16, _buf: &mut [u8], _recovery: bool, ) -> Result { Err(crate::error::Error::IoError { source: std::io::Error::from(std::io::ErrorKind::TimedOut), }) } } /// The CSS path is a no-op for sectors whose scrambling-control /// bits are clear. Per CSS, the sector's mode-2 subheader byte at /// offset 0x14 carries the copyright/scramble flags; descrambling /// only runs when `(byte[0x14] >> 4) & 0x03 != 0`. With those bits /// clear (byte 0x14 == 0) the descrambler returns immediately, so /// the decorator must hand back the bytes unchanged. Grounding: /// `css::lfsr::descramble_sector` early-return on `flags == 0`. #[test] fn css_unscrambled_sector_passes_through() { struct FixedSector { template: [u8; 2048], } impl SectorSource for FixedSector { fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { let bytes = count as usize * 2048; for s in 0..count as usize { buf[s * 2048..(s + 1) * 2048].copy_from_slice(&self.template); } Ok(bytes) } } let mut template = [0u8; 2048]; for (i, b) in template.iter_mut().enumerate() { *b = (i as u8).wrapping_mul(13).wrapping_add(7); } // Byte 0x14: clear the scramble-control bits (bits 4-5) so the // descrambler treats the sector as already in the clear. template[0x14] = 0x00; let expected = template; let mut wrapped = DecryptingSectorSource::new( FixedSector { template }, DecryptKeys::Css { title_key: [0x11, 0x22, 0x33, 0x44, 0x55], }, ); let mut got = [0u8; 2048]; let n = wrapped.read_sectors(0, 1, &mut got, false).unwrap(); assert_eq!(n, 2048); assert_eq!( got, expected, "unscrambled CSS sector (flags=0) must pass through untouched" ); } /// The decorator must decrypt ONLY the `n` bytes the inner source /// reported as read — never the full `buf`. We use a CSS key and a /// sector whose flags ARE set (so descramble would mutate bytes if /// applied), but the inner source reports a short `n` of 0. With /// n=0 the decrypt span is empty, so the whole buffer must come /// back exactly as the inner source filled it. Grounding: /// `decrypt_sectors(&mut buf[..n], ...)`. #[test] fn decrypt_span_bounded_by_reported_n() { // Inner fills a CSS-scrambled-FLAGGED sector but reports n=0, so // the decrypt span is empty and the buffer must come back // byte-identical to what the inner source wrote. A whole-`buf` // decrypt would clear byte 0x14's scramble bits and XOR the data // region — this asserts that does NOT happen for the n=0 span. let mut wrapped = DecryptingSectorSource::new( ShortReportSource { report_n: 0 }, DecryptKeys::Css { title_key: [1, 2, 3, 4, 5], }, ); let mut expected = vec![0u8; 2048]; ShortReportSource::fill_one(&mut expected); let mut got = vec![0u8; 2048]; let n = wrapped.read_sectors(5, 1, &mut got, false).unwrap(); assert_eq!(n, 0, "decorator must return the inner source's n"); assert_eq!( got, expected, "with n=0 the decrypt span is empty; buffer must be untouched" ); // Belt-and-braces: the scramble flag bits must still be set // (a whole-buf descramble would have cleared them). assert_eq!(got[0x14] & 0x30, 0x30, "scramble flags must remain set"); } /// lba / count / recovery must be forwarded to the inner source /// verbatim. Grounding: `read_sectors` calls /// `self.inner.read_sectors(lba, count, buf, recovery)`. #[test] fn args_forwarded_verbatim() { let calls = Arc::new(Mutex::new(Vec::new())); let mut wrapped = DecryptingSectorSource::new( ArgRecorder { calls: calls.clone(), }, DecryptKeys::None, ); let mut buf = vec![0u8; 2 * 2048]; wrapped.read_sectors(12345, 2, &mut buf, true).unwrap(); wrapped.read_sectors(0, 1, &mut buf, false).unwrap(); assert_eq!( *calls.lock().unwrap(), vec![(12345, 2, true), (0, 1, false)], "lba/count/recovery must pass through unchanged" ); } /// A read error from the inner source must propagate unchanged and /// the decrypt step must NOT run after it. Grounding: the `?` on the /// inner read in `read_sectors`. #[test] fn inner_read_error_propagates() { let mut wrapped = DecryptingSectorSource::new(FailingSource, DecryptKeys::None); let mut buf = vec![0u8; 2048]; let r = wrapped.read_sectors(0, 1, &mut buf, false); let err = r.expect_err("inner error must propagate"); let io: std::io::Error = err.into(); assert_eq!(io.kind(), std::io::ErrorKind::TimedOut); } /// With AACS keys but an out-of-range `unit_key_idx`, the decrypt /// step must fail (DecryptFailed) rather than silently returning /// still-encrypted bytes. Grounding: `decrypt_sectors`' unit-key /// lookup — `unit_keys.get(idx)` → None → Error::DecryptFailed. #[test] fn aacs_missing_unit_key_errors() { let src = PatternedSource { capacity: 16 }; // idx 0 requested, but unit_keys is empty → get(0) == None. let mut wrapped = DecryptingSectorSource::new( src, DecryptKeys::Aacs { unit_keys: Vec::new(), read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ); let mut buf = vec![0u8; 2048]; let r = wrapped.read_sectors(0, 1, &mut buf, false); let err = r.expect_err("missing unit key must error, not pass through encrypted"); assert_eq!( err.code(), crate::error::Error::DecryptFailed.code(), "must surface DecryptFailed" ); } /// A source that yields exactly one CLEAR AACS aligned unit (6144 /// bytes = 3 sectors) with MPEG-TS sync bytes (0x47) at the BD-TS /// stride (offset 4, then every 192 bytes). `is_clean` /// reports such a unit as clear (not scrambled), so the AACS decrypt path /// reaches the per-unit closure and leaves it untouched — letting /// us prove the unit-key LOOKUP (not the cipher) is what fails for /// an out-of-range index. struct ClearUnitSource; impl SectorSource for ClearUnitSource { fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { let bytes = count as usize * 2048; buf[..bytes].fill(0); // BD-TS sync byte at offset 4 of every 192-byte packet. let mut off = 4usize; while off < bytes { buf[off] = 0x47; off += 192; } Ok(bytes) } } /// `with_unit_key_idx` selects which unit key the AACS path uses. /// idx=2 against a single populated key is out of range → the /// `unit_keys.get(idx)` lookup returns None → DecryptFailed. idx=0 /// is in range → the lookup succeeds, and on a clear (TS-sync /// intact) full unit the cipher is a no-op, so the read returns Ok /// with the bytes unchanged. Grounding: `decrypt_sectors`' /// `unit_keys.get(unit_key_idx)`. #[test] fn with_unit_key_idx_selects_key() { let keys = DecryptKeys::Aacs { unit_keys: vec![(0u32, [0u8; 16])], read_data_key: None, format: crate::disc::ContentFormat::BdTs, }; // 3 sectors = one 6144-byte aligned unit (so partial_len == 0). let mut buf = vec![0u8; 3 * 2048]; // idx=2 out of range → lookup fails. let mut bad = DecryptingSectorSource::new(ClearUnitSource, keys.clone()).with_unit_key_idx(2); assert!( bad.read_sectors(0, 3, &mut buf, false).is_err(), "out-of-range unit_key_idx must fail the lookup" ); // idx=0 in range → lookup ok, clear unit left untouched. let mut good = DecryptingSectorSource::new(ClearUnitSource, keys).with_unit_key_idx(0); let mut buf2 = vec![0u8; 3 * 2048]; let n = good.read_sectors(0, 3, &mut buf2, false).unwrap(); assert_eq!(n, 3 * 2048); // Clear unit: sync byte preserved at offset 4. assert_eq!( buf2[4], 0x47, "clear unit must be left intact under valid idx" ); } /// `set_keys` must replace the active keys mid-life. We use a /// CSS-SCRAMBLED-flagged sector (byte 0x14 scramble bits set) so the /// effect of the active key is observable: under a CSS key the /// descrambler XORs a keystream into bytes 128..2048 AND clears the /// scramble flags (`sector[0x14] &= 0xCF`); under `None` the bytes /// pass through unchanged. Flipping keys mid-life must change which /// behavior runs. Grounding: `set_keys` + `css::lfsr::descramble_sector` /// (keystream XOR + flag-clear on flags != 0). #[test] fn set_keys_swaps_active_keys() { struct ScrambledSector { template: [u8; 2048], } impl SectorSource for ScrambledSector { fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { let bytes = count as usize * 2048; for s in 0..count as usize { buf[s * 2048..(s + 1) * 2048].copy_from_slice(&self.template); } Ok(bytes) } } // Build a sector flagged as scrambled (bits 4-5 of byte 0x14 // set) with non-zero payload so the keystream XOR is visible. let mut template = [0u8; 2048]; for (i, b) in template.iter_mut().enumerate() { *b = (i as u8).wrapping_mul(29).wrapping_add(3); } template[0x14] = 0x30; // scramble bits (4-5) set → flags == 0x03 let pristine = template; // Start with None → pass-through (no descramble, flags stay set). let mut wrapped = DecryptingSectorSource::new(ScrambledSector { template }, DecryptKeys::None); let mut got = [0u8; 2048]; wrapped.read_sectors(0, 1, &mut got, false).unwrap(); assert_eq!( got, pristine, "None keys must pass the sector through unchanged" ); assert_eq!( got[0x14] & 0x30, 0x30, "None must leave the scramble flags set" ); // Swap to a CSS key: now the descrambler runs and must clear the // scramble flags (and XOR the data region), so the bytes differ. wrapped.set_keys(DecryptKeys::Css { title_key: [0xa1, 0xb2, 0xc3, 0xd4, 0xe5], }); let mut got2 = [0u8; 2048]; wrapped.read_sectors(0, 1, &mut got2, false).unwrap(); assert_eq!( got2[0x14] & 0x30, 0x00, "CSS descramble must clear the scramble-control bits" ); assert_ne!( &got2[128..2048], &pristine[128..2048], "CSS descramble must alter the encrypted data region" ); } /// Defense-in-depth: an AACS decrypting read whose START LBA is not /// unit-aligned (lba % 3 != 0) must be rejected with DecryptFailed BEFORE /// touching the cipher — a mid-unit start would decrypt every unit under the /// wrong CBC/unit alignment and silently mis-decrypt. A unit-aligned start /// (lba % 3 == 0) must pass the guard and proceed normally. /// /// Grounding: the `lba % UNIT_SECTORS != 0` guard in `read_sectors`. #[test] fn aacs_unaligned_start_lba_rejected() { let keys = DecryptKeys::Aacs { unit_keys: vec![(0u32, [0u8; 16])], read_data_key: None, format: crate::disc::ContentFormat::BdTs, }; // Unaligned starts (1, 2, 4, 5, 32 — note 32 % 3 == 2) must all reject. for lba in [1u32, 2, 4, 5, 32, 64] { let mut wrapped = DecryptingSectorSource::new(ClearUnitSource, keys.clone()); let mut buf = vec![0u8; 3 * 2048]; let r = wrapped.read_sectors(lba, 3, &mut buf, false); let err = r.expect_err("unaligned AACS start LBA must reject"); assert_eq!( err.code(), crate::error::Error::DecryptFailed.code(), "lba {lba} (% 3 = {}) must reject with DecryptFailed", lba % 3 ); } // Unit-aligned starts (0, 3, 33, 66) must pass the guard. ClearUnitSource // yields TS-clear units, so decrypt is a no-op and the read succeeds. for lba in [0u32, 3, 33, 66] { let mut wrapped = DecryptingSectorSource::new(ClearUnitSource, keys.clone()); let mut buf = vec![0u8; 3 * 2048]; let n = wrapped .read_sectors(lba, 3, &mut buf, false) .unwrap_or_else(|_| panic!("aligned lba {lba} must pass the guard")); assert_eq!(n, 3 * 2048); } } /// Clip-anchored gate (the Watership Down "Decryption failed" regression): /// AACS aligned units are anchored at the clip's encrypted-region start /// (`unit_base`), NOT absolute disc LBA 0. A clip whose `start_lba` is not /// itself 3-aligned must gate on ITS OWN units, so the clip's base LBA /// (which the old `lba % 3` gate wrongly rejected) now passes, and only /// reads off the clip-relative unit grid reject. #[test] fn aacs_gate_is_clip_anchored_not_absolute() { let keys = DecryptKeys::Aacs { unit_keys: vec![(0u32, [0u8; 16])], read_data_key: None, format: crate::disc::ContentFormat::BdTs, }; // base = 64 (abs % 3 == 1): the non-3-aligned clip start that triggered // the bug. The old absolute gate rejected every read here; the clip- // anchored gate must accept the clip's own unit grid. let base = 64u32; // Clip-relative aligned starts (base + {0,3,6,30}) pass. for off in [0u32, 3, 6, 30] { let mut w = DecryptingSectorSource::new(ClearUnitSource, keys.clone()); w.set_unit_base(base); let mut buf = vec![0u8; 3 * 2048]; let n = w .read_sectors(base + off, 3, &mut buf, false) .unwrap_or_else(|_| panic!("clip-relative aligned lba {} must pass", base + off)); assert_eq!(n, 3 * 2048); } // The clip's base LBA itself (abs % 3 == 1) — the exact read the old gate // wrongly rejected — must now decrypt. let mut w = DecryptingSectorSource::new(ClearUnitSource, keys.clone()); w.set_unit_base(base); let mut buf = vec![0u8; 3 * 2048]; assert!( w.read_sectors(base, 3, &mut buf, false).is_ok(), "a clip starting at a non-3-aligned LBA must decrypt from its own base" ); // Clip-relative MISaligned starts (base + {1,2,4,5}) still reject. for off in [1u32, 2, 4, 5] { let mut w = DecryptingSectorSource::new(ClearUnitSource, keys.clone()); w.set_unit_base(base); let mut buf = vec![0u8; 3 * 2048]; let err = w .read_sectors(base + off, 3, &mut buf, false) .expect_err("clip-relative unaligned start must reject"); assert_eq!( err.code(), crate::error::Error::DecryptFailed.code(), "base+{off} is off the clip-relative unit grid" ); } } /// The unit-alignment guard is AACS-only. A CSS decrypting read (per-sector, /// stateless — DVDs) must NOT be gated on a 3-sector boundary: a single /// sector at lba 1 must read fine. Grounding: the guard is inside /// `matches!(self.keys, DecryptKeys::Aacs { .. })`. #[test] fn css_start_lba_not_unit_gated() { let mut wrapped = DecryptingSectorSource::new( ClearUnitSource, DecryptKeys::Css { title_key: [0u8; 5], }, ); let mut buf = vec![0u8; 2048]; // lba 1 (not a multiple of 3) must succeed under CSS — no AACS gate. let n = wrapped.read_sectors(1, 1, &mut buf, false).unwrap(); assert_eq!(n, 2048, "CSS reads must not be unit-alignment gated"); } /// Build a clear 6144-byte AACS unit (TS syncs at the BD-TS stride) then /// encrypt it under `unit_key` so `aacs::content::decrypt_unit` recovers it. Mirrors /// the encrypt helper in `crate::decrypt`'s tests. fn encrypt_aacs_unit(unit_key: &[u8; 16]) -> Vec { use aes::Aes128; use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray}; let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN]; let mut off = 4; while off < unit.len() { unit[off] = 0x47; off += 192; } // CPI bits on byte 0 so it reads as encrypted; set before key derivation. unit[0] |= 0xC0; let header: [u8; 16] = unit[..16].try_into().unwrap(); let derived = crate::aacs::crypto::aes_ecb_encrypt(unit_key, &header); let mut k = [0u8; 16]; for i in 0..16 { k[i] = derived[i] ^ header[i]; } let cipher = Aes128::new(GenericArray::from_slice(&k)); let mut prev = crate::aacs::crypto::AACS_IV; let blocks = (crate::aacs::content::ALIGNED_UNIT_LEN - 16) / 16; for i in 0..blocks { let o = 16 + i * 16; for j in 0..16 { unit[o + j] ^= prev[j]; } let mut blk = GenericArray::clone_from_slice(&unit[o..o + 16]); cipher.encrypt_block(&mut blk); unit[o..o + 16].copy_from_slice(&blk); prev.copy_from_slice(&unit[o..o + 16]); } unit } /// Like [`encrypt_aacs_unit`] but knocks out the TS sync on `bad_pkts` (kept as /// NON-zero content, so they read as authored-bad packets, not padding) BEFORE /// encryption — a unit the correct key still OPENS on its remaining good /// packets, but that carries bad-encoded content the muxer must drop. fn encrypt_aacs_unit_bad(unit_key: &[u8; 16], bad_pkts: &[usize]) -> Vec { use aes::Aes128; use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray}; let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN]; let mut off = 4; while off < unit.len() { unit[off] = 0x47; off += 192; } for &p in bad_pkts { let o = p * 192; unit[o + 4] = 0x00; // no TS sync after decrypt unit[o + 5] = 0xAB; // non-zero payload => real content, not padding } unit[0] |= 0xC0; let header: [u8; 16] = unit[..16].try_into().unwrap(); let derived = crate::aacs::crypto::aes_ecb_encrypt(unit_key, &header); let mut k = [0u8; 16]; for i in 0..16 { k[i] = derived[i] ^ header[i]; } let cipher = Aes128::new(GenericArray::from_slice(&k)); let mut prev = crate::aacs::crypto::AACS_IV; let blocks = (crate::aacs::content::ALIGNED_UNIT_LEN - 16) / 16; for i in 0..blocks { let o = 16 + i * 16; for j in 0..16 { unit[o + j] ^= prev[j]; } let mut blk = GenericArray::clone_from_slice(&unit[o..o + 16]); cipher.encrypt_block(&mut blk); unit[o..o + 16].copy_from_slice(&blk); prev.copy_from_slice(&unit[o..o + 16]); } unit } /// MUX (read > decrypt > mux): an undecryptable AACS content unit must NOT /// fail the read and must NOT be nulled. The best key is applied and the (bad) /// bytes pass through to the muxer; broken TS is a muxer concern. The read only /// hard-fails on a genuine can't-decrypt (no key at all / misaligned unit). #[test] fn mux_passes_undecryptable_unit_through_without_nulling() { let real_key = [0x33u8; 16]; let wrong_key = [0x44u8; 16]; // One unit encrypted under real_key, plus one trailing CLEAR (TS-sync) // unit so we can confirm conceal touches ONLY the undecryptable unit. let enc = encrypt_aacs_unit(&real_key); let mut clear = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN]; let mut o = 4; while o < clear.len() { clear[o] = 0x47; o += 192; } let mut two_units = enc; two_units.extend_from_slice(&clear); struct TwoUnitSource { data: Vec, } impl SectorSource for TwoUnitSource { fn capacity_sectors(&self) -> u32 { (self.data.len() / 2048) as u32 } fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { let bytes = count as usize * 2048; buf[..bytes].copy_from_slice(&self.data[..bytes]); Ok(bytes) } } let mut wrapped = DecryptingSectorSource::new( TwoUnitSource { data: two_units }, DecryptKeys::Aacs { unit_keys: vec![(0, wrong_key)], // can't open the encrypted unit read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ); let mut buf = vec![0u8; 6 * 2048]; // Must SUCCEED (no DecryptFailed) — the mux never aborts on bad decrypt. let n = wrapped .read_sectors(0, 6, &mut buf, false) .expect("the mux never aborts on a bad-decrypt unit"); assert_eq!(n, 6 * 2048); // Unit 0 is passed through DECRYPTED (the wrong key was applied), NOT // null-TS concealed: it is not the all-0x47/PID-0x1FFF null pattern. let unit0 = &buf[..crate::aacs::content::ALIGNED_UNIT_LEN]; let all_null = (0..32).all(|p| unit0[p * 192 + 4] == 0x47 && unit0[p * 192 + 6] == 0xFF); assert!( !all_null, "the undecryptable unit is passed through, never null-TS concealed" ); // Unit 1 (clear) passed through untouched. let unit1 = &buf [crate::aacs::content::ALIGNED_UNIT_LEN..2 * crate::aacs::content::ALIGNED_UNIT_LEN]; assert_eq!(unit1, &clear[..], "the clear unit is left exactly as read"); } /// MUX pass-through, mixed buffer: a unit the pool CAN decrypt (a /// content-fragment TAIL — a few real packets + source-zero padding, the 1.2.0 /// shape, <16 TS syncs) comes out byte-for-byte correct, and a unit it CANNOT /// (encrypted under an absent key) is passed through best-effort — never /// null-TS filled, never counted as loss. The old path nulled the good tail /// (silent data loss) whenever it shared a buffer with an undecryptable unit. #[test] fn mux_passes_both_decryptable_and_undecryptable_units_through() { let bad_key = [0x77u8; 16]; // encrypts the undecryptable unit (NOT provided) let good_key = [0x33u8; 16]; // encrypts the padding-tail unit (provided) // Unit A: a full content unit encrypted under `bad_key` — with only // `good_key` in the pool it cannot be decrypted → restored to ciphertext. let bad_unit = encrypt_aacs_unit(&bad_key); // Unit B: a SHORT-PADDING-TAIL unit — encrypt a full clear unit under // `good_key`, then zero the trailing source packets (from packet 11 on) so // they decrypt back to clean zero padding. Only 11 of 32 packets are real // content → 11 TS syncs after decrypt (well under the majority-vote 16). const KEEP: usize = 11; let mut good_tail = encrypt_aacs_unit(&good_key); for b in good_tail[KEEP * 192..].iter_mut() { *b = 0; } // The byte-exact expected post-decrypt form of unit B (independent decrypt). let mut expected_tail = good_tail.clone(); crate::aacs::content::decrypt_unit(&mut expected_tail, &good_key); let mut two_units = bad_unit; two_units.extend_from_slice(&good_tail); struct TwoUnitSource { data: Vec, } impl SectorSource for TwoUnitSource { fn capacity_sectors(&self) -> u32 { (self.data.len() / 2048) as u32 } fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { let bytes = count as usize * 2048; buf[..bytes].copy_from_slice(&self.data[..bytes]); Ok(bytes) } } let mut wrapped = DecryptingSectorSource::new( TwoUnitSource { data: two_units }, DecryptKeys::Aacs { unit_keys: vec![(0, good_key)], // opens unit B, NOT unit A read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ); let mut buf = vec![0u8; 6 * 2048]; let n = wrapped .read_sectors(0, 6, &mut buf, false) .expect("the mux never aborts on a bad-decrypt unit"); assert_eq!(n, 6 * 2048); // Unit A (absent key) → passed through best-effort, NOT null-TS concealed. let unit0 = &buf[..crate::aacs::content::ALIGNED_UNIT_LEN]; let all_null = (0..32).all(|p| unit0[p * 192 + 4] == 0x47 && unit0[p * 192 + 6] == 0xFF); assert!( !all_null, "the undecryptable unit is passed through, never null-TS concealed" ); // Unit B → the GOOD decrypted padding tail, byte-for-byte intact. let unit1 = &buf [crate::aacs::content::ALIGNED_UNIT_LEN..2 * crate::aacs::content::ALIGNED_UNIT_LEN]; assert_eq!( unit1, &expected_tail[..], "the decryptable padding-tail unit comes out byte-for-byte correct" ); // Sanity: its real content packets carry their TS sync; its padding is zero. for p in 0..KEEP { assert_eq!(unit1[p * 192 + 4], 0x47, "content pkt {p} sync preserved"); } for p in KEEP..32 { let o = p * 192; assert!( unit1[o..o + 192].iter().all(|&b| b == 0), "padding pkt {p} stayed zero (not NULL-TS-filled)" ); } } /// Fresh-key-on-failure: a unit encrypted under a key NOT in the initial set /// would normally count as decrypt loss. With a [`with_key_fetch`] callback /// that returns that key, the decorator must hand the still-scrambled unit to /// the callback, add the returned key, re-decrypt, and register ZERO loss. /// Without the callback the same read accumulates loss (the baseline). /// /// Grounding: `read_sectors` invokes `fetch_failed_units` when /// `decrypt_sectors` leaves a scrambled unit and a callback is installed. #[test] fn key_fetch_recovers_unit_with_a_fresh_key() { let real_key = [0x5au8; 16]; // the key the unit is actually under let wrong_key = [0x11u8; 16]; // the only key we start with struct EncUnitSource { unit: Vec, } impl SectorSource for EncUnitSource { fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { let bytes = count as usize * 2048; buf[..bytes].copy_from_slice(&self.unit); Ok(bytes) } } let unit = encrypt_aacs_unit(&real_key); // Capture what the callback was handed, and how many times it fired. let seen: Arc>>> = Arc::new(Mutex::new(Vec::new())); let seen_cb = Arc::clone(&seen); let fetch: super::KeyFetch = super::KeyFetch::unit_only(std::sync::Arc::new(move |samples: &[Vec]| { seen_cb.lock().unwrap().extend_from_slice(samples); vec![real_key] })); let mut wrapped = DecryptingSectorSource::new( EncUnitSource { unit: unit.clone() }, DecryptKeys::Aacs { unit_keys: vec![(0, wrong_key)], read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ) .with_key_fetch(fetch); let mut buf = vec![0u8; 3 * 2048]; wrapped.read_sectors(0, 3, &mut buf, false).unwrap(); // The recovered key decrypts the unit: it is now clean TS in `buf`. let unit0 = &buf[..crate::aacs::content::ALIGNED_UNIT_LEN]; assert!( crate::aacs::content::is_clean(unit0, crate::disc::ContentFormat::BdTs), "fetch supplied the key → the unit decrypts to clean TS" ); let got = seen.lock().unwrap(); assert_eq!( got.len(), 1, "callback must be invoked once with the failing unit" ); assert!( !crate::aacs::content::is_clean(&got[0], crate::disc::ContentFormat::BdTs), "the sample handed to the callback is the still-scrambled ciphertext" ); assert_eq!( got[0], unit, "the exact on-disc unit is forwarded for fetch" ); } /// THE MUX-STORM REGRESSION. A unit the held key OPENS (>= the 4-packet proof /// floor) but that carries many authored-bad packets (< half synced) must /// NEVER be handed to the key-fetch closure — its key is already in hand. Only /// a GENUINE miss (no held key opens it) is sampled. Before the min(E,4) /// unification, the bad-encoded unit tripped the old >50% majority in /// `aacs_unit_needs_decrypt`, so every batch re-sampled it to the key service /// (the Jason Bourne / Stand By Me stall). This drives the REAL /// `DecryptingSectorSource` recovery path, not a synthetic check. #[test] fn bad_encoded_opened_unit_is_never_sampled_to_the_key_service() { use crate::aacs::content::ALIGNED_UNIT_LEN; let held = [0x5au8; 16]; // opens the bad-encoded unit let orphan = [0x77u8; 16]; // opens the genuine-miss unit (NOT held) // Knock out packets 1..27 (26 authored-bad) → ~5 synced: >= the 4-packet // floor (OPENED) yet < half (what the old >50% majority false-flagged). let bad_pkts: Vec = (1..27).collect(); let bad_encoded = encrypt_aacs_unit_bad(&held, &bad_pkts); let genuine_miss = encrypt_aacs_unit(&orphan); // One 6-sector read spans both units: bad-encoded at [0,3), miss at [3,6). struct TwoUnits { a: Vec, b: Vec, } impl SectorSource for TwoUnits { fn capacity_sectors(&self) -> u32 { 6 } fn read_sectors( &mut self, _lba: u32, _count: u16, buf: &mut [u8], _r: bool, ) -> Result { let n = crate::aacs::content::ALIGNED_UNIT_LEN; buf[..n].copy_from_slice(&self.a); buf[n..2 * n].copy_from_slice(&self.b); Ok(2 * n) } } let seen: Arc>>> = Arc::new(Mutex::new(Vec::new())); let seen_cb = Arc::clone(&seen); let fetch: super::KeyFetch = super::KeyFetch::unit_only(std::sync::Arc::new(move |samples: &[Vec]| { seen_cb.lock().unwrap().extend_from_slice(samples); Vec::new() // service has nothing for the orphan — forces the sampling path })); let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(0u32, 6u32)]); let mut dec = DecryptingSectorSource::new( TwoUnits { a: bad_encoded.clone(), b: genuine_miss.clone(), }, DecryptKeys::Aacs { unit_keys: vec![(0, held)], // opens bad_encoded, NOT genuine_miss read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ) .with_content_ranges(ranges) .with_key_fetch(fetch); let mut buf = vec![0u8; 6 * 2048]; let _ = dec.read_sectors(0, 6, &mut buf, false); let got = seen.lock().unwrap(); assert!( !got.is_empty(), "the genuine orphan-key miss must trigger a fetch" ); for s in got.iter() { assert_ne!( &s[..ALIGNED_UNIT_LEN.min(s.len())], &bad_encoded[..], "a bad-encoded unit the key OPENED must NEVER be sampled (the storm)" ); } assert!( got.iter().any(|s| s.as_slice() == genuine_miss.as_slice()), "only the genuine miss is sampled to the key service" ); } /// A fetch that comes back EMPTY for one unit must NOT block a later fetch /// for a DIFFERENT unit (the multi-CPS case). The old global `fetch_spent` /// latch wrongly blocked it; the per-sample `fetch_dry` set must let unit B /// be asked for after unit A came back dry. #[test] fn fetch_dry_does_not_block_a_distinct_later_unit() { let key_a = [0x5au8; 16]; let key_b = [0x77u8; 16]; let unit_a = encrypt_aacs_unit(&key_a); let unit_b = encrypt_aacs_unit(&key_b); assert_ne!(unit_a, unit_b, "distinct ciphertext under distinct keys"); struct AltSource { units: Vec>, } impl SectorSource for AltSource { fn capacity_sectors(&self) -> u32 { 6 } // LBA-addressable (like a real File/drive): unit A at LBA 0..3, unit B // at LBA 3..6. Re-reading the same LBA returns the same ciphertext — the // key-fetch recovery re-reads on a miss, so a call-order-stateful mock // would hand it the wrong unit. fn read_sectors( &mut self, lba: u32, count: u16, buf: &mut [u8], _r: bool, ) -> Result { let bytes = count as usize * 2048; let u = if lba < 3 { &self.units[0] } else { &self.units[1] }; buf[..bytes].copy_from_slice(u); Ok(bytes) } } // Callback serves key_b only when asked about unit B; nothing for A. let unit_b_cb = unit_b.clone(); let calls = Arc::new(Mutex::new(0usize)); let calls_cb = Arc::clone(&calls); let fetch: super::KeyFetch = super::KeyFetch::unit_only(std::sync::Arc::new(move |samples: &[Vec]| { *calls_cb.lock().unwrap() += 1; if samples.iter().any(|s| *s == unit_b_cb) { vec![key_b] } else { vec![] } })); let mut wrapped = DecryptingSectorSource::new( AltSource { units: vec![unit_a, unit_b], }, DecryptKeys::Aacs { unit_keys: vec![(0, [0x11u8; 16])], // neither real key held up front read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ) .with_key_fetch(fetch); // Read A: fetch fires, returns nothing → A undecryptable (read errors). let mut buf = vec![0u8; 3 * 2048]; let _ = wrapped.read_sectors(0, 3, &mut buf, false); // Read B: fetch must STILL fire (B's sample isn't in the dry set) and // recover key_b → B decrypts cleanly. let mut buf2 = vec![0u8; 3 * 2048]; wrapped .read_sectors(3, 3, &mut buf2, false) .expect("unit B recovers via its own fetch"); assert_eq!( *calls.lock().unwrap(), 2, "fetch fired for BOTH units — the dry result for A did not latch off B" ); assert!( crate::aacs::content::is_clean(&buf2, crate::disc::ContentFormat::BdTs), "unit B is decrypted after its on-demand fetch" ); } /// `into_inner` / `inner` / `inner_mut` must hand back the original /// source unchanged. Grounding: the accessor methods. #[test] fn inner_accessors_round_trip() { let src = PatternedSource { capacity: 42 }; let mut wrapped = DecryptingSectorSource::new(src, DecryptKeys::None); assert_eq!(wrapped.inner().capacity_sectors(), 42); assert_eq!(wrapped.inner_mut().capacity_sectors(), 42); let recovered = wrapped.into_inner(); assert_eq!(recovered.capacity_sectors(), 42); } /// Source that returns a fixed unit's bytes for any read. struct FixedUnit { unit: Vec, } impl SectorSource for FixedUnit { fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _recovery: bool, ) -> Result { let bytes = count as usize * 2048; buf[..bytes].copy_from_slice(&self.unit); Ok(bytes) } } /// In-place decrypt + content map: a NON-content read passes through unchanged /// (ciphertext, not decrypted); an in-content read is decrypted IN PLACE. #[test] fn inplace_decrypt_content_gate_passes_clear_decrypts_content() { let key = [0x5a; 16]; let cipher_unit = encrypt_aacs_unit(&key); let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(1002u32, 99u32)]); // content @ 1002.. let mut dec = DecryptingSectorSource::new( FixedUnit { unit: cipher_unit.clone(), }, DecryptKeys::Aacs { unit_keys: vec![(0, key)], read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ) .with_content_ranges(ranges); // decrypt in place, content-gated // Non-content read (LBA 0): not decrypted → buf stays ciphertext. let mut buf = vec![0u8; 3 * 2048]; dec.read_sectors(0, 3, &mut buf, false).unwrap(); assert_eq!( buf, cipher_unit, "a non-content read is passed through, not decrypted" ); // In-content read (LBA 1002): decrypted in place → TS sync restored. let mut buf2 = vec![0u8; 3 * 2048]; dec.read_sectors(1002, 3, &mut buf2, false).unwrap(); assert_ne!( buf2, cipher_unit, "an in-content read is decrypted in place" ); assert_eq!(buf2[4], 0x47, "decrypted content carries the TS sync byte"); } /// A source that returns a fixed encrypted unit for ANY read — used to drive /// the verify-only fetch + cache tests below. struct AnyLbaUnit { unit: Vec, } impl SectorSource for AnyLbaUnit { fn read_sectors( &mut self, _lba: u32, count: u16, buf: &mut [u8], _r: bool, ) -> Result { let b = count as usize * 2048; buf[..b].copy_from_slice(&self.unit); Ok(b) } } /// CPS-2 key recovery at the read level: a content unit no HELD key opens hands /// its on-disc ciphertext to the fetch closure, the returned key is added to the /// pool (the CACHE) and the read is re-decrypted IN PLACE. The cached key then /// serves the NEXT unit WITHOUT another callback (≈one fetch per CPS unit) — /// what stops an orphan CPS unit from producing garbage. #[test] fn fetch_recovers_and_caches_the_cps_key() { let real_key = [0x5au8; 16]; // the key the unit is actually under let wrong_key = [0x11u8; 16]; // the only key we start with let unit = encrypt_aacs_unit(&real_key); let calls = Arc::new(Mutex::new(0usize)); let calls_cb = Arc::clone(&calls); let fetch: super::KeyFetch = super::KeyFetch::unit_only(std::sync::Arc::new(move |samples: &[Vec]| { *calls_cb.lock().unwrap() += 1; // The closure is handed the still-scrambled on-disc ciphertext. assert!(!samples.is_empty(), "fetch receives the failing units"); assert_eq!(samples[0].len(), crate::aacs::content::ALIGNED_UNIT_LEN); vec![real_key] })); let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(0u32, 6u32)]); // LBA 0..6 content let mut dec = DecryptingSectorSource::new( AnyLbaUnit { unit: unit.clone() }, DecryptKeys::Aacs { unit_keys: vec![(0, wrong_key)], read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ) .with_content_ranges(ranges) .with_key_fetch(fetch); // First read (LBA 0): wrong key fails → fetch supplies real_key → the read // is re-decrypted IN PLACE, so buf comes out clean TS (not the ciphertext). let mut buf = vec![0u8; 3 * 2048]; dec.read_sectors(0, 3, &mut buf, false) .expect("fetch recovers the orphan unit's key"); assert_ne!(buf, unit, "the fetched key decrypts the unit in place"); assert!( crate::aacs::content::is_clean(&buf, crate::disc::ContentFormat::BdTs), "the recovered read is clean TS" ); assert_eq!(*calls.lock().unwrap(), 1, "fetch called exactly once"); // Second read (LBA 3): real_key now CACHED → decrypts with no new callback. let mut buf2 = vec![0u8; 3 * 2048]; dec.read_sectors(3, 3, &mut buf2, false) .expect("cached key serves the next unit"); assert_eq!( *calls.lock().unwrap(), 1, "cache hit — the fetch callback must NOT fire again" ); } /// Bad-encoding pass-through: a unit the held key OPENS (the proof floor is >=4 /// good packets) but that carries many authored-bad packets reads Ok and is /// DECRYPTED in place — never fails loud, never grinds on a physically fine /// read. The old 75% proportion false-failed this exact unit. #[test] fn bad_encoded_unit_the_key_opened_passes_through_decrypted() { let key = [0x5au8; 16]; // 20 authored-bad packets (1..21); packets 0 + 21..31 stay clean → 11 good // encrypted packets ≥ the 4-packet proof floor, so the key OPENED the unit. let bad: Vec = (1..21).collect(); let unit = encrypt_aacs_unit_bad(&key, &bad); let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(0u32, 3u32)]); let mut dec = DecryptingSectorSource::new( FixedUnit { unit: unit.clone() }, DecryptKeys::Aacs { unit_keys: vec![(0, key)], read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ) .with_content_ranges(ranges); let mut buf = vec![0u8; 3 * 2048]; dec.read_sectors(0, 3, &mut buf, false) .expect("a bad-encoded unit the key OPENED reads Ok, never fail-loud"); assert_ne!( buf, unit, "the unit is decrypted in place, not left ciphertext" ); // The 11 good packets recovered their TS sync (the muxer drops the bad ones). assert_eq!(buf[21 * 192 + 4], 0x47, "a good packet restored its sync"); } /// The fetch is content-gated: a scrambled unit OUTSIDE the content extents /// is clear filesystem, not ciphertext, so the read succeeds and the fetch /// callback is never consulted (no wasted key-server traffic on nav/UDF). #[test] fn fetch_not_called_outside_content() { let real_key = [0x5au8; 16]; let wrong = [0x11u8; 16]; let unit = encrypt_aacs_unit(&real_key); let calls = Arc::new(Mutex::new(0usize)); let calls_cb = Arc::clone(&calls); let fetch: super::KeyFetch = super::KeyFetch::unit_only(std::sync::Arc::new(move |_: &[Vec]| { *calls_cb.lock().unwrap() += 1; vec![real_key] })); // Content lives far away; LBA 0 is "filesystem". let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(1002u32, 99u32)]); let mut dec = DecryptingSectorSource::new( AnyLbaUnit { unit }, DecryptKeys::Aacs { unit_keys: vec![(0, wrong)], read_data_key: None, format: crate::disc::ContentFormat::BdTs, }, ) .with_content_ranges(ranges) .with_key_fetch(fetch); let mut buf = vec![0u8; 3 * 2048]; dec.read_sectors(0, 3, &mut buf, false) .expect("non-content scrambled-looking bytes read OK (gated out)"); assert_eq!( *calls.lock().unwrap(), 0, "fetch must NOT fire for a non-content unit" ); } }