Files
libfreemkv/docs/lfsr.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
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.
2026-09-02 12:40:55 -07:00

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.