#![deny(unsafe_code)]
pub(crate) fn skinny_128_384_plus_enc(block: &mut [u8; 16], userkey: &[u8; 48]) {
let mut state = [[0u8; 4]; 4];
let mut key_cells = [[[0u8; 4]; 4]; 3];
for (i, &byte) in block.iter().enumerate() {
let r = i >> 2;
let c = i & 3;
state[r][c] = byte;
key_cells[0][r][c] = userkey[i];
key_cells[1][r][c] = userkey[i + 16];
key_cells[2][r][c] = userkey[i + 32];
}
for rnd in 0..40 {
sub_cell8(&mut state);
add_constants(&mut state, rnd);
add_key(&mut state, &mut key_cells);
shift_rows(&mut state);
mix_column(&mut state);
}
for (i, byte) in block.iter_mut().enumerate() {
let r = i >> 2;
let c = i & 3;
*byte = state[r][c];
}
}
#[cfg(test)]
const SBOX_8: [u8; 256] = [
0x65, 0x4C, 0x6A, 0x42, 0x4B, 0x63, 0x43, 0x6B, 0x55, 0x75, 0x5A, 0x7A, 0x53, 0x73, 0x5B, 0x7B,
0x35, 0x8C, 0x3A, 0x81, 0x89, 0x33, 0x80, 0x3B, 0x95, 0x25, 0x98, 0x2A, 0x90, 0x23, 0x99, 0x2B,
0xE5, 0xCC, 0xE8, 0xC1, 0xC9, 0xE0, 0xC0, 0xE9, 0xD5, 0xF5, 0xD8, 0xF8, 0xD0, 0xF0, 0xD9, 0xF9,
0xA5, 0x1C, 0xA8, 0x12, 0x1B, 0xA0, 0x13, 0xA9, 0x05, 0xB5, 0x0A, 0xB8, 0x03, 0xB0, 0x0B, 0xB9,
0x32, 0x88, 0x3C, 0x85, 0x8D, 0x34, 0x84, 0x3D, 0x91, 0x22, 0x9C, 0x2C, 0x94, 0x24, 0x9D, 0x2D,
0x62, 0x4A, 0x6C, 0x45, 0x4D, 0x64, 0x44, 0x6D, 0x52, 0x72, 0x5C, 0x7C, 0x54, 0x74, 0x5D, 0x7D,
0xA1, 0x1A, 0xAC, 0x15, 0x1D, 0xA4, 0x14, 0xAD, 0x02, 0xB1, 0x0C, 0xBC, 0x04, 0xB4, 0x0D, 0xBD,
0xE1, 0xC8, 0xEC, 0xC5, 0xCD, 0xE4, 0xC4, 0xED, 0xD1, 0xF1, 0xDC, 0xFC, 0xD4, 0xF4, 0xDD, 0xFD,
0x36, 0x8E, 0x38, 0x82, 0x8B, 0x30, 0x83, 0x39, 0x96, 0x26, 0x9A, 0x28, 0x93, 0x20, 0x9B, 0x29,
0x66, 0x4E, 0x68, 0x41, 0x49, 0x60, 0x40, 0x69, 0x56, 0x76, 0x58, 0x78, 0x50, 0x70, 0x59, 0x79,
0xA6, 0x1E, 0xAA, 0x11, 0x19, 0xA3, 0x10, 0xAB, 0x06, 0xB6, 0x08, 0xBA, 0x00, 0xB3, 0x09, 0xBB,
0xE6, 0xCE, 0xEA, 0xC2, 0xCB, 0xE3, 0xC3, 0xEB, 0xD6, 0xF6, 0xDA, 0xFA, 0xD3, 0xF3, 0xDB, 0xFB,
0x31, 0x8A, 0x3E, 0x86, 0x8F, 0x37, 0x87, 0x3F, 0x92, 0x21, 0x9E, 0x2E, 0x97, 0x27, 0x9F, 0x2F,
0x61, 0x48, 0x6E, 0x46, 0x4F, 0x67, 0x47, 0x6F, 0x51, 0x71, 0x5E, 0x7E, 0x57, 0x77, 0x5F, 0x7F,
0xA2, 0x18, 0xAE, 0x16, 0x1F, 0xA7, 0x17, 0xAF, 0x01, 0xB2, 0x0E, 0xBE, 0x07, 0xB7, 0x0F, 0xBF,
0xE2, 0xCA, 0xEE, 0xC6, 0xCF, 0xE7, 0xC7, 0xEF, 0xD2, 0xF2, 0xDE, 0xFE, 0xD7, 0xF7, 0xDF, 0xFF,
];
const P: [u8; 16] = [0, 1, 2, 3, 7, 4, 5, 6, 10, 11, 8, 9, 13, 14, 15, 12];
const TWEAKEY_P: [u8; 16] = [9, 15, 8, 13, 10, 14, 12, 11, 0, 1, 2, 3, 4, 5, 6, 7];
const RC: [u8; 40] = [
0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3E, 0x3D, 0x3B, 0x37, 0x2F, 0x1E, 0x3C, 0x39, 0x33, 0x27, 0x0E,
0x1D, 0x3A, 0x35, 0x2B, 0x16, 0x2C, 0x18, 0x30, 0x21, 0x02, 0x05, 0x0B, 0x17, 0x2E, 0x1C, 0x38,
0x31, 0x23, 0x06, 0x0D, 0x1B, 0x36, 0x2D, 0x1A,
];
fn sub_cell8(state: &mut [[u8; 4]; 4]) {
let planes = cells_to_bitplanes(state);
let planes = sbox8_bitsliced(planes);
bitplanes_to_cells(&planes, state);
}
fn cells_to_bitplanes(state: &[[u8; 4]; 4]) -> [u16; 8] {
let mut planes = [0u16; 8];
for (i, byte) in state.iter().flatten().enumerate() {
for (b, plane) in planes.iter_mut().enumerate() {
*plane |= u16::from((byte >> b) & 1) << i;
}
}
planes
}
fn bitplanes_to_cells(planes: &[u16; 8], state: &mut [[u8; 4]; 4]) {
for (i, byte) in state.iter_mut().flatten().enumerate() {
let mut v = 0u8;
for (b, &plane) in planes.iter().enumerate() {
v |= (((plane >> i) & 1) as u8) << b;
}
*byte = v;
}
}
#[allow(clippy::many_single_char_names)]
fn sbox8_bitsliced(planes: [u16; 8]) -> [u16; 8] {
let [b0, b1, b2, b3, b4, b5, b6, b7] = planes;
let mut y0 = !0u16;
y0 ^= b0;
y0 ^= b1;
y0 ^= b0 & b1;
y0 ^= b0 & b2;
y0 ^= b1 & b2;
y0 ^= b0 & b1 & b2;
y0 ^= b0 & b3;
y0 ^= b0 & b1 & b3;
y0 ^= b0 & b2 & b3;
y0 ^= b0 & b1 & b2 & b3;
y0 ^= b0 & b1 & b4;
y0 ^= b1 & b2 & b4;
y0 ^= b0 & b1 & b3 & b4;
y0 ^= b2 & b3 & b4;
y0 ^= b0 & b2 & b3 & b4;
y0 ^= b0 & b1 & b2 & b3 & b4;
y0 ^= b0 & b1 & b5;
y0 ^= b0 & b2 & b5;
y0 ^= b1 & b2 & b5;
y0 ^= b0 & b1 & b2 & b5;
y0 ^= b0 & b1 & b3 & b5;
y0 ^= b2 & b3 & b5;
y0 ^= b0 & b2 & b3 & b5;
y0 ^= b0 & b1 & b2 & b3 & b5;
y0 ^= b6;
y0 ^= b0 & b6;
y0 ^= b1 & b6;
y0 ^= b2 & b6;
y0 ^= b3 & b6;
y0 ^= b1 & b3 & b6;
y0 ^= b0 & b1 & b3 & b6;
y0 ^= b0 & b2 & b3 & b6;
y0 ^= b1 & b2 & b3 & b6;
y0 ^= b0 & b1 & b2 & b3 & b6;
y0 ^= b0 & b1 & b4 & b6;
y0 ^= b1 & b2 & b4 & b6;
y0 ^= b3 & b4 & b6;
y0 ^= b0 & b3 & b4 & b6;
y0 ^= b1 & b3 & b4 & b6;
y0 ^= b1 & b2 & b3 & b4 & b6;
y0 ^= b5 & b6;
y0 ^= b0 & b5 & b6;
y0 ^= b1 & b5 & b6;
y0 ^= b2 & b5 & b6;
y0 ^= b0 & b3 & b5 & b6;
y0 ^= b2 & b3 & b5 & b6;
y0 ^= b7;
y0 ^= b0 & b7;
y0 ^= b1 & b7;
y0 ^= b0 & b1 & b7;
y0 ^= b2 & b7;
y0 ^= b1 & b2 & b7;
y0 ^= b0 & b3 & b7;
y0 ^= b0 & b1 & b3 & b7;
y0 ^= b0 & b2 & b3 & b7;
y0 ^= b0 & b1 & b2 & b3 & b7;
y0 ^= b0 & b1 & b6 & b7;
y0 ^= b1 & b2 & b6 & b7;
y0 ^= b0 & b1 & b3 & b6 & b7;
y0 ^= b2 & b3 & b6 & b7;
y0 ^= b0 & b2 & b3 & b6 & b7;
y0 ^= b0 & b1 & b2 & b3 & b6 & b7;
let mut y1 = 0u16;
y1 ^= b1;
y1 ^= b2;
y1 ^= b1 & b2;
y1 ^= b0 & b1 & b4;
y1 ^= b2 & b4;
y1 ^= b0 & b2 & b4;
y1 ^= b0 & b1 & b2 & b4;
y1 ^= b1 & b3 & b4;
y1 ^= b1 & b2 & b3 & b4;
y1 ^= b1 & b5;
y1 ^= b2 & b5;
y1 ^= b1 & b2 & b5;
y1 ^= b6;
y1 ^= b0 & b6;
y1 ^= b0 & b1 & b6;
y1 ^= b0 & b2 & b6;
y1 ^= b0 & b1 & b2 & b6;
y1 ^= b3 & b6;
y1 ^= b1 & b3 & b6;
y1 ^= b2 & b3 & b6;
y1 ^= b1 & b2 & b3 & b6;
y1 ^= b0 & b4 & b6;
y1 ^= b2 & b4 & b6;
y1 ^= b3 & b4 & b6;
y1 ^= b2 & b3 & b4 & b6;
y1 ^= b5 & b6;
y1 ^= b7;
y1 ^= b0 & b1 & b7;
y1 ^= b2 & b7;
y1 ^= b0 & b2 & b7;
y1 ^= b0 & b1 & b2 & b7;
y1 ^= b1 & b3 & b7;
y1 ^= b1 & b2 & b3 & b7;
y1 ^= b0 & b1 & b6 & b7;
y1 ^= b2 & b6 & b7;
y1 ^= b0 & b2 & b6 & b7;
y1 ^= b0 & b1 & b2 & b6 & b7;
y1 ^= b1 & b3 & b6 & b7;
y1 ^= b1 & b2 & b3 & b6 & b7;
let mut y2 = !0u16;
y2 ^= b1;
y2 ^= b2;
y2 ^= b1 & b2;
y2 ^= b6;
let mut y3 = 0u16;
y3 ^= b0;
y3 ^= b1;
y3 ^= b2;
y3 ^= b0 & b3;
y3 ^= b2 & b3;
let mut y4 = 0u16;
y4 ^= b3;
y4 ^= b4;
y4 ^= b0 & b4;
y4 ^= b2 & b4;
y4 ^= b3 & b4;
y4 ^= b2 & b3 & b4;
y4 ^= b4 & b5;
y4 ^= b6;
y4 ^= b0 & b6;
y4 ^= b2 & b6;
y4 ^= b3 & b6;
y4 ^= b2 & b3 & b6;
y4 ^= b5 & b6;
y4 ^= b7;
y4 ^= b0 & b7;
y4 ^= b2 & b7;
y4 ^= b3 & b7;
y4 ^= b2 & b3 & b7;
y4 ^= b5 & b7;
y4 ^= b6 & b7;
y4 ^= b0 & b6 & b7;
y4 ^= b2 & b6 & b7;
y4 ^= b3 & b6 & b7;
y4 ^= b2 & b3 & b6 & b7;
y4 ^= b5 & b6 & b7;
let mut y5 = !0u16;
y5 ^= b0;
y5 ^= b2;
y5 ^= b3;
y5 ^= b2 & b3;
let mut y6 = !0u16;
y6 ^= b4;
y6 ^= b6;
y6 ^= b7;
y6 ^= b6 & b7;
let mut y7 = 0u16;
y7 ^= b0 & b4;
y7 ^= b2 & b4;
y7 ^= b3 & b4;
y7 ^= b2 & b3 & b4;
y7 ^= b5;
y7 ^= b0 & b6;
y7 ^= b2 & b6;
y7 ^= b3 & b6;
y7 ^= b2 & b3 & b6;
y7 ^= b0 & b7;
y7 ^= b2 & b7;
y7 ^= b3 & b7;
y7 ^= b2 & b3 & b7;
y7 ^= b0 & b6 & b7;
y7 ^= b2 & b6 & b7;
y7 ^= b3 & b6 & b7;
y7 ^= b2 & b3 & b6 & b7;
[y0, y1, y2, y3, y4, y5, y6, y7]
}
fn add_constants(state: &mut [[u8; 4]; 4], r: usize) {
state[0][0] ^= RC[r] & 0xF;
state[1][0] ^= (RC[r] >> 4) & 0x3;
state[2][0] ^= 0x2;
}
fn add_key(state: &mut [[u8; 4]; 4], key_cells: &mut [[[u8; 4]; 4]; 3]) {
let mut key_cells_tmp = [[[0u8; 4]; 4]; 3];
for i in 0..=1 {
for (j, cell) in state[i].iter_mut().enumerate() {
*cell ^= key_cells[0][i][j] ^ key_cells[1][i][j] ^ key_cells[2][i][j];
}
}
for (k, tmp_plane) in key_cells_tmp.iter_mut().enumerate() {
for i in 0..4 {
for j in 0..4 {
let pos = TWEAKEY_P[j + 4 * i];
tmp_plane[i][j] = key_cells[k][usize::from(pos >> 2)][usize::from(pos & 3)];
}
}
}
for (k, plane) in key_cells_tmp.iter_mut().enumerate() {
for row in &mut plane[..2] {
for cell in row.iter_mut() {
let v = *cell;
*cell = if k == 1 {
((v << 1) & 0xFE) ^ ((v >> 7) & 0x01) ^ ((v >> 5) & 0x01)
} else if k == 2 {
((v >> 1) & 0x7F) ^ ((v << 7) & 0x80) ^ ((v << 1) & 0x80)
} else {
v
};
}
}
}
*key_cells = key_cells_tmp;
}
fn shift_rows(state: &mut [[u8; 4]; 4]) {
let mut state_tmp = [[0u8; 4]; 4];
for i in 0..4 {
for j in 0..4 {
let pos = P[j + 4 * i];
state_tmp[i][j] = state[usize::from(pos >> 2)][usize::from(pos & 3)];
}
}
*state = state_tmp;
}
fn mix_column(state: &mut [[u8; 4]; 4]) {
#[allow(clippy::needless_range_loop)]
for j in 0..4 {
state[1][j] ^= state[2][j];
state[2][j] ^= state[0][j];
state[3][j] ^= state[2][j];
let temp = state[3][j];
state[3][j] = state[2][j];
state[2][j] = state[1][j];
state[1][j] = state[0][j];
state[0][j] = temp;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sbox_bitslice_matches_table_exhaustively() {
for x in 0u16..256 {
let byte = x as u8;
let mut state = [[byte; 4]; 4];
sub_cell8(&mut state);
let expected = SBOX_8[usize::from(byte)];
for row in state.iter() {
for &cell in row.iter() {
assert_eq!(
cell, expected,
"sub_cell8({byte:#04x}) via bitslice = {cell:#04x}, want {expected:#04x}"
);
}
}
}
}
#[test]
fn sbox_is_a_bijection() {
let mut seen = [false; 256];
for &v in SBOX_8.iter() {
assert!(
!seen[usize::from(v)],
"SBOX_8 value {v:#04x} appears more than once"
);
seen[usize::from(v)] = true;
}
assert!(seen.iter().all(|&s| s), "SBOX_8 does not cover 0..=255");
}
fn linear_state() -> [[u8; 4]; 4] {
let mut state = [[0u8; 4]; 4];
let mut v = 0u8;
for row in state.iter_mut() {
for cell in row.iter_mut() {
*cell = v;
v = v.wrapping_add(1);
}
}
state
}
#[test]
fn shift_rows_has_order_four() {
let original = linear_state();
let mut state = original;
for k in 1..=4 {
shift_rows(&mut state);
if k < 4 {
assert_ne!(
state, original,
"shift_rows returned to identity early at application {k}"
);
}
}
assert_eq!(
state, original,
"four applications of shift_rows must be the identity"
);
}
#[test]
fn mix_column_has_order_four() {
let original = linear_state();
let mut state = original;
for k in 1..=4 {
mix_column(&mut state);
if k < 4 {
assert_ne!(
state, original,
"mix_column returned to identity early at application {k}"
);
}
}
assert_eq!(
state, original,
"four applications of mix_column must be the identity"
);
}
#[test]
fn round_constants_match_independent_lfsr_derivation() {
let mut rc: u8 = 0;
for &expected in RC.iter() {
rc = ((rc << 1) | ((rc >> 5) ^ (rc >> 4) ^ 1) & 1) & 0x3F;
assert_eq!(rc, expected, "round-constant LFSR diverged from RC table");
}
}
#[test]
fn add_constants_touches_only_documented_cells() {
let mut state = [[0u8; 4]; 4];
add_constants(&mut state, 5);
let r = 5usize;
assert_eq!(state[0][0], RC[r] & 0xF);
assert_eq!(state[1][0], (RC[r] >> 4) & 0x3);
assert_eq!(state[2][0], 0x2);
assert_eq!(state[3][0], 0);
for row in state.iter() {
for &cell in row[1..].iter() {
assert_eq!(cell, 0, "add_constants must not touch columns 1..3");
}
}
}
#[test]
fn permutation_tables_are_bijections() {
for table in [&P, &TWEAKEY_P] {
let mut seen = [false; 16];
for &p in table.iter() {
assert!(usize::from(p) < 16, "permutation index out of range");
assert!(!seen[usize::from(p)], "permutation table has a duplicate");
seen[usize::from(p)] = true;
}
}
}
#[test]
fn add_key_tk2_lfsr_matches_independent_bit_array_reference() {
fn lfsr2_bit_array_reference(v: u8) -> u8 {
let bits: [u8; 8] = core::array::from_fn(|i| (v >> (7 - i)) & 1);
let feedback = bits[0] ^ bits[2]; let new_bits = [
bits[1], bits[2], bits[3], bits[4], bits[5], bits[6], bits[7], feedback,
];
new_bits.iter().fold(0u8, |acc, &b| (acc << 1) | b)
}
let test_byte: u8 = 0xA5;
let mut state = [[0u8; 4]; 4];
let mut key_cells = [[[0u8; 4]; 4]; 3];
key_cells[1][2][1] = test_byte;
add_key(&mut state, &mut key_cells);
let expected = lfsr2_bit_array_reference(test_byte);
assert_eq!(
key_cells[1][0][0], expected,
"TK2 LFSR output at the permuted destination did not match the reference"
);
for (i, row) in key_cells[1].iter().enumerate() {
for (j, &cell) in row.iter().enumerate() {
if (i, j) != (0, 0) {
assert_eq!(cell, 0, "unexpected nonzero cell at ({i},{j})");
}
}
}
}
#[test]
fn add_key_tk3_lfsr_matches_independent_bit_array_reference() {
fn lfsr3_bit_array_reference(v: u8) -> u8 {
let bits: [u8; 8] = core::array::from_fn(|i| (v >> (7 - i)) & 1);
let feedback = bits[7] ^ bits[1]; let new_bits = [
feedback, bits[0], bits[1], bits[2], bits[3], bits[4], bits[5], bits[6],
];
new_bits.iter().fold(0u8, |acc, &b| (acc << 1) | b)
}
let test_byte: u8 = 0xA5;
let mut state = [[0u8; 4]; 4];
let mut key_cells = [[[0u8; 4]; 4]; 3];
key_cells[2][2][1] = test_byte;
add_key(&mut state, &mut key_cells);
let expected = lfsr3_bit_array_reference(test_byte);
assert_eq!(key_cells[2][0][0], expected);
for (i, row) in key_cells[2].iter().enumerate() {
for (j, &cell) in row.iter().enumerate() {
if (i, j) != (0, 0) {
assert_eq!(cell, 0, "unexpected nonzero cell at ({i},{j})");
}
}
}
}
#[test]
fn add_key_tk1_has_no_lfsr() {
let test_byte: u8 = 0x7E;
let mut state = [[0u8; 4]; 4];
let mut key_cells = [[[0u8; 4]; 4]; 3];
key_cells[0][2][1] = test_byte;
add_key(&mut state, &mut key_cells);
assert_eq!(key_cells[0][0][0], test_byte);
}
#[test]
fn add_key_xors_state_rows_zero_and_one_only() {
let mut state = [[0xFFu8; 4]; 4];
let mut key_cells = [[[0u8; 4]; 4]; 3];
key_cells[0][0][0] = 0x01;
key_cells[1][0][0] = 0x02;
key_cells[2][0][0] = 0x04;
add_key(&mut state, &mut key_cells);
assert_eq!(state[0][0], 0xFF ^ 0x01 ^ 0x02 ^ 0x04);
for &cell in state[3].iter() {
assert_eq!(cell, 0xFF, "add_key must not touch state rows 2-3");
}
}
#[test]
fn encrypt_is_deterministic_and_not_identity() {
let key = [0x2Bu8; 48];
let block = [0x11u8; 16];
let mut a = block;
let mut b = block;
skinny_128_384_plus_enc(&mut a, &key);
skinny_128_384_plus_enc(&mut b, &key);
assert_eq!(a, b, "encryption must be deterministic");
assert_ne!(a, block, "encryption must not be the identity function");
}
#[test]
fn single_key_bit_flip_changes_many_output_bits() {
let mut key_a = [0u8; 48];
for (i, b) in key_a.iter_mut().enumerate() {
*b = i as u8;
}
let mut key_b = key_a;
key_b[0] ^= 0x01;
let block = [0x42u8; 16];
let mut out_a = block;
let mut out_b = block;
skinny_128_384_plus_enc(&mut out_a, &key_a);
skinny_128_384_plus_enc(&mut out_b, &key_b);
let differing_bits: u32 = out_a
.iter()
.zip(out_b.iter())
.map(|(x, y)| (x ^ y).count_ones())
.sum();
assert!(
differing_bits >= 32,
"single key bit flip only changed {differing_bits}/128 output bits"
);
}
}