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
use ;
let cw = from_bytes;
let mut payload = vec!;
scramble;
descramble;
// payload is back to all 0xAA
For TS-packet-level operation (extracts the payload, respects the adaptation field):
use ;
let cw = from_bytes;
let mut packet = ;
// ... fill packet ...
let _ = scramble_ts_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.
[]
= { = "0.2", = ["bitsliced"] }
use ;
let cw = from_bytes;
let mut payloads: = .map.collect;
let mut batch: = payloads.iter_mut.map.collect;
scramble_batch;
descramble_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 or MIT license at your option.