1use serde::{Deserialize, Serialize};
12
13use super::engine::ByteMatrix;
14use super::{Rijndael8b, mul_bytes};
15
16const ORBIT: usize = 8;
18
19const SQUARE: ByteMatrix = {
21 let mut images = [0u8; 8];
22 let mut b = 0;
23 while b < 8 {
24 images[b] = mul_bytes(1 << b, 1 << b);
25 b += 1;
26 }
27 ByteMatrix::from_images(images)
28};
29
30const FROBENIUS: [ByteMatrix; ORBIT] = {
34 let mut maps = [ByteMatrix::IDENTITY; ORBIT];
35 let mut k = 1;
36 while k < ORBIT {
37 maps[k] = SQUARE.compose(maps[k - 1]);
38 k += 1;
39 }
40 maps
41};
42
43impl Rijndael8b {
44 pub const fn frobenius_map(power_log: usize) -> ByteMatrix {
48 FROBENIUS[power_log % ORBIT]
49 }
50
51 pub fn frobenius_orbit(self) -> [Self; ORBIT] {
55 core::array::from_fn(|k| Self::from_byte(FROBENIUS[k].apply(self.to_byte())))
56 }
57}
58
59#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
70#[serde(transparent)]
71#[repr(transparent)]
72#[must_use]
73pub struct LinearizedPoly8b([Rijndael8b; ORBIT]);
74
75impl LinearizedPoly8b {
76 pub const fn new(coefficients: [Rijndael8b; ORBIT]) -> Self {
78 Self(coefficients)
79 }
80
81 pub const fn coefficients(&self) -> [Rijndael8b; ORBIT] {
83 self.0
84 }
85
86 pub fn eval(&self, x: Rijndael8b) -> Rijndael8b {
90 let orbit = x.frobenius_orbit();
91 self.0
92 .iter()
93 .zip(orbit)
94 .fold(Rijndael8b::from_byte(0), |acc, (&c, power)| acc + c * power)
95 }
96
97 pub const fn to_matrix(&self) -> ByteMatrix {
109 let mut images = [0u8; 8];
110 let mut b = 0;
111 while b < 8 {
112 let vector = 1u8 << b;
113 let mut sum = 0u8;
114 let mut j = 0;
115 while j < ORBIT {
116 sum ^= mul_bytes(self.0[j].to_byte(), FROBENIUS[j].apply(vector));
117 j += 1;
118 }
119 images[b] = sum;
120 b += 1;
121 }
122 ByteMatrix::from_images(images)
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use alloc::vec::Vec;
129
130 use p3_field::{Field, PackedValue, PrimeCharacteristicRing};
131 use proptest::prelude::*;
132
133 use super::{FROBENIUS, LinearizedPoly8b, ORBIT};
134 use crate::aes::mul_bytes;
135 use crate::{PackedRijndael8b, Rijndael8b};
136
137 fn repeated_square(mut byte: u8, count: usize) -> u8 {
143 for _ in 0..count {
144 byte = mul_bytes(byte, byte);
145 }
146 byte
147 }
148
149 #[test]
150 fn the_tabulated_orbit_matches_repeated_squaring() {
151 for (k, map) in FROBENIUS.iter().enumerate() {
153 for byte in 0..=u8::MAX {
154 assert_eq!(map.apply(byte), repeated_square(byte, k), "power {k}");
155 }
156 }
157 }
158
159 #[test]
160 fn the_orbit_closes_and_starts_at_the_identity() {
161 assert_eq!(FROBENIUS[0], crate::ByteMatrix::IDENTITY);
163 for byte in 0..=u8::MAX {
164 assert_eq!(repeated_square(byte, ORBIT), byte, "{byte:#x}");
165 }
166
167 let x = Rijndael8b::from_byte(0x53);
169 let orbit = x.frobenius_orbit();
170 assert_eq!(orbit[0], x);
171 for k in 1..ORBIT {
172 assert_eq!(orbit[k], orbit[k - 1].square(), "power {k}");
173 }
174 }
175
176 #[test]
177 fn the_frobenius_fixes_only_the_prime_subfield() {
178 let fixed: Vec<u8> = (0..=u8::MAX)
180 .filter(|&b| Rijndael8b::frobenius_map(1).apply(b) == b)
181 .collect();
182 assert_eq!(fixed, alloc::vec![0, 1]);
183 }
184
185 #[test]
186 fn the_square_root_is_the_seventh_power_of_squaring() {
187 for byte in 0..=u8::MAX {
189 let x = Rijndael8b::from_byte(byte);
190 let root = x
191 .try_sqrt()
192 .expect("every element of a binary field is a square");
193 assert_eq!(root.square(), x, "{byte:#x}");
194 assert_eq!(root.to_byte(), Rijndael8b::frobenius_map(7).apply(byte));
195 }
196 }
197
198 proptest! {
199 #![proptest_config(ProptestConfig::with_cases(512))]
200
201 #[test]
203 fn the_tabulated_linearized_map_matches_its_definition(coefficients: [u8; ORBIT], x: u8) {
204 let poly = LinearizedPoly8b::new(coefficients.map(Rijndael8b::from_byte));
205 let point = Rijndael8b::from_byte(x);
206 prop_assert_eq!(poly.to_matrix().apply(x), poly.eval(point).to_byte());
207 }
208
209 #[test]
211 fn a_linearized_polynomial_is_additive(coefficients: [u8; ORBIT], x: u8, y: u8) {
212 let poly = LinearizedPoly8b::new(coefficients.map(Rijndael8b::from_byte));
213 let (a, b) = (Rijndael8b::from_byte(x), Rijndael8b::from_byte(y));
214 prop_assert_eq!(poly.eval(a + b), poly.eval(a) + poly.eval(b));
215 }
216
217 #[test]
221 fn one_constant_coefficient_is_scaling(c: u8) {
222 let scalar = Rijndael8b::from_byte(c);
223 let mut coefficients = [Rijndael8b::ZERO; ORBIT];
224 coefficients[0] = scalar;
225 prop_assert_eq!(
226 LinearizedPoly8b::new(coefficients).to_matrix(),
227 scalar.scaling_matrix()
228 );
229 }
230
231 #[test]
233 fn the_packed_sweep_matches_the_scalar_map(
234 coefficients: [u8; ORBIT],
235 values in prop::collection::vec(any::<u8>(), 64),
236 power_log in 0usize..16,
237 ) {
238 let poly = LinearizedPoly8b::new(coefficients.map(Rijndael8b::from_byte));
239 let block: PackedRijndael8b<64> =
240 PackedValue::from_fn(|i| Rijndael8b::from_byte(values[i]));
241
242 let expected: PackedRijndael8b<64> =
243 PackedValue::from_fn(|i| poly.eval(Rijndael8b::from_byte(values[i])));
244 prop_assert_eq!(block.apply(poly.to_matrix()), expected);
245
246 let expected: PackedRijndael8b<64> = PackedValue::from_fn(|i| {
247 Rijndael8b::from_byte(repeated_square(values[i], power_log % ORBIT))
248 });
249 prop_assert_eq!(block.frobenius(power_log), expected);
250
251 prop_assert_eq!(block.exp_power_of_2(power_log), expected);
253 }
254 }
255
256 #[test]
257 fn a_single_unit_coefficient_is_one_map_of_the_orbit() {
258 for j in 0..ORBIT {
262 let mut coefficients = [Rijndael8b::ZERO; ORBIT];
263 coefficients[j] = Rijndael8b::ONE;
264 assert_eq!(
265 LinearizedPoly8b::new(coefficients).to_matrix(),
266 Rijndael8b::frobenius_map(j),
267 "term {j}"
268 );
269 }
270 }
271
272 #[test]
273 fn a_known_answer_with_coefficients_outside_the_prime_subfield() {
274 let poly = LinearizedPoly8b::new(
283 [0x02, 0x03, 0x00, 0x8d, 0x00, 0x00, 0x1f, 0x00].map(Rijndael8b::from_byte),
284 );
285 for (x, want) in [(0x01, 0x93), (0x53, 0xa2), (0xff, 0x34), (0x80, 0x30)] {
286 assert_eq!(
287 poly.eval(Rijndael8b::from_byte(x)).to_byte(),
288 want,
289 "{x:#04x}"
290 );
291 assert_eq!(poly.to_matrix().apply(x), want, "{x:#04x}");
292 }
293 }
294
295 #[test]
296 fn repeated_squaring_matches_the_reference_at_every_exponent() {
297 for byte in [0u8, 1, 2, 0x1b, 0x80, 0x53, u8::MAX] {
303 let x = Rijndael8b::from_byte(byte);
304 for power_log in 0..=(2 * ORBIT + 3) {
305 assert_eq!(
306 x.exp_power_of_2(power_log).to_byte(),
307 repeated_square(byte, power_log % ORBIT),
308 "{byte:#04x} at {power_log}"
309 );
310 }
311 assert_eq!(
312 x.exp_power_of_2(usize::MAX),
313 x.exp_power_of_2(usize::MAX % ORBIT),
314 "{byte:#04x} at the largest exponent"
315 );
316 }
317 }
318
319 #[test]
323 fn a_hand_checkable_linearized_polynomial() {
324 let one = Rijndael8b::ONE;
325 let zero = Rijndael8b::ZERO;
326 let poly = LinearizedPoly8b::new([one, one, zero, zero, zero, zero, zero, zero]);
327
328 assert_eq!(poly.eval(zero), zero);
330 assert_eq!(poly.eval(one), zero);
331
332 assert_eq!(poly.eval(Rijndael8b::from_byte(2)).to_byte(), 6);
334
335 assert_eq!(poly.to_matrix().apply(2), 6);
337 }
338
339 #[test]
340 fn the_coefficients_round_trip_through_serde() {
341 let poly = LinearizedPoly8b::new(
343 [0x02, 0x03, 0x00, 0x8d, 0x00, 0x00, 0x1f, 0x00].map(Rijndael8b::from_byte),
344 );
345 let encoded = serde_json::to_string(&poly).unwrap();
346 assert_eq!(
347 serde_json::from_str::<LinearizedPoly8b>(&encoded).unwrap(),
348 poly
349 );
350 assert_eq!(poly.coefficients()[3].to_byte(), 0x8d);
351 }
352}