sector: generic recovery seam; FMTS forensic segments as decrypt loss
Replace the AACS-specific inline key-fetch in the decrypt decorator with
a scheme-neutral recovery seam: the input stream (L3) installs a Recover
closure (none / AACS key-fetch) and the decorator (L2) runs it at the
single decrypt-miss point. FMTS (AACS 2.1) forensic-segment units that no
key opens are just undecryptable units, concealed and counted as ordinary
decrypt loss with no FMTS-specific branch ("a loss is a loss"), so the
separate bytes_undecryptable bucket collapses into one loss count.
- sector/recovery.rs: the seam (MissOutcome, none/key_fetch factories),
naming no encryption scheme in its type.
- FMTS: segment routing primitives + BYPASS_FMTS_KEY, and an upfront
ensure_forensic_segments_decryptable gate (Error::FmtsKeyMissing) in
the mux input path, parallel to the unit-key gate.
- CSS descramble/rekey moves from decrypt_sectors into
css::descramble_region: CSS self-recovers from the data itself, so it
stays OFF the seam (which is only for external inputs).
- disc/mod.rs also: main-title selection aligned to largest physical
size; is_regular read from the open file handle, not metadata(path),
fixing a swallowed sync_all on a fresh-rip ISO. decrypt_threads()
resolved once via OnceLock off the per-buffer hot path.
This commit is contained in:
+6
-7
@@ -24,13 +24,12 @@ use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5};
|
||||
/// hierarchy, not the content cipher). Bytes 0x80..0x800 are recovered with
|
||||
/// `*p = TAB1[*p] ^ (i_t5 & 0xff)`.
|
||||
///
|
||||
/// The scramble flag at byte 0x14 (bits 4-5) indicates encryption. Like
|
||||
/// libdvdcss, the flag byte is NOT modified here — the caller treats a
|
||||
/// nonzero `sector[0x14] & 0x30` as "needs unscrambling" and the descramble
|
||||
/// is its own inverse, so re-running it on plaintext would re-scramble.
|
||||
/// (freemkv historically cleared the flag; we keep clearing it so callers
|
||||
/// and the existing tests can distinguish a descrambled sector. This does
|
||||
/// not affect the recovered body.)
|
||||
/// The scramble flag at byte 0x14 (bits 4-5) indicates encryption. This
|
||||
/// descrambler CLEARS that flag after unscrambling, so a descrambled sector
|
||||
/// reads as `sector[0x14] & 0x30 == 0`; callers and the tests use that to tell
|
||||
/// it from ciphertext, and re-running descramble on an already-cleared sector
|
||||
/// is a no-op (the flag guard below skips it). Clearing does not affect the
|
||||
/// recovered body.
|
||||
///
|
||||
/// No-op (returns without modifying `sector`) in two cases:
|
||||
/// - `sector.len() < 2048`: the encrypted region (0x80..0x800) is not
|
||||
|
||||
@@ -271,6 +271,52 @@ pub fn descramble_sector(state: &CssState, sector: &mut [u8]) {
|
||||
lfsr::descramble_sector(&state.title_key, sector);
|
||||
}
|
||||
|
||||
/// Descramble a whole CSS buffer in place, re-cracking the title key on a VOB
|
||||
/// region boundary. `title_key` is a CACHE of the last crack, not a fixed disc
|
||||
/// key: it changes per VTS/VOB region, so it is validated on every scrambled
|
||||
/// sector and re-cracked on a miss (libdvdcss's on-demand per-region rekey).
|
||||
///
|
||||
/// This CSS key acquisition is intrinsic to the cipher — CSS has no external key
|
||||
/// source, the ONLY way to a title key is cracking the data — so it lives with
|
||||
/// the CSS primitives and runs inside `decrypt::decrypt_sectors` (a public,
|
||||
/// self-contained CSS decrypt), NOT at the post-decrypt recovery seam that AACS
|
||||
/// key-fetch and FMTS segment-skip use (those consume external inputs).
|
||||
///
|
||||
/// The clear header (`<0x80`) is never scrambled, so its periodic crib predicts
|
||||
/// the plaintext at `0x80`. Descramble with the cached key; if the crib fails to
|
||||
/// reappear the key region changed (or the primed key was wrong) — restore the
|
||||
/// ciphertext, re-crack from this very sector, and descramble again. A crib-less
|
||||
/// sector (no periodic run) can be neither validated nor cracked, so it rides the
|
||||
/// cached key — correct, because it lives in the same region as the nearby crib
|
||||
/// sector that set the cache.
|
||||
pub fn descramble_region(buf: &mut [u8], title_key: &mut [u8; 5]) {
|
||||
for chunk in buf.chunks_mut(2048) {
|
||||
if chunk.len() < 2048 || !is_scrambled(chunk) {
|
||||
continue;
|
||||
}
|
||||
let crib = stevenson::attack_crib(chunk);
|
||||
// Snapshot the ciphertext (chunk is exactly 2048 here) only when there is
|
||||
// a crib to validate against, so the common cache-hit path costs no
|
||||
// per-sector heap allocation.
|
||||
let mut original = [0u8; 2048];
|
||||
if crib.is_some() {
|
||||
original.copy_from_slice(chunk);
|
||||
}
|
||||
lfsr::descramble_sector(title_key, chunk);
|
||||
if let Some(crib) = crib {
|
||||
if chunk[0x80..0x80 + 10] != crib[..] {
|
||||
// Cached key is stale for this region — restore the ciphertext and
|
||||
// crack this sector's own key.
|
||||
chunk.copy_from_slice(&original);
|
||||
if let Some(fresh) = stevenson::crack_title_key(chunk) {
|
||||
*title_key = fresh;
|
||||
}
|
||||
lfsr::descramble_sector(title_key, chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a sector has the CSS scramble flag set.
|
||||
///
|
||||
/// This is the RAW flag test — bits 4-5 of the sub-header byte 0x14 — used by
|
||||
|
||||
@@ -260,9 +260,6 @@ pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
|
||||
result
|
||||
}
|
||||
|
||||
/// Inner body of [`crack_title_key`] — the actual AttackPattern search. Split
|
||||
/// out so the public entry point can wall-clock the whole attempt for the
|
||||
/// runaway guard without threading a timer through every return path.
|
||||
/// AttackPattern crib: the predicted 10-byte plaintext at byte 0x80.
|
||||
///
|
||||
/// Scans the clear header `sec[0x00..0x80]` (never scrambled) for the longest
|
||||
|
||||
Reference in New Issue
Block a user