# 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.