From 25893f1be405ff4ed4a9e40677f796d3c82b7913 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:26:22 -0700 Subject: [PATCH] CSS: fix the decrypted-HD-DVD false E7023 at the detection layer, not the public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 4cd9b7b ("key the DVD crack on the disc, not on the container") fixed a real bug — a decrypted HD-DVD hit E7023 (CssKeyMissing) because the per-title CSS crack keyed on the MPEG-PS container, which DVD and HD-DVD share — but did it by adding a required `disc_format: DiscFormat` parameter to the PUBLIC `DiscStream::new` and `build_iso_pipeline`, a `disc_format` field to `MuxInput::Iso`/`Live`, and `DiscFormat::may_have_css`, threading the axis down through mux/driver and mux/resolve. That changed the public API and broke every downstream caller's compilation (freemkv-engine's integration test now needed 8 args, autorip's MuxInput arms a new field). 1.6.4 shipped and worked with these exact signatures; a bug fix must not reshape them, and needing a whole disc-format plumb for HD-DVD was a code smell. Revert all of that plumbing (public signatures restored to their pre-4cd9b7b form; no `disc_format` parameter or field, no `may_have_css`, anywhere), and fix the ACTUAL bug where it lives: the scramble-detection heuristic. Root cause: `is_scrambled_pack` counted a sector as CSS scramble evidence on pack-start (00 00 01 BA) + bits 4-5 of byte 0x14. Offset 0x14 is only the PES scrambling-control field when the sector is a genuine elementary-stream pack. An HD-DVD `.evo` RDI navigation pack is private_stream_2 (stream_id 0xBF), an MPEG-PS pack exactly like a DVD VOB, whose byte 0x14 is raw nav payload that routinely has bits 4-5 set. On a decrypted HD-DVD (None keys, MPEG-PS, so it reaches the crack) those nav packs flipped the scan's `saw_scrambled` flag; the crack then found no key — there is no CSS on an HD-DVD — and the scan returned ScrambledUncracked, hard-failing a good disc with E7023. Fix: exclude the MPEG-PS structural stream_ids CSS never scrambles — system header (0xBB), padding (0xBE), private_stream_2 (0xBF) — by the stream_id at offset 0x11. This is the refinement the DVD design notes already called for ("matches CrackTitleKey"). It needs no format plumbing because byte 0x11 lives in the CSS-clear header (0x00-0x7F, untouched by scrambling), so it is the true stream_id even on ciphertext. A decrypted HD-DVD now scans to Unencrypted and muxes cleanly. The DVD CSS crack is preserved and proven: a genuinely CSS-scrambled DVD sector is always video (0xE0-0xEF) or private_stream_1 (0xBD), never an excluded id, so its scrambled packs still set saw_scrambled and still hard-fail an uncrackable disc — the "ciphertext muxed as plaintext at rc=0" catastrophe cannot slip through. Red-before-green both directions: dropping the 0x11 exclusion turns the decrypted-HD-DVD case back into E7023; inverting it (only nav ids count) turns a real uncrackable DVD into Unencrypted and strands a crackable one. Both mutations are caught by tests. Gate (cargo +1.97): fmt, clippy --all-targets -D warnings, 3539 tests green; freemkv-engine and autorip both compile against this tree again; precommit.sh libfreemkv clean. --- src/consts.rs | 4 + src/css/mod.rs | 273 +++++++++++++++++++++--------------------- src/disc/mod.rs | 41 ------- src/mux/disc.rs | 73 +---------- src/mux/driver.rs | 31 +---- src/mux/resolve.rs | 31 +---- tests/fvi_pipeline.rs | 4 - 7 files changed, 150 insertions(+), 307 deletions(-) diff --git a/src/consts.rs b/src/consts.rs index c997f2b..dad1cf7 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -114,6 +114,10 @@ pub mod coding_type { pub mod pes_stream_id { /// Video stream (`110x xxxx`; freemkv emits the base id `0xE0`). pub const VIDEO: u8 = 0xE0; + /// system_header start code — the MPEG-PS `00 00 01 BB` structural header + /// (rate/bound bounds), never an elementary stream. On a DVD NAV pack it + /// follows the pack header, so it lands at sector offset 0x11. + pub const SYSTEM_HEADER: u8 = 0xBB; /// private_stream_1 — AC-3 / DTS / LPCM / PGS subtitle payloads. pub const PRIVATE_STREAM_1: u8 = 0xBD; /// padding_stream — stuffing bytes only, no payload to demux. diff --git a/src/css/mod.rs b/src/css/mod.rs index b3ebe07..c1602c9 100644 --- a/src/css/mod.rs +++ b/src/css/mod.rs @@ -146,14 +146,7 @@ pub fn crack_key_outcome( /// ("reading is reading"). CSS keys are per-VTS and crackable from the scrambled /// data itself, so a `None`/MPEG-PS title cracks its own key here, in playback /// order over `extents`. Everything else is left untouched: -/// - a disc format that cannot carry CSS (`!disc_format.may_have_css()`, i.e. -/// HD-DVD and the BD families) — no CSS exists there to crack. This is the -/// DISC-FORMAT axis and it is separate from `format`, the container: HD-DVD -/// `.evo` is MPEG-PS exactly like DVD `.vob`, so the container alone cannot -/// tell them apart. `DiscFormat::Unknown` counts as "may have CSS" — see -/// [`crate::disc::DiscFormat::may_have_css`] for why the safe default is to -/// crack. -/// - AACS keys (an encrypted HD-DVD `.evo` also arrives as `Aacs`) — no CSS. +/// - AACS keys (HD-DVD `.evo` is also MPEG-PS but arrives as `Aacs`) — no CSS. /// - a title that already carries a key — nothing to resolve. /// - a genuinely clear DVD (no scrambled sector) — stays `None`, a mux no-op. /// @@ -164,18 +157,12 @@ pub fn crack_key_outcome( /// yield its key, so an all-titles rip skips this title and finishes the rest. /// The whole-disc failure is [`crate::error::Error::CssNoDiscKey`], raised by /// `Disc::ensure_decryptable_keys` from the scan's `css_error`. -// Eight reader/extent/key/format/mode params is inherent to a shared step that -// must be callable identically from both read paths; the two format params are -// the whole point of this function's contract (container vs disc family) and -// bundling them into a struct would only move the same fields around. -#[allow(clippy::too_many_arguments)] pub(crate) fn resolve_dvd_title_key( reader: &mut dyn SectorSource, extents: &[Extent], keys: &mut crate::decrypt::DecryptKeys, batch_sectors: u16, format: crate::disc::ContentFormat, - disc_format: crate::disc::DiscFormat, raw: bool, halt: Option<&crate::halt::Halt>, ) -> std::io::Result<()> { @@ -186,23 +173,8 @@ pub(crate) fn resolve_dvd_title_key( if raw { return Ok(()); } - // The crack is gated on TWO axes, and both are load-bearing: - // * `format == MpegPs` — the CONTAINER, i.e. "CSS descrambles 2048-byte - // program-stream sectors, not BD transport packets"; - // * `disc_format.may_have_css()` — the DISC FORMAT, i.e. "this family can - // carry CSS at all". - // Keying on the container ALONE was the defect: `ContentFormat::MpegPs` - // covers HD-DVD `.evo` as well as DVD `.vob` (both arms of the tree - // dispatch in `Disc::scan_with` set it), and HD-DVD is AACS — it has no CSS - // to find. Every HD-DVD title therefore paid a 50_000-sector crack scan - // that could not succeed, and a scan that came back `ScrambledUncracked` - // hard-failed a good disc with `CssKeyMissing` (E7023). `may_have_css` is - // deliberately false ONLY for the families proven CSS-free, so `Unknown` - // still cracks: skipping the crack on a real DVD would mux ciphertext as - // plaintext at exit 0, which is far worse than a wasted scan. if matches!(keys, crate::decrypt::DecryptKeys::None) && format == crate::disc::ContentFormat::MpegPs - && disc_format.may_have_css() { // `halt` threads the caller's cancellation token so /api/stop can // interrupt a long crack scan (the old scan-time crack honored it too). @@ -227,19 +199,6 @@ pub(crate) fn resolve_dvd_title_key( } CrackOutcome::Unencrypted => {} } - } else if matches!(keys, crate::decrypt::DecryptKeys::None) - && format == crate::disc::ContentFormat::MpegPs - { - // The skip is the interesting event, so it must not be silent: an - // MPEG-PS title with no key that does NOT get cracked is precisely the - // shape of the catastrophic bug (scrambled passthrough), so the log - // records WHICH disc format bought the skip. On an HD-DVD this line is - // the proof the 50_000-sector scan was avoided on purpose. - tracing::debug!( - target: "mux", - disc_format = ?disc_format, - "css crack skipped: disc format cannot carry CSS" - ); } Ok(()) } @@ -590,6 +549,35 @@ pub(crate) const PACK_START: [u8; 4] = [0x00, 0x00, 0x01, 0xBA]; /// scrambled feature is made of valid PS packs, so its scrambled sectors still /// pass this check and still drive `ScrambledUncracked` when no key cracks. /// +/// The pack-start check alone is not enough, and the sector's PES `stream_id` +/// (offset 0x11, the byte after the 14-byte pack header's `00 00 01` PES +/// prefix) is the second load-bearing gate. CSS scrambles ONLY elementary +/// streams — video (`0xE0..=0xEF`) and private_stream_1 audio/subpicture +/// (`0xBD`) — and it never touches the clear header, so byte 0x11 is the TRUE +/// stream_id even on a scrambled sector. The MPEG-PS structural packets that +/// are never CSS-scrambled — system_header (`0xBB`), padding (`0xBE`) and +/// private_stream_2 (`0xBF`, DVD PCI/DSI navigation) — must be excluded, +/// because on those the byte at 0x14 is NOT a PES scrambling-control field but +/// raw payload/structure whose bits 4-5 land set by chance. +/// +/// This is the DETECTION defect a decrypted HD-DVD tripped: an HD-DVD `.evo` is +/// MPEG-PS exactly like a DVD `.vob`, and its RDI navigation packs are +/// private_stream_2 (`0xBF`) whose payload byte at 0x14 routinely has bits 4-5 +/// set. With no `0x11` gate those nav packs flipped the crack scan's +/// `saw_scrambled` flag on a disc that carries no CSS at all; the crack then +/// found no key (there is none) and the scan returned `ScrambledUncracked`, +/// hard-failing a perfectly good HD-DVD with `CssKeyMissing` — E7023. Excluding +/// `0xBB/0xBE/0xBF` at 0x11 makes the evidence gate match what the crack itself +/// can act on (the Stevenson attack only recovers a key from a scrambled ES +/// pack), so a decrypted HD-DVD now scans to `Unencrypted` and muxes cleanly. +/// +/// This does NOT weaken the genuine "encrypted but uncrackable" hard-fail on a +/// real DVD: a scrambled DVD feature is made of video (`0xE0..`) and +/// private_stream_1 (`0xBD`) packs, none of which are excluded, so its +/// scrambled sectors still set `saw_scrambled` and still drive +/// `ScrambledUncracked` when no key cracks. Byte 0x11 is in the clear header, +/// so a scrambled DVD pack can never masquerade as `0xBB/0xBE/0xBF`. +/// /// The DESCRAMBLE path gates on this same function — [`descramble_sector`] and /// [`descramble_region`] both call it, not the raw flag test — because the raw /// test does not merely mis-skip a sector there: it descrambles one that was @@ -598,7 +586,14 @@ pub(crate) const PACK_START: [u8; 4] = [0x00, 0x00, 0x01, 0xBA]; /// lost 1912 of its 2048 bytes, taking TT_SRPT with it, and the disc's 38 /// titles became 10 — silently, at exit 0. One gate, both paths. pub fn is_scrambled_pack(sector: &[u8]) -> bool { - sector.len() >= 2048 && sector[0x00..0x04] == PACK_START && (sector[0x14] >> 4) & 0x03 != 0 + use crate::consts::pes_stream_id::{PADDING_STREAM, PRIVATE_STREAM_2, SYSTEM_HEADER}; + sector.len() >= 2048 + && sector[0x00..0x04] == PACK_START + && !matches!( + sector[0x11], + SYSTEM_HEADER | PADDING_STREAM | PRIVATE_STREAM_2 + ) + && (sector[0x14] >> 4) & 0x03 != 0 } #[cfg(test)] @@ -798,6 +793,52 @@ mod tests { ); } + /// Detection fix: `is_scrambled_pack` must EXCLUDE the MPEG-PS structural + /// packets that CSS never scrambles — system_header (0xBB), padding (0xBE) + /// and private_stream_2 (0xBF, DVD PCI/DSI and HD-DVD `.evo` RDI nav) — by + /// their `stream_id` at offset 0x11. On those packets byte 0x14 is raw + /// payload/structure, not a PES scrambling-control field, so its bits 4-5 + /// land set by chance. + /// + /// This is the exact decrypted-HD-DVD defect: an `.evo` RDI pack is + /// private_stream_2 (0xBF) with the pack-start code and bits set at 0x14, so + /// the old gate flipped `saw_scrambled` on a CSS-free disc → the crack found + /// no key → `ScrambledUncracked` → E7023 on a good HD-DVD. + /// + /// Grounding: `!matches!(sector[0x11], SYSTEM_HEADER | PADDING_STREAM | + /// PRIVATE_STREAM_2)`. Mutation: drop the 0x11 exclusion → the 0xBF nav pack + /// counts as scrambled evidence and the first assert fails. The video and + /// private_stream_1 packs prove the gate does NOT reject a real scrambled + /// DVD sector (the catastrophic direction). + #[test] + fn is_scrambled_pack_excludes_nav_and_structural_stream_ids() { + use crate::consts::pes_stream_id::{ + PADDING_STREAM, PRIVATE_STREAM_1, PRIVATE_STREAM_2, SYSTEM_HEADER, VIDEO, VIDEO_MAX, + }; + // A pack-start pack with 0x14 scramble bits set, varying only 0x11. + let mut s = vec![0u8; 2048]; + s[0x00..0x04].copy_from_slice(&PACK_START); + s[0x14] = 0x30; + for excluded in [SYSTEM_HEADER, PADDING_STREAM, PRIVATE_STREAM_2] { + s[0x11] = excluded; + assert!( + !is_scrambled_pack(&s), + "stream_id {excluded:#04x} is a structural/nav pack CSS never scrambles — \ + it must NOT count as scramble evidence (else a decrypted HD-DVD RDI pack → E7023)" + ); + } + // A genuinely scramblable elementary-stream pack must STILL register — + // proving the exclusion did not weaken real-DVD CSS detection. + for scramblable in [VIDEO, VIDEO_MAX, PRIVATE_STREAM_1, 0xE2] { + s[0x11] = scramblable; + assert!( + is_scrambled_pack(&s), + "stream_id {scramblable:#04x} is a scramblable ES pack — a real CSS DVD's \ + scrambled sector must still be counted, never passed through as plaintext" + ); + } + } + // ── crack_key scanning over a mock SectorSource ──────────────────────── /// Records every (lba, count) read; returns a caller-supplied flag byte at @@ -819,6 +860,11 @@ mod tests { /// a damaged region. `Some(0)` is the degenerate case that must not /// spin the scan. short_read: Option, + /// PES `stream_id` written at offset 0x11 of each uniform-fill sector. + /// `0x00` (the default) is a scramblable-looking pack; set to + /// private_stream_2 (`0xBF`) to model an HD-DVD `.evo` RDI nav pack, + /// which carries the pack-start code and 0x14 bits but no CSS. + stream_id: u8, } impl MockSource { @@ -830,6 +876,7 @@ mod tests { lock_all: false, crackable: None, short_read: None, + stream_id: 0x00, } } } @@ -904,11 +951,14 @@ mod tests { } _ => { // Real DVD video sectors always open with the MPEG-PS - // pack-start code; `is_scrambled_pack` (Fix 3) requires it - // before trusting the 0x14 scramble bits, so the fixture - // must include it for a `flag_byte` of 0x30 to register - // as scrambled. + // pack-start code; `is_scrambled_pack` requires it before + // trusting the 0x14 scramble bits, so the fixture must + // include it for a `flag_byte` of 0x30 to register as + // scrambled. `stream_id` (byte 0x11) defaults to 0x00 (a + // scramblable pack); an HD-DVD RDI nav pack sets it to + // private_stream_2 (0xBF), which the 0x11 exclusion drops. buf[base..base + 4].copy_from_slice(&PACK_START); + buf[base + 0x11] = self.stream_id; buf[base + 0x14] = self.flag_byte; } } @@ -1496,7 +1546,6 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, false, None, ) @@ -1527,7 +1576,6 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, false, None, ) @@ -1546,6 +1594,44 @@ mod tests { ); } + /// The decrypted-HD-DVD regression, end to end through `resolve_dvd_title_key`. + /// An `.evo` is MPEG-PS (`ContentFormat::MpegPs`) exactly like a DVD `.vob` + /// and carries `None` keys once decrypted, so it reaches the CSS crack. Its + /// RDI navigation packs are private_stream_2 (0xBF) with the pack-start code + /// and bits set at offset 0x14 — but HD-DVD carries no CSS at all. The 0x11 + /// exclusion in `is_scrambled_pack` keeps those nav packs from flipping the + /// scan's `saw_scrambled` gate, so the scan returns `Unencrypted` and the + /// title muxes cleanly instead of hard-failing. + /// + /// Catches the mutation of dropping the 0x11 exclusion: without it every 0xBF + /// RDI pack counts as scramble evidence, the crack finds no key (there is + /// none), and the scan returns `ScrambledUncracked` → `CssKeyMissing` (E7023) + /// on a perfectly good HD-DVD — the exact defect a real CI run produced. + #[test] + fn resolve_dvd_title_key_decrypted_hddvd_rdi_packs_scan_clean_no_e7023() { + let mut src = MockSource::new(0x30); // 0x14 bits set… + src.stream_id = crate::consts::pes_stream_id::PRIVATE_STREAM_2; // …but a 0xBF nav pack + let extents = [Extent { + start_lba: 0, + sector_count: 64, + }]; + let mut keys = crate::decrypt::DecryptKeys::None; + resolve_dvd_title_key( + &mut src, + &extents, + &mut keys, + 8, + crate::disc::ContentFormat::MpegPs, + false, + None, + ) + .expect("a decrypted HD-DVD's RDI nav packs are not CSS — the scan must not hard-fail"); + assert!( + matches!(keys, crate::decrypt::DecryptKeys::None), + "no CSS key exists on an HD-DVD; keys must stay None and the title mux clean" + ); + } + /// `raw` is deliberate ciphertext passthrough: even a scrambled-uncrackable /// title must return `Ok` and leave `keys` untouched (`None`) — no crack, no /// hard-fail. This is the `--raw` guarantee. @@ -1564,7 +1650,6 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, true, // raw None, ) @@ -1601,7 +1686,6 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, false, None, ) @@ -1616,91 +1700,6 @@ mod tests { ); } - /// HD-DVD: `.evo` is MPEG-PS exactly like DVD `.vob`, so the CONTAINER - /// cannot tell the two apart — the disc-format axis must. HD-DVD is an AACS - /// family and carries no CSS at all, yet the old container-only gate sent - /// every HD-DVD title into a 50_000-sector crack scan and, when the scan - /// came back `ScrambledUncracked`, refused a good disc with `CssKeyMissing` - /// (E7023) — what a real CI run produced on the HD-DVD fixture. - /// - /// Catches the mutation of dropping `disc_format.may_have_css()` from the - /// gate (or listing HD-DVD as CSS-capable): the source here is `lock_all`, - /// so ANY read the crack performs drives `ScrambledUncracked` → the call - /// returns `Err`. The zero-reads assertion is the stronger claim: the scan - /// must not merely survive, it must never start. - #[test] - fn resolve_dvd_title_key_hddvd_never_enters_css_crack() { - let mut src = MockSource::new(0x00); - src.lock_all = true; // would hard-fail E7023 IF the crack ran - let extents = [Extent { - start_lba: 0, - sector_count: 4, - }]; - let mut keys = crate::decrypt::DecryptKeys::None; - resolve_dvd_title_key( - &mut src, - &extents, - &mut keys, - 4, - crate::disc::ContentFormat::MpegPs, - crate::disc::DiscFormat::HdDvd, - false, - None, - ) - .expect("an HD-DVD carries no CSS — it must never be refused for a missing CSS key"); - assert!( - matches!(keys, crate::decrypt::DecryptKeys::None), - "no CSS key may be installed on an AACS-family disc" - ); - assert!( - src.reads.borrow().is_empty(), - "the crack scan must not read a single sector on an HD-DVD" - ); - } - - /// The safety valve, and the reason the gate is a NEGATIVE test rather than - /// `disc_format == DiscFormat::Dvd`: a caller that cannot name the disc - /// (`DiscFormat::Unknown` — e.g. a bare reader with no scan behind it) must - /// STILL reach the crack. The two failure directions are asymmetric — a - /// needless scan is recoverable, while skipping the crack on a real DVD - /// muxes ciphertext as plaintext at exit 0. - /// - /// Catches the mutation of "simplifying" `may_have_css()` into a positive - /// `== DiscFormat::Dvd` allow-list, which would silently strand every - /// unknown-format DVD in scrambled passthrough: the crackable sector here - /// stops being cracked and `keys` stays `None`. - #[test] - fn resolve_dvd_title_key_unknown_disc_format_still_cracks() { - let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF]; - let seed = [0x11, 0x22, 0x33, 0x44, 0x55]; - let crackable = crackable_sector(&title_key, &seed, 8); - let mut src = MockSource::new(0x00); - src.crackable = Some((1003, crackable)); - let extents = [Extent { - start_lba: 1000, - sector_count: 50, - }]; - let mut keys = crate::decrypt::DecryptKeys::None; - resolve_dvd_title_key( - &mut src, - &extents, - &mut keys, - 4, - crate::disc::ContentFormat::MpegPs, - crate::disc::DiscFormat::Unknown, - false, - None, - ) - .expect("an unknown disc format must still resolve a crackable CSS title"); - match keys { - crate::decrypt::DecryptKeys::Css { title_key: got } => assert_eq!( - got, title_key, - "an unknown-format MPEG-PS title must be cracked, not passed through" - ), - _ => panic!("expected Css key: Unknown must default to CSS-capable, never skip"), - } - } - /// Clear DVD: a `None`-keyed MPEG-PS title with no scrambled sector stays /// `None` (a mux no-op) and returns `Ok` — genuinely-unencrypted DVDs pass. #[test] @@ -1717,7 +1716,6 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, false, None, ) @@ -1749,7 +1747,6 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, false, Some(&halt), ) diff --git a/src/disc/mod.rs b/src/disc/mod.rs index a610da5..65987af 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -112,47 +112,6 @@ pub enum DiscFormat { Unknown, } -impl DiscFormat { - /// Can a disc of this format carry CSS (DVD Content Scramble System)? - /// - /// This is the axis the CSS crack MUST be gated on. It is NOT - /// [`ContentFormat`]: `ContentFormat::MpegPs` is the CONTAINER (MPEG - /// program stream) and the tree dispatch in [`Disc::scan_with`] assigns it - /// to the HD-DVD (`/HVDVD_TS`, `.evo`) arm exactly as it does to the DVD - /// (`/VIDEO_TS`, `.vob`) arm. HD-DVD is an AACS family and carries no CSS - /// whatsoever, so gating on the container sent every HD-DVD title into a - /// 50_000-sector CSS crack scan it could never satisfy — and when that scan - /// reported `ScrambledUncracked`, refused a perfectly good HD-DVD with - /// `Error::CssKeyMissing` (E7023). That is what a real CI run produced on - /// the HD-DVD fixture. `Disc::scan_image`'s eager image crack had already - /// learned this lesson and gates on `DiscFormat::Dvd`; the mux-side gate in - /// [`crate::css::resolve_dvd_title_key`] had not. - /// - /// The test is deliberately NEGATIVE — "everything except the families - /// proven CSS-free" — rather than a positive `== DiscFormat::Dvd`, because - /// the two failure directions are wildly asymmetric: - /// - /// * running CSS on a disc that has none costs a wasted scan (and, at - /// worst, a false refusal): loud, visible, recoverable; - /// * NOT running CSS on a real DVD makes the mux pass SCRAMBLED bytes - /// through as plaintext and exit 0 with garbage — a failure that looks - /// like success. That one already shipped once (~9 MB of ciphertext - /// inside a main-movie m2ts at rc=0). - /// - /// So the safe default is "attempt the crack". [`DiscFormat::Unknown`] — - /// the value a caller that never scanned the disc supplies — therefore - /// answers `true`, and any variant added to this enum in future answers - /// `true` until someone deliberately proves it CSS-free and adds it to the - /// exclusion list. A positive `matches!` list would default the other way, - /// i.e. toward the catastrophic direction. - pub fn may_have_css(self) -> bool { - !matches!( - self, - DiscFormat::HdDvd | DiscFormat::BluRay | DiscFormat::Uhd | DiscFormat::Fmts - ) - } -} - /// Disc playback region. #[derive(Debug, Clone, PartialEq)] pub enum DiscRegion { diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 0206216..fa2cee2 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -208,25 +208,12 @@ impl DiscStream { /// Works with physical drives and ISO files — both implement SectorSource. /// The caller opens the source, scans for titles/keys, and passes them in. /// The stream handles demuxing, decryption, and codec parsing internally. - /// - /// `content_format` is the CONTAINER (TS vs PS demuxer). `disc_format` is - /// the DISC FAMILY, and it exists as its own parameter because the two are - /// not interchangeable: DVD and HD-DVD are both `ContentFormat::MpegPs`, - /// yet only DVD can carry CSS. It gates the per-title CSS crack below. A - /// caller that genuinely does not know the disc passes - /// [`crate::disc::DiscFormat::Unknown`], which still attempts the crack — - /// the safe direction (see [`crate::disc::DiscFormat::may_have_css`]). - // Eight params is inherent to a constructor that takes the source, the - // title, the keys, both format axes (container and disc family) and the - // read-mode flags; grouping them would only relocate the same fields. - #[allow(clippy::too_many_arguments)] pub fn new( mut reader: Box, title: DiscTitle, mut decrypt_keys: crate::decrypt::DecryptKeys, batch_sectors: u16, content_format: crate::disc::ContentFormat, - disc_format: crate::disc::DiscFormat, raw: bool, halt: Option, ) -> std::io::Result { @@ -236,20 +223,17 @@ impl DiscStream { // Resolve this title's CSS key from the reader if the caller supplied // none — the SAME shared step the file-backed mux highway // (`build_iso_pipeline`) uses, so single-pass and multi-pass descramble a - // DVD identically. No-op for a disc format that cannot carry CSS (HD-DVD - // and the BD families — `disc_format`, NOT the MPEG-PS container, which - // DVD and HD-DVD share), for AACS / already-keyed / genuinely-clear - // input, and for `raw`; a scrambled-but-uncrackable DVD is a hard - // `CssKeyMissing`. `halt` is passed here (not deferred to `with_halt`) - // so a Stop during the crack scan is honored — the scan runs at - // construction, before the caller can attach a token. + // DVD identically. No-op for AACS / already-keyed / genuinely-clear input + // or `raw`; a scrambled-but-uncrackable DVD is a hard `CssKeyMissing`. + // `halt` is passed here (not deferred to `with_halt`) so a Stop during the + // crack scan is honored — the scan runs at construction, before the caller + // can attach a token. crate::css::resolve_dvd_title_key( &mut *reader, &extents, &mut decrypt_keys, batch_sectors, content_format, - disc_format, raw, halt.as_ref(), )?; @@ -1218,7 +1202,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, // request 8 sectors (16384 B); the source delivers 1 (2048 B) ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -1309,7 +1292,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -1346,7 +1328,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -1459,7 +1440,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -1503,7 +1483,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, crate::disc::ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -1539,7 +1518,6 @@ mod tests { aacs, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -1702,7 +1680,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -1819,7 +1796,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -1883,7 +1859,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -2004,7 +1979,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -2064,7 +2038,6 @@ mod tests { keys, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -2164,7 +2137,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -2209,7 +2181,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, crate::disc::ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) @@ -2267,7 +2238,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, false, None, ); @@ -2277,36 +2247,6 @@ mod tests { ); } - /// The HD-DVD counterpart of the test above, pinned at the SAME boundary so - /// the disc-format axis is proven to reach the shared CSS step through this - /// constructor and not just inside `css::resolve_dvd_title_key`. - /// - /// Byte-for-byte identical input to `disc_stream_new_dvd_none_scrambled_hard_fails` - /// — same `LockedReader`, same MPEG-PS title, same `None` keys — with only - /// the disc format changed. The DVD case must still be refused (E7023) and - /// the HD-DVD case must construct: an HD-DVD is AACS and has no CSS, so - /// there is no CSS key for it to be missing. Catches the mutation of - /// dropping `disc_format` from `DiscStream::new`'s plumbing (or hardcoding - /// a CSS-capable value there), which is exactly the shape of the shipped - /// defect: E7023 on a perfectly good HD-DVD. - #[test] - fn disc_stream_new_hddvd_none_scrambled_does_not_hard_fail() { - let res = DiscStream::new( - Box::new(LockedReader), - mpegps_title(8), - crate::decrypt::DecryptKeys::None, - 8, - ContentFormat::MpegPs, - crate::disc::DiscFormat::HdDvd, - false, - None, - ); - assert!( - res.is_ok(), - "an HD-DVD must never be refused for a missing CSS key — it carries no CSS" - ); - } - /// `raw` must bypass the CSS crack at the DiscStream boundary too: the same /// scrambled-uncrackable input that hard-fails above must CONSTRUCT in raw /// mode (ciphertext passthrough), never hard-fail. @@ -2318,7 +2258,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, true, // raw None, ); @@ -2532,7 +2471,6 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, false, None, ) @@ -2654,7 +2592,6 @@ mod tests { }, 3, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, ) diff --git a/src/mux/driver.rs b/src/mux/driver.rs index b1951e4..e6d407d 100644 --- a/src/mux/driver.rs +++ b/src/mux/driver.rs @@ -129,15 +129,6 @@ pub enum MuxInput<'a> { title: DiscTitle, /// Container format of the title (TS vs PS demuxer selection). format: crate::disc::ContentFormat, - /// The scanned disc's FAMILY (`disc.format`) — a different axis from - /// `format`, which is only the container. DVD and HD-DVD are both - /// `ContentFormat::MpegPs`, yet only DVD can carry CSS, so this is what - /// gates the per-title CSS crack in [`build_iso_pipeline`]. Pass - /// [`crate::disc::DiscFormat::Unknown`] only when the disc was genuinely - /// never scanned: that value still runs the crack, which is the safe - /// direction (skipping it on a real DVD would mux ciphertext as - /// plaintext at exit 0). - disc_format: crate::disc::DiscFormat, /// Decryption keys for the title (`DecryptKeys::None` for raw/clear). keys: DecryptKeys, /// Optional read-time key fetch closure (banked by `resolve_keys`). @@ -160,12 +151,6 @@ pub enum MuxInput<'a> { title: DiscTitle, /// Container format (TS vs PS demux selection). format: crate::disc::ContentFormat, - /// The scanned disc's FAMILY (`disc.format`), the CSS-eligibility axis - /// — see [`MuxInput::Iso::disc_format`]. Without it the inline - /// `DiscStream` cannot tell an HD-DVD `.evo` from a DVD `.vob` (both - /// are `ContentFormat::MpegPs`) and would run a CSS crack that an - /// AACS-family disc can never satisfy. - disc_format: crate::disc::DiscFormat, /// Decryption keys the consumer already banked (`DecryptKeys::None` for /// raw/clear). The driver consumes them as-is — never re-resolves. keys: DecryptKeys, @@ -366,7 +351,6 @@ pub fn mux_stream( path, title, format, - disc_format, keys, key_fetch, } => { @@ -398,7 +382,6 @@ pub fn mux_stream( keys, opts.batch_sectors, format, - disc_format, opts.raw, Some(halt.clone()), Some(reader_event_fn(events.clone())), @@ -413,7 +396,7 @@ pub fn mux_stream( // Pull everything we need out of the disc as owned values so the // immutable disc borrow is released before the mutable // `take_reader` below. - let (mut title, format, disc_format, mut keys, playlist, source) = { + let (mut title, format, mut keys, playlist, source) = { let disc = session.disc().ok_or_else(|| Error::DeviceNotReady { path: session.device_path().to_string(), })?; @@ -441,15 +424,9 @@ pub fn mux_stream( }; // DVD CSS is per-VTS: resolve the per-title key via the pipeline // (see `session_mux_keys`), never the whole-disc `decrypt_keys()`. - // `disc.content_format` is the container; `disc.format` is - // the disc FAMILY. Both are carried out of the borrow: the - // first picks the demuxer, the second decides whether a CSS - // crack is even meaningful (an HD-DVD is MPEG-PS too, and - // has no CSS). ( title, disc.content_format, - disc.format, session_mux_keys(disc), playlist, source, @@ -491,7 +468,6 @@ pub fn mux_stream( keys, opts.batch_sectors, format, - disc_format, opts.raw, Some(halt.clone()), )?; @@ -512,7 +488,6 @@ pub fn mux_stream( mut reader, title, format, - disc_format, mut keys, key_map, } => { @@ -571,7 +546,6 @@ pub fn mux_stream( keys, opts.batch_sectors, format, - disc_format, opts.raw, Some(halt.clone()), )?; @@ -1657,7 +1631,6 @@ mod tests { path: &iso_path, title, format: crate::disc::ContentFormat::BdTs, - disc_format: crate::disc::DiscFormat::BluRay, keys: DecryptKeys::None, key_fetch: None, }, @@ -1769,7 +1742,6 @@ mod tests { reader, title, format: crate::disc::ContentFormat::BdTs, - disc_format: crate::disc::DiscFormat::BluRay, keys: DecryptKeys::None, key_map: Some(map), }, @@ -1892,7 +1864,6 @@ mod tests { reader, title, format: crate::disc::ContentFormat::BdTs, - disc_format: crate::disc::DiscFormat::BluRay, keys, key_map: None, // plain AACS disc: the driver must resolve the base map }, diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index 33a4a81..71c7679 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -613,12 +613,6 @@ where } let title = disc.titles[idx].clone(); let format = disc.content_format; - // The CSS-eligibility axis handed to the pipeline below. `content_format` - // above is only the container and cannot carry this decision: HD-DVD `.evo` - // is MPEG-PS exactly like DVD `.vob`, and gating the crack on the container - // is what sent every HD-DVD through a CSS scan it could never satisfy. - // Same value `is_dvd` was derived from further up. - let disc_format = disc.format; // ISO file: 8192-sector batch (16 MiB at 2048 B/sector) — // sequential read from fast storage, no bad sectors. Empirically // optimal; bumping to 16384 sectors (32 MiB) regressed (more cache @@ -650,7 +644,6 @@ where effective_keys, ISO_MUX_BATCH_SECTORS, format, - disc_format, opts.raw, None, None, @@ -2116,11 +2109,6 @@ pub(crate) fn resolve_mux_key_map_cached( /// - `batch_sectors`: read batch size in logical (2048-byte) sectors — a /// throughput/latency tuning knob, not a correctness parameter. /// - `format`: container format (`BdTs` → TS demuxer, `MpegPs` → PS demuxer). -/// - `disc_format`: the disc FAMILY, a separate axis from `format` — DVD and -/// HD-DVD are both `MpegPs`, but only DVD can carry CSS. Gates the per-title -/// CSS crack below. A caller with no scanned disc passes -/// [`crate::disc::DiscFormat::Unknown`], which still cracks (the safe -/// direction — see [`crate::disc::DiscFormat::may_have_css`]). /// - `raw`: ciphertext passthrough. When `true`, the per-title CSS crack /// (`resolve_dvd_title_key`) is skipped entirely — no key is resolved and a /// scrambled title is neither descrambled nor hard-failed. @@ -2140,7 +2128,6 @@ pub fn build_iso_pipeline( mut keys: crate::decrypt::DecryptKeys, batch_sectors: u16, format: ContentFormat, - disc_format: crate::disc::DiscFormat, raw: bool, halt: Option, event_fn: Option, @@ -2148,20 +2135,17 @@ pub fn build_iso_pipeline( ) -> io::Result { let extents = title.extents.clone(); // CSS (DVD) key resolution — the shared per-title step (also used by the - // live-drive single-pass `DiscStream`). A `None`/MPEG-PS title on a - // CSS-capable DISC FORMAT cracks its own key from the reader in playback - // order; an HD-DVD (also MPEG-PS, but AACS — no CSS exists to find) is - // skipped on the `disc_format` axis; AACS keys are untouched; a clear DVD - // stays `None`; `raw` skips it entirely. Without this a detection-miss CSS - // DVD would mux scrambled sectors as corrupt video. `halt` lets /api/stop - // interrupt the crack scan. + // live-drive single-pass `DiscStream`). A `None`/MPEG-PS title cracks its own + // key from the reader in playback order; AACS `.evo` (also MPEG-PS) arrives as + // `Aacs` and is untouched; a clear DVD stays `None`; `raw` skips it entirely. + // Without this a detection-miss CSS DVD would mux scrambled sectors as corrupt + // video. `halt` lets /api/stop interrupt the crack scan. crate::css::resolve_dvd_title_key( &mut reader, &extents, &mut keys, batch_sectors, format, - disc_format, raw, halt.as_ref(), )?; @@ -3007,7 +2991,6 @@ mod tests { DecryptKeys::None, 8192, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, None, @@ -3050,7 +3033,6 @@ mod tests { DecryptKeys::None, 8192, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, None, @@ -3148,7 +3130,6 @@ mod tests { DecryptKeys::None, 8192, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, None, @@ -3188,7 +3169,6 @@ mod tests { DecryptKeys::None, 0, ContentFormat::BdTs, - crate::disc::DiscFormat::BluRay, false, None, None, @@ -3231,7 +3211,6 @@ mod tests { DecryptKeys::None, 8192, ContentFormat::MpegPs, - crate::disc::DiscFormat::Dvd, false, None, None, diff --git a/tests/fvi_pipeline.rs b/tests/fvi_pipeline.rs index 738553b..8b5baa0 100644 --- a/tests/fvi_pipeline.rs +++ b/tests/fvi_pipeline.rs @@ -175,10 +175,6 @@ fn run_to_fvi(image: Vec, title: DiscTitle, path: &std::path::Path) { DecryptKeys::None, 3, // 3-sector (one AACS unit) batches → one source stamp per GOP region ContentFormat::MpegPs, - // A DVD-family fixture: the disc-format axis must keep the CSS crack - // reachable here exactly as it is in production (the image is clear, so - // the crack finds nothing and the mux proceeds). - libfreemkv::DiscFormat::Dvd, false, None, None,