Skip to main content

dvb_csa/bitsliced/
mod.rs

1//! Bitsliced fast path — [`LANES`] payloads scrambled or descrambled at once.
2//!
3//! # What is actually parallel in CSA2
4//!
5//! Bitslicing transposes the data so that bit *i* of every lane lives in one
6//! machine word, then evaluates the cipher as a boolean circuit; every gate
7//! then operates on all [`LANES`] lanes at once. That only pays off where the
8//! algorithm has real independence, and CSA2 has less of it than it looks:
9//!
10//! - **Block cipher, scramble.** `C[i] = E(P[i] ^ C[i+1])` — a reverse CBC.
11//!   Block *i* cannot start until block *i+1* is finished, so there is **no
12//!   parallelism within one payload**.
13//! - **Block cipher, descramble.** `P[i] = D(C[i]) ^ C[i+1]`. The chaining XOR
14//!   uses the *ciphertext* of the next block, which is already in the buffer,
15//!   so every `D(C[i])` is independent — this half *is* parallel within a
16//!   single payload.
17//! - **Stream cipher.** Two chained 40-bit shift registers; round *n+1* needs
18//!   round *n*'s state. Sequential within one payload, in both directions.
19//!
20//! Two of those three are sequential within a payload, and the stream cipher —
21//! the sequential one — is about two thirds of the total work. So the honest
22//! unit of parallelism for CSA2 is **the payload, not the block**: this module
23//! exposes a batch API that scrambles or descrambles up to [`LANES`]
24//! *independent* payloads (TS packets, typically) in one pass. There is no
25//! bitsliced single-payload entry point, because for a single payload there is
26//! nothing worth slicing.
27//!
28//! # Correctness
29//!
30//! The bitsliced path is bit-exact with the scalar path — it is the same
31//! cipher, re-expressed. Three independent gates hold it there:
32//!
33//! - every generated circuit in `src/bitsliced/circuits.rs` is checked against
34//!   the table it came from over its **entire** input domain;
35//! - `tests/bitsliced_differential.rs` compares batch output against
36//!   [`crate::scramble`] / [`crate::descramble`] over randomised payloads and
37//!   lengths, in both directions;
38//! - `tests/golden_vectors.rs` runs the libdvbcsa known-answer vectors through
39//!   the batch API too, so the fast path answers to the external oracle and
40//!   not merely to our own scalar code.
41//!
42//! # Example
43//!
44//! ```
45//! use dvb_csa::{ControlWord, bitsliced, descramble, scramble};
46//!
47//! let cw = ControlWord::from_bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
48//! let mut packets = [[0xAAu8; 184], [0xBBu8; 184], [0xCCu8; 184]];
49//! let expected = packets;
50//!
51//! let mut batch: [&mut [u8]; 3] = {
52//!     let [a, b, c] = &mut packets;
53//!     [a.as_mut_slice(), b.as_mut_slice(), c.as_mut_slice()]
54//! };
55//! bitsliced::scramble_batch(&cw, &mut batch);
56//! bitsliced::descramble_batch(&cw, &mut batch);
57//!
58//! assert_eq!(packets, expected);
59//! ```
60mod block;
61#[cfg(test)]
62mod circuit_tests;
63mod circuits;
64mod stream;
65
66use crate::key::ControlWord;
67use block::BitslicedBlock;
68use stream::BitslicedStream;
69
70/// The machine word the cipher state is sliced across. One bit per lane.
71type Word = u64;
72
73/// Number of payloads processed per bitsliced pass — the slicing width.
74///
75/// A batch longer than this is split into consecutive groups of `LANES`; a
76/// shorter one simply leaves the surplus lanes idle, so a batch well below
77/// `LANES` gets proportionally less of the speed-up.
78pub const LANES: usize = Word::BITS as usize;
79
80/// Bytes in one CSA block.
81const BLOCK_BYTES: usize = 8;
82/// Bits in one byte — the width of the block cipher's byte-wise state words.
83const BITS_PER_BYTE: usize = 8;
84/// Bits in one CSA block; also the side length of the transpose matrix.
85const BLOCK_BITS: usize = BLOCK_BYTES * BITS_PER_BYTE;
86
87const _: () = assert!(BLOCK_BITS == LANES, "the transpose is square");
88
89/// Transpose a `LANES` x `LANES` bit matrix in place.
90///
91/// Row `i` on input becomes column `i` on output, which is exactly the
92/// scalar <-> bitsliced conversion in both directions: feed it one packed
93/// `Word` per lane and it returns one `Word` per bit position, and vice versa.
94///
95/// The recursive block-swap algorithm is Hacker's Delight 2nd ed. §7-3, in its
96/// least-significant-bit-is-column-zero form: it costs `log2(LANES)` passes
97/// rather than the `LANES^2` of the naive loop, which keeps transposition
98/// negligible beside the cipher rounds it feeds.
99///
100/// Each pass exchanges the two off-diagonal quadrants of every `2j x 2j`
101/// sub-matrix — `mask` selects the low `j` columns of each such block, and
102/// halves alongside `j`.
103fn transpose(m: &mut [Word; LANES]) {
104    let mut j = LANES / 2;
105    let mut mask: Word = !0 >> (LANES / 2);
106    while j != 0 {
107        let mut k = 0;
108        while k < LANES {
109            let t = ((m[k] >> j) ^ m[k | j]) & mask;
110            m[k] ^= t << j;
111            m[k | j] ^= t;
112            k = ((k | j) + 1) & !j;
113        }
114        j >>= 1;
115        mask ^= mask << j;
116    }
117}
118
119/// Per-lane bookkeeping for one group of at most [`LANES`] payloads.
120struct Group {
121    /// Payload length per lane; `0` for an idle lane.
122    len: [usize; LANES],
123    /// Complete 8-byte blocks per lane.
124    blocks: [usize; LANES],
125    /// Largest `blocks` in the group.
126    max_blocks: usize,
127    /// Largest stream-ciphered byte count (`len - BLOCK_BYTES`) in the group.
128    max_stream: usize,
129}
130
131impl Group {
132    fn new(payloads: &[&mut [u8]]) -> Self {
133        let mut g = Self {
134            len: [0; LANES],
135            blocks: [0; LANES],
136            max_blocks: 0,
137            max_stream: 0,
138        };
139        for (lane, p) in payloads.iter().enumerate() {
140            // Payloads shorter than one block pass through untouched, exactly
141            // as the scalar path leaves them.
142            if p.len() < BLOCK_BYTES {
143                continue;
144            }
145            g.len[lane] = p.len();
146            g.blocks[lane] = p.len() / BLOCK_BYTES;
147            g.max_blocks = g.max_blocks.max(g.blocks[lane]);
148            g.max_stream = g.max_stream.max(p.len() - BLOCK_BYTES);
149        }
150        g
151    }
152}
153
154/// Read the 8 bytes at `off` of every lane into one packed `Word` per lane.
155///
156/// Lanes with no block at that offset contribute zero; their results are
157/// discarded, so the value does not matter.
158fn gather(payloads: &[&mut [u8]], offsets: &[Option<usize>; LANES], m: &mut [Word; LANES]) {
159    *m = [0; LANES];
160    for (lane, off) in offsets.iter().enumerate() {
161        if let Some(off) = *off {
162            let bytes: [u8; BLOCK_BYTES] = payloads[lane][off..off + BLOCK_BYTES]
163                .try_into()
164                .expect("slice is exactly one block");
165            m[lane] = Word::from_le_bytes(bytes);
166        }
167    }
168}
169
170/// Write one packed `Word` per lane back to the 8 bytes at `off`.
171fn scatter(payloads: &mut [&mut [u8]], offsets: &[Option<usize>; LANES], m: &[Word; LANES]) {
172    for (lane, off) in offsets.iter().enumerate() {
173        if let Some(off) = *off {
174            payloads[lane][off..off + BLOCK_BYTES].copy_from_slice(&m[lane].to_le_bytes());
175        }
176    }
177}
178
179/// Byte offset of block `index` in each lane, or `None` where the lane is
180/// shorter than that.
181fn block_offsets(g: &Group, index: usize) -> [Option<usize>; LANES] {
182    let mut o = [None; LANES];
183    for (slot, &blocks) in o.iter_mut().zip(g.blocks.iter()) {
184        if index < blocks {
185            *slot = Some(index * BLOCK_BYTES);
186        }
187    }
188    o
189}
190
191/// Byte offset of the block `from_end` places before each lane's last block.
192fn block_offsets_from_end(g: &Group, from_end: usize) -> [Option<usize>; LANES] {
193    let mut o = [None; LANES];
194    for (slot, &blocks) in o.iter_mut().zip(g.blocks.iter()) {
195        if from_end < blocks {
196            *slot = Some((blocks - 1 - from_end) * BLOCK_BYTES);
197        }
198    }
199    o
200}
201
202/// Scramble (encrypt) up to [`LANES`] payloads per pass with one control word.
203///
204/// Bit-for-bit identical to calling [`crate::scramble`] on each payload in
205/// turn, including the pass-through of payloads shorter than 8 bytes. The
206/// payloads are independent of one another; their lengths may all differ.
207pub fn scramble_batch(cw: &ControlWord, payloads: &mut [&mut [u8]]) {
208    for group in payloads.chunks_mut(LANES) {
209        scramble_group(cw, group);
210    }
211}
212
213/// Descramble (decrypt) up to [`LANES`] payloads per pass with one control word.
214///
215/// Bit-for-bit identical to calling [`crate::descramble`] on each payload in
216/// turn, including the pass-through of payloads shorter than 8 bytes. The
217/// payloads are independent of one another; their lengths may all differ.
218pub fn descramble_batch(cw: &ControlWord, payloads: &mut [&mut [u8]]) {
219    for group in payloads.chunks_mut(LANES) {
220        descramble_group(cw, group);
221    }
222}
223
224fn scramble_group(cw: &ControlWord, payloads: &mut [&mut [u8]]) {
225    let g = Group::new(payloads);
226    if g.max_blocks == 0 {
227        return;
228    }
229    let bc = BitslicedBlock::new(cw.expand_block());
230    let mut m = [0 as Word; LANES];
231
232    // Phase 1 — block cipher, reverse CBC. Sequential within a payload, so the
233    // lanes are aligned on the *last* block of each and walk backwards
234    // together; a lane drops out as soon as its payload runs out of blocks.
235    for from_end in 0..g.max_blocks {
236        let here = block_offsets_from_end(&g, from_end);
237        if from_end > 0 {
238            // XOR the already-encrypted following block into this one.
239            let next = block_offsets_from_end(&g, from_end - 1);
240            for lane in 0..LANES {
241                if let (Some(h), Some(n)) = (here[lane], next[lane]) {
242                    let following: [u8; BLOCK_BYTES] = payloads[lane][n..n + BLOCK_BYTES]
243                        .try_into()
244                        .expect("slice is exactly one block");
245                    for (dst, src) in payloads[lane][h..h + BLOCK_BYTES].iter_mut().zip(following) {
246                        *dst ^= src;
247                    }
248                }
249            }
250        }
251        gather(payloads, &here, &mut m);
252        transpose(&mut m);
253        bc.encrypt(&mut m);
254        transpose(&mut m);
255        scatter(payloads, &here, &m);
256    }
257
258    // Phase 2 — stream cipher over bytes 8.., seeded from the now-encrypted
259    // first block of each payload.
260    stream_xor(cw, payloads, &g);
261}
262
263fn descramble_group(cw: &ControlWord, payloads: &mut [&mut [u8]]) {
264    let g = Group::new(payloads);
265    if g.max_blocks == 0 {
266        return;
267    }
268    let bc = BitslicedBlock::new(cw.expand_block());
269
270    // Phase 1 — stream cipher over bytes 8.., seeded from the still-encrypted
271    // first block of each payload.
272    stream_xor(cw, payloads, &g);
273
274    // Phase 2 — block cipher, forward CBC undo. Every `D(C[i])` is independent
275    // here, so the lanes are aligned on block 0 and simply walk forwards.
276    let mut m = [0 as Word; LANES];
277    let mut cipher = [0 as Word; LANES];
278    for index in 0..g.max_blocks {
279        let here = block_offsets(&g, index);
280        gather(payloads, &here, &mut m);
281        // C[index] is needed to un-chain the *previous* block, and decryption
282        // is about to overwrite it, so keep it.
283        cipher.copy_from_slice(&m);
284        transpose(&mut m);
285        bc.decrypt(&mut m);
286        transpose(&mut m);
287        if index > 0 {
288            let prev = block_offsets(&g, index - 1);
289            for lane in 0..LANES {
290                if let (Some(p), Some(_)) = (prev[lane], here[lane]) {
291                    let c = cipher[lane].to_le_bytes();
292                    for (dst, src) in payloads[lane][p..p + BLOCK_BYTES].iter_mut().zip(c) {
293                        *dst ^= src;
294                    }
295                }
296            }
297        }
298        scatter(payloads, &here, &m);
299    }
300}
301
302/// XOR the keystream of every lane into its own bytes `8..len`.
303///
304/// The initialisation vector is each payload's own first block, so the lanes
305/// diverge after the first round and stay independent from there.
306fn stream_xor(cw: &ControlWord, payloads: &mut [&mut [u8]], g: &Group) {
307    if g.max_stream == 0 {
308        return;
309    }
310    let mut iv = [0 as Word; LANES];
311    let mut first = [None; LANES];
312    for (slot, &len) in first.iter_mut().zip(g.len.iter()) {
313        if len >= BLOCK_BYTES {
314            *slot = Some(0);
315        }
316    }
317    gather(payloads, &first, &mut iv);
318    transpose(&mut iv);
319
320    let mut sc = BitslicedStream::new(&cw.expand_stream(), &iv);
321
322    let mut ks = [0 as Word; LANES];
323    let mut done = 0;
324    while done < g.max_stream {
325        // One transpose covers BLOCK_BYTES keystream bytes for every lane.
326        for byte in 0..BLOCK_BYTES {
327            let bits = sc.keystream_byte();
328            for bit in 0..BITS_PER_BYTE {
329                ks[byte * BITS_PER_BYTE + bit] = bits[bit];
330            }
331        }
332        transpose(&mut ks);
333        for lane in 0..LANES {
334            // A lane drops out once its own payload is exhausted; the group
335            // keeps going for whichever lane is longest.
336            let base = BLOCK_BYTES + done;
337            if base >= g.len[lane] {
338                continue;
339            }
340            let bytes = ks[lane].to_le_bytes();
341            let n = (g.len[lane] - base).min(BLOCK_BYTES);
342            for (j, b) in bytes.iter().take(n).enumerate() {
343                payloads[lane][base + j] ^= b;
344            }
345        }
346        done += BLOCK_BYTES;
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn transpose_is_an_involution() {
356        let mut m = [0 as Word; LANES];
357        for (i, w) in m.iter_mut().enumerate() {
358            // A deterministic, asymmetric fill: any bug that mixed rows and
359            // columns up would survive a symmetric one.
360            *w = (i as Word).wrapping_mul(0x9E37_79B9_7F4A_7C15);
361        }
362        let original = m;
363        transpose(&mut m);
364        assert_ne!(
365            m, original,
366            "transpose of an asymmetric matrix is not a no-op"
367        );
368        transpose(&mut m);
369        assert_eq!(m, original);
370    }
371
372    #[test]
373    fn transpose_moves_row_bits_to_column_bits() {
374        let mut m = [0 as Word; LANES];
375        m[3] = 1 << 5;
376        transpose(&mut m);
377        for (i, w) in m.iter().enumerate() {
378            let want: Word = if i == 5 { 1 << 3 } else { 0 };
379            assert_eq!(*w, want, "row {i}");
380        }
381    }
382
383    #[test]
384    fn short_payloads_pass_through() {
385        let cw = ControlWord::from_bytes([1, 2, 3, 4, 5, 6, 7, 8]);
386        let mut a = [0xAAu8; 7];
387        let mut b = [0xBBu8; 0];
388        let mut batch: [&mut [u8]; 2] = [&mut a, &mut b];
389        scramble_batch(&cw, &mut batch);
390        descramble_batch(&cw, &mut batch);
391        assert_eq!(a, [0xAAu8; 7]);
392    }
393
394    #[test]
395    fn empty_batch_is_a_no_op() {
396        let cw = ControlWord::from_bytes([1, 2, 3, 4, 5, 6, 7, 8]);
397        scramble_batch(&cw, &mut []);
398        descramble_batch(&cw, &mut []);
399    }
400}