logicaffeine_base/gf256.rs
1//! GF(2^8) — the finite field with 256 elements, the shared substrate for erasure
2//! coding, multivariate cryptography (Oil-and-Vinegar / Rainbow), and the algebraic
3//! (XL) cryptanalysis engine.
4//!
5//! Elements are bytes; addition is XOR (characteristic two, so `add == sub`), and
6//! multiplication is polynomial multiplication modulo the primitive polynomial
7//! `x^8 + x^4 + x^3 + x^2 + 1` (`0x11d`) — the reduction polynomial used by the
8//! Reed-Solomon / QR-code lineage. (Note this is NOT the AES/Rijndael polynomial
9//! `0x11b`; a scheme that fixes a different field representation needs its own.)
10//!
11//! `mul`/`div` run off a log/exp table generated once from the generator `2`, whose
12//! multiplicative order is the full 255, so `exp`/`log` are true inverses across the
13//! whole nonzero field. On top of the field this module provides the linear algebra
14//! that multivariate signing and kernel extraction need: matrix `invert`, `matmul`,
15//! `matvec`, `rank`, `solve` (a particular solution of `A x = b`), and `nullspace`
16//! (a basis of `{x : A x = 0}`) — all exact, no floating point.
17
18use std::sync::OnceLock;
19
20/// GF(2^8) log/exp tables under the primitive polynomial `0x11d`. `exp` is doubled to
21/// 512 entries so a `log[a] + log[b]` sum never indexes out of range.
22struct Gf {
23 exp: [u8; 512],
24 log: [u8; 256],
25}
26
27fn gf() -> &'static Gf {
28 static GF: OnceLock<Gf> = OnceLock::new();
29 GF.get_or_init(|| {
30 let mut exp = [0u8; 512];
31 let mut log = [0u8; 256];
32 let mut x: u16 = 1;
33 for i in 0..255 {
34 exp[i] = x as u8;
35 log[x as usize] = i as u8;
36 x <<= 1;
37 if x & 0x100 != 0 {
38 x ^= 0x11d;
39 }
40 }
41 for i in 255..512 {
42 exp[i] = exp[i - 255];
43 }
44 Gf { exp, log }
45 })
46}
47
48/// Field addition. In characteristic two this is XOR, and it is its own inverse, so
49/// subtraction is the same operation.
50#[inline]
51pub fn add(a: u8, b: u8) -> u8 {
52 a ^ b
53}
54
55/// Field subtraction — identical to [`add`] in characteristic two. Named for call-site
56/// clarity where the algebra reads as a subtraction.
57#[inline]
58pub fn sub(a: u8, b: u8) -> u8 {
59 a ^ b
60}
61
62/// Field multiplication modulo `0x11d`. `0` absorbs.
63#[inline]
64pub fn mul(a: u8, b: u8) -> u8 {
65 if a == 0 || b == 0 {
66 return 0;
67 }
68 let g = gf();
69 g.exp[g.log[a as usize] as usize + g.log[b as usize] as usize]
70}
71
72/// Field division `a / b`. `0` has no inverse, so a zero divisor yields `0` — a safe, defined
73/// sentinel (NOT a meaningful quotient) that holds identically in debug and release; callers must
74/// still treat `b == 0` as a precondition violation, never as a value.
75#[inline]
76pub fn div(a: u8, b: u8) -> u8 {
77 if a == 0 || b == 0 {
78 return 0;
79 }
80 let g = gf();
81 g.exp[g.log[a as usize] as usize + 255 - g.log[b as usize] as usize]
82}
83
84/// Multiplicative inverse. `inv(a) = a^254` since the group has order 255; `inv(0) = 0` — `0`
85/// has no inverse, so this is a safe sentinel rather than the silent garbage a raw table lookup
86/// would return.
87#[inline]
88pub fn inv(a: u8) -> u8 {
89 div(1, a)
90}
91
92/// `base^exp` in GF(2^8). `base^0 == 1` for every base (including `0`).
93pub fn pow(base: u8, exp: usize) -> u8 {
94 let mut r = 1u8;
95 for _ in 0..exp {
96 r = mul(r, base);
97 }
98 r
99}
100
101/// Invert a `k×k` matrix over GF(2^8) by Gauss-Jordan elimination; `None` if singular.
102pub fn invert(m: &[Vec<u8>]) -> Option<Vec<Vec<u8>>> {
103 let k = m.len();
104 let mut a: Vec<Vec<u8>> = m
105 .iter()
106 .enumerate()
107 .map(|(i, row)| {
108 let mut r = row.clone();
109 r.extend((0..k).map(|j| if i == j { 1 } else { 0 }));
110 r
111 })
112 .collect();
113 for col in 0..k {
114 let mut piv = col;
115 while piv < k && a[piv][col] == 0 {
116 piv += 1;
117 }
118 if piv == k {
119 return None; // singular
120 }
121 a.swap(col, piv);
122 let inv_p = div(1, a[col][col]);
123 for j in 0..2 * k {
124 a[col][j] = mul(a[col][j], inv_p);
125 }
126 for row in 0..k {
127 if row == col {
128 continue;
129 }
130 let f = a[row][col];
131 if f != 0 {
132 for j in 0..2 * k {
133 a[row][j] ^= mul(f, a[col][j]);
134 }
135 }
136 }
137 }
138 Some(a.iter().map(|r| r[k..2 * k].to_vec()).collect())
139}
140
141/// Multiply an `n×k` matrix by a `k×c` matrix over GF(2^8).
142pub fn matmul(a: &[Vec<u8>], b: &[Vec<u8>]) -> Vec<Vec<u8>> {
143 let inner = b.len();
144 let cols = b[0].len();
145 a.iter()
146 .map(|row| {
147 (0..cols)
148 .map(|j| {
149 let mut acc = 0u8;
150 for t in 0..inner {
151 acc ^= mul(row[t], b[t][j]);
152 }
153 acc
154 })
155 .collect()
156 })
157 .collect()
158}
159
160/// Multiply an `m×n` matrix by an `n`-vector over GF(2^8), giving an `m`-vector.
161pub fn matvec(a: &[Vec<u8>], x: &[u8]) -> Vec<u8> {
162 a.iter()
163 .map(|row| {
164 let mut acc = 0u8;
165 for (j, &c) in row.iter().enumerate() {
166 acc ^= mul(c, x[j]);
167 }
168 acc
169 })
170 .collect()
171}
172
173/// Reduce `rows` to reduced row-echelon form in place, pivoting only within the first
174/// `pivot_limit` columns (the variable columns; any trailing columns — e.g. an augmented
175/// right-hand side — are eliminated along but never pivoted on). Returns the pivot column
176/// of each pivot row, in row order.
177fn rref(rows: &mut [Vec<u8>], pivot_limit: usize) -> Vec<usize> {
178 let mut pivots = Vec::new();
179 if rows.is_empty() {
180 return pivots;
181 }
182 let width = rows[0].len();
183 let mut r = 0usize;
184 for c in 0..pivot_limit {
185 let mut piv = None;
186 for i in r..rows.len() {
187 if rows[i][c] != 0 {
188 piv = Some(i);
189 break;
190 }
191 }
192 let Some(pi) = piv else { continue };
193 rows.swap(r, pi);
194 let inv_p = inv(rows[r][c]);
195 for j in 0..width {
196 rows[r][j] = mul(rows[r][j], inv_p);
197 }
198 for i in 0..rows.len() {
199 if i != r && rows[i][c] != 0 {
200 let f = rows[i][c];
201 for j in 0..width {
202 rows[i][j] ^= mul(f, rows[r][j]);
203 }
204 }
205 }
206 pivots.push(c);
207 r += 1;
208 if r == rows.len() {
209 break;
210 }
211 }
212 pivots
213}
214
215/// Rank of an `m×n` matrix over GF(2^8).
216pub fn rank(a: &[Vec<u8>]) -> usize {
217 let mut rows: Vec<Vec<u8>> = a.to_vec();
218 let ncols = rows.first().map_or(0, |r| r.len());
219 rref(&mut rows, ncols).len()
220}
221
222/// A particular solution of `A x = b` over GF(2^8), or `None` if the system is
223/// inconsistent. `a` is `m×n` (row per equation), `b` has length `m`; the returned `x`
224/// has length `n` with free variables set to zero. When the caller knows the system is
225/// square and full-rank (Oil-and-Vinegar signing after fixing the vinegar variables),
226/// the solution is unique.
227pub fn solve(a: &[Vec<u8>], b: &[u8]) -> Option<Vec<u8>> {
228 let m = a.len();
229 if m == 0 {
230 return Some(Vec::new());
231 }
232 let n = a[0].len();
233 let mut rows: Vec<Vec<u8>> = a
234 .iter()
235 .zip(b.iter())
236 .map(|(row, &rhs)| {
237 let mut r = row.clone();
238 r.push(rhs);
239 r
240 })
241 .collect();
242 let pivots = rref(&mut rows, n);
243 // Consistency: a row that is all-zero across the n variable columns but has a
244 // non-zero right-hand side is the equation 0 = c, a contradiction.
245 for row in &rows {
246 if row[..n].iter().all(|&v| v == 0) && row[n] != 0 {
247 return None;
248 }
249 }
250 let mut x = vec![0u8; n];
251 for (row_idx, &col) in pivots.iter().enumerate() {
252 x[col] = rows[row_idx][n];
253 }
254 Some(x)
255}
256
257/// A basis of the null space `{x : A x = 0}` over GF(2^8). The basis has `n - rank(A)`
258/// vectors of length `n`; together they span every solution of the homogeneous system.
259pub fn nullspace(a: &[Vec<u8>]) -> Vec<Vec<u8>> {
260 let n = a.first().map_or(0, |r| r.len());
261 if n == 0 {
262 return Vec::new();
263 }
264 let mut rows: Vec<Vec<u8>> = a.to_vec();
265 let pivots = rref(&mut rows, n);
266 let is_pivot = {
267 let mut v = vec![false; n];
268 for &c in &pivots {
269 v[c] = true;
270 }
271 v
272 };
273 let mut basis = Vec::new();
274 for free in 0..n {
275 if is_pivot[free] {
276 continue;
277 }
278 let mut v = vec![0u8; n];
279 v[free] = 1;
280 for (row_idx, &pcol) in pivots.iter().enumerate() {
281 // Pivot equation: x[pcol] + Σ_{free j} rows[row_idx][j]·x[j] = 0.
282 // With this free var = 1 and all other frees = 0, x[pcol] = rows[row_idx][free].
283 v[pcol] = rows[row_idx][free];
284 }
285 basis.push(v);
286 }
287 basis
288}
289
290/// A 256-entry lookup table for multiplication by a fixed constant `c`: `table[b] = c · b` in
291/// GF(2^8). Precompute once per coefficient; a whole region then multiplies by one table lookup
292/// per byte — the SIMD-friendly hot path of Reed-Solomon coding.
293pub fn mul_table(c: u8) -> [u8; 256] {
294 let mut t = [0u8; 256];
295 for (b, slot) in t.iter_mut().enumerate() {
296 *slot = mul(c, b as u8);
297 }
298 t
299}
300
301/// Region multiply-accumulate over GF(2^8): `dst[i] ^= c · src[i]` for every byte — the inner loop
302/// of Reed-Solomon encode/reconstruct. `src` and `dst` must be the same length. The body is a
303/// per-`c` table lookup XORed into the destination, a shape the compiler AUTO-VECTORIZES to SSE/AVX
304/// on native and to wasm `v128` under `simd128` — the SIMD speedup for browser FEC, in safe Rust
305/// with no hand-written intrinsics to silently miscompile (its result is pinned byte-exact to the
306/// scalar field by the tests).
307pub fn mul_add_region(c: u8, src: &[u8], dst: &mut [u8]) {
308 debug_assert_eq!(
309 src.len(),
310 dst.len(),
311 "region multiply operands must be equal length"
312 );
313 let table = mul_table(c);
314 for (d, &s) in dst.iter_mut().zip(src.iter()) {
315 *d ^= table[s as usize];
316 }
317}
318
319#[cfg(test)]
320#[path = "../../logicaffeine_tests/unit/logicaffeine_base/gf256/tests.rs"]
321mod tests;