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.
167 lines
7.4 KiB
Markdown
167 lines
7.4 KiB
Markdown
# CSS content cipher (`src/css/lfsr.rs`)
|
|
|
|
## Module overview
|
|
|
|
The algorithm is the publicly documented CSS stream cipher, described in the
|
|
open CSS cryptanalysis literature. It is implemented here from that public
|
|
description; its constants (see [`super::tables`]) are the cipher's own
|
|
defined values. Nothing in this file is copied or translated from any
|
|
particular CSS software.
|
|
|
|
The cipher uses two table-driven linear-feedback circuits:
|
|
- **LFSR1** — a 17-bit register (a 9-bit and an 8-bit half seeded from
|
|
`key[0..2] XOR seed[0..2]`), stepped through `TAB2`/`TAB3`/`TAB5`.
|
|
- **LFSR0** — a 24-bit feedback register (seeded from `key[2..5] XOR
|
|
seed[2..5]`), stepped through a feedback polynomial and `TAB4`.
|
|
|
|
Each output byte is the sum-with-carry of the two register outputs. A body
|
|
byte is recovered as `plain = TAB1[cipher] ^ keystream` — a `TAB1`
|
|
substitution of the ciphertext byte followed by an XOR with the keystream
|
|
(so the cipher is deliberately not its own inverse).
|
|
|
|
## `descramble_sector`
|
|
|
|
The two feedback registers are seeded **directly** from
|
|
`title_key XOR sector_seed` (bytes `0x54..0x59`) — there is no title-key
|
|
mangling on the content path (that belongs to the disc/title-key hierarchy,
|
|
not the sector cipher). Only the body, bytes `0x80..0x800`, is transformed:
|
|
`body[i] = TAB1[body[i]] ^ (keystream & 0xff)`.
|
|
|
|
The scramble flag at byte `0x14` (bits 4-5) marks an encrypted sector. This
|
|
routine CLEARS that flag after unscrambling, so a descrambled sector reads as
|
|
`sector[0x14] & 0x30 == 0`; callers and 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.
|
|
|
|
`sector.len() < 2048` (the encrypted region `0x80..0x800` not fully present):
|
|
callers chunk by 2048, so a trailing partial chunk is left untouched. The
|
|
`debug_assert!` flags this misuse in debug/test builds; a DVD sector is
|
|
always exactly 2048 bytes.
|
|
|
|
## `scramble_sector` (test helper)
|
|
|
|
Exact inverse of `descramble_sector`: turn a plaintext sector body into
|
|
CSS ciphertext under `title_key`.
|
|
|
|
Descramble computes `plain = TAB1[cipher] ^ (keystream & 0xff)`, so the
|
|
inverse is `cipher = TAB1_INV[plain ^ (keystream & 0xff)]` with the identical
|
|
keystream. The keystream derivation is the same as `descramble_sector`;
|
|
only the final substitution differs. Bytes `0x80..0x800` are rewritten in
|
|
place; the scramble flag is set to `0x10` so a subsequent descramble runs.
|
|
|
|
Not on any production read path — it exists so the key-recovery tests (and
|
|
any caller that needs a known CSS-encrypted sector) can build genuine
|
|
ciphertext rather than approximating it.
|
|
|
|
## Test notes
|
|
|
|
### `descramble_produces_the_reference_css_vector`
|
|
|
|
Regression vector: the deterministic output of the CSS content cipher for
|
|
a fixed key/seed/body. The value is generated by this implementation and
|
|
is self-consistent with the scramble/descramble round-trip test — any
|
|
correct CSS descrambler yields the same bytes, since the cipher is
|
|
deterministic. Pins the implementation against accidental change.
|
|
|
|
key = 42 13 37 BE EF, seed (0x54..0x59) = DE AD BE EF 42, body = 0xAA.
|
|
|
|
### `css_descramble_inverts_scramble_over_body`
|
|
|
|
The content cipher is NOT a plain XOR involution (it applies TAB1 to the
|
|
ciphertext: `plain = TAB1[cipher] ^ ks`). The true inverse is
|
|
`scramble_sector`. Scrambling a plaintext body and then descrambling
|
|
with the same key must reproduce the original body exactly.
|
|
|
|
### `css_tab1_is_permutation`
|
|
|
|
Verify the structure of TAB1: it is a substitution table used in key
|
|
mangling. Check that no two inputs map to the same output (TAB1 is a
|
|
permutation of 0..255).
|
|
|
|
### `descramble_treats_high_bits_of_0x14_as_clear`
|
|
|
|
Only bits 4-5 of byte 0x14 are the CSS scramble flag: the code reads
|
|
`sector[0x14] & 0x30 == 0` (bits 6-7, i.e. 0x40/0x80, are masked out by
|
|
0x30). A sector with 0x14 == 0x40 or 0x80 must therefore be treated as
|
|
UNSCRAMBLED and left byte-for-byte unchanged. This guards against a
|
|
too-wide mask silently "descrambling" (and thus corrupting) clear data.
|
|
|
|
Grounding: CSS sector header byte 0x14 — copyright/scramble bits live in
|
|
bits 4-5; the masked value 0 means not scrambled.
|
|
Mutation: widen the mask `0x30` to `0x70`/`0xF0` -> 0x40/0x80 would be
|
|
seen as scrambled and the body would change.
|
|
|
|
### `descramble_triggers_on_either_flag_bit`
|
|
|
|
Each individual scramble bit (4 and 5) independently marks the sector as
|
|
encrypted: 0x10 and 0x20 must both trigger descrambling.
|
|
|
|
Grounding: `(0x10 >> 4) & 3 == 1`, `(0x20 >> 4) & 3 == 2` — both nonzero.
|
|
Mutation: change `!= 0` early-return condition to `== 3` -> a sector
|
|
flagged only 0x10 or 0x20 would be skipped and left scrambled.
|
|
|
|
### `descramble_clear_preserves_high_bits_of_0x14`
|
|
|
|
After descrambling, ONLY the two scramble bits are cleared (`& 0xCF`);
|
|
bits 6 and 7 of byte 0x14 must be preserved. A sector with 0x14 == 0xF0
|
|
becomes 0xC0 (bits 6,7 kept, bits 4,5 cleared), NOT 0x00.
|
|
|
|
Grounding: code does `sector[0x14] &= 0xCF`; 0xF0 & 0xCF == 0xC0.
|
|
Mutation: change `&= 0xCF` to `= 0` or `&= 0x0F` -> the preserved high
|
|
bits assert fails.
|
|
|
|
### `descramble_leaves_header_and_seed_intact`
|
|
|
|
The encrypted region is exactly bytes 0x80..0x800. Bytes 0x00..0x80 (the
|
|
header) must NOT be modified by the keystream — except byte 0x14 whose
|
|
flag is cleared. In particular the sector-seed bytes 0x54..0x59 (which
|
|
live inside the header) must survive untouched, since the descrambler
|
|
reads them but never writes them.
|
|
|
|
Grounding: loop is `sector.iter_mut().take(2048).skip(128)` -> indices
|
|
128..2048 only.
|
|
Mutation: change `.skip(128)` to `.skip(0)` -> header bytes (incl. the
|
|
seed) get XORed and this fails.
|
|
|
|
### `descramble_covers_final_body_byte`
|
|
|
|
The descrambler must touch the WHOLE body 0x80..0x800, not just a prefix.
|
|
With a constant body and constant key, the keystream is non-degenerate
|
|
enough that the very last sector byte (index 2047) is altered. This guards
|
|
the loop bound `.take(2048)` against an off-by-one that would leave the
|
|
final byte(s) scrambled.
|
|
|
|
Grounding: encrypted region end is 0x800 == 2048 (exclusive).
|
|
Mutation: change `.take(2048)` to `.take(2047)` -> last byte unchanged,
|
|
assert fires (this body is all-zero so any keystream XOR shows).
|
|
|
|
### `descramble_processes_the_first_sector_of_an_over_long_buffer`
|
|
|
|
The length guard is a FLOOR, not a ceiling: `descramble_sector` is a
|
|
no-op below one sector, and processes the FIRST sector of anything at
|
|
least that long (the loop is `.take(2048)`). `css::descramble_sector` is
|
|
a public entry taking `&mut [u8]` of any length, so a caller handing it a
|
|
multi-sector buffer must get its first sector descrambled — a guard that
|
|
rejected over-long buffers would hand that caller its ciphertext back
|
|
unchanged, with the scramble flag cleared as if it had worked.
|
|
|
|
### `descramble_output_depends_on_title_key`
|
|
|
|
Descramble is keyed by `title_key XOR seed`: two different title keys
|
|
produce two different bodies for the same scrambled input. A cipher that
|
|
ignored the title key (or mixed it in wrongly) would yield identical
|
|
output — silent wrong-key decryption.
|
|
|
|
Grounding: per-sector key = title_key[i] ^ sector[0x54+i].
|
|
Mutation: in the `key` array drop the `title_key[i] ^` term -> both keys
|
|
give the same body, assert fires.
|
|
|
|
### `descramble_output_depends_on_seed`
|
|
|
|
Descramble is keyed by the sector seed too: same title key, different
|
|
seed -> different body. Pins that bytes 0x54..0x59 actually feed the
|
|
keystream (not just the per-sector XOR key).
|
|
|
|
Mutation: replace `seed` array reads with a constant -> both seeds give
|
|
the same body, assert fires.
|