dvb-csa 0.2.0

DVB Common Scrambling Algorithm (CSA2) — pure-Rust, with an optional bitsliced 64-payload batch fast path, validated against libdvbcsa known-answer vectors.
Documentation
# dvb-csa

Pure-Rust implementation of the **DVB Common Scrambling Algorithm (CSA2)** —
the cipher underneath conditional access on DVB-S, DVB-T, and DVB-C.

## Status: oracle-validated, not spec-cited

DVB-CSA has no public normative specification — the algorithm was confidential
and licensed through ETSI. Every open implementation is reverse-engineered.
Correctness is established by byte-exact agreement with **libdvbcsa 1.1.0**
(VideoLAN's reference free implementation): 18 committed golden vectors in
`tests/fixtures/libdvbcsa-vectors.hex` — 13 full 184-byte TS payloads plus one
each at 8 B, 16 B, 32 B, 64 B and 100 B, the last covering a residual block
that is neither empty nor a whole multiple of the 8-byte block size.

## Algorithm overview

DVB-CSA2 combines a **block cipher** and a **stream cipher**, both keyed by the
same 8-byte control word:

- **Block cipher** — 56-round substitution/permutation network on 8-byte blocks,
  applied in a CBC-like chained mode across all complete blocks.
- **Stream cipher** — LFSR-based byte-stream generator seeded from the
  nibble-swapped control word and the encrypted first block as IV, XOR'd with
  bytes 8..end.

Encrypt = block CBC then stream XOR; decrypt = stream XOR then block CBC undo.
Payloads shorter than 8 bytes pass through unchanged.

## Usage

```rust
use dvb_csa::{ControlWord, scramble, descramble};

let cw = ControlWord::from_bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);

let mut payload = vec![0xAA; 184];
scramble(&cw, &mut payload);
descramble(&cw, &mut payload);
// payload is back to all 0xAA
```

For TS-packet-level operation (extracts the payload, respects the adaptation
field):

```rust
use dvb_csa::{ControlWord, ts};

let cw = ControlWord::from_bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
let mut packet = [0u8; 188];
// ... fill packet ...
let _ = ts::scramble_ts_packet(&cw, &mut packet);
```

## Bitsliced batch fast path

The optional `bitsliced` feature adds `dvb_csa::bitsliced`, which transposes
the data and evaluates the cipher as a branch-free boolean circuit, so every
gate acts on `LANES` (64) payloads at once.

```toml
[dependencies]
dvb-csa = { version = "0.2", features = ["bitsliced"] }
```

```rust
use dvb_csa::{ControlWord, bitsliced};

let cw = ControlWord::from_bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
let mut payloads: Vec<Vec<u8>> = (0..64).map(|_| vec![0xAA; 184]).collect();

let mut batch: Vec<&mut [u8]> = payloads.iter_mut().map(|p| p.as_mut_slice()).collect();
bitsliced::scramble_batch(&cw, &mut batch);
bitsliced::descramble_batch(&cw, &mut batch);
```

**The unit of parallelism is the payload, not the block.** CSA2 offers little
independence *within* one payload — scrambling's block cipher is a reverse CBC
(`C[i] = E(P[i] ^ C[i+1])`) and the stream cipher is a chained LFSR, both
strictly sequential; only descrambling's block half is independent per block,
and the stream cipher (the sequential part) is about two thirds of the work.
So the API batches **independent** payloads — TS packets, typically — and there
is deliberately no bitsliced single-payload entry point. A batch well below 64
leaves lanes idle and scales down accordingly.

Measured by `benches/throughput.rs` on an Apple M2 Ultra (rustc 1.86.0), over
64 x 184-byte TS payloads:

| Operation  | Scalar     | Bitsliced  | Speed-up |
|------------|------------|------------|----------|
| scramble   | 15.7 MiB/s | 91.3 MiB/s | **5.8x** |
| descramble | 15.1 MiB/s | 99.8 MiB/s | **6.6x** |

The fast path is bit-exact with the scalar path, held there by three
independent gates: every generated circuit is checked against the table in
`src/tables.rs` it came from over its **entire** input domain; a differential
test requires byte-identical agreement with the scalar path over randomised
payloads and lengths in both directions; and the libdvbcsa known-answer
vectors are run through the batch path too, so it answers to the external
oracle and not merely to our own scalar code.

The circuits are generated by `tools/gen_circuits.py` from `src/tables.rs`,
which remains the single source of truth for the cipher.

## Features

| Feature     | Default | What it does |
|-------------|---------|--------------|
| `std`       | yes     | Enables `std::error::Error` on `Error` |
| `bitsliced` | no      | Adds the `bitsliced` batch fast path (above) |

Builds `no_std` without default features, with or without `bitsliced`.

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.