use serde::{Deserialize, Serialize};
use super::engine::ByteMatrix;
use super::{Rijndael8b, mul_bytes};
const ORBIT: usize = 8;
const SQUARE: ByteMatrix = {
let mut images = [0u8; 8];
let mut b = 0;
while b < 8 {
images[b] = mul_bytes(1 << b, 1 << b);
b += 1;
}
ByteMatrix::from_images(images)
};
const FROBENIUS: [ByteMatrix; ORBIT] = {
let mut maps = [ByteMatrix::IDENTITY; ORBIT];
let mut k = 1;
while k < ORBIT {
maps[k] = SQUARE.compose(maps[k - 1]);
k += 1;
}
maps
};
impl Rijndael8b {
pub const fn frobenius_map(power_log: usize) -> ByteMatrix {
FROBENIUS[power_log % ORBIT]
}
pub fn frobenius_orbit(self) -> [Self; ORBIT] {
core::array::from_fn(|k| Self::from_byte(FROBENIUS[k].apply(self.to_byte())))
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[repr(transparent)]
#[must_use]
pub struct LinearizedPoly8b([Rijndael8b; ORBIT]);
impl LinearizedPoly8b {
pub const fn new(coefficients: [Rijndael8b; ORBIT]) -> Self {
Self(coefficients)
}
pub const fn coefficients(&self) -> [Rijndael8b; ORBIT] {
self.0
}
pub fn eval(&self, x: Rijndael8b) -> Rijndael8b {
let orbit = x.frobenius_orbit();
self.0
.iter()
.zip(orbit)
.fold(Rijndael8b::from_byte(0), |acc, (&c, power)| acc + c * power)
}
pub const fn to_matrix(&self) -> ByteMatrix {
let mut images = [0u8; 8];
let mut b = 0;
while b < 8 {
let vector = 1u8 << b;
let mut sum = 0u8;
let mut j = 0;
while j < ORBIT {
sum ^= mul_bytes(self.0[j].to_byte(), FROBENIUS[j].apply(vector));
j += 1;
}
images[b] = sum;
b += 1;
}
ByteMatrix::from_images(images)
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use p3_field::{Field, PackedValue, PrimeCharacteristicRing};
use proptest::prelude::*;
use super::{FROBENIUS, LinearizedPoly8b, ORBIT};
use crate::aes::mul_bytes;
use crate::{PackedRijndael8b, Rijndael8b};
fn repeated_square(mut byte: u8, count: usize) -> u8 {
for _ in 0..count {
byte = mul_bytes(byte, byte);
}
byte
}
#[test]
fn the_tabulated_orbit_matches_repeated_squaring() {
for (k, map) in FROBENIUS.iter().enumerate() {
for byte in 0..=u8::MAX {
assert_eq!(map.apply(byte), repeated_square(byte, k), "power {k}");
}
}
}
#[test]
fn the_orbit_closes_and_starts_at_the_identity() {
assert_eq!(FROBENIUS[0], crate::ByteMatrix::IDENTITY);
for byte in 0..=u8::MAX {
assert_eq!(repeated_square(byte, ORBIT), byte, "{byte:#x}");
}
let x = Rijndael8b::from_byte(0x53);
let orbit = x.frobenius_orbit();
assert_eq!(orbit[0], x);
for k in 1..ORBIT {
assert_eq!(orbit[k], orbit[k - 1].square(), "power {k}");
}
}
#[test]
fn the_frobenius_fixes_only_the_prime_subfield() {
let fixed: Vec<u8> = (0..=u8::MAX)
.filter(|&b| Rijndael8b::frobenius_map(1).apply(b) == b)
.collect();
assert_eq!(fixed, alloc::vec![0, 1]);
}
#[test]
fn the_square_root_is_the_seventh_power_of_squaring() {
for byte in 0..=u8::MAX {
let x = Rijndael8b::from_byte(byte);
let root = x
.try_sqrt()
.expect("every element of a binary field is a square");
assert_eq!(root.square(), x, "{byte:#x}");
assert_eq!(root.to_byte(), Rijndael8b::frobenius_map(7).apply(byte));
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn the_tabulated_linearized_map_matches_its_definition(coefficients: [u8; ORBIT], x: u8) {
let poly = LinearizedPoly8b::new(coefficients.map(Rijndael8b::from_byte));
let point = Rijndael8b::from_byte(x);
prop_assert_eq!(poly.to_matrix().apply(x), poly.eval(point).to_byte());
}
#[test]
fn a_linearized_polynomial_is_additive(coefficients: [u8; ORBIT], x: u8, y: u8) {
let poly = LinearizedPoly8b::new(coefficients.map(Rijndael8b::from_byte));
let (a, b) = (Rijndael8b::from_byte(x), Rijndael8b::from_byte(y));
prop_assert_eq!(poly.eval(a + b), poly.eval(a) + poly.eval(b));
}
#[test]
fn one_constant_coefficient_is_scaling(c: u8) {
let scalar = Rijndael8b::from_byte(c);
let mut coefficients = [Rijndael8b::ZERO; ORBIT];
coefficients[0] = scalar;
prop_assert_eq!(
LinearizedPoly8b::new(coefficients).to_matrix(),
scalar.scaling_matrix()
);
}
#[test]
fn the_packed_sweep_matches_the_scalar_map(
coefficients: [u8; ORBIT],
values in prop::collection::vec(any::<u8>(), 64),
power_log in 0usize..16,
) {
let poly = LinearizedPoly8b::new(coefficients.map(Rijndael8b::from_byte));
let block: PackedRijndael8b<64> =
PackedValue::from_fn(|i| Rijndael8b::from_byte(values[i]));
let expected: PackedRijndael8b<64> =
PackedValue::from_fn(|i| poly.eval(Rijndael8b::from_byte(values[i])));
prop_assert_eq!(block.apply(poly.to_matrix()), expected);
let expected: PackedRijndael8b<64> = PackedValue::from_fn(|i| {
Rijndael8b::from_byte(repeated_square(values[i], power_log % ORBIT))
});
prop_assert_eq!(block.frobenius(power_log), expected);
prop_assert_eq!(block.exp_power_of_2(power_log), expected);
}
}
#[test]
fn a_single_unit_coefficient_is_one_map_of_the_orbit() {
for j in 0..ORBIT {
let mut coefficients = [Rijndael8b::ZERO; ORBIT];
coefficients[j] = Rijndael8b::ONE;
assert_eq!(
LinearizedPoly8b::new(coefficients).to_matrix(),
Rijndael8b::frobenius_map(j),
"term {j}"
);
}
}
#[test]
fn a_known_answer_with_coefficients_outside_the_prime_subfield() {
let poly = LinearizedPoly8b::new(
[0x02, 0x03, 0x00, 0x8d, 0x00, 0x00, 0x1f, 0x00].map(Rijndael8b::from_byte),
);
for (x, want) in [(0x01, 0x93), (0x53, 0xa2), (0xff, 0x34), (0x80, 0x30)] {
assert_eq!(
poly.eval(Rijndael8b::from_byte(x)).to_byte(),
want,
"{x:#04x}"
);
assert_eq!(poly.to_matrix().apply(x), want, "{x:#04x}");
}
}
#[test]
fn repeated_squaring_matches_the_reference_at_every_exponent() {
for byte in [0u8, 1, 2, 0x1b, 0x80, 0x53, u8::MAX] {
let x = Rijndael8b::from_byte(byte);
for power_log in 0..=(2 * ORBIT + 3) {
assert_eq!(
x.exp_power_of_2(power_log).to_byte(),
repeated_square(byte, power_log % ORBIT),
"{byte:#04x} at {power_log}"
);
}
assert_eq!(
x.exp_power_of_2(usize::MAX),
x.exp_power_of_2(usize::MAX % ORBIT),
"{byte:#04x} at the largest exponent"
);
}
}
#[test]
fn a_hand_checkable_linearized_polynomial() {
let one = Rijndael8b::ONE;
let zero = Rijndael8b::ZERO;
let poly = LinearizedPoly8b::new([one, one, zero, zero, zero, zero, zero, zero]);
assert_eq!(poly.eval(zero), zero);
assert_eq!(poly.eval(one), zero);
assert_eq!(poly.eval(Rijndael8b::from_byte(2)).to_byte(), 6);
assert_eq!(poly.to_matrix().apply(2), 6);
}
#[test]
fn the_coefficients_round_trip_through_serde() {
let poly = LinearizedPoly8b::new(
[0x02, 0x03, 0x00, 0x8d, 0x00, 0x00, 0x1f, 0x00].map(Rijndael8b::from_byte),
);
let encoded = serde_json::to_string(&poly).unwrap();
assert_eq!(
serde_json::from_str::<LinearizedPoly8b>(&encoded).unwrap(),
poly
);
assert_eq!(poly.coefficients()[3].to_byte(), 0x8d);
}
}