Security - Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged. CI / tooling - clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny dependency-audit gate with first-party git sources allowed (#15); audience-based comment-guard (#17). Docs - README (keydb ownership, BD-J parser count), drive-access transport/reset notes, mux timeline module-doc links.
17 KiB
src/css/mod.rs — extended notes
Overflow detail relocated from doc comments in src/css/mod.rs (kept short
there per the comment-guard caps). Function/item names below match the code.
CSS_LOCKED_BAIL
The bus-auth read gate is global (all-or-nothing), so a run this long of
05/6F/03 (CSS-locked) reads means the gate is shut and nothing here is
crackable — bail instead of grinding the full 50_000-sector budget. That
grind is what made rc5 appear to hang on a wedged USB bridge.
crack_key
Recovers the CSS title key with no keys, by scanning scrambled sectors and
running the known-plaintext attack (see the keyless module). The crib comes
from the periodic-run detector: a scrambled sector's cleartext region
(bytes 0x00..0x80) often ends in a short-period repeating run (stuffing /
constant fill); the attack assumes that run continues across the 0x80
boundary into the encrypted region, giving the known plaintext the 2^16 LFSR
recovery needs. Scans up to 50000 sectors across the extents and returns the
first sector that yields a key — no player keys, no disc-key crack. Works on
a live drive (after bus-auth unlocks reads) and on disc images alike.
This convenience form runs to completion (no cancellation) and returns just
the key; callers needing an operator-Stop / watchdog cancel, or the
three-way CrackOutcome (to distinguish "unencrypted" from
"encrypted-but-uncracked"), use crack_key_outcome, which takes a halt
token.
CrackOutcome
Distinguishes the three cases the bare Option<CssState> conflated (and
which caused a silent-failure bug: scrambled-but-uncracked content was
treated as "unencrypted" and muxed as plaintext garbage at exit 0):
Cracked— a scrambled sector yielded a title key.Unencrypted— NO scrambled sector was seen across the scanned extents (is_scrambled_packnever true): the content is genuinely plaintext, so proceeding without a key is correct.ScrambledUncracked— scrambled sectors WERE seen but no key could be recovered (the recovery found no crackable crib, or the scrambled region was unreadable). The content is encrypted; muxing it as plaintext would emit garbage, so callers MUST surface a hard error instead of falling through to "unencrypted" — the per-titlecrate::error::Error::CssKeyMissingwhen it is ONE title's own re-crack that failed (skippable: a sibling VTS may still crack), or the disc-levelcrate::error::Error::CssNoDiscKeywhen it is the disc-wide scan (Disc::css_error, every title fails identically).
resolve_dvd_title_key
The SINGLE place every DVD read path obtains a title key, so the file-backed
mux highway (crate::build_iso_pipeline) and the live-drive single-pass
crate::DiscStream descramble a DVD identically ("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:
- AACS keys (HD-DVD
.evois also MPEG-PS but arrives asAacs) — 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.
A scrambled-but-uncrackable title is a hard crate::error::Error::CssKeyMissing,
never a silent scrambled-passthrough mux. That code is the PER-TITLE one
(error::is_skippable_title_stub), which is correct here: this function
cracks ONE title's own extents, and another VTS on the same disc may still
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.
crack_key_scan (private)
Tracks a saw_scrambled flag so a scrambled-but-uncracked disc is
distinguished from a genuinely-unencrypted one (the crack_key Option
wrapper collapses both to None via CrackOutcome::into_state).
descramble_sector
A no-op unless the sector is a scrambled MPEG-2 PS PACK. The pack start code
is checked, not just the byte 0x14 flag bits, for the same reason
descramble_region checks it: 0x14 only means "scrambling control" inside a
pack, and in an IFO it is whatever that format stores there. A real
VIDEO_TS.IFO sector holds 0x15 there while starting 00 26 00 00;
descrambling it destroyed 1912 of its 2048 bytes, and because that sector
carries TT_SRPT the disc enumerated 38 titles while an image decrypted from
it enumerated 10, silently, at exit 0.
This function has no callers inside the crate, but the module-level example prescribes it — so the crate's own documented guidance led straight into that defect. Making the guard part of the function, rather than something each caller must remember, is what keeps the safe path the easy one.
descramble_region
Descrambles 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 (the standard 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.
This section used to document an Error::DecryptFailed for the case where a
sector's crib rejects the cached key and the re-crack from that sector also
fails. That behaviour was tried and REVERTED: crib mismatch plus crack
failure is the signature of a crib FALSE POSITIVE, not of a stale key, and
failing there made real discs unrippable. The code now descrambles with the
cached key and returns Ok.
has_scramble_flag_bits
Deliberately NOT called is_scrambled. Byte 0x14 only means "scrambling
control" inside an MPEG-2 Program Stream pack; in an IFO, UDF or ISO 9660
sector it is whatever that format stores there. Treating the flag alone as
proof of scrambling is what destroyed 1912 bytes of a real disc's
VIDEO_TS.IFO — the sector carrying TT_SRPT — so the disc enumerated 38
titles and an image decrypted from it enumerated 10, silently, at exit 0.
Callers want is_scrambled_pack. It asks the same question and also
requires the pack start code, which every genuinely scrambled VOB sector
carries and no IFO sector does.
It stays public only because an integration test asserts the flag
extraction directly. It has no production callers, and its previous doc
comment claimed one (decrypt::decrypt_sectors) that did not exist — so the
name was an invitation and the documentation was an argument for accepting
it.
is_scrambled_pack
The HARDENED test the crack scan uses to set its saw_scrambled evidence
flag (Fix 3).
has_scramble_flag_bits keys solely on bits 4-5 of byte 0x14. That byte is
only meaningful inside a real DVD sector — an MPEG-2 Program Stream pack,
which ALWAYS begins with the 32-bit pack-start code 00 00 01 BA at offset
0x00. A tiny clear / nav-only stub (a 0.5 s menu loop, an FBI-warning title)
can carry arbitrary bytes that happen to set bits 4-5 of byte 0x14; trusting
byte 0x14 alone there would flip the scan's saw_scrambled gate and make a
genuinely-UNENCRYPTED title report ScrambledUncracked — a false E7023.
Requiring the pack-start signature FIRST means only a sector that is
structurally a DVD video pack can be counted as scramble evidence. This does
NOT weaken the genuine "encrypted but uncrackable" hard-fail: a real
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 recovery 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
never scrambled and destroys it. The measured case is written up above under
descramble_region: a VIDEO_TS.IFO sector holding 0x15 at offset 0x14 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.
Tests (mod tests)
Detail relocated from test doc comments:
-
a_crib_false_positive_keeps_the_cached_key_rather_than_failing:attack_cribis a heuristic — it finds a periodic run in the unscrambled header and predicts the run continues past 0x80. When that prediction does not hold, the crib reports a mismatch even though the cached key is correct, and the re-crack then fails BECAUSE the crib was never valid. So this combination is the signature of a crib false positive, not of a stale key, and the cached key remains the best available evidence. This test exists because a prior round read the same code as "descrambling with a key we just proved stale" and made itDecryptFailedto match the AACS path; real DVDs hit this constantly, and the change made a real disc unrippable (no unit test caught it — the real-media acceptance gate did). CSS is not AACS: an AACS unit key either opens a unit or does not, whereas a CSS title key is recovered from data whose recoverability varies sector by sector. -
has_scramble_flag_bits_short_buffer_is_false_no_panic: grounding —sector.len() >= 2048 && (sector[0x14] >> 4) & 0x03 != 0short-circuits so a short buffer never reads index 0x14. Mutation: swapping operand order would panic indexing a 20-byte slice; this test catches it. -
has_scramble_flag_bits_uses_bits_4_5_only: grounding —(sector[0x14] >> 4) & 0x03. Mutation: widening the mask to& 0x0Fmakes 0x40 report scrambled; the 0x40 assert catches it. -
has_scramble_flag_bits_exact_sector_length_accepted: grounding —sector.len() >= 2048. Mutation:>= 2048→> 2048makes an exact 2048-byte scrambled sector report false; this catches it. -
is_scrambled_pack_requires_pack_start_signature: Fix 3 hardening —is_scrambled_packrequires BOTH the pack-start code and the 0x14 bits, so a clear/nav-only stub with stray 0x14 bits isn't scramble evidence (else a genuinely unencrypted title reports the false E7023). -
is_scrambled_pack_excludes_nav_and_structural_stream_ids: detection fix —is_scrambled_packmust exclude system_header/padding/private_stream_2 bystream_idat 0x11, since CSS never scrambles those and byte 0x14 there is raw payload, not a scrambling-control field. This is the exact decrypted-HD-DVD defect (an.evoRDI pack is 0xBF with pack-start and 0x14 bits set); the video/private_stream_1 cases prove the exclusion doesn't weaken real-DVD detection. -
MockSourcefieldslock_all,crackable,short_read,stream_id: model a CSS-locked drive (05/6F/03on every read), a designated crackable LBA serving a full synthetic scrambled sector, a short-read count (therecovery: trueshort-read contract,Some(0)being the degenerate spin-guard case), and the PESstream_idwritten at 0x11 (used to model an HD-DVD.evoRDI nav pack viaPRIVATE_STREAM_2). -
crackable_sector: builds a crackable scrambled sector whose cleartext header (0x59..0x80) carries a periodic run continuing across the 0x80 boundary, mirroring thesynth_periodic_sectorfixture in the keyless tests but built from the crate-internalscramble_sector. -
a_source_that_returns_zero_sectors_still_obeys_the_scan_budget:triedincrements only per inspected sector insidefor s in 0..usable; anOk(0)never runs that loop, soadvanceis forced to 1 to keep the cursor moving and the extent's full declared length is walked. Mutation: deleting thetriedcharge in theusable == 0arm goes red at 60,000 reads for a misbehaving/adversarial source. -
crack_key_caps_total_tries_at_50000: grounding —max_tries = 50_000,tried += 1before the read, loop guardtried < max_tries. Mutation: widening the cap, or removing the increment, both go red. -
crack_key_budget_is_shared_across_extents: grounding —triedis declared outside the extent loop with a sharedif tried >= max_triesbreak. Mutation: moving the declaration inside the loop would give each extent its own budget (80,000 total instead of 50,000). -
crack_key_scans_from_extent_start_lba: grounding —reader.read_sectors(ext.start_lba + i, ...). Mutation: droppingext.start_lba +would start LBAs at 0 instead of the extent's start. -
crack_key_continues_past_read_errors: grounding — a readErrfalls through to the next sector rather than aborting. Mutation: propagating the error with?would stop the scan after the first failure. -
crack_key_empty_extent_reads_nothing/crack_key_no_extents_is_none: grounding —while i < ext.sector_count/for ext in extentsare no-ops on empty input; an off-by-one (<=) or a pre-loop read would break these. -
crack_outcome_reaches_cracked_with_span: audit gap "MockSource never yields a crackable sector" — drives the fullcrack_key_scanover a synthetic ISO to exercise theCrackedbranch andcrack_spanrecording end-to-end, previously untested. -
is_scrambled_uncracked_is_true_for_that_case_and_false_for_the_other_two: a prior round introduced the three-way split precisely because conflating the cases made an uncrackable disc exit 0 with garbage output; earlier tests only asserted the TRUE direction, so a predicate answering "yes" unconditionally was indistinguishable from a correct one. All three outcomes here come from realcrack_key_outcomescans, not hand-built enum values. -
resolve_dvd_title_key_*tests:resolve_dvd_title_keyis the SINGLE shared per-title CSS step both read paths (build_iso_pipelinemulti-pass andDiscStream::newsingle-pass) call, so these pin its full contract at the shared boundary — crack success, hard-fail on scrambled-uncrackable (never leavingkeysasNone, which would mux scrambled bytes as plaintext), the decrypted-HD-DVD RDI-pack regression (end to end),--rawpassthrough, the AACS-keys-untouched gate, the clear-DVD no-op, and the halt-cancellation-surfaces-as-Halted fix (not a truncated-scan verdict). -
all_locked_synthetic_iso_yields_css_key_missing_signal: audit §2/§5 #7 — pins that an all-locked synthetic ISO across MULTIPLE extents producesScrambledUncracked, the exact signaldisc/mod.rsconverts intodisc.css_error = Some(Error::CssKeyMissing). -
recrack_succeeds_on_other_vts_extents: audit gap "success path missing" — the prior re-crack test only covered the locked→None path; this proves a key cracked for one VTS is genuinely re-derived (not reused) for another.