Skip to main content

p3_binary_field/aes/
engine.rs

1//! Byte-wise `F_2`-linear maps and AES-field products, one register at a time.
2//!
3//! One instruction multiplies bytes in the AES field.
4//!
5//! Another applies an arbitrary `F_2`-linear map to each byte, and a third inverts first.
6//!
7//! Every such map is a matrix over `F_2`.
8//!
9//! Basis change, Frobenius and any tabulated map therefore share one kernel.
10
11use super::{invert_byte, mul_bytes};
12
13/// The quadword whose byte `k` is `1 << (7 - k)`.
14///
15/// Read as a matrix it is the identity.
16///
17/// Used as a multiplier it collects the low bit of every byte into one byte.
18const BIT_LADDER: u64 = 0x0102_0408_1020_4080;
19
20/// The same byte in all eight positions of a quadword.
21const SPREAD: u64 = 0x0101_0101_0101_0101;
22
23/// An `F_2`-linear map on a byte, held the way the affine-byte instruction reads it.
24///
25/// Output bit `i` is the parity of the input byte against byte `7 - i` of the quadword.
26///
27/// ```text
28///     out[i] = XOR over b of row[7 - i][b] * in[b]
29/// ```
30///
31/// Squaring, scaling by a constant, a basis change and a linearized polynomial all fit here.
32///
33/// Tabulated once, each becomes a single instruction per register.
34#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
35#[repr(transparent)]
36#[must_use]
37pub struct ByteMatrix(u64);
38
39impl ByteMatrix {
40    /// The map sending every byte to zero.
41    pub const ZERO: Self = Self(0);
42
43    /// The map leaving every byte alone.
44    pub const IDENTITY: Self = Self(BIT_LADDER);
45
46    /// The map sending basis vector `1 << b` to entry `b` of the argument.
47    ///
48    /// A linear map is pinned down by where it sends each of the eight basis vectors.
49    pub const fn from_images(images: [u8; 8]) -> Self {
50        let mut quadword = 0u64;
51        let mut i = 0;
52        while i < 8 {
53            // Row `7 - i` pairs bit `b` of the input with bit `i` of that input's image.
54            let mut row = 0u64;
55            let mut b = 0;
56            while b < 8 {
57                if (images[b] >> i) & 1 == 1 {
58                    row |= 1 << b;
59                }
60                b += 1;
61            }
62            quadword |= row << (8 * (7 - i));
63            i += 1;
64        }
65        Self(quadword)
66    }
67
68    /// Where the map sends each of the eight basis vectors.
69    #[must_use]
70    pub const fn images(self) -> [u8; 8] {
71        let mut images = [0u8; 8];
72        let mut b = 0;
73        while b < 8 {
74            let mut i = 0;
75            while i < 8 {
76                if (self.0 >> (8 * (7 - i) + b)) & 1 == 1 {
77                    images[b] |= 1 << i;
78                }
79                i += 1;
80            }
81            b += 1;
82        }
83        images
84    }
85
86    /// The image of one byte.
87    #[must_use]
88    pub const fn apply(self, byte: u8) -> u8 {
89        // The input byte against all eight rows at once.
90        let hit = self.0 & (byte as u64).wrapping_mul(SPREAD);
91
92        // Fold each byte onto its own low bit, which then holds that row's parity.
93        let mut parity = hit;
94        parity ^= parity >> 4;
95        parity ^= parity >> 2;
96        parity ^= parity >> 1;
97        parity &= SPREAD;
98
99        // The ladder gathers those eight bits into the top byte, with no two sharing a bit.
100        //
101        // Row `7 - i` drives output bit `i`, so the gathered byte comes out reversed.
102        ((parity.wrapping_mul(BIT_LADDER) >> 56) as u8).reverse_bits()
103    }
104
105    /// The map applying the argument first and this one second.
106    pub const fn compose(self, first: Self) -> Self {
107        let images = first.images();
108        let mut composed = [0u8; 8];
109        let mut b = 0;
110        while b < 8 {
111            composed[b] = self.apply(images[b]);
112            b += 1;
113        }
114        Self::from_images(composed)
115    }
116
117    /// The row layout the affine-byte instruction takes as its matrix operand.
118    #[must_use]
119    pub const fn to_quadword(self) -> u64 {
120        self.0
121    }
122
123    /// The map already held in that row layout.
124    ///
125    /// Every quadword is a matrix, so this cannot fail.
126    pub const fn from_quadword(rows: u64) -> Self {
127        Self(rows)
128    }
129
130    /// Replaces every byte of the slice with its image.
131    ///
132    /// Inlined so a caller with a fixed-length block sweeps it without a call.
133    #[inline]
134    pub fn apply_slice(self, bytes: &mut [u8]) {
135        map_slice(&Affine(self), bytes);
136    }
137}
138
139/// A lane-wise map, written once and instantiated at every register width the target has.
140///
141/// The matrix operand is built once per sweep rather than once per register.
142trait ByteMap {
143    /// The matrix the map carries, as one quadword.
144    fn quadword(&self) -> u64;
145
146    /// The map on one register, against the matrix already in register shape.
147    fn wide<L: ByteLanes>(&self, x: L, matrix: L::Matrix) -> L;
148
149    /// The map on one byte.
150    fn scalar(&self, x: u8) -> u8;
151}
152
153/// One fixed `F_2`-linear map applied to every byte.
154struct Affine(ByteMatrix);
155
156impl ByteMap for Affine {
157    #[inline(always)]
158    fn quadword(&self) -> u64 {
159        self.0.to_quadword()
160    }
161
162    #[inline(always)]
163    fn wide<L: ByteLanes>(&self, x: L, matrix: L::Matrix) -> L {
164        x.affine(matrix)
165    }
166
167    #[inline(always)]
168    fn scalar(&self, x: u8) -> u8 {
169        self.0.apply(x)
170    }
171}
172
173/// Inversion of every byte in the AES field, with zero left alone.
174struct Invert;
175
176impl ByteMap for Invert {
177    /// The hardware inverse composes with a map, so the identity leaves a bare inverse.
178    #[inline(always)]
179    fn quadword(&self) -> u64 {
180        ByteMatrix::IDENTITY.to_quadword()
181    }
182
183    #[inline(always)]
184    fn wide<L: ByteLanes>(&self, x: L, matrix: L::Matrix) -> L {
185        x.inverse_then_affine(matrix)
186    }
187
188    #[inline(always)]
189    fn scalar(&self, x: u8) -> u8 {
190        invert_byte(x)
191    }
192}
193
194/// The byte-lane operations the kernels below are written against.
195///
196/// Every method acts on each byte of the register independently.
197///
198/// # Safety
199///
200/// Reading or writing one value must touch exactly its own width in contiguous bytes.
201///
202/// That is what lets the kernels hand a slice chunk of that length to the two accessors.
203unsafe trait ByteLanes: Copy {
204    /// Bytes per register.
205    const WIDTH: usize;
206
207    /// A byte matrix in the shape one register of this width consumes.
208    type Matrix: Copy;
209
210    /// The matrix this width consumes, from its quadword form.
211    fn matrix(quadword: u64) -> Self::Matrix;
212
213    /// Reads one register of consecutive bytes.
214    ///
215    /// # Safety
216    ///
217    /// The address must be readable for one register of bytes, at any alignment.
218    unsafe fn load(from: *const u8) -> Self;
219
220    /// Writes one register of consecutive bytes.
221    ///
222    /// # Safety
223    ///
224    /// The address must be writable for one register of bytes, at any alignment.
225    unsafe fn store(to: *mut u8, value: Self);
226
227    /// The AES-field product of the bytes at each position.
228    fn mul(self, other: Self) -> Self;
229
230    /// One `F_2`-linear map applied to every byte.
231    fn affine(self, matrix: Self::Matrix) -> Self;
232
233    /// The AES-field inverse of every byte, sending zero to zero, then one map.
234    fn inverse_then_affine(self, matrix: Self::Matrix) -> Self;
235}
236
237// One byte at a time, where the target has no byte-wise instruction to sweep with.
238unsafe impl ByteLanes for u8 {
239    const WIDTH: usize = 1;
240
241    type Matrix = u64;
242
243    #[inline(always)]
244    fn matrix(quadword: u64) -> Self::Matrix {
245        quadword
246    }
247
248    #[inline(always)]
249    unsafe fn load(from: *const u8) -> Self {
250        // SAFETY: the readability of the address is the caller's obligation.
251        unsafe { *from }
252    }
253
254    #[inline(always)]
255    unsafe fn store(to: *mut u8, value: Self) {
256        // SAFETY: the writability of the address is the caller's obligation.
257        unsafe { *to = value }
258    }
259
260    #[inline(always)]
261    fn mul(self, other: Self) -> Self {
262        mul_bytes(self, other)
263    }
264
265    #[inline(always)]
266    fn affine(self, matrix: Self::Matrix) -> Self {
267        ByteMatrix(matrix).apply(self)
268    }
269
270    #[inline(always)]
271    fn inverse_then_affine(self, matrix: Self::Matrix) -> Self {
272        let inverse = invert_byte(self);
273
274        // Only the hardware form composes the two, and it does so for free.
275        //
276        // Here the map is a parity fold per byte, so an identity is worth skipping.
277        if matrix == ByteMatrix::IDENTITY.to_quadword() {
278            inverse
279        } else {
280            ByteMatrix(matrix).apply(inverse)
281        }
282    }
283}
284
285#[cfg(all(target_arch = "x86_64", target_feature = "gfni"))]
286mod x86_64;
287
288// Which registers the kernels sweep with, widest first.
289#[cfg(all(target_arch = "x86_64", target_feature = "gfni"))]
290use x86_64::{Narrow, Wide};
291
292/// The widest register this target runs these operations in.
293#[cfg(not(all(target_arch = "x86_64", target_feature = "gfni")))]
294type Wide = u8;
295
296/// The narrower register this target runs these operations in.
297///
298/// Both widths collapse onto the byte where neither instruction set is available.
299#[cfg(not(all(target_arch = "x86_64", target_feature = "gfni")))]
300type Narrow = u8;
301
302/// Maps the longest prefix of whole registers, returning what is left over.
303//
304// The chunk size comes from a trait constant, which a fixed-size chunking cannot take.
305#[allow(clippy::chunks_exact_to_as_chunks)]
306#[inline(always)]
307fn map_registers<'a, L: ByteLanes, M: ByteMap>(map: &M, bytes: &'a mut [u8]) -> &'a mut [u8] {
308    const {
309        assert!(
310            size_of::<L>() == L::WIDTH,
311            "a register must be its own width in bytes"
312        );
313    };
314
315    let covered = (bytes.len() / L::WIDTH) * L::WIDTH;
316    let (head, tail) = bytes.split_at_mut(covered);
317
318    // Built once here, so the sweep below is one instruction per register.
319    let matrix = L::matrix(map.quadword());
320
321    for chunk in head.chunks_exact_mut(L::WIDTH) {
322        // SAFETY: the chunk is one register of bytes, by the length just computed.
323        // Both accesses are unaligned forms.
324        unsafe {
325            let mapped = map.wide(L::load(chunk.as_ptr()), matrix);
326            L::store(chunk.as_mut_ptr(), mapped);
327        }
328    }
329    tail
330}
331
332/// Maps every byte of the slice, widest register first.
333#[inline]
334fn map_slice<M: ByteMap>(map: &M, bytes: &mut [u8]) {
335    let rest = map_registers::<Wide, M>(map, bytes);
336    let rest = map_registers::<Narrow, M>(map, rest);
337    for byte in rest {
338        *byte = map.scalar(*byte);
339    }
340}
341
342/// Multiplies the longest prefix of whole registers, returning what is left over.
343//
344// The chunk size comes from a trait constant, which a fixed-size chunking cannot take.
345#[allow(clippy::chunks_exact_to_as_chunks)]
346#[inline(always)]
347fn mul_registers<'a, 'b, L: ByteLanes>(
348    dst: &'a mut [u8],
349    src: &'b [u8],
350) -> (&'a mut [u8], &'b [u8]) {
351    const {
352        assert!(
353            size_of::<L>() == L::WIDTH,
354            "a register must be its own width in bytes"
355        );
356    };
357
358    let covered = (dst.len() / L::WIDTH) * L::WIDTH;
359    let (head, tail) = dst.split_at_mut(covered);
360    let (factors, rest) = src.split_at(covered);
361    for (chunk, factor) in head
362        .chunks_exact_mut(L::WIDTH)
363        .zip(factors.chunks_exact(L::WIDTH))
364    {
365        // SAFETY: both chunks are one register of bytes, by the length just computed.
366        // Every access is an unaligned form.
367        unsafe {
368            let product = L::load(chunk.as_ptr()).mul(L::load(factor.as_ptr()));
369            L::store(chunk.as_mut_ptr(), product);
370        }
371    }
372    (tail, rest)
373}
374
375/// Replaces every byte of the first slice with its AES-field product against the second.
376///
377/// # Panics
378///
379/// Panics if the slice lengths differ.
380#[inline]
381pub(crate) fn mul_slice(dst: &mut [u8], src: &[u8]) {
382    assert_eq!(dst.len(), src.len(), "elementwise product lengths differ");
383    let (dst, src) = mul_registers::<Wide>(dst, src);
384    let (dst, src) = mul_registers::<Narrow>(dst, src);
385    for (value, factor) in dst.iter_mut().zip(src) {
386        *value = mul_bytes(*value, *factor);
387    }
388}
389
390/// Replaces every byte of the slice with its AES-field inverse, leaving zero alone.
391#[inline]
392pub(crate) fn invert_slice(bytes: &mut [u8]) {
393    map_slice(&Invert, bytes);
394}
395
396#[cfg(test)]
397mod tests {
398    use proptest::prelude::*;
399
400    use super::{ByteMatrix, invert_slice, mul_slice};
401    use crate::aes::{invert_byte, mul_bytes};
402
403    /// The image of a byte under the map whose columns are given, summed column by column.
404    ///
405    /// This is the definition of a linear map, written without the row packing under test.
406    fn apply_by_columns(images: [u8; 8], byte: u8) -> u8 {
407        (0..8).fold(0, |acc, b| {
408            acc ^ if (byte >> b) & 1 == 1 { images[b] } else { 0 }
409        })
410    }
411
412    #[test]
413    fn the_row_layout_is_the_one_the_instruction_documents() {
414        // Invariant: the identity matrix is the byte ladder the hardware manual spells out.
415        //
416        // Row `7 - i` must be `1 << i`, which stacks to `0x0102040810204080`.
417        assert_eq!(ByteMatrix::IDENTITY.to_quadword(), 0x0102_0408_1020_4080);
418        assert_eq!(ByteMatrix::IDENTITY.images(), [1, 2, 4, 8, 16, 32, 64, 128]);
419
420        // A single entry: sending bit 0 to bit 7 and everything else to zero.
421        //
422        //     column 0 = 0x80  ->  row 0 (byte 7) has bit 0 set
423        let one_entry = ByteMatrix::from_images([0x80, 0, 0, 0, 0, 0, 0, 0]);
424        assert_eq!(one_entry.to_quadword(), 1);
425        assert_eq!(one_entry.apply(0xff), 0x80);
426        assert_eq!(one_entry.apply(0xfe), 0);
427    }
428
429    #[test]
430    fn the_constant_maps_behave() {
431        // Fixture state: the two maps every linear algebra starts from.
432        for byte in 0..=u8::MAX {
433            assert_eq!(ByteMatrix::ZERO.apply(byte), 0);
434            assert_eq!(ByteMatrix::IDENTITY.apply(byte), byte);
435        }
436    }
437
438    proptest! {
439        #![proptest_config(ProptestConfig::with_cases(512))]
440
441        /// The packed rows must agree with summing the columns of the set input bits.
442        #[test]
443        fn the_row_packing_agrees_with_the_column_definition(images: [u8; 8], byte: u8) {
444            let matrix = ByteMatrix::from_images(images);
445            prop_assert_eq!(matrix.apply(byte), apply_by_columns(images, byte));
446        }
447
448        /// Reading the columns back out must return the ones that went in.
449        #[test]
450        fn the_column_round_trip_is_the_identity(images: [u8; 8]) {
451            prop_assert_eq!(ByteMatrix::from_images(images).images(), images);
452        }
453
454        /// Composition must be the same map as applying one after the other.
455        #[test]
456        fn composition_applies_the_second_map_to_the_first_image(
457            outer: [u8; 8],
458            inner: [u8; 8],
459            byte: u8,
460        ) {
461            let (outer, inner) = (ByteMatrix::from_images(outer), ByteMatrix::from_images(inner));
462            prop_assert_eq!(outer.compose(inner).apply(byte), outer.apply(inner.apply(byte)));
463        }
464
465        /// The kernels must agree with the scalar routines they vectorize, at every length.
466        ///
467        /// Lengths past one register exercise the wide pass, the narrow pass and the tail.
468        #[test]
469        fn the_slice_kernels_agree_with_the_scalar_routines(
470            values in prop::collection::vec(any::<u8>(), 0..200),
471            factors in prop::collection::vec(any::<u8>(), 0..200),
472            images: [u8; 8],
473        ) {
474            let len = values.len().min(factors.len());
475            let (values, factors) = (&values[..len], &factors[..len]);
476
477            let mut products = values.to_vec();
478            mul_slice(&mut products, factors);
479            let expected: alloc::vec::Vec<u8> =
480                values.iter().zip(factors).map(|(&a, &b)| mul_bytes(a, b)).collect();
481            prop_assert_eq!(&products, &expected);
482
483            let mut inverses = values.to_vec();
484            invert_slice(&mut inverses);
485            let expected: alloc::vec::Vec<u8> = values.iter().map(|&a| invert_byte(a)).collect();
486            prop_assert_eq!(&inverses, &expected);
487
488            let matrix = ByteMatrix::from_images(images);
489            let mut mapped = values.to_vec();
490            matrix.apply_slice(&mut mapped);
491            let expected: alloc::vec::Vec<u8> =
492                values.iter().map(|&a| apply_by_columns(images, a)).collect();
493            prop_assert_eq!(&mapped, &expected);
494        }
495    }
496
497    #[test]
498    #[should_panic = "elementwise product lengths differ"]
499    fn the_product_kernel_rejects_mismatched_lengths() {
500        mul_slice(&mut [0, 0], &[0]);
501    }
502}