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.
11 KiB
AACS Content Decryption — Implementation Notes
Rationale and background trimmed out of src/aacs/content.rs doc comments to
satisfy ci/comment-guard.py's per-audience prose caps. See
AACS Encryption for the public-facing key resolution story and
AACS Crypto Primitives for the low-level AES primitives.
is_unit_aligned
AACS aligned units (6144 B / 3 sectors) are anchored at the start of each
clip's encrypted region, so a read must begin a whole number of units past
that base for decrypt_sectors (which anchors units at buffer offset 0) to
align the CBC correctly. This is the single source of truth for the
decrypt-on-read gate, the inline and highway mux read paths, and the
key-validation sample reader — all key off this, never absolute lba % 3. A
disc whose clip start_lba is not itself 3-aligned would otherwise mis-gate
(reject readable units, then report "Decryption failed") on exactly the
titles whose clips land off a 3-boundary.
saturating_sub makes the lba < unit_base case well-defined — it clamps
the offset to 0, a unit boundary — rather than the latent wrapping_sub trap
where an underflow wraps to ~2^32 and, because 2^32 ≡ 1 (mod 3),
mis-reports the alignment (e.g. lba == unit_base - 1 would falsely read as
aligned).
aacs_unit_encrypted
The mux is the principal consumer of this signal for BdTs discs (via
[is_clean], see below). It is readable WITHOUT a key. CRITICAL: only
meaningful when unit is read at the correct clip-FILE-anchored boundary — a
disc-absolute / mis-aligned read makes the flag byte arbitrary mid-stream
data, which is why per-unit checks run clip-anchored, not in the whole-disc
sweep.
aacs_unit_needs_decrypt
Buffer-iterating sites that may run twice over the same buf (the post-fetch
re-decrypt, sample collection, failure diagnosis) need an IDEMPOTENT "does
this still need work?" test, so aacs_unit_encrypted (the authoritative flag,
which decryption never rewrites) is composed with a "structure restored?"
check that flips once decrypted: TS syncs come back for BdTs; valid
00 00 01 BA packs come back for MpegPs.
HD-DVD scramble flag (PS_SCRAMBLE_OFF / PS_SCRAMBLE_MASK)
BD/UHD/FMTS flag encryption uses the Copy Permission Indicator in the top 2
bits of byte 0 (the M2TS TP_extra_header). HD-DVD .evo is Program Stream —
byte 0 is a 00 00 01 BA pack_start_code, NOT a CPI — so AACS reuses the
MPEG-2 PES_scrambling_control field instead: pack_header (14 bytes) + PES
start-code/stream_id (4) + PES_packet_length (2) puts the PES flags byte at
offset 20, with PES_scrambling_control in bits 5-4 (& 0x30). Non-zero =
encrypted. Derived from BackupHDDVD (Header[20] & 0x30) cross-checked
against MPEG-2 systems (ISO/IEC 13818-1).
UNVERIFIED against a real ENCRYPTED HD-DVD disc — none available to confirm byte-exactly. Two open questions a real disc must settle:
- A pack carrying an MPEG
system_header(00 00 01 BB) before the PES packet shifts this offset past 20. - Whether offset 20 is even readable pre-decrypt. BD keeps only the first 16
bytes clear (the seed) and AES-CBC-encrypts 16..6144 — under that model
offset 20 is ciphertext. BackupHDDVD reading
Header[20]pre-decrypt implies HD-DVD instead encrypts PES payloads with clear pack/PES headers (per-PES model). If so,decrypt_unit's 16-byte-seed model also would not fit HD-DVD and needs its own path. TS is unaffected either way.
KEY_PROOF_PACKETS
Four 0x47 syncs are 32 bits of MPEG-TS structure, but the per-UNIT
false-pass risk is NOT 2^-32: is_clean_ts accepts ANY four of the ~31
encrypted packets in a 6144-byte aligned unit, so for a wrong key (uniform
AES noise, 0x47 at 1/256 per packet) it is ≈ C(31,4)·256^-4 ≈ 7e-6, i.e.
~1e-5. 1-in-4-billion is the probability for four SPECIFIC packets and
overstates the margin by ~4000x; at KEY_PROOF_PACKETS = 3 the per-unit rate
is ≈ C(31,3)·256^-3 ≈ 2.6e-4, so do NOT lower it on the strength of slack
that is not there. It is an ABSOLUTE proof floor, NOT a proportion — a unit
the key opened but whose content is bad-encoded (many non-conforming
packets) is proven by ANY four good packets, not rejected for the bad ones.
is_clean_ts
The mux is its principal consumer for BdTs discs, reaching it through
is_clean: mux::resolve's multi-CPS pick closure selects a unit key by
it, probe_index_phase reports each FMTS index's interleave parity by it,
and decrypt::decrypt_sectors_mapped uses it as the forensic-range verify
net. (The doc used to say "the mux never calls this", which invited a
maintainer to tighten or loosen the proof rule believing only whole-disc read
verification was affected — while it in fact changes which unit key a
multi-CPS disc muxes with and which phase an FMTS index is muxed at.)
Rule — evidence is ABSOLUTE, scaled to the packets that exist. Over the
ENCRYPTED packets (skip packet 0: its 0x47 sits in the clear 16-byte seed,
so it reads 0x47 for ANY key and is never evidence), let E = non-padding
content packets and synced = those carrying 0x47. The key opened the unit
iff E == 0 (nothing encrypted to prove) OR synced >= min(E, KEY_PROOF_PACKETS).
- WRONG key → ~0 synced → fails (reaching 4 by chance ≈ 1e-5/unit, and every unit of a clip would have to fluke — astronomically safe).
- RIGHT key, bad-encoded content → any 4 good packets pass; the bad ones are the muxer's problem. (This is the false-negative the old 75% PROPORTION caused — a mostly-bad unit the key opened was wrongly rejected.)
min(E, 4)handles the end-of-clip fragment TAIL: a unit with only E=1 real packet (then source-zero padding) needs just that one to sync, so a sparse-but-valid tail is never false-rejected. Padding (all-zero payload) is excluded throughout.
is_clean_ps
The Program-Stream arm of is_clean (HD-DVD .evo): every 2048-byte pack
begins with the pack_start_code 00 00 01 BA; a 6144-byte AACS unit spans
three packs. Pack 0's start sits in the clear 16-byte seed (present
regardless of the key — mirrors is_clean_ts skipping packet 0); packs 1
and 2 (offsets 2048 and 4096) are in the encrypted region, so a wrong key
garbles them and this returns false (64 bits of discrimination). Validated
against real decrypted HD-DVD .evo (two real retail titles): pack starts
are exactly 2048-aligned, three per aligned unit, at offsets 0 / 2048 / 4096.
UNVERIFIED-HDDVD-DECRYPT: the pack structure here is confirmed on decrypted
rips, but that a real ENCRYPTED .evo decrypts to it via the same
6144-byte aligned unit (16-byte seed + AES-CBC over 16..6144) as BD is the
unverified assumption — we have no encrypted HD DVD. If HD-DVD decryption
yields garbage with a known-good key, the unit granularity is the suspect.
decrypt_unit / encrypt_unit
Block Key = AES-128E(Kcu, seed) ⊕ seed ([BD] §3.10.1 Fig 3-8: encrypt the
clear 16-byte seed under the CPS Unit Key, XOR the seed back in — the
trailing ⊕ seed is load-bearing); then AES-128-CBC decrypt/encrypt bytes
16..6144 under the AACS IV. encrypt_unit is the exact inverse of
decrypt_unit, for authoring an encrypted disc image (and for building
genuinely-encrypted read-path fixtures); it does NOT set the encrypted flag,
because where that flag lives is container-specific, and keeping it out
keeps the crypto container-agnostic.
Source-zero padding packets (all 192 bytes zero on disc) are restored to
zero by decrypt_unit: their decrypted bytes are AES-noise from decrypting
zeros, but the source WAS zero, so writing the true source back is faithful
and gives the demux a tidy gap. Content packets are left EXACTLY as
decrypted — the decrypt path never rewrites content, so an authored-bad
packet passes through verbatim. encrypt_unit deliberately does NOT mirror
this: an all-zero plaintext packet encrypts to ciphertext that is not
all-zero, so it is not mistaken for padding on the way back and the round
trip is still exact. Authoring that wants true source-zero padding leaves
those packets unencrypted instead.
encrypt_unit returns false — encrypting nothing — when unit is shorter
than ALIGNED_UNIT_LEN. That case MUST be checked: the caller has already
set the container's encrypted flag by then (the contract requires
flag-before-crypto, since the header is the key seed), so ignoring the
result leaves a unit marked encrypted while still carrying plaintext, which
is the worst possible outcome for an authoring tool.
decrypt_bus key-schedule regression (test: decrypt_bus_expands_the_read_data_key_once_per_unit)
MEASURED, not reasoned: decrypt_bus used to call aes_cbc_decrypt once per
2048-byte sector, and each call built its own AES-128 key schedule, so a
6144-byte aligned unit performed THREE key expansions under the same
loop-invariant read_data_key. On a 90 GB UHD read on a stock
(non-firmware-unlocked) drive — ~14.6 million aligned units — that is ~29
million redundant expansions on the per-unit decrypt hot path, for a key
that is constant for the whole disc. The counter is incremented inside
crypto::new_cipher, the single construction site.
IV0_PUBLISHED (test fixture)
[C] §2.1.2 fixes one default CBC IV for every AACS AES-CBC operation. Both
IV tests used to compute their expected value from crypto::AACS_IV itself,
so the constant was asserted against itself and NOTHING in the suite pinned
its bytes: swapping AACS_IV for [0u8; 16] left both tests passing (one
builds its ciphertext with the same value and the other cancels the change
in a triple XOR) while every real AACS disc decrypted to noise — block 0 of
every 6144-byte aligned unit and of every bus-encrypted sector XORed with the
wrong IV. IV0_PUBLISHED is the independent witness the tests assert
against instead.
Encrypted-flag readers (aacs_unit_seed_encrypted, aacs_unit_encrypted)
aacs_unit_seed_encrypted is the flag reader for a PARTIAL unit — the guard
that stops a truncated encrypted fragment from being emitted as clear
content. It reads ONLY the two Copy Permission Indicator bits ([BD]
§3.10.2, byte 0 bits 6-7); the remaining six bits are TP_extra_header
arrival-timestamp bits and carry no encryption meaning. Both failure
directions are damaging and silent: a reader that answers "encrypted" for a
clear fragment discards good content, and one that answers "clear" for an
encrypted fragment writes ciphertext into the output as if it were video.
aacs_unit_encrypted is the AUTHORITATIVE gate and requires a WHOLE
6144-byte aligned unit: on anything shorter the flag byte is not guaranteed
to be the unit's, so it must answer false and leave the partial-unit case
to aacs_unit_seed_encrypted. A reversed length guard would both classify
fragments off arbitrary mid-stream bytes and, on an empty slice, index out
of bounds.