Skip to main content

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;