mp4 demuxer (untrusted input): bound every allocation sized from a box field (stsz/stco/stsc counts, stts/ctts run-lengths, per-sample and moov sizes, plus an absolute cap so a sparse file can't inflate file_len); guard the parse_stsd slice and a zero mdhd timescale; cap track count so the per-track PID can't overflow; rewrite read_moov to handle size==0 / size<8 / 64-bit largesize; parse esds/AudioSpecificConfig for AAC; write tkhd duration in the movie timescale. decrypt: resolve_mux_key_map now fails loud on an extent no key can classify instead of inheriting the previous extent's key, so a keymap never silently carries a wrong key; the sweep/patch key-fetch recovery fails loud when a unit is still unresolved after the retry. AACS: reject inverted forensic segments in both range builders; compare the forensic index in u16 space so an out-of-range value can't truncate onto a valid u8 index. RECOVERED_ERROR no longer latches the damage zone, preserving the 30s wedge cooldown for a following hard error. audio: AAC/MP2/MP3/FLAC carry the last PTS across a PES with no timestamp; the DTS-HD extension-sync search is bounded to after the core; the MP4 16.16 sample-rate field saturates. demux_sink records the video reference before the kind filter so audio:// / sub:// keep multi-clip PTS continuity and the DELAY tag. Remove a dead error variant and the AACS-unsupported-video code; codec comments cite the primary format specs; assorted doc/naming fixes and regression tests throughout.
1661 lines
68 KiB
Rust
1661 lines
68 KiB
Rust
//! `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<dyn Fn(&[Vec<u8>]) -> 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<Fn>` — 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<u8>]) -> 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<u8>]) -> 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<S: SectorSource> {
|
|
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<crate::sector::recovery::Recover>,
|
|
/// 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<Arc<[(u32, u32)]>>,
|
|
/// 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<u8>,
|
|
/// 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<Arc<crate::decrypt::AacsKeyMap>>,
|
|
}
|
|
|
|
impl<S: SectorSource> DecryptingSectorSource<S> {
|
|
/// 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<crate::decrypt::AacsKeyMap>) -> 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<crate::decrypt::AacsKeyMap>) {
|
|
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<usize> {
|
|
match content {
|
|
Some(ranges) => decrypt_sectors_in_content(buf, keys, unit_key_idx, lba, ranges),
|
|
None => decrypt_sectors(buf, keys, unit_key_idx),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|
fn capacity_sectors(&self) -> u32 {
|
|
self.inner.capacity_sectors()
|
|
}
|
|
|
|
fn read_sectors(
|
|
&mut self,
|
|
lba: u32,
|
|
count: u16,
|
|
buf: &mut [u8],
|
|
recovery: bool,
|
|
) -> Result<usize> {
|
|
// 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<usize> {
|
|
// 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<usize> {
|
|
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<u16>,
|
|
}
|
|
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<usize> {
|
|
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<usize> {
|
|
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<Mutex<Vec<(u32, u16, bool)>>>,
|
|
}
|
|
impl SectorSource for ArgRecorder {
|
|
fn read_sectors(
|
|
&mut self,
|
|
lba: u32,
|
|
count: u16,
|
|
buf: &mut [u8],
|
|
recovery: bool,
|
|
) -> Result<usize> {
|
|
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<usize> {
|
|
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<usize> {
|
|
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<usize> {
|
|
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<usize> {
|
|
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<u8> {
|
|
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<u8> {
|
|
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<u8>,
|
|
}
|
|
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<usize> {
|
|
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<u8>,
|
|
}
|
|
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<usize> {
|
|
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<u8>,
|
|
}
|
|
impl SectorSource for EncUnitSource {
|
|
fn read_sectors(
|
|
&mut self,
|
|
_lba: u32,
|
|
count: u16,
|
|
buf: &mut [u8],
|
|
_recovery: bool,
|
|
) -> Result<usize> {
|
|
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<Mutex<Vec<Vec<u8>>>> = 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<u8>]| {
|
|
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<usize> = (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<u8>,
|
|
b: Vec<u8>,
|
|
}
|
|
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<usize> {
|
|
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<Mutex<Vec<Vec<u8>>>> = 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<u8>]| {
|
|
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<Vec<u8>>,
|
|
}
|
|
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<usize> {
|
|
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<u8>]| {
|
|
*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<u8>,
|
|
}
|
|
impl SectorSource for FixedUnit {
|
|
fn read_sectors(
|
|
&mut self,
|
|
_lba: u32,
|
|
count: u16,
|
|
buf: &mut [u8],
|
|
_recovery: bool,
|
|
) -> Result<usize> {
|
|
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<u8>,
|
|
}
|
|
impl SectorSource for AnyLbaUnit {
|
|
fn read_sectors(
|
|
&mut self,
|
|
_lba: u32,
|
|
count: u16,
|
|
buf: &mut [u8],
|
|
_r: bool,
|
|
) -> Result<usize> {
|
|
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<u8>]| {
|
|
*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<usize> = (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<u8>]| {
|
|
*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"
|
|
);
|
|
}
|
|
}
|