verify: container-kind seam (HD-DVD-ready) + skip units with unread sectors
ContainerKind {Ts,Ps} + ClipLayout.container thread the post-decrypt structural check per clip; decryptability() dispatches it (TS: unit_is_clean_ts, PS: unit_is_clean_ps). New decrypt_unit_checked(unit,key,accept) decouples the container-agnostic AACS crypto from the format-specific acceptance (decrypt_unit delegates with the TS check). unit_is_clean_ps is the MPEG-2 PS pack-start check, documented UNVALIDATED for HD-DVD (.evo unit/seed/pack alignment must be confirmed on real media). clip_layouts assigns Ts today; .evo->Ps is the one-line HD-DVD hook.
reverify_iso now takes an is_finished predicate and SKIPS any unit with a non-Finished backing sector: we can't verify what wasn't read (a non-Finished sector is zero-filled because the drive read failed there), and must never waste a key lookup on a block the read already knows is bad. observe() (sweep) was already safe (only fed Good bytes).
This commit is contained in:
+50
-9
@@ -217,6 +217,33 @@ pub fn unit_is_clean_ts(unit: &[u8]) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Structural "is this a clean MPEG-2 Program Stream aligned unit?" check — the
|
||||||
|
/// PS-container analogue of [`unit_is_clean_ts`], for AACS content carried as
|
||||||
|
/// program stream (HD-DVD `.evo`): every 2048-byte pack must begin with the
|
||||||
|
/// pack_start_code `00 00 01 BA`. A 6144-byte aligned unit spans three packs.
|
||||||
|
///
|
||||||
|
/// UNVALIDATED against real HD-DVD media. It assumes (a) HD-DVD uses the
|
||||||
|
/// standard AACS 6144-byte unit, (b) `.evo` clips are 2048-pack-aligned so unit
|
||||||
|
/// boundaries fall on pack starts, and (c) byte 0 of the unit is the pack start
|
||||||
|
/// — i.e. where the AACS seed and CPI indicator sit for PS content is the same
|
||||||
|
/// as BD-TS. Each of these must be confirmed against a real HD-DVD disc before
|
||||||
|
/// the `.evo` path is turned on (see `disc::verify::ContainerKind`). It exists
|
||||||
|
/// now only so the verify gate is structurally ready for that wiring.
|
||||||
|
pub fn unit_is_clean_ps(unit: &[u8]) -> bool {
|
||||||
|
if unit.len() < ALIGNED_UNIT_LEN {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const PACK_START: [u8; 4] = [0x00, 0x00, 0x01, 0xBA];
|
||||||
|
let mut o = 0;
|
||||||
|
while o < ALIGNED_UNIT_LEN {
|
||||||
|
if unit[o..o + 4] != PACK_START {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
o += SECTOR_BYTES; // one MPEG-2 PS pack per 2048-byte sector
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Decrypt one AACS aligned unit (6144 bytes) in-place.
|
/// Decrypt one AACS aligned unit (6144 bytes) in-place.
|
||||||
/// Returns true if the unit is now clear MPEG-TS: either it was already
|
/// Returns true if the unit is now clear MPEG-TS: either it was already
|
||||||
/// unscrambled (returned untouched, no key used) or it was decrypted and
|
/// unscrambled (returned untouched, no key used) or it was decrypted and
|
||||||
@@ -231,6 +258,21 @@ pub fn unit_is_clean_ts(unit: &[u8]) -> bool {
|
|||||||
/// Decryption restores the TS sync bytes, so the unit reads as clear afterward;
|
/// Decryption restores the TS sync bytes, so the unit reads as clear afterward;
|
||||||
/// there is no flag to clear.
|
/// there is no flag to clear.
|
||||||
pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
|
pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
|
||||||
|
decrypt_unit_checked(unit, unit_key, unit_is_clean_ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt an AACS aligned unit in place, accepting the key only when `accept`
|
||||||
|
/// passes on the decrypted bytes. The AACS crypto is container-agnostic; the
|
||||||
|
/// post-decrypt acceptance is the only format-specific part — so this is the
|
||||||
|
/// extension seam for non-TS containers. [`decrypt_unit`] is this with the BD-TS
|
||||||
|
/// check ([`unit_is_clean_ts`]); HD-DVD PS content would pass
|
||||||
|
/// [`unit_is_clean_ps`] instead. A CPI-clear unit is plaintext and passes
|
||||||
|
/// through untouched (no key consumed), exactly as before.
|
||||||
|
pub fn decrypt_unit_checked(
|
||||||
|
unit: &mut [u8],
|
||||||
|
unit_key: &[u8; 16],
|
||||||
|
accept: fn(&[u8]) -> bool,
|
||||||
|
) -> bool {
|
||||||
if unit.len() < ALIGNED_UNIT_LEN {
|
if unit.len() < ALIGNED_UNIT_LEN {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -238,27 +280,26 @@ pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
|
|||||||
return true; // CPI flag clear → plaintext, pass through untouched
|
return true; // CPI flag clear → plaintext, pass through untouched
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save original first 16 bytes (they're plaintext TP_extra_header)
|
// Save original first 16 bytes (they're the plaintext seed / header).
|
||||||
let mut header = [0u8; 16];
|
let mut header = [0u8; 16];
|
||||||
header.copy_from_slice(&unit[..16]);
|
header.copy_from_slice(&unit[..16]);
|
||||||
|
|
||||||
// Step 1: Encrypt header with unit key to derive per-unit key
|
// Step 1: Encrypt header with unit key to derive per-unit key.
|
||||||
let derived = aes_ecb_encrypt(unit_key, &header);
|
let derived = aes_ecb_encrypt(unit_key, &header);
|
||||||
|
|
||||||
// Step 2: XOR to get the actual decryption key
|
// Step 2: XOR to get the actual decryption key.
|
||||||
let mut decrypt_key = [0u8; 16];
|
let mut decrypt_key = [0u8; 16];
|
||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
decrypt_key[i] = derived[i] ^ header[i];
|
decrypt_key[i] = derived[i] ^ header[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Decrypt bytes 16..6143 with AES-CBC
|
// Step 3: Decrypt bytes 16..6143 with AES-CBC.
|
||||||
aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]);
|
aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]);
|
||||||
|
|
||||||
// Decryption restored the TS syncs; accept the key only if the unit is now
|
// Accept the key only if the decrypted unit passes the container's strict
|
||||||
// STRICTLY clean MPEG-TS (all 32 syncs) — the standards-correct gate, shared
|
// structural check. A wrong key that coincidentally restores a majority of
|
||||||
// with the post-read verify stage. A wrong key that coincidentally restores
|
// markers is rejected here, not silently accepted.
|
||||||
// a majority of syncs is rejected here, not silently accepted.
|
accept(unit)
|
||||||
unit_is_clean_ts(unit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fast, NON-MUTATING unit-key validation for the brute-force key search.
|
/// Fast, NON-MUTATING unit-key validation for the brute-force key search.
|
||||||
|
|||||||
+2
-2
@@ -33,8 +33,8 @@ pub use trace::{KeyNode, KeyOutcome, KeyStep, ResolutionTrace, UnlockOutcome, Un
|
|||||||
pub use decrypt::{
|
pub use decrypt::{
|
||||||
ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, UnitKeyResult, aacs_unit_encrypted,
|
ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, UnitKeyResult, aacs_unit_encrypted,
|
||||||
aacs_unit_needs_decrypt, decrypt_bus, decrypt_unit, decrypt_unit_full, decrypt_unit_try_keys,
|
aacs_unit_needs_decrypt, decrypt_bus, decrypt_unit, decrypt_unit_full, decrypt_unit_try_keys,
|
||||||
is_unit_aligned, ts_packet_total, ts_sync_count, ts_sync_destroyed, unit_is_clean_ts,
|
decrypt_unit_checked, is_unit_aligned, ts_packet_total, ts_sync_count, ts_sync_destroyed,
|
||||||
unit_key_validates,
|
unit_is_clean_ps, unit_is_clean_ts, unit_key_validates,
|
||||||
};
|
};
|
||||||
// `probe` is a reproduction-harness helper (see keys.rs), not part of the
|
// `probe` is a reproduction-harness helper (see keys.rs), not part of the
|
||||||
// documented 1.0 surface; keep it reachable but off the rendered docs so we
|
// documented 1.0 surface; keep it reachable but off the rendered docs so we
|
||||||
|
|||||||
@@ -347,6 +347,10 @@ pub(crate) fn clip_layouts(reader: &mut dyn SectorSource) -> Vec<crate::disc::ve
|
|||||||
.map(|pf| crate::disc::verify::ClipLayout {
|
.map(|pf| crate::disc::verify::ClipLayout {
|
||||||
size: pf.size,
|
size: pf.size,
|
||||||
extents: pf.extents,
|
extents: pf.extents,
|
||||||
|
// Every AACS clip we enumerate today is BD-TS (`.m2ts`/`.ssif`).
|
||||||
|
// HD-DVD `.evo` (program stream) maps to `ContainerKind::Ps` here
|
||||||
|
// once `is_aacs_clip` recognises it — the one-line HD-DVD hook.
|
||||||
|
container: crate::disc::verify::ContainerKind::Ts,
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
})();
|
})();
|
||||||
|
|||||||
+13
-4
@@ -2119,10 +2119,19 @@ impl Disc {
|
|||||||
// terminalizes it. Reuses the same verifier as the sweep. Fail-safe:
|
// terminalizes it. Reuses the same verifier as the sweep. Fail-safe:
|
||||||
// disabled gate / unreadable ISO / load failure all leave the pass as-is.
|
// disabled gate / unreadable ISO / load failure all leave the pass as-is.
|
||||||
if let Some(mut v) = verifier.take() {
|
if let Some(mut v) = verifier.take() {
|
||||||
if let Ok(mut iso) = crate::io::file_sector_source::FileSectorSource::open(path) {
|
if let Ok(mut m) = mapfile::Mapfile::load(&mapfile_path) {
|
||||||
let bad = v.reverify_iso(&mut iso, &bad_ranges);
|
// Only units whose every backing sector was actually READ
|
||||||
if !bad.is_empty() {
|
// (Finished) may be re-verified — we can't verify what wasn't read
|
||||||
if let Ok(mut m) = mapfile::Mapfile::load(&mapfile_path) {
|
// (a non-Finished sector is zero-filled because the read failed),
|
||||||
|
// and must not waste a key lookup on a known-bad block.
|
||||||
|
let finished = m.ranges_with(&[mapfile::SectorStatus::Finished]);
|
||||||
|
let is_finished = |lba: u32| -> bool {
|
||||||
|
let p = lba as u64 * 2048;
|
||||||
|
finished.iter().any(|&(s, sz)| p >= s && p < s + sz)
|
||||||
|
};
|
||||||
|
if let Ok(mut iso) = crate::io::file_sector_source::FileSectorSource::open(path) {
|
||||||
|
let bad = v.reverify_iso(&mut iso, &bad_ranges, &is_finished);
|
||||||
|
if !bad.is_empty() {
|
||||||
let n: usize = bad.len();
|
let n: usize = bad.len();
|
||||||
for (lba, cnt) in bad {
|
for (lba, cnt) in bad {
|
||||||
let _ = m.record(
|
let _ = m.record(
|
||||||
|
|||||||
+157
-55
@@ -47,13 +47,29 @@ const MAX_INFLIGHT_UNITS: usize = 4096; // ~24 MiB ceiling
|
|||||||
/// one fetch resolves all orphan units; the cap is a runaway backstop only.
|
/// one fetch resolves all orphan units; the cap is a runaway backstop only.
|
||||||
const MAX_FETCH_CALLS: u32 = 8;
|
const MAX_FETCH_CALLS: u32 = 8;
|
||||||
|
|
||||||
/// A clip's on-disc layout: declared file size plus its absolute disc extents in
|
/// The stream container of an AACS clip — selects the post-decrypt structural
|
||||||
/// FILE order. `extents` is `(disc_lba, byte_len)`; the verifier reuses exactly
|
/// check the verify gate applies. This is the extension seam: the AACS crypto is
|
||||||
/// the same `(abs_lba, byte_len)` extents the extractor enumerates.
|
/// container-agnostic, only the "is this clean?" check differs.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum ContainerKind {
|
||||||
|
/// BD/UHD `.m2ts` / `.ssif` — MPEG-2 transport stream (all-32 TS syncs).
|
||||||
|
#[default]
|
||||||
|
Ts,
|
||||||
|
/// HD-DVD `.evo` — MPEG-2 program stream (pack-start `00 00 01 BA`).
|
||||||
|
/// NOT yet enabled by enumeration; present so adding HD-DVD is a one-mapping
|
||||||
|
/// change. See [`crate::aacs::unit_is_clean_ps`] for the (unvalidated) check.
|
||||||
|
Ps,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clip's on-disc layout: declared file size, its absolute disc extents in
|
||||||
|
/// FILE order, and its stream container. `extents` is `(disc_lba, byte_len)`;
|
||||||
|
/// the verifier reuses exactly the same `(abs_lba, byte_len)` extents the
|
||||||
|
/// extractor enumerates.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ClipLayout {
|
pub struct ClipLayout {
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub extents: Vec<(u32, u32)>,
|
pub extents: Vec<(u32, u32)>,
|
||||||
|
pub container: ContainerKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One extent placed in the (disc-LBA -> clip-file-offset) space, for routing an
|
/// One extent placed in the (disc-LBA -> clip-file-offset) space, for routing an
|
||||||
@@ -96,6 +112,8 @@ pub struct UnitVerifier {
|
|||||||
extents: Vec<ExtentRec>,
|
extents: Vec<ExtentRec>,
|
||||||
/// Number of FULL (6144) units per clip; the partial tail unit is excluded.
|
/// Number of FULL (6144) units per clip; the partial tail unit is excluded.
|
||||||
full_units: Vec<u32>,
|
full_units: Vec<u32>,
|
||||||
|
/// Stream container per clip — selects the post-decrypt structural check.
|
||||||
|
containers: Vec<ContainerKind>,
|
||||||
/// Content unit keys to try (resolved keys plus any fetched + cached).
|
/// Content unit keys to try (resolved keys plus any fetched + cached).
|
||||||
keys: Vec<[u8; 16]>,
|
keys: Vec<[u8; 16]>,
|
||||||
fetch: Option<KeyFetch>,
|
fetch: Option<KeyFetch>,
|
||||||
@@ -128,10 +146,12 @@ impl UnitVerifier {
|
|||||||
|
|
||||||
let mut extents = Vec::new();
|
let mut extents = Vec::new();
|
||||||
let mut full_units = Vec::new();
|
let mut full_units = Vec::new();
|
||||||
|
let mut containers = Vec::new();
|
||||||
for (clip, layout) in clips.iter().enumerate() {
|
for (clip, layout) in clips.iter().enumerate() {
|
||||||
// Full units only; the partial tail (size not a multiple of 6144) is
|
// Full units only; the partial tail (size not a multiple of 6144) is
|
||||||
// never verified (a < 6144 buffer can't satisfy the strict gate).
|
// never verified (a < 6144 buffer can't satisfy the strict gate).
|
||||||
full_units.push((layout.size / ALIGNED_UNIT_LEN as u64) as u32);
|
full_units.push((layout.size / ALIGNED_UNIT_LEN as u64) as u32);
|
||||||
|
containers.push(layout.container);
|
||||||
let mut file_off: u64 = 0;
|
let mut file_off: u64 = 0;
|
||||||
for &(disc_lba, byte_len) in &layout.extents {
|
for &(disc_lba, byte_len) in &layout.extents {
|
||||||
let sectors = (byte_len as u64).div_ceil(SECTOR_BYTES_U64) as u32;
|
let sectors = (byte_len as u64).div_ceil(SECTOR_BYTES_U64) as u32;
|
||||||
@@ -154,6 +174,7 @@ impl UnitVerifier {
|
|||||||
Some(Self {
|
Some(Self {
|
||||||
extents,
|
extents,
|
||||||
full_units,
|
full_units,
|
||||||
|
containers,
|
||||||
keys: held,
|
keys: held,
|
||||||
fetch,
|
fetch,
|
||||||
fetch_calls: 0,
|
fetch_calls: 0,
|
||||||
@@ -163,6 +184,14 @@ impl UnitVerifier {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The post-decrypt structural check for a clip's container.
|
||||||
|
fn accept_for(&self, clip: u32) -> fn(&[u8]) -> bool {
|
||||||
|
match self.containers[clip as usize] {
|
||||||
|
ContainerKind::Ts => aacs::unit_is_clean_ts,
|
||||||
|
ContainerKind::Ps => aacs::unit_is_clean_ps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Feed a just-read, just-`Finished` disc byte range (`bytes` starts at disc
|
/// Feed a just-read, just-`Finished` disc byte range (`bytes` starts at disc
|
||||||
/// sector `disc_lba`). Routes each backing sector into its clip-file unit;
|
/// sector `disc_lba`). Routes each backing sector into its clip-file unit;
|
||||||
/// every unit that becomes fully assembled is verified immediately. Returns
|
/// every unit that becomes fully assembled is verified immediately. Returns
|
||||||
@@ -184,7 +213,8 @@ impl UnitVerifier {
|
|||||||
let off = s * sector;
|
let off = s * sector;
|
||||||
self.fill(clip, unit, slot, lba, &bytes[off..off + sector]);
|
self.fill(clip, unit, slot, lba, &bytes[off..off + sector]);
|
||||||
if let Some((raw, lbas)) = self.take_if_complete(clip, unit) {
|
if let Some((raw, lbas)) = self.take_if_complete(clip, unit) {
|
||||||
match self.decryptability(&raw) {
|
let accept = self.accept_for(clip);
|
||||||
|
match self.decryptability(&raw, accept) {
|
||||||
Decryptability::Undecryptable => push_ranges(&mut bad, &lbas),
|
Decryptability::Undecryptable => push_ranges(&mut bad, &lbas),
|
||||||
Decryptability::Decryptable | Decryptability::Unknown => {}
|
Decryptability::Decryptable | Decryptability::Unknown => {}
|
||||||
}
|
}
|
||||||
@@ -270,27 +300,33 @@ impl UnitVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Can this fully-assembled unit be decrypted + verified? The authoritative
|
/// Can this fully-assembled unit be decrypted + verified? `accept` is the
|
||||||
/// check is the strict [`aacs::unit_is_clean_ts`]; `decrypt_unit` only
|
/// container's strict structural check ([`aacs::unit_is_clean_ts`] for TS,
|
||||||
/// restores the body. Returns the 3-state [`Decryptability`].
|
/// [`aacs::unit_is_clean_ps`] for PS) — the only format-specific part; the
|
||||||
fn decryptability(&mut self, raw: &[u8; ALIGNED_UNIT_LEN]) -> Decryptability {
|
/// AACS crypto is container-agnostic. Returns the 3-state [`Decryptability`].
|
||||||
|
fn decryptability(
|
||||||
|
&mut self,
|
||||||
|
raw: &[u8; ALIGNED_UNIT_LEN],
|
||||||
|
accept: fn(&[u8]) -> bool,
|
||||||
|
) -> Decryptability {
|
||||||
// CPI clear -> the unit is plaintext by spec (no key needed). If it is
|
// CPI clear -> the unit is plaintext by spec (no key needed). If it is
|
||||||
// clean TS, it is decryptable-as-is. If it ISN'T, we DELIBERATELY return
|
// structurally clean, it is decryptable-as-is. If it ISN'T, we
|
||||||
// Unknown, not Undecryptable: with no key to crypto-prove anything, a
|
// DELIBERATELY return Unknown, not Undecryptable: with no key to
|
||||||
// clear-but-not-clean unit could be a genuine bad read OR a mis-aligned
|
// crypto-prove anything, a clear-but-not-clean unit could be a genuine
|
||||||
// read OR legitimately-odd clear content (some menu/nav units). We refuse
|
// bad read OR a mis-aligned read OR legitimately-odd clear content (some
|
||||||
// to assert "bad" without proof — only ENCRYPTED units that no key opens
|
// menu/nav units). We refuse to assert "bad" without proof — only
|
||||||
// are ever flagged. (A real bad READ of clear content is still caught by
|
// ENCRYPTED units that no key opens are ever flagged. (A real bad READ of
|
||||||
// the normal SCSI read-error path; this gate just won't false-flag it.)
|
// clear content is still caught by the normal SCSI read-error path; this
|
||||||
|
// gate just won't false-flag it.)
|
||||||
if !aacs::aacs_unit_encrypted(raw) {
|
if !aacs::aacs_unit_encrypted(raw) {
|
||||||
return if aacs::unit_is_clean_ts(raw) {
|
return if accept(raw) {
|
||||||
Decryptability::Decryptable
|
Decryptability::Decryptable
|
||||||
} else {
|
} else {
|
||||||
Decryptability::Unknown
|
Decryptability::Unknown
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Encrypted: any held key that decrypts to clean TS -> decryptable.
|
// Encrypted: any held key that decrypts to a structurally-clean unit.
|
||||||
if self.try_keys(raw) {
|
if self.try_keys(raw, accept) {
|
||||||
return Decryptability::Decryptable;
|
return Decryptability::Decryptable;
|
||||||
}
|
}
|
||||||
// No held key works. Ask the application's key source ONCE for this
|
// No held key works. Ask the application's key source ONCE for this
|
||||||
@@ -313,7 +349,7 @@ impl UnitVerifier {
|
|||||||
self.fetch_spent = true; // service has nothing new; stop asking
|
self.fetch_spent = true; // service has nothing new; stop asking
|
||||||
return Decryptability::Unknown;
|
return Decryptability::Unknown;
|
||||||
}
|
}
|
||||||
if self.try_keys(raw) {
|
if self.try_keys(raw, accept) {
|
||||||
return Decryptability::Decryptable;
|
return Decryptability::Decryptable;
|
||||||
}
|
}
|
||||||
return Decryptability::Undecryptable; // service's keys don't open it -> bad ciphertext
|
return Decryptability::Undecryptable; // service's keys don't open it -> bad ciphertext
|
||||||
@@ -322,11 +358,12 @@ impl UnitVerifier {
|
|||||||
Decryptability::Unknown
|
Decryptability::Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if any currently-held key decrypts `raw` to strictly clean TS.
|
/// True if any currently-held key decrypts `raw` to a structurally-clean unit
|
||||||
fn try_keys(&self, raw: &[u8; ALIGNED_UNIT_LEN]) -> bool {
|
/// under the container's `accept` check.
|
||||||
|
fn try_keys(&self, raw: &[u8; ALIGNED_UNIT_LEN], accept: fn(&[u8]) -> bool) -> bool {
|
||||||
for k in &self.keys {
|
for k in &self.keys {
|
||||||
let mut scratch = *raw;
|
let mut scratch = *raw;
|
||||||
if aacs::decrypt_unit(&mut scratch, k) {
|
if aacs::decrypt_unit_checked(&mut scratch, k, accept) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,10 +382,18 @@ impl UnitVerifier {
|
|||||||
/// ISO). Reading the whole unit back from the just-patched ISO is the only
|
/// ISO). Reading the whole unit back from the just-patched ISO is the only
|
||||||
/// alignment-correct way to re-check it. FAIL-SAFE: an ISO read error on any
|
/// alignment-correct way to re-check it. FAIL-SAFE: an ISO read error on any
|
||||||
/// of a unit's sectors skips that unit (no false-bad).
|
/// of a unit's sectors skips that unit (no false-bad).
|
||||||
|
///
|
||||||
|
/// CRITICAL: `is_finished(lba)` must report whether each disc sector was
|
||||||
|
/// actually READ (mapfile `Finished`). A unit with ANY non-Finished sector is
|
||||||
|
/// zero-filled there (the drive read failed) — we CANNOT verify what was
|
||||||
|
/// never read, so such a unit is skipped entirely. This both avoids asserting
|
||||||
|
/// "undecryptable" on unread data and avoids wasting a key lookup on a block
|
||||||
|
/// the read already knows is bad.
|
||||||
pub fn reverify_iso<S: crate::sector::SectorSource>(
|
pub fn reverify_iso<S: crate::sector::SectorSource>(
|
||||||
&mut self,
|
&mut self,
|
||||||
iso: &mut S,
|
iso: &mut S,
|
||||||
ranges: &[(u64, u64)],
|
ranges: &[(u64, u64)],
|
||||||
|
is_finished: &dyn Fn(u32) -> bool,
|
||||||
) -> Vec<(u32, u32)> {
|
) -> Vec<(u32, u32)> {
|
||||||
let mut seen: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
|
let mut seen: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
|
||||||
let mut bad: Vec<(u32, u32)> = Vec::new();
|
let mut bad: Vec<(u32, u32)> = Vec::new();
|
||||||
@@ -368,6 +413,13 @@ impl UnitVerifier {
|
|||||||
let Some(lbas) = self.unit_disc_sectors(clip, unit) else {
|
let Some(lbas) = self.unit_disc_sectors(clip, unit) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
// We can only verify a unit whose EVERY backing sector was read.
|
||||||
|
// A non-Finished sector is zero-filled (the read failed there);
|
||||||
|
// verifying it would judge data we never read and waste a key
|
||||||
|
// lookup on a known-bad block. Skip the whole unit.
|
||||||
|
if !lbas.iter().all(|&l| is_finished(l)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let mut raw = [0u8; ALIGNED_UNIT_LEN];
|
let mut raw = [0u8; ALIGNED_UNIT_LEN];
|
||||||
let mut readable = true;
|
let mut readable = true;
|
||||||
for (slot, &slba) in lbas.iter().enumerate() {
|
for (slot, &slba) in lbas.iter().enumerate() {
|
||||||
@@ -385,7 +437,10 @@ impl UnitVerifier {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if readable && matches!(self.decryptability(&raw), Decryptability::Undecryptable) {
|
let accept = self.accept_for(clip);
|
||||||
|
if readable
|
||||||
|
&& matches!(self.decryptability(&raw, accept), Decryptability::Undecryptable)
|
||||||
|
{
|
||||||
push_ranges(&mut bad, &lbas);
|
push_ranges(&mut bad, &lbas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,6 +501,16 @@ mod tests {
|
|||||||
u
|
u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A clear MPEG-2 PS aligned unit: pack-start `00 00 01 BA` at each 2048
|
||||||
|
/// boundary, CPI bits clear (byte 0 == 0x00).
|
||||||
|
fn clear_ps_unit() -> Vec<u8> {
|
||||||
|
let mut u = vec![0u8; ALIGNED_UNIT_LEN];
|
||||||
|
for o in [0usize, 2048, 4096] {
|
||||||
|
u[o..o + 4].copy_from_slice(&[0x00, 0x00, 0x01, 0xBA]);
|
||||||
|
}
|
||||||
|
u
|
||||||
|
}
|
||||||
|
|
||||||
/// Encrypt a clear unit in place under `unit_key` (sets CPI, AES-CBC body) —
|
/// Encrypt a clear unit in place under `unit_key` (sets CPI, AES-CBC body) —
|
||||||
/// the exact inverse of `decrypt_unit`, so the right key restores clean TS.
|
/// the exact inverse of `decrypt_unit`, so the right key restores clean TS.
|
||||||
fn encrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) {
|
fn encrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) {
|
||||||
@@ -485,10 +550,16 @@ mod tests {
|
|||||||
|
|
||||||
/// One contiguous full unit at disc LBA `lba` (size 6144 = 3 sectors).
|
/// One contiguous full unit at disc LBA `lba` (size 6144 = 3 sectors).
|
||||||
fn one_clip(lba: u32) -> Vec<ClipLayout> {
|
fn one_clip(lba: u32) -> Vec<ClipLayout> {
|
||||||
vec![ClipLayout {
|
vec![ts_clip(ALIGNED_UNIT_LEN as u64, vec![(lba, ALIGNED_UNIT_LEN as u32)])]
|
||||||
size: ALIGNED_UNIT_LEN as u64,
|
}
|
||||||
extents: vec![(lba, ALIGNED_UNIT_LEN as u32)],
|
|
||||||
}]
|
/// Build a BD-TS `ClipLayout` (the only container current enumeration emits).
|
||||||
|
fn ts_clip(size: u64, extents: Vec<(u32, u32)>) -> ClipLayout {
|
||||||
|
ClipLayout {
|
||||||
|
size,
|
||||||
|
extents,
|
||||||
|
container: ContainerKind::Ts,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── fail-safe: when the gate must NOT exist ────────────────────────────
|
// ── fail-safe: when the gate must NOT exist ────────────────────────────
|
||||||
@@ -519,10 +590,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_is_none_with_no_extents() {
|
fn new_is_none_with_no_extents() {
|
||||||
let clips = vec![ClipLayout {
|
let clips = vec![ts_clip(0, vec![])];
|
||||||
size: 0,
|
|
||||||
extents: vec![],
|
|
||||||
}];
|
|
||||||
assert!(UnitVerifier::new(&clips, &aacs_keys(&[[1; 16]]), None).is_none());
|
assert!(UnitVerifier::new(&clips, &aacs_keys(&[[1; 16]]), None).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,10 +699,7 @@ mod tests {
|
|||||||
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
vec![real]
|
vec![real]
|
||||||
});
|
});
|
||||||
let clips = vec![ClipLayout {
|
let clips = vec![ts_clip(2 * ALIGNED_UNIT_LEN as u64, vec![(200, 2 * ALIGNED_UNIT_LEN as u32)])];
|
||||||
size: 2 * ALIGNED_UNIT_LEN as u64,
|
|
||||||
extents: vec![(200, 2 * ALIGNED_UNIT_LEN as u32)],
|
|
||||||
}];
|
|
||||||
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[[0x01; 16]]), Some(fetch)).unwrap();
|
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[[0x01; 16]]), Some(fetch)).unwrap();
|
||||||
let mut u0 = clear_unit();
|
let mut u0 = clear_unit();
|
||||||
encrypt_unit(&mut u0, &real);
|
encrypt_unit(&mut u0, &real);
|
||||||
@@ -659,10 +724,7 @@ mod tests {
|
|||||||
let real = [0x42; 16];
|
let real = [0x42; 16];
|
||||||
let mut u = clear_unit();
|
let mut u = clear_unit();
|
||||||
encrypt_unit(&mut u, &real);
|
encrypt_unit(&mut u, &real);
|
||||||
let clips = vec![ClipLayout {
|
let clips = vec![ts_clip(ALIGNED_UNIT_LEN as u64, vec![(10, 4096), (5000, 2048)])];
|
||||||
size: ALIGNED_UNIT_LEN as u64,
|
|
||||||
extents: vec![(10, 4096), (5000, 2048)],
|
|
||||||
}];
|
|
||||||
// Wrong key + a fetch that yields wrong keys => confident bad, fragmented.
|
// Wrong key + a fetch that yields wrong keys => confident bad, fragmented.
|
||||||
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
|
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
|
||||||
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[[0x01; 16]]), Some(fetch)).unwrap();
|
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[[0x01; 16]]), Some(fetch)).unwrap();
|
||||||
@@ -686,10 +748,7 @@ mod tests {
|
|||||||
let key = [0x5a; 16];
|
let key = [0x5a; 16];
|
||||||
let mut u0 = clear_unit();
|
let mut u0 = clear_unit();
|
||||||
encrypt_unit(&mut u0, &key);
|
encrypt_unit(&mut u0, &key);
|
||||||
let clips = vec![ClipLayout {
|
let clips = vec![ts_clip(ALIGNED_UNIT_LEN as u64 + 2048, vec![(100, ALIGNED_UNIT_LEN as u32 + 2048)])];
|
||||||
size: ALIGNED_UNIT_LEN as u64 + 2048,
|
|
||||||
extents: vec![(100, ALIGNED_UNIT_LEN as u32 + 2048)],
|
|
||||||
}];
|
|
||||||
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[key]), None).unwrap();
|
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[key]), None).unwrap();
|
||||||
// Feed full unit 0 (good) + the tail sector (garbage). Only unit 0 is
|
// Feed full unit 0 (good) + the tail sector (garbage). Only unit 0 is
|
||||||
// judged; the tail is never a verdict.
|
// judged; the tail is never a verdict.
|
||||||
@@ -787,7 +846,7 @@ mod tests {
|
|||||||
let mut iso = MockIso { sectors: Default::default(), err_lba: None };
|
let mut iso = MockIso { sectors: Default::default(), err_lba: None };
|
||||||
place_unit(&mut iso, [100, 101, 102], &u);
|
place_unit(&mut iso, [100, 101, 102], &u);
|
||||||
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[key]), None).unwrap();
|
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[key]), None).unwrap();
|
||||||
let bad = v.reverify_iso(&mut iso, &[(100 * 2048, 3 * 2048)]);
|
let bad = v.reverify_iso(&mut iso, &[(100 * 2048, 3 * 2048)], &|_| true);
|
||||||
assert!(bad.is_empty(), "decryptable unit re-read clean -> not bad");
|
assert!(bad.is_empty(), "decryptable unit re-read clean -> not bad");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,7 +862,7 @@ mod tests {
|
|||||||
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
|
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
|
||||||
// A range covering only ONE sector of the unit still re-reads the WHOLE
|
// A range covering only ONE sector of the unit still re-reads the WHOLE
|
||||||
// unit from the ISO (patch re-reads partial units).
|
// unit from the ISO (patch re-reads partial units).
|
||||||
let bad = v.reverify_iso(&mut iso, &[(101 * 2048, 2048)]);
|
let bad = v.reverify_iso(&mut iso, &[(101 * 2048, 2048)], &|_| true);
|
||||||
assert_eq!(bad, vec![(100, 3)], "undecryptable unit -> full 3-sector range");
|
assert_eq!(bad, vec![(100, 3)], "undecryptable unit -> full 3-sector range");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,15 +872,12 @@ mod tests {
|
|||||||
let mut u = clear_unit();
|
let mut u = clear_unit();
|
||||||
encrypt_unit(&mut u, &key);
|
encrypt_unit(&mut u, &key);
|
||||||
// Unit 0: sectors at 10, 11 (extent A) and 5000 (extent B).
|
// Unit 0: sectors at 10, 11 (extent A) and 5000 (extent B).
|
||||||
let clips = vec![ClipLayout {
|
let clips = vec![ts_clip(ALIGNED_UNIT_LEN as u64, vec![(10, 4096), (5000, 2048)])];
|
||||||
size: ALIGNED_UNIT_LEN as u64,
|
|
||||||
extents: vec![(10, 4096), (5000, 2048)],
|
|
||||||
}];
|
|
||||||
let mut iso = MockIso { sectors: Default::default(), err_lba: None };
|
let mut iso = MockIso { sectors: Default::default(), err_lba: None };
|
||||||
place_unit(&mut iso, [10, 11, 5000], &u);
|
place_unit(&mut iso, [10, 11, 5000], &u);
|
||||||
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[key]), None).unwrap();
|
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[key]), None).unwrap();
|
||||||
// Range touches only the distant fragment; whole unit still assembled.
|
// Range touches only the distant fragment; whole unit still assembled.
|
||||||
let bad = v.reverify_iso(&mut iso, &[(5000 * 2048, 2048)]);
|
let bad = v.reverify_iso(&mut iso, &[(5000 * 2048, 2048)], &|_| true);
|
||||||
assert!(bad.is_empty(), "fragmented decryptable unit re-read clean");
|
assert!(bad.is_empty(), "fragmented decryptable unit re-read clean");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,19 +893,65 @@ mod tests {
|
|||||||
place_unit(&mut iso, [100, 101, 102], &u);
|
place_unit(&mut iso, [100, 101, 102], &u);
|
||||||
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
|
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
|
||||||
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
|
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
|
||||||
let bad = v.reverify_iso(&mut iso, &[(100 * 2048, 3 * 2048)]);
|
let bad = v.reverify_iso(&mut iso, &[(100 * 2048, 3 * 2048)], &|_| true);
|
||||||
assert!(bad.is_empty(), "ISO read error on a sector -> skip (fail-safe)");
|
assert!(bad.is_empty(), "ISO read error on a sector -> skip (fail-safe)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reverify_iso_skips_unit_with_unread_sector_and_never_fetches() {
|
||||||
|
// Sector 102 was NOT read (Unreadable -> zero-filled in the ISO). Even
|
||||||
|
// though the partly-zero unit wouldn't decrypt, we CANNOT verify what
|
||||||
|
// wasn't read: the unit is skipped, and crucially NO key lookup is made
|
||||||
|
// on a block the read already knows is bad.
|
||||||
|
let real = [0x11; 16];
|
||||||
|
let mut u = clear_unit();
|
||||||
|
encrypt_unit(&mut u, &real);
|
||||||
|
let mut iso = MockIso { sectors: Default::default(), err_lba: None };
|
||||||
|
place_unit(&mut iso, [100, 101, 102], &u);
|
||||||
|
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| panic!("must NOT key-fetch an unread unit"));
|
||||||
|
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
|
||||||
|
// 102 not Finished -> the whole unit is skipped.
|
||||||
|
let is_finished = |lba: u32| lba != 102;
|
||||||
|
let bad = v.reverify_iso(&mut iso, &[(100 * 2048, 3 * 2048)], &is_finished);
|
||||||
|
assert!(bad.is_empty(), "unit with an unread sector is skipped, not flagged or fetched");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ps_container_routes_through_pack_check() {
|
||||||
|
// A clip declared HD-DVD PS. The verifier must dispatch the structural
|
||||||
|
// check to unit_is_clean_ps (pack starts), not unit_is_clean_ts.
|
||||||
|
let clips = vec![ClipLayout {
|
||||||
|
size: ALIGNED_UNIT_LEN as u64,
|
||||||
|
extents: vec![(100, ALIGNED_UNIT_LEN as u32)],
|
||||||
|
container: ContainerKind::Ps,
|
||||||
|
}];
|
||||||
|
// A clear, valid PS unit passes the PS pack-start check -> not flagged.
|
||||||
|
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[[1; 16]]), None).unwrap();
|
||||||
|
assert!(
|
||||||
|
v.observe(100, &clear_ps_unit()).is_empty(),
|
||||||
|
"valid PS unit passes unit_is_clean_ps"
|
||||||
|
);
|
||||||
|
// An encrypted unit (CPI set) whose decrypt never yields PS pack-starts,
|
||||||
|
// with a fetch returning a non-working key -> confidently Undecryptable
|
||||||
|
// through decrypt_unit_checked(.., unit_is_clean_ps). Exercises the Ps
|
||||||
|
// path end-to-end (and constructs ContainerKind::Ps so it isn't dead).
|
||||||
|
let mut enc = clear_ps_unit();
|
||||||
|
enc[0] |= 0xC0; // CPI set; body is garbage to any key
|
||||||
|
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
|
||||||
|
let mut v2 = UnitVerifier::new(&clips, &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v2.observe(100, &enc),
|
||||||
|
vec![(100, 3)],
|
||||||
|
"encrypted PS unit no key opens -> bad via the PS check"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn eviction_bounds_inflight_partials() {
|
fn eviction_bounds_inflight_partials() {
|
||||||
// Open more partials than the cap with single-sector feeds; the map must
|
// Open more partials than the cap with single-sector feeds; the map must
|
||||||
// never exceed the cap (oldest evicted, unverified — fail-safe).
|
// never exceed the cap (oldest evicted, unverified — fail-safe).
|
||||||
let mut v = UnitVerifier::new(
|
let mut v = UnitVerifier::new(
|
||||||
&vec![ClipLayout {
|
&vec![ts_clip((MAX_INFLIGHT_UNITS as u64 + 100) * ALIGNED_UNIT_LEN as u64, vec![(0, u32::MAX / 2)])],
|
||||||
size: (MAX_INFLIGHT_UNITS as u64 + 100) * ALIGNED_UNIT_LEN as u64,
|
|
||||||
extents: vec![(0, u32::MAX / 2)],
|
|
||||||
}],
|
|
||||||
&aacs_keys(&[[1; 16]]),
|
&aacs_keys(&[[1; 16]]),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user