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}