use super::tables::{
apply_inverse_matrix, forward_sbox_mds, inverse_sbox_mds, ROWS, SBOXES, SBOXES_DEC,
};
use zeroize::{Zeroize, ZeroizeOnDrop};
type Column = [u8; ROWS];
const MAX_NB: usize = 8;
const ROUND_KEYS_LEN: usize = 19;
const ZERO_COLUMN: Column = [0u8; ROWS];
#[allow(dead_code)]
fn sub_bytes(state: &mut [Column]) {
for column in state.iter_mut() {
for (row, byte) in column.iter_mut().enumerate() {
*byte = SBOXES[row % 4][*byte as usize];
}
}
}
fn inv_sub_bytes(state: &mut [Column]) {
for column in state.iter_mut() {
for (row, byte) in column.iter_mut().enumerate() {
*byte = SBOXES_DEC[row % 4][*byte as usize];
}
}
}
#[allow(dead_code)]
fn shift_rows(state: &mut [Column]) {
let nb = state.len();
let mut shifted = [ZERO_COLUMN; MAX_NB];
for row in 0..ROWS {
let shift = row * nb / ROWS;
for col in 0..nb {
shifted[(col + shift) % nb][row] = state[col][row];
}
}
state.copy_from_slice(&shifted[..nb]);
}
fn inv_shift_rows(state: &mut [Column]) {
let nb = state.len();
let mut shifted = [ZERO_COLUMN; MAX_NB];
for row in 0..ROWS {
let shift = row * nb / ROWS;
for col in 0..nb {
shifted[col][row] = state[(col + shift) % nb][row];
}
}
state.copy_from_slice(&shifted[..nb]);
}
fn add_round_key(state: &mut [Column], key: &[Column]) {
for (s, k) in state.iter_mut().zip(key) {
let word = u64::from_le_bytes(*s).wrapping_add(u64::from_le_bytes(*k));
*s = word.to_le_bytes();
}
}
fn sub_round_key(state: &mut [Column], key: &[Column]) {
for (s, k) in state.iter_mut().zip(key) {
let word = u64::from_le_bytes(*s).wrapping_sub(u64::from_le_bytes(*k));
*s = word.to_le_bytes();
}
}
fn xor_round_key(state: &mut [Column], key: &[Column]) {
for (s, k) in state.iter_mut().zip(key) {
for (b, kb) in s.iter_mut().zip(k) {
*b ^= kb;
}
}
}
fn encipher_round(state: &mut [Column]) {
let nb = state.len();
debug_assert!(nb.is_power_of_two());
let nb_mask = nb - 1;
let mut result = [ZERO_COLUMN; MAX_NB];
for (out_col, out_word) in result[..nb].iter_mut().enumerate() {
let mut acc = 0u64;
#[allow(clippy::needless_range_loop)]
for row in 0..ROWS {
let shift = row * nb / ROWS;
let src_col = (out_col + nb - shift) & nb_mask;
let byte = state[src_col][row];
acc ^= forward_sbox_mds(row, byte);
}
*out_word = acc.to_le_bytes();
}
state.copy_from_slice(&result[..nb]);
}
#[allow(dead_code)]
fn decipher_round(state: &mut [Column]) {
apply_inverse_matrix(state);
inv_shift_rows(state);
inv_sub_bytes(state);
}
#[allow(dead_code)]
fn fused_inv_round(state: &mut [Column]) {
let nb = state.len();
debug_assert!(nb.is_power_of_two());
let nb_mask = nb - 1;
let mut result = [ZERO_COLUMN; MAX_NB];
for (out_col, out_word) in result[..nb].iter_mut().enumerate() {
let mut acc = 0u64;
#[allow(clippy::needless_range_loop)]
for row in 0..ROWS {
let shift = row * nb / ROWS;
let src_col = (out_col + shift) & nb_mask;
let byte = state[src_col][row];
acc ^= inverse_sbox_mds(row, byte);
}
*out_word = acc.to_le_bytes();
}
state.copy_from_slice(&result[..nb]);
}
fn encipher_round_n<const NB: usize>(state: &mut [Column; NB]) {
debug_assert!(NB.is_power_of_two());
let nb_mask = NB - 1;
let mut result = [ZERO_COLUMN; NB];
for (out_col, out_word) in result.iter_mut().enumerate() {
let mut acc = 0u64;
#[allow(clippy::needless_range_loop)]
for row in 0..ROWS {
let shift = row * NB / ROWS;
let src_col = (out_col + NB - shift) & nb_mask;
let byte = state[src_col][row];
acc ^= forward_sbox_mds(row, byte);
}
*out_word = acc.to_le_bytes();
}
*state = result;
}
fn fused_inv_round_n<const NB: usize>(state: &mut [Column; NB]) {
debug_assert!(NB.is_power_of_two());
let nb_mask = NB - 1;
let mut result = [ZERO_COLUMN; NB];
for (out_col, out_word) in result.iter_mut().enumerate() {
let mut acc = 0u64;
#[allow(clippy::needless_range_loop)]
for row in 0..ROWS {
let shift = row * NB / ROWS;
let src_col = (out_col + shift) & nb_mask;
let byte = state[src_col][row];
acc ^= inverse_sbox_mds(row, byte);
}
*out_word = acc.to_le_bytes();
}
*state = result;
}
fn transform_keys_for_decrypt(round_keys: &RoundKeys, nb: usize, nr: usize) -> RoundKeys {
let mut dec_keys = *round_keys;
for key in dec_keys.iter_mut().take(nr).skip(1) {
apply_inverse_matrix(&mut key[..nb]);
}
dec_keys
}
fn round_key_from(base: &[Column], tmp: &[Column]) -> [Column; MAX_NB] {
let nb = base.len();
let mut state = [ZERO_COLUMN; MAX_NB];
state[..nb].copy_from_slice(base);
add_round_key(&mut state[..nb], tmp);
encipher_round(&mut state[..nb]);
xor_round_key(&mut state[..nb], tmp);
encipher_round(&mut state[..nb]);
add_round_key(&mut state[..nb], tmp);
state
}
fn columns_from_bytes(bytes: &[u8], count: usize) -> [Column; MAX_NB] {
let mut out = [ZERO_COLUMN; MAX_NB];
for c in 0..count {
out[c].copy_from_slice(&bytes[c * ROWS..(c + 1) * ROWS]);
}
out
}
fn shift_left_words(state: &mut [Column]) {
for column in state.iter_mut() {
let word = u64::from_le_bytes(*column) << 1;
*column = word.to_le_bytes();
}
}
fn rotate_words_left(buf: &mut [Column], count: usize) {
if count == 0 {
return;
}
let first = buf[0];
for i in 1..count {
buf[i - 1] = buf[i];
}
buf[count - 1] = first;
}
fn rotate_bytes_left(buf: &mut [u8], shift: usize) {
let len = buf.len();
let mut rotated = [0u8; MAX_NB * ROWS];
for (i, slot) in rotated[..len].iter_mut().enumerate() {
*slot = buf[(i + shift) % len];
}
buf.copy_from_slice(&rotated[..len]);
}
fn key_expand_kt(key: &[u8], nb: usize, nk: usize) -> [Column; MAX_NB] {
let mut state = [ZERO_COLUMN; MAX_NB];
#[allow(clippy::cast_possible_truncation)] let tmv = (nb + nk + 1) as u64;
state[0] = tmv.to_le_bytes();
let k0 = columns_from_bytes(key, nb);
let k1 = if nk == nb {
k0
} else {
columns_from_bytes(&key[nb * ROWS..], nb)
};
add_round_key(&mut state[..nb], &k0[..nb]);
encipher_round(&mut state[..nb]);
xor_round_key(&mut state[..nb], &k1[..nb]);
encipher_round(&mut state[..nb]);
add_round_key(&mut state[..nb], &k0[..nb]);
encipher_round(&mut state[..nb]);
state
}
fn key_expand_even(
key: &[u8],
kt: &[Column; MAX_NB],
nb: usize,
nk: usize,
nr: usize,
round_keys: &mut [[Column; MAX_NB]; ROUND_KEYS_LEN],
) {
let mut initial_data = columns_from_bytes(key, nk);
let mut tmv = [ZERO_COLUMN; MAX_NB];
for column in &mut tmv[..nb] {
*column = 0x0001_0001_0001_0001u64.to_le_bytes();
}
let mut round = 0usize;
loop {
let kt_round = mod_add_columns(&kt[..nb], &tmv[..nb]);
let key_a = round_key_from(&initial_data[..nb], &kt_round[..nb]);
round_keys[round][..nb].copy_from_slice(&key_a[..nb]);
if round == nr {
break;
}
if nk != nb {
round += 2;
shift_left_words(&mut tmv[..nb]);
let kt_round = mod_add_columns(&kt[..nb], &tmv[..nb]);
let key_b = round_key_from(&initial_data[nb..nk], &kt_round[..nb]);
round_keys[round][..nb].copy_from_slice(&key_b[..nb]);
if round == nr {
break;
}
}
round += 2;
shift_left_words(&mut tmv[..nb]);
rotate_words_left(&mut initial_data[..nk], nk);
}
}
fn mod_add_columns(a: &[Column], b: &[Column]) -> [Column; MAX_NB] {
let mut out = [ZERO_COLUMN; MAX_NB];
for (o, (x, y)) in out.iter_mut().zip(a.iter().zip(b)) {
let word = u64::from_le_bytes(*x).wrapping_add(u64::from_le_bytes(*y));
*o = word.to_le_bytes();
}
out
}
fn key_expand_odd(round_keys: &mut [[Column; MAX_NB]; ROUND_KEYS_LEN], nb: usize, nr: usize) {
let mut i = 1;
while i < nr {
let previous = round_keys[i - 1];
round_keys[i][..nb].copy_from_slice(&previous[..nb]);
let mut bytes = [0u8; MAX_NB * ROWS];
let len = nb * ROWS;
for c in 0..nb {
bytes[c * ROWS..(c + 1) * ROWS].copy_from_slice(&round_keys[i][c]);
}
rotate_bytes_left(&mut bytes[..len], 2 * nb + 3);
for c in 0..nb {
round_keys[i][c].copy_from_slice(&bytes[c * ROWS..(c + 1) * ROWS]);
}
i += 2;
}
}
fn key_expand(key: &[u8], nb: usize, nk: usize, nr: usize) -> RoundKeys {
let kt = key_expand_kt(key, nb, nk);
let mut round_keys = [[ZERO_COLUMN; MAX_NB]; ROUND_KEYS_LEN];
key_expand_even(key, &kt, nb, nk, nr, &mut round_keys);
key_expand_odd(&mut round_keys, nb, nr);
round_keys
}
type RoundKeys = [[Column; MAX_NB]; ROUND_KEYS_LEN];
fn state_array_mut<const NB: usize>(full: &mut [Column; MAX_NB]) -> &mut [Column; NB] {
match (&mut full[..NB]).try_into() {
Ok(array) => array,
Err(_) => unreachable!("NB <= MAX_NB is guaranteed by every kalyna_variant! call site"),
}
}
#[cfg(not(feature = "small-tables"))]
macro_rules! unroll_rounds {
($round_fn:ident, $state:expr, $keys:expr, $nb:expr; $($idx:literal),+ $(,)?) => {
{
$(
$round_fn($state);
xor_round_key($state, &$keys[$idx][..$nb]);
)+
}
};
}
fn encrypt_with_schedule<const NB: usize, const NR: usize>(
round_keys: &RoundKeys,
plaintext: &[u8],
) -> [u8; MAX_NB * ROWS] {
const {
assert!(
NR == 10 || NR == 14 || NR == 18,
"NR must be one of the three round counts every kalyna_variant! call site uses"
);
};
let mut full_state = columns_from_bytes(plaintext, NB);
let state = state_array_mut::<NB>(&mut full_state);
add_round_key(state, &round_keys[0][..NB]);
#[cfg(not(feature = "small-tables"))]
match NR {
10 => unroll_rounds!(encipher_round_n, state, round_keys, NB; 1, 2, 3, 4, 5, 6, 7, 8, 9),
14 => {
unroll_rounds!(encipher_round_n, state, round_keys, NB; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
}
18 => {
unroll_rounds!(encipher_round_n, state, round_keys, NB; 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17);
}
_ => unreachable!("ruled out by the const assert above"),
}
#[cfg(feature = "small-tables")]
for round_key in &round_keys[1..NR] {
encipher_round_n(state);
xor_round_key(state, &round_key[..NB]);
}
encipher_round_n(state);
add_round_key(state, &round_keys[NR][..NB]);
let mut out = [0u8; MAX_NB * ROWS];
for c in 0..NB {
out[c * ROWS..(c + 1) * ROWS].copy_from_slice(&state[c]);
}
out
}
fn decrypt_with_schedule<const NB: usize, const NR: usize>(
round_keys: &RoundKeys,
dec_keys: &RoundKeys,
ciphertext: &[u8],
) -> [u8; MAX_NB * ROWS] {
const {
assert!(
NR == 10 || NR == 14 || NR == 18,
"NR must be one of the three round counts every kalyna_variant! call site uses"
);
};
let mut full_state = columns_from_bytes(ciphertext, NB);
let state = state_array_mut::<NB>(&mut full_state);
sub_round_key(state, &round_keys[NR][..NB]);
apply_inverse_matrix(state);
#[cfg(not(feature = "small-tables"))]
match NR {
10 => unroll_rounds!(fused_inv_round_n, state, dec_keys, NB; 9, 8, 7, 6, 5, 4, 3, 2, 1),
14 => {
unroll_rounds!(fused_inv_round_n, state, dec_keys, NB; 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);
}
18 => {
unroll_rounds!(fused_inv_round_n, state, dec_keys, NB; 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);
}
_ => unreachable!("ruled out by the const assert above"),
}
#[cfg(feature = "small-tables")]
for dec_key in dec_keys[1..NR].iter().rev() {
fused_inv_round_n(state);
xor_round_key(state, &dec_key[..NB]);
}
inv_shift_rows(state);
inv_sub_bytes(state);
sub_round_key(state, &round_keys[0][..NB]);
let mut out = [0u8; MAX_NB * ROWS];
for c in 0..NB {
out[c * ROWS..(c + 1) * ROWS].copy_from_slice(&state[c]);
}
out
}
fn encrypt_generic<const NB: usize, const NR: usize>(
key: &[u8],
plaintext: &[u8],
nk: usize,
nr: usize,
) -> [u8; MAX_NB * ROWS] {
let mut round_keys = key_expand(key, NB, nk, nr);
let out = encrypt_with_schedule::<NB, NR>(&round_keys, plaintext);
round_keys.zeroize();
out
}
fn decrypt_generic<const NB: usize, const NR: usize>(
key: &[u8],
ciphertext: &[u8],
nk: usize,
nr: usize,
) -> [u8; MAX_NB * ROWS] {
let mut round_keys = key_expand(key, NB, nk, nr);
let mut dec_keys = transform_keys_for_decrypt(&round_keys, NB, nr);
let out = decrypt_with_schedule::<NB, NR>(&round_keys, &dec_keys, ciphertext);
round_keys.zeroize();
dec_keys.zeroize();
out
}
macro_rules! kalyna_variant {
($name:ident, $expanded_name:ident, $key_bytes:literal, $block_bytes:literal, $nb:literal, $nk:literal, $nr:literal) => {
#[doc = concat!(
stringify!($block_bytes), "-byte block, ", stringify!($key_bytes),
"-byte key, ", stringify!($nr), " rounds."
)]
pub struct $name;
impl $name {
#[must_use]
pub fn encrypt(
key: &[u8; $key_bytes],
block: &[u8; $block_bytes],
) -> [u8; $block_bytes] {
let out = encrypt_generic::<$nb, $nr>(key, block, $nk, $nr);
let mut result = [0u8; $block_bytes];
result.copy_from_slice(&out[..$block_bytes]);
result
}
#[must_use]
pub fn decrypt(
key: &[u8; $key_bytes],
block: &[u8; $block_bytes],
) -> [u8; $block_bytes] {
let out = decrypt_generic::<$nb, $nr>(key, block, $nk, $nr);
let mut result = [0u8; $block_bytes];
result.copy_from_slice(&out[..$block_bytes]);
result
}
}
#[doc = concat!(
"Cached round-key schedule for [`", stringify!($name), "`] - `key_expand` runs once, ",
"in [`new`](Self::new), instead of once per [`encrypt`](Self::encrypt_block)/",
"[`decrypt`](Self::decrypt_block) call. Use this instead of the raw `", stringify!($name),
"::encrypt`/`decrypt` functions whenever multiple blocks are encrypted/decrypted under ",
"the same key - `docs/TASKS.md` D-28 stage 3: on this project's own measurements, the ",
"schedule was ~60-79% of ", stringify!($name), "'s single-call time, so reusing it ",
"across calls is the largest remaining lever against re-expanding it every time."
)]
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct $expanded_name {
round_keys: RoundKeys,
dec_keys: RoundKeys,
}
impl $expanded_name {
#[must_use]
pub fn new(key: &[u8; $key_bytes]) -> Self {
let round_keys = key_expand(key, $nb, $nk, $nr);
let dec_keys = transform_keys_for_decrypt(&round_keys, $nb, $nr);
Self {
round_keys,
dec_keys,
}
}
#[must_use]
pub fn encrypt_block(&self, block: &[u8; $block_bytes]) -> [u8; $block_bytes] {
let out = encrypt_with_schedule::<$nb, $nr>(&self.round_keys, block);
let mut result = [0u8; $block_bytes];
result.copy_from_slice(&out[..$block_bytes]);
result
}
#[must_use]
pub fn decrypt_block(&self, block: &[u8; $block_bytes]) -> [u8; $block_bytes] {
let out = decrypt_with_schedule::<$nb, $nr>(&self.round_keys, &self.dec_keys, block);
let mut result = [0u8; $block_bytes];
result.copy_from_slice(&out[..$block_bytes]);
result
}
}
};
}
kalyna_variant!(Kalyna128_128, Kalyna128_128ExpandedKey, 16, 16, 2, 2, 10);
kalyna_variant!(Kalyna128_256, Kalyna128_256ExpandedKey, 32, 16, 2, 4, 14);
kalyna_variant!(Kalyna256_256, Kalyna256_256ExpandedKey, 32, 32, 4, 4, 14);
kalyna_variant!(Kalyna256_512, Kalyna256_512ExpandedKey, 64, 32, 4, 8, 18);
kalyna_variant!(Kalyna512_512, Kalyna512_512ExpandedKey, 64, 64, 8, 8, 18);
#[cfg(test)]
mod fused_round_tests {
use super::{encipher_round, shift_rows, sub_bytes, Column, MAX_NB, ZERO_COLUMN};
use crate::hazmat::tables::apply_forward_matrix;
use proptest::prelude::*;
fn naive_encipher_round(state: &mut [Column]) {
sub_bytes(state);
shift_rows(state);
apply_forward_matrix(state);
}
fn arb_state(nb: usize) -> impl Strategy<Value = Vec<Column>> {
proptest::collection::vec(proptest::array::uniform8(any::<u8>()), nb)
}
proptest! {
#[test]
fn fused_encipher_round_matches_naive_nb2(state in arb_state(2)) {
let mut fused = [ZERO_COLUMN; MAX_NB];
fused[..2].copy_from_slice(&state);
let mut naive = fused;
encipher_round(&mut fused[..2]);
naive_encipher_round(&mut naive[..2]);
prop_assert_eq!(fused, naive);
}
#[test]
fn fused_encipher_round_matches_naive_nb4(state in arb_state(4)) {
let mut fused = [ZERO_COLUMN; MAX_NB];
fused[..4].copy_from_slice(&state);
let mut naive = fused;
encipher_round(&mut fused[..4]);
naive_encipher_round(&mut naive[..4]);
prop_assert_eq!(fused, naive);
}
#[test]
fn fused_encipher_round_matches_naive_nb8(state in arb_state(8)) {
let mut fused = [ZERO_COLUMN; MAX_NB];
fused[..8].copy_from_slice(&state);
let mut naive = fused;
encipher_round(&mut fused[..8]);
naive_encipher_round(&mut naive[..8]);
prop_assert_eq!(fused, naive);
}
}
}
#[cfg(test)]
mod const_round_tests {
use super::{encipher_round, encipher_round_n, fused_inv_round, fused_inv_round_n, Column};
use proptest::prelude::*;
fn arb_state(nb: usize) -> impl Strategy<Value = Vec<Column>> {
proptest::collection::vec(proptest::array::uniform8(any::<u8>()), nb)
}
macro_rules! const_matches_dyn_test {
($enc_test:ident, $dec_test:ident, $nb:literal) => {
proptest! {
#[test]
fn $enc_test(state in arb_state($nb)) {
let mut dynamic = state.clone();
let mut constant: [Column; $nb] = state.try_into().unwrap();
encipher_round(&mut dynamic[..]);
encipher_round_n(&mut constant);
prop_assert_eq!(dynamic.as_slice(), constant.as_slice());
}
#[test]
fn $dec_test(state in arb_state($nb)) {
let mut dynamic = state.clone();
let mut constant: [Column; $nb] = state.try_into().unwrap();
fused_inv_round(&mut dynamic[..]);
fused_inv_round_n(&mut constant);
prop_assert_eq!(dynamic.as_slice(), constant.as_slice());
}
}
};
}
const_matches_dyn_test!(
encipher_round_n_matches_dyn_nb2,
fused_inv_round_n_matches_dyn_nb2,
2
);
const_matches_dyn_test!(
encipher_round_n_matches_dyn_nb4,
fused_inv_round_n_matches_dyn_nb4,
4
);
const_matches_dyn_test!(
encipher_round_n_matches_dyn_nb8,
fused_inv_round_n_matches_dyn_nb8,
8
);
}
#[cfg(test)]
mod decrypt_fusion_tests {
use super::{
columns_from_bytes, decipher_round, decrypt_with_schedule, sub_round_key,
transform_keys_for_decrypt, xor_round_key, Column, MAX_NB, ROUND_KEYS_LEN, ROWS,
ZERO_COLUMN,
};
use proptest::prelude::*;
type RoundKeys = [[Column; MAX_NB]; ROUND_KEYS_LEN];
fn naive_decrypt_with_schedule(
round_keys: &RoundKeys,
ciphertext: &[u8],
nb: usize,
nr: usize,
) -> [u8; MAX_NB * ROWS] {
let mut state = columns_from_bytes(ciphertext, nb);
sub_round_key(&mut state[..nb], &round_keys[nr][..nb]);
for round_key in round_keys[1..nr].iter().rev() {
decipher_round(&mut state[..nb]);
xor_round_key(&mut state[..nb], &round_key[..nb]);
}
decipher_round(&mut state[..nb]);
sub_round_key(&mut state[..nb], &round_keys[0][..nb]);
let mut out = [0u8; MAX_NB * ROWS];
for c in 0..nb {
out[c * ROWS..(c + 1) * ROWS].copy_from_slice(&state[c]);
}
out
}
fn arb_round_key_bytes(nb: usize, nr: usize) -> impl Strategy<Value = Vec<u8>> {
proptest::collection::vec(any::<u8>(), (nr + 1) * nb * ROWS)
}
fn arb_block(nb: usize) -> impl Strategy<Value = Vec<u8>> {
proptest::collection::vec(any::<u8>(), nb * ROWS)
}
macro_rules! fusion_matches_naive_test {
($test_name:ident, $nb:literal, $nr:literal) => {
proptest! {
#[test]
fn $test_name(
key_bytes in arb_round_key_bytes($nb, $nr),
ciphertext in arb_block($nb),
) {
let mut round_keys: RoundKeys = [[ZERO_COLUMN; MAX_NB]; ROUND_KEYS_LEN];
for i in 0..=$nr {
let start = i * $nb * ROWS;
round_keys[i] = columns_from_bytes(&key_bytes[start..start + $nb * ROWS], $nb);
}
let dec_keys = transform_keys_for_decrypt(&round_keys, $nb, $nr);
let naive = naive_decrypt_with_schedule(&round_keys, &ciphertext, $nb, $nr);
let fused = decrypt_with_schedule::<$nb, $nr>(&round_keys, &dec_keys, &ciphertext);
prop_assert_eq!(naive, fused);
}
}
};
}
fusion_matches_naive_test!(decrypt_fusion_matches_naive_nb2_nr10, 2, 10);
fusion_matches_naive_test!(decrypt_fusion_matches_naive_nb2_nr14, 2, 14);
fusion_matches_naive_test!(decrypt_fusion_matches_naive_nb4_nr14, 4, 14);
fusion_matches_naive_test!(decrypt_fusion_matches_naive_nb4_nr18, 4, 18);
fusion_matches_naive_test!(decrypt_fusion_matches_naive_nb8_nr18, 8, 18);
}
#[cfg(test)]
mod encrypt_fusion_tests {
use super::{
add_round_key, columns_from_bytes, encipher_round, encrypt_with_schedule, xor_round_key,
Column, MAX_NB, ROUND_KEYS_LEN, ROWS, ZERO_COLUMN,
};
use proptest::prelude::*;
type RoundKeys = [[Column; MAX_NB]; ROUND_KEYS_LEN];
fn naive_encrypt_with_schedule(
round_keys: &RoundKeys,
plaintext: &[u8],
nb: usize,
nr: usize,
) -> [u8; MAX_NB * ROWS] {
let mut state = columns_from_bytes(plaintext, nb);
add_round_key(&mut state[..nb], &round_keys[0][..nb]);
for round_key in &round_keys[1..nr] {
encipher_round(&mut state[..nb]);
xor_round_key(&mut state[..nb], &round_key[..nb]);
}
encipher_round(&mut state[..nb]);
add_round_key(&mut state[..nb], &round_keys[nr][..nb]);
let mut out = [0u8; MAX_NB * ROWS];
for c in 0..nb {
out[c * ROWS..(c + 1) * ROWS].copy_from_slice(&state[c]);
}
out
}
fn arb_round_key_bytes(nb: usize, nr: usize) -> impl Strategy<Value = Vec<u8>> {
proptest::collection::vec(any::<u8>(), (nr + 1) * nb * ROWS)
}
fn arb_block(nb: usize) -> impl Strategy<Value = Vec<u8>> {
proptest::collection::vec(any::<u8>(), nb * ROWS)
}
macro_rules! fusion_matches_naive_test {
($test_name:ident, $nb:literal, $nr:literal) => {
proptest! {
#[test]
fn $test_name(
key_bytes in arb_round_key_bytes($nb, $nr),
plaintext in arb_block($nb),
) {
let mut round_keys: RoundKeys = [[ZERO_COLUMN; MAX_NB]; ROUND_KEYS_LEN];
for i in 0..=$nr {
let start = i * $nb * ROWS;
round_keys[i] = columns_from_bytes(&key_bytes[start..start + $nb * ROWS], $nb);
}
let naive = naive_encrypt_with_schedule(&round_keys, &plaintext, $nb, $nr);
let unrolled = encrypt_with_schedule::<$nb, $nr>(&round_keys, &plaintext);
prop_assert_eq!(naive, unrolled);
}
}
};
}
fusion_matches_naive_test!(encrypt_fusion_matches_naive_nb2_nr10, 2, 10);
fusion_matches_naive_test!(encrypt_fusion_matches_naive_nb2_nr14, 2, 14);
fusion_matches_naive_test!(encrypt_fusion_matches_naive_nb4_nr14, 4, 14);
fusion_matches_naive_test!(encrypt_fusion_matches_naive_nb4_nr18, 4, 18);
fusion_matches_naive_test!(encrypt_fusion_matches_naive_nb8_nr18, 8, 18);
}