Decrypt is keymap-only: sweep/patch/extract, no AACS trial-decrypt

Every AACS decrypt now goes through the resolved key map (decrypt_sectors_
mapped): the map keys each content unit up front and a missing key fails at
resolve time. The old trial-decrypt path — try each held key per unit, keep
the first-tried plaintext on a miss — is gone; decrypt_sectors_impl's AACS
arm now fails loud (reaching it means a reader was built without its map,
which would silently apply a wrong key). CSS (self-descramble) and the clear
no-op path are unchanged.

Disc::sweep and Disc::patch resolve a whole-disc key map up front for a
decrypting pass (the fetch secures any missing CPS-unit key, fail-loud) and
decrypt via the map — clear nav/filesystem sectors are in no range and pass
through, so the separate content-range gate and the reactive per-unit
key-fetch recovery are no longer needed. extract_tree keys every unit with
the base Unit Key through the map (its encrypted-flag gate skips clear
files). Multipass sweeps stay --raw.

Removes the obsolete non-mapped-AACS trial/gate/recovery tests (the mapped
path and resolve fail-loud are tested directly).
This commit is contained in:
Matthew Jackson
2026-07-23 13:24:36 -07:00
parent 279ba0dd7c
commit 88e58bfc95
6 changed files with 89 additions and 1545 deletions
+10
View File
@@ -192,6 +192,16 @@ impl Disc {
// borrowing wrapper (so the caller keeps `reader`), swap keys per CSS
// VTS group via `set_keys`; AACS/None keep `base_keys` throughout.
let mut dec = DecryptingSectorSource::new(Borrowed(reader), base_keys.clone());
// AACS decrypts via the key map. Extract reads arbitrary files (not resolved
// title extents), so key every unit with the disc's base Unit Key: the mapped
// decrypt applies it to encrypted units and passes clear filesystem/nav
// through (its encrypted-flag gate). Single-CPS is exact; a multi-CPS disc's
// secondary units are not separately keyed here (extract is not the mux path).
if matches!(base_keys, DecryptKeys::Aacs { .. }) {
dec = dec.with_key_map(std::sync::Arc::new(
crate::decrypt::AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]),
));
}
let mut result = ExtractResult::default();
let total_bytes = required;
+50 -9
View File
@@ -2357,6 +2357,37 @@ impl Disc {
}
}
/// Resolve a WHOLE-DISC AACS key map for a decrypting sweep (`disc:// → iso://`):
/// the union of every title's proactive key map ([`crate::mux::resolve_mux_key_map`]),
/// so a sequential read of the entire disc decrypts each content unit with its
/// mapped key and passes clear filesystem/nav sectors (in no range) through.
/// Fails loud (via the per-title resolve) if any content unit's key is missing.
/// `keys` is mutated as fetched keys are banked; the merged ranges are disjoint
/// (titles that share a clip resolve the same span — the duplicate is dropped).
pub(crate) fn resolve_content_key_map(
&self,
reader: &mut dyn SectorSource,
keys: &mut crate::decrypt::DecryptKeys,
fetch: Option<&crate::sector::KeyFetch>,
) -> Result<crate::decrypt::AacsKeyMap> {
let mut ranges: Vec<(u32, u32, usize, crate::decrypt::Phase)> = Vec::new();
for title in &self.titles {
let map =
crate::mux::resolve_mux_key_map(reader, title, keys, fetch, self.content_format)?;
ranges.extend_from_slice(map.ranges());
}
ranges.sort_by_key(|&(s, _, _, _)| s);
let mut merged: Vec<(u32, u32, usize, crate::decrypt::Phase)> = Vec::new();
for r in ranges {
// Drop a range that overlaps one already kept (a clip shared by two
// titles resolves the same span twice) — entry_for needs disjoint ranges.
if merged.last().is_none_or(|&(_, e, _, _)| r.0 >= e) {
merged.push(r);
}
}
Ok(crate::decrypt::AacsKeyMap::from_ranges_phased(merged))
}
/// The disc's AACS-encrypted content as a sorted, merged, disjoint set of
/// `(start_lba, sector_count)` ranges — the union of every title's m2ts
/// stream extents.
@@ -3138,27 +3169,37 @@ impl Disc {
// clips like Dunkirk's orphan-CPS clip — was removed. There is no scratch
// verify and no post-sweep clip-anchored pass; decryptability is proven at
// mux time, not at capture time.)
let keys = if opts.decrypt {
let mut keys = if opts.decrypt {
self.decrypt_keys()
} else {
crate::decrypt::DecryptKeys::None
};
let decrypt_is_aacs = matches!(keys, crate::decrypt::DecryptKeys::Aacs { .. });
// Content extent map — only the in-place decrypt path (`opts.decrypt`) gates
// on it so clear filesystem / nav sectors pass through untouched.
// AACS decrypting sweep: resolve a WHOLE-DISC key map up front (the fetch
// secures any missing CPS-unit key, fail-loud) and decrypt via the map —
// a clear nav/filesystem sector is in no range and passes through, so no
// separate content gate is needed. CSS keeps the content-gated
// self-descramble path (the map path is AACS-only).
let key_map = if opts.decrypt && decrypt_is_aacs {
Some(std::sync::Arc::new(self.resolve_content_key_map(
reader,
&mut keys,
opts.key_fetch.as_ref(),
)?))
} else {
None
};
let content_ranges = self.encrypted_content_ranges();
let can_gate = !content_ranges.is_empty();
let mut reader = {
let mut dec = DecryptingSectorSource::new(reader, keys);
if opts.decrypt && can_gate {
if let Some(map) = key_map {
dec = dec.with_key_map(map);
} else if opts.decrypt && can_gate {
// CSS / clear decrypt: content-gate the self-descramble path.
dec = dec.with_content_ranges(std::sync::Arc::from(content_ranges));
}
if decrypt_is_aacs && opts.decrypt {
if let Some(cb) = &opts.key_fetch {
dec = dec.with_key_fetch(cb.clone());
}
}
dec
};
let reader = &mut reader;
+16 -7
View File
@@ -1329,24 +1329,33 @@ impl Disc {
// PHYSICAL read success, not by decrypt structure: a re-read that returns
// good bytes recovers the range; a read that errors leaves it NonTrimmed
// for the next pass. (The old decrypt-VERIFY read gate was removed.)
let keys = if opts.decrypt {
let mut keys = if opts.decrypt {
self.decrypt_keys()
} else {
crate::decrypt::DecryptKeys::None
};
let decrypt_is_aacs = matches!(keys, crate::decrypt::DecryptKeys::Aacs { .. });
// AACS decrypting patch: resolve the whole-disc key map up front and decrypt
// via the map (identical to `Disc::sweep`). CSS keeps the content-gated
// self-descramble path. (Multipass patch is `--raw`, so decrypt is a no-op.)
let key_map = if opts.decrypt && decrypt_is_aacs {
Some(std::sync::Arc::new(self.resolve_content_key_map(
reader,
&mut keys,
opts.key_fetch.as_ref(),
)?))
} else {
None
};
let content_ranges = self.encrypted_content_ranges();
let can_gate = !content_ranges.is_empty();
let mut reader = {
let mut dec = DecryptingSectorSource::new(reader, keys);
if opts.decrypt && can_gate {
if let Some(map) = key_map {
dec = dec.with_key_map(map);
} else if opts.decrypt && can_gate {
dec = dec.with_content_ranges(std::sync::Arc::from(content_ranges));
}
if decrypt_is_aacs && opts.decrypt {
if let Some(cb) = &opts.key_fetch {
dec = dec.with_key_fetch(cb.clone());
}
}
dec
};
let reader = &mut reader;