mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
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.
312 lines
17 KiB
Markdown
312 lines
17 KiB
Markdown
# `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_pack` never 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-title `crate::error::Error::CssKeyMissing`
|
|
when it is ONE title's own re-crack that failed (skippable: a sibling VTS
|
|
may still crack), or the disc-level `crate::error::Error::CssNoDiscKey`
|
|
when 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 `.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.
|
|
|
|
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_crib` is 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 it `DecryptFailed` to 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 != 0` short-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 `& 0x0F` makes
|
|
0x40 report scrambled; the 0x40 assert catches it.
|
|
|
|
- `has_scramble_flag_bits_exact_sector_length_accepted`: grounding —
|
|
`sector.len() >= 2048`. Mutation: `>= 2048` → `> 2048` makes an exact
|
|
2048-byte scrambled sector report false; this catches it.
|
|
|
|
- `is_scrambled_pack_requires_pack_start_signature`: Fix 3 hardening —
|
|
`is_scrambled_pack` requires 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_pack` must exclude system_header/padding/private_stream_2 by
|
|
`stream_id` at 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 `.evo` RDI 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.
|
|
|
|
- `MockSource` fields `lock_all`, `crackable`, `short_read`, `stream_id`: model
|
|
a CSS-locked drive (`05/6F/03` on every read), a designated crackable LBA
|
|
serving a full synthetic scrambled sector, a short-read count (the
|
|
`recovery: true` short-read contract, `Some(0)` being the degenerate
|
|
spin-guard case), and the PES `stream_id` written at 0x11 (used to model an
|
|
HD-DVD `.evo` RDI nav pack via `PRIVATE_STREAM_2`).
|
|
|
|
- `crackable_sector`: builds a crackable scrambled sector whose cleartext
|
|
header (0x59..0x80) carries a periodic run continuing across the 0x80
|
|
boundary, mirroring the `synth_periodic_sector` fixture in the keyless
|
|
tests but built from the crate-internal `scramble_sector`.
|
|
|
|
- `a_source_that_returns_zero_sectors_still_obeys_the_scan_budget`: `tried`
|
|
increments only per inspected sector inside `for s in 0..usable`; an
|
|
`Ok(0)` never runs that loop, so `advance` is forced to 1 to keep the
|
|
cursor moving and the extent's full declared length is walked. Mutation:
|
|
deleting the `tried` charge in the `usable == 0` arm goes red at 60,000
|
|
reads for a misbehaving/adversarial source.
|
|
|
|
- `crack_key_caps_total_tries_at_50000`: grounding — `max_tries = 50_000`,
|
|
`tried += 1` before the read, loop guard `tried < max_tries`. Mutation:
|
|
widening the cap, or removing the increment, both go red.
|
|
|
|
- `crack_key_budget_is_shared_across_extents`: grounding — `tried` is
|
|
declared outside the extent loop with a shared `if tried >= max_tries`
|
|
break. 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: dropping
|
|
`ext.start_lba +` would start LBAs at 0 instead of the extent's start.
|
|
|
|
- `crack_key_continues_past_read_errors`: grounding — a read `Err` falls
|
|
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 extents` are 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 full `crack_key_scan` over a
|
|
synthetic ISO to exercise the `Cracked` branch and `crack_span` recording
|
|
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 real `crack_key_outcome` scans, not hand-built enum
|
|
values.
|
|
|
|
- `resolve_dvd_title_key_*` tests: `resolve_dvd_title_key` is the SINGLE
|
|
shared per-title CSS step both read paths (`build_iso_pipeline` multi-pass
|
|
and `DiscStream::new` single-pass) call, so these pin its full contract at
|
|
the shared boundary — crack success, hard-fail on scrambled-uncrackable
|
|
(never leaving `keys` as `None`, which would mux scrambled bytes as
|
|
plaintext), the decrypted-HD-DVD RDI-pack regression (end to end), `--raw`
|
|
passthrough, 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 produces
|
|
`ScrambledUncracked`, the exact signal `disc/mod.rs` converts into
|
|
`disc.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.
|