From 9fa6c0ac1a9413fab0545d9c153f1a012e94fa70 Mon Sep 17 00:00:00 2001 From: Tom Dohrmann Date: Thu, 10 Sep 2026 14:24:26 +0200 Subject: [PATCH 1/7] rephrase uniform_row around nibbles Up until now, u32 was the smallest supported word size. With u32, the bitsliced soft backend can work on two blocks at the same time. A side- effect of this is that up until now our code assumes that two blocks is the smallest supported batch size and some of the constants are centered around that. In order to support a batch of 1, we need adjust accordingly by deduplicating the bits in the input for uniform_row and doubling or quadrupling the bits for u32 and u64 respectively. --- aes/src/backends/fixslice/aes192.rs | 36 ++++++------ aes/src/backends/fixslice/mix_columns.rs | 16 +++--- aes/src/backends/fixslice/utils.rs | 8 +-- aes/src/backends/fixslice/word.rs | 73 +++++++++++++++++------- 4 files changed, 82 insertions(+), 51 deletions(-) diff --git a/aes/src/backends/fixslice/aes192.rs b/aes/src/backends/fixslice/aes192.rs index 0edf37f1e..d1bb0117f 100644 --- a/aes/src/backends/fixslice/aes192.rs +++ b/aes/src/backends/fixslice/aes192.rs @@ -16,8 +16,8 @@ pub(crate) fn key_schedule(key: &[u8; 24]) -> RoundKeys { loop { for i in 0..8 { - rkeys[rk_off + i] = (W::uniform_row(0x0f) & (tmp[i] >> W::HALF_ROW)) - | (W::uniform_row(0xf0) & (rkeys[(rk_off - 8) + i] << W::HALF_ROW)); + rkeys[rk_off + i] = (W::uniform_row(0x3) & (tmp[i] >> W::HALF_ROW)) + | (W::uniform_row(0xc) & (rkeys[(rk_off - 8) + i] << W::HALF_ROW)); } sub_bytes(&mut tmp); @@ -28,8 +28,8 @@ pub(crate) fn key_schedule(key: &[u8; 24]) -> RoundKeys { for i in 0..8 { let mut ti = rkeys[rk_off + i]; - ti ^= W::uniform_row(0x30) & tmp[i].ror(W::ror_distance(1, 1)); - ti ^= W::uniform_row(0xc0) & (ti << W::QUARTER_ROW); + ti ^= W::uniform_row(0x4) & tmp[i].ror(W::ror_distance(1, 1)); + ti ^= W::uniform_row(0x8) & (ti << W::QUARTER_ROW); tmp[i] = ti; } rkeys[rk_off..(rk_off + 8)].copy_from_slice(&tmp); @@ -37,13 +37,13 @@ pub(crate) fn key_schedule(key: &[u8; 24]) -> RoundKeys { for i in 0..8 { let ui = tmp[i]; - let mut ti = (W::uniform_row(0x0f) & (rkeys[(rk_off - 16) + i] >> W::HALF_ROW)) - | (W::uniform_row(0xf0) & (ui << W::HALF_ROW)); - ti ^= W::uniform_row(0x03) & (ui >> (3 * W::QUARTER_ROW)); + let mut ti = (W::uniform_row(0x3) & (rkeys[(rk_off - 16) + i] >> W::HALF_ROW)) + | (W::uniform_row(0xc) & (ui << W::HALF_ROW)); + ti ^= W::uniform_row(0x1) & (ui >> (3 * W::QUARTER_ROW)); tmp[i] = ti - ^ (W::uniform_row(0xfc) & (ti << W::QUARTER_ROW)) - ^ (W::uniform_row(0xf0) & (ti << W::HALF_ROW)) - ^ (W::uniform_row(0xc0) & (ti << (3 * W::QUARTER_ROW))); + ^ (W::uniform_row(0xe) & (ti << W::QUARTER_ROW)) + ^ (W::uniform_row(0xc) & (ti << W::HALF_ROW)) + ^ (W::uniform_row(0x8) & (ti << (3 * W::QUARTER_ROW))); } rkeys[rk_off..(rk_off + 8)].copy_from_slice(&tmp); rk_off += 8; @@ -55,13 +55,13 @@ pub(crate) fn key_schedule(key: &[u8; 24]) -> RoundKeys { rcon += 1; for i in 0..8 { - let mut ti = (W::uniform_row(0x0f) & (rkeys[(rk_off - 16) + i] >> W::HALF_ROW)) - | (W::uniform_row(0xf0) & (rkeys[(rk_off - 8) + i] << W::HALF_ROW)); - ti ^= W::uniform_row(0x03) & tmp[i].ror(W::ror_distance(1, 3)); + let mut ti = (W::uniform_row(0x3) & (rkeys[(rk_off - 16) + i] >> W::HALF_ROW)) + | (W::uniform_row(0xc) & (rkeys[(rk_off - 8) + i] << W::HALF_ROW)); + ti ^= W::uniform_row(0x1) & tmp[i].ror(W::ror_distance(1, 3)); rkeys[rk_off + i] = ti - ^ (W::uniform_row(0xfc) & (ti << W::QUARTER_ROW)) - ^ (W::uniform_row(0xf0) & (ti << W::HALF_ROW)) - ^ (W::uniform_row(0xc0) & (ti << (3 * W::QUARTER_ROW))); + ^ (W::uniform_row(0xe) & (ti << W::QUARTER_ROW)) + ^ (W::uniform_row(0xc) & (ti << W::HALF_ROW)) + ^ (W::uniform_row(0x8) & (ti << (3 * W::QUARTER_ROW))); } rk_off += 8; @@ -72,8 +72,8 @@ pub(crate) fn key_schedule(key: &[u8; 24]) -> RoundKeys { for i in 0..8 { let ui = rkeys[(rk_off - 8) + i]; let mut ti = rkeys[(rk_off - 16) + i]; - ti ^= W::uniform_row(0x30) & (ui >> W::QUARTER_ROW); - ti ^= W::uniform_row(0xc0) & (ti << W::QUARTER_ROW); + ti ^= W::uniform_row(0x4) & (ui >> W::QUARTER_ROW); + ti ^= W::uniform_row(0x8) & (ti << W::QUARTER_ROW); tmp[i] = ti; } } diff --git a/aes/src/backends/fixslice/mix_columns.rs b/aes/src/backends/fixslice/mix_columns.rs index fdffa0235..60aad2da8 100644 --- a/aes/src/backends/fixslice/mix_columns.rs +++ b/aes/src/backends/fixslice/mix_columns.rs @@ -145,30 +145,30 @@ fn rotate_rows_2(x: W) -> W { #[inline(always)] fn rotate_rows_and_columns_1_1(x: W) -> W { - let a = x.ror(W::ror_distance(1, 1)) & W::uniform_row(0x3f); - let b = x.ror(W::ror_distance(0, 1)) & W::uniform_row(0xc0); + let a = x.ror(W::ror_distance(1, 1)) & W::uniform_row(0x7); + let b = x.ror(W::ror_distance(0, 1)) & W::uniform_row(0x8); a | b } #[cfg(not(aes_backend_soft = "compact"))] #[inline(always)] fn rotate_rows_and_columns_1_2(x: W) -> W { - let a = x.ror(W::ror_distance(1, 2)) & W::uniform_row(0x0f); - let b = x.ror(W::ror_distance(0, 2)) & W::uniform_row(0xf0); + let a = x.ror(W::ror_distance(1, 2)) & W::uniform_row(0x3); + let b = x.ror(W::ror_distance(0, 2)) & W::uniform_row(0xc); a | b } #[cfg(not(aes_backend_soft = "compact"))] #[inline(always)] fn rotate_rows_and_columns_1_3(x: W) -> W { - let a = x.ror(W::ror_distance(1, 3)) & W::uniform_row(0x03); - let b = x.ror(W::ror_distance(0, 3)) & W::uniform_row(0xfc); + let a = x.ror(W::ror_distance(1, 3)) & W::uniform_row(0x1); + let b = x.ror(W::ror_distance(0, 3)) & W::uniform_row(0xe); a | b } #[inline(always)] fn rotate_rows_and_columns_2_2(x: W) -> W { - let a = x.ror(W::ror_distance(2, 2)) & W::uniform_row(0x0f); - let b = x.ror(W::ror_distance(1, 2)) & W::uniform_row(0xf0); + let a = x.ror(W::ror_distance(2, 2)) & W::uniform_row(0x3); + let b = x.ror(W::ror_distance(1, 2)) & W::uniform_row(0xc); a | b } diff --git a/aes/src/backends/fixslice/utils.rs b/aes/src/backends/fixslice/utils.rs index 698225ef7..92d1a9fc7 100644 --- a/aes/src/backends/fixslice/utils.rs +++ b/aes/src/backends/fixslice/utils.rs @@ -83,11 +83,11 @@ pub(super) fn inv_shift_rows_3(state: &mut [W]) { pub(super) fn xor_columns(rkeys: &mut [W], offset: usize, idx_xor: usize, idx_ror: u32) { for i in 0..8 { let off_i = offset + i; - let rk = rkeys[off_i - idx_xor] ^ (W::uniform_row(0x03) & rkeys[off_i].ror(idx_ror)); + let rk = rkeys[off_i - idx_xor] ^ (W::uniform_row(0x1) & rkeys[off_i].ror(idx_ror)); rkeys[off_i] = rk - ^ (W::uniform_row(0xfc) & (rk << W::QUARTER_ROW)) - ^ (W::uniform_row(0xf0) & (rk << W::HALF_ROW)) - ^ (W::uniform_row(0xc0) & (rk << (3 * W::QUARTER_ROW))); + ^ (W::uniform_row(0xe) & (rk << W::QUARTER_ROW)) + ^ (W::uniform_row(0xc) & (rk << W::HALF_ROW)) + ^ (W::uniform_row(0x8) & (rk << (3 * W::QUARTER_ROW))); } } diff --git a/aes/src/backends/fixslice/word.rs b/aes/src/backends/fixslice/word.rs index e24abc817..92081a5b7 100644 --- a/aes/src/backends/fixslice/word.rs +++ b/aes/src/backends/fixslice/word.rs @@ -42,7 +42,7 @@ pub(crate) trait Word: /// Rotate right by `n` bits. fn ror(self, n: u32) -> Self; - /// Pack the same byte across all 4 rows of the word. + /// Pack the same nibble across all 4 rows of the word. fn uniform_row(b: u8) -> Self; /// Place one byte at each of the 4 row positions of the word (row 0 = LSB). @@ -58,6 +58,51 @@ pub(crate) trait Word: fn inv_bitslice(input: &[Self]) -> Array; } +/// Expand an 8-bit row pattern to a 16-bit row pattern by doubling each bit: +/// input bit `i` becomes output bits `2i` and `2i+1`. Branchless SWAR so LLVM +/// folds it to a single 16-bit immediate when `b` is a constant. +#[inline(always)] +const fn double_bits_8_to_16(b: u8) -> u16 { + let x = b as u16; + // Spread the 8 bits of x to even positions 0,2,4,6,8,10,12,14. + let x = (x | (x << 4)) & 0x0f0f; + let x = (x | (x << 2)) & 0x3333; + let x = (x | (x << 1)) & 0x5555; + // Duplicate each spread bit to its adjacent odd position. + x | (x << 1) +} + +/// Expand a 16-bit row pattern to a 32-bit row pattern by doubling each bit: +/// input bit `i` becomes output bits `2i` and `2i+1`. Branchless SWAR so LLVM +/// folds it to a single 32-bit immediate when `b` is a constant. +#[inline(always)] +const fn double_bits_16_to_32(b: u16) -> u32 { + let x = b as u32; + // Spread the 16 bits of x to even positions 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30. + let x = (x | (x << 8)) & 0x00FF00FF; + let x = (x | (x << 4)) & 0x0F0F0F0F; + let x = (x | (x << 2)) & 0x33333333; + let x = (x | (x << 1)) & 0x55555555; + // Duplicate each spread bit to its adjacent odd position. + x | (x << 1) +} + +/// Expand a 16-bit row pattern to a 64-bit row pattern by quadrupling each bit: +/// input bit `i` becomes output bits `4i`, `4i+1`, `4i+2`, and `4i+3`. +/// Branchless SWAR so LLVM folds it to a single 64-bit immediate when `b` is +/// a constant. +#[inline(always)] +const fn quad_bits_16_to_64(b: u16) -> u64 { + let x = b as u64; + // Spread the 16 bits of x to positions 0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60. + let x = (x | (x << 24)) & 0x000000FF000000FF; + let x = (x | (x << 12)) & 0x000F000F000F000F; + let x = (x | (x << 6)) & 0x0303030303030303; + let x = (x | (x << 3)) & 0x1111111111111111; + // Duplicate each spread bit to its adjacent odd position. + x | (x << 1) | (x << 2) | (x << 3) +} + impl Word for u32 { type Blocks = U2; @@ -68,7 +113,7 @@ impl Word for u32 { #[inline(always)] fn uniform_row(b: u8) -> u32 { - (b as u32) * 0x01010101 + double_bits_8_to_16(b) as u32 * 0x01010101 } #[inline(always)] @@ -151,20 +196,6 @@ impl Word for u32 { } } -/// Expand an 8-bit row pattern to a 16-bit row pattern by doubling each bit: -/// input bit `i` becomes output bits `2i` and `2i+1`. Branchless SWAR so LLVM -/// folds it to a single 16-bit immediate when `b` is a constant. -#[inline(always)] -const fn double_bits(b: u8) -> u16 { - let x = b as u16; - // Spread the 8 bits of x to even positions 0,2,4,6,8,10,12,14. - let x = (x | (x << 4)) & 0x0f0f; - let x = (x | (x << 2)) & 0x3333; - let x = (x | (x << 1)) & 0x5555; - // Duplicate each spread bit to its adjacent odd position. - x | (x << 1) -} - impl Word for u64 { type Blocks = U4; @@ -175,15 +206,15 @@ impl Word for u64 { #[inline(always)] fn uniform_row(b: u8) -> u64 { - (double_bits(b) as u64) * 0x0001_0001_0001_0001 + quad_bits_16_to_64(b as u16) * 0x0001_0001_0001_0001 } #[inline(always)] fn pack_rows(r0: u8, r1: u8, r2: u8, r3: u8) -> u64 { - (double_bits(r0) as u64) - | ((double_bits(r1) as u64) << 16) - | ((double_bits(r2) as u64) << 32) - | ((double_bits(r3) as u64) << 48) + (double_bits_16_to_32(r0 as u16) as u64) + | ((double_bits_16_to_32(r1 as u16) as u64) << 16) + | ((double_bits_16_to_32(r2 as u16) as u64) << 32) + | ((double_bits_16_to_32(r3 as u16) as u64) << 48) } #[inline(always)] From 355c6764d93be95f9d78e15907b4165010767102 Mon Sep 17 00:00:00 2001 From: Tom Dohrmann Date: Thu, 10 Sep 2026 14:33:37 +0200 Subject: [PATCH 2/7] rephrase pack_rows around nibbles Same as previous commit. Instead of assuming two blocks as the smallest common denominator, assume 1 and duplicate/quadruple accordingly. --- aes/src/backends/fixslice/utils.rs | 12 ++++++------ aes/src/backends/fixslice/word.rs | 16 ++++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/aes/src/backends/fixslice/utils.rs b/aes/src/backends/fixslice/utils.rs index 92d1a9fc7..fe38f495d 100644 --- a/aes/src/backends/fixslice/utils.rs +++ b/aes/src/backends/fixslice/utils.rs @@ -32,8 +32,8 @@ pub(super) fn delta_swap_2(a: &mut W, b: &mut W, shift: u32, mask: W) { pub(super) fn shift_rows_1(state: &mut [W]) { debug_assert_eq!(state.len(), 8); for x in state.iter_mut() { - delta_swap_1(x, W::HALF_ROW, W::pack_rows(0x00, 0x03, 0x0f, 0x0c)); - delta_swap_1(x, W::QUARTER_ROW, W::pack_rows(0x00, 0x33, 0x00, 0x33)); + delta_swap_1(x, W::HALF_ROW, W::pack_rows(0x0, 0x1, 0x3, 0x2)); + delta_swap_1(x, W::QUARTER_ROW, W::pack_rows(0x0, 0x5, 0x0, 0x5)); } } @@ -42,7 +42,7 @@ pub(super) fn shift_rows_1(state: &mut [W]) { pub(super) fn shift_rows_2(state: &mut [W]) { debug_assert_eq!(state.len(), 8); for x in state.iter_mut() { - delta_swap_1(x, W::HALF_ROW, W::pack_rows(0x00, 0x0f, 0x00, 0x0f)); + delta_swap_1(x, W::HALF_ROW, W::pack_rows(0x0, 0x3, 0x0, 0x3)); } } @@ -51,8 +51,8 @@ pub(super) fn shift_rows_2(state: &mut [W]) { pub(super) fn shift_rows_3(state: &mut [W]) { debug_assert_eq!(state.len(), 8); for x in state.iter_mut() { - delta_swap_1(x, W::HALF_ROW, W::pack_rows(0x00, 0x0c, 0x0f, 0x03)); - delta_swap_1(x, W::QUARTER_ROW, W::pack_rows(0x00, 0x33, 0x00, 0x33)); + delta_swap_1(x, W::HALF_ROW, W::pack_rows(0x0, 0x2, 0x3, 0x1)); + delta_swap_1(x, W::QUARTER_ROW, W::pack_rows(0x0, 0x5, 0x0, 0x5)); } } @@ -115,5 +115,5 @@ pub(super) fn add_round_key(state: &mut State, rkey: &[W]) { #[inline(always)] pub(super) fn add_round_constant_bit(state: &mut [W], bit: usize) { - state[bit] ^= W::pack_rows(0x00, 0xc0, 0x00, 0x00); + state[bit] ^= W::pack_rows(0x0, 0x8, 0x0, 0x0); } diff --git a/aes/src/backends/fixslice/word.rs b/aes/src/backends/fixslice/word.rs index 92081a5b7..9c9377ee1 100644 --- a/aes/src/backends/fixslice/word.rs +++ b/aes/src/backends/fixslice/word.rs @@ -45,7 +45,7 @@ pub(crate) trait Word: /// Pack the same nibble across all 4 rows of the word. fn uniform_row(b: u8) -> Self; - /// Place one byte at each of the 4 row positions of the word (row 0 = LSB). + /// Place one nibble at each of the 4 row positions of the word (row 0 = LSB). fn pack_rows(r0: u8, r1: u8, r2: u8, r3: u8) -> Self; /// Replicate byte `b` across every byte of the word. @@ -76,6 +76,7 @@ const fn double_bits_8_to_16(b: u8) -> u16 { /// input bit `i` becomes output bits `2i` and `2i+1`. Branchless SWAR so LLVM /// folds it to a single 32-bit immediate when `b` is a constant. #[inline(always)] +#[expect(dead_code)] // Will be used in a future commit. const fn double_bits_16_to_32(b: u16) -> u32 { let x = b as u32; // Spread the 16 bits of x to even positions 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30. @@ -118,7 +119,10 @@ impl Word for u32 { #[inline(always)] fn pack_rows(r0: u8, r1: u8, r2: u8, r3: u8) -> u32 { - (r0 as u32) | ((r1 as u32) << 8) | ((r2 as u32) << 16) | ((r3 as u32) << 24) + (double_bits_8_to_16(r0) as u32) + | ((double_bits_8_to_16(r1) as u32) << 8) + | ((double_bits_8_to_16(r2) as u32) << 16) + | ((double_bits_8_to_16(r3) as u32) << 24) } #[inline(always)] @@ -211,10 +215,10 @@ impl Word for u64 { #[inline(always)] fn pack_rows(r0: u8, r1: u8, r2: u8, r3: u8) -> u64 { - (double_bits_16_to_32(r0 as u16) as u64) - | ((double_bits_16_to_32(r1 as u16) as u64) << 16) - | ((double_bits_16_to_32(r2 as u16) as u64) << 32) - | ((double_bits_16_to_32(r3 as u16) as u64) << 48) + quad_bits_16_to_64(r0 as u16) + | (quad_bits_16_to_64(r1 as u16) << 16) + | (quad_bits_16_to_64(r2 as u16) << 32) + | (quad_bits_16_to_64(r3 as u16) << 48) } #[inline(always)] From 16832d6ce8c50f90855d4ddc07949f18b6937e6a Mon Sep 17 00:00:00 2001 From: Tom Dohrmann Date: Thu, 10 Sep 2026 15:41:11 +0200 Subject: [PATCH 3/7] implement Word for u16 --- .github/workflows/aes.yml | 20 ++++++ aes/src/backends/fixslice/mod.rs | 5 +- aes/src/backends/fixslice/word.rs | 110 +++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/.github/workflows/aes.yml b/.github/workflows/aes.yml index d7f72efd3..dcb3a5e69 100644 --- a/.github/workflows/aes.yml +++ b/.github/workflows/aes.yml @@ -232,6 +232,26 @@ jobs: - run: cargo test --target ${{ matrix.target }} - run: cargo test --target ${{ matrix.target }} --all-features + # Test for the portable software backend with a 16-bit word size + soft-16-bit: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - rust: 1.89.0 # MSRV + - rust: stable + env: + RUSTFLAGS: '-Dwarnings --cfg aes_backend="soft" --cfg cpubits="16"' + steps: + - uses: actions/checkout@v7 + - uses: RustCrypto/actions/cargo-cache@master + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - run: ${{ matrix.deps }} + - run: cargo test + - run: cargo test --all-features + # Cross-compiled tests cross: strategy: diff --git a/aes/src/backends/fixslice/mod.rs b/aes/src/backends/fixslice/mod.rs index f5d29d836..dfd340f51 100644 --- a/aes/src/backends/fixslice/mod.rs +++ b/aes/src/backends/fixslice/mod.rs @@ -31,7 +31,10 @@ use word::Word; type State = [W; 8]; cpubits::cpubits! { - 16 | 32 => { + 16 => { + pub(super) type NativeWord = u16; + } + 32 => { pub(super) type NativeWord = u32; } 64 => { diff --git a/aes/src/backends/fixslice/word.rs b/aes/src/backends/fixslice/word.rs index 9c9377ee1..c90e7f4e1 100644 --- a/aes/src/backends/fixslice/word.rs +++ b/aes/src/backends/fixslice/word.rs @@ -2,7 +2,7 @@ use crate::Block; use cipher::{ Array, array::ArraySize, - consts::{U2, U4}, + consts::{U1, U2, U4}, }; use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, Shl, Shr}; @@ -25,7 +25,8 @@ pub(crate) trait Word: /// Number of 128-bit blocks bitsliced together in one state. type Blocks: ArraySize; - /// Width in bits of one row of the bitsliced state (8 for `u32`, 16 for `u64`). + /// Width in bits of one row of the bitsliced state (4 for `u16`, 8 for + /// `u32`, 16 for `u64`). const ROW_BITS: u32 = (size_of::() * 2) as u32; /// Half of `ROW_BITS`. @@ -58,6 +59,111 @@ pub(crate) trait Word: fn inv_bitslice(input: &[Self]) -> Array; } +impl Word for u16 { + type Blocks = U1; + + #[inline(always)] + fn ror(self, n: u32) -> u16 { + self.rotate_right(n) + } + + #[inline(always)] + fn uniform_row(b: u8) -> u16 { + (b as u16) * 0x1111 + } + + #[inline(always)] + fn pack_rows(r0: u8, r1: u8, r2: u8, r3: u8) -> u16 { + (r0 as u16) | ((r1 as u16) << 4) | ((r2 as u16) << 8) | ((r3 as u16) << 12) + } + + #[inline(always)] + fn byte_repeat(b: u8) -> u16 { + (b as u16) * 0x0101 + } + + /// Bitslice one 128-bit input block into a 128-bit internal state. + fn bitslice(output: &mut [u16], input: &Array) { + debug_assert_eq!(output.len(), 8); + let b = input[0].as_slice(); + + // Bitslicing is a bit index manipulation. 128 bits of data means each bit is positioned at + // a 7-bit index. AES data is a single 4x4 column-major matrix of bytes, so the index is + // initially ([c]olumn, [r]ow, [p]osition): + // c1 c0 r1 r0 p2 p1 p0 + // + // The desired bitsliced data groups first by bit position, then row, then column: + // p2 p1 p0 r1 r0 c1 c0 + + fn read_reordered(input: &[u8]) -> u16 { + (u16::from(input[0x0])) | (u16::from(input[0x2]) << 8) + } + + // Reorder each block's bytes on input + // c1 c0 r1 r0 __ __ __ => c1 c0 r0 r1 __ __ __ + // Reorder by relabeling (note the order of input) + // c1 c0 r0 __ __ __ __ => r0 c1 c0 __ __ __ __ + let mut t = [ + read_reordered(&b[0x00..0x03]), + read_reordered(&b[0x04..0x07]), + read_reordered(&b[0x08..0x0b]), + read_reordered(&b[0x0c..0x0f]), + read_reordered(&b[0x01..0x04]), + read_reordered(&b[0x05..0x08]), + read_reordered(&b[0x09..0x0c]), + read_reordered(&b[0x0d..0x10]), + ]; + + bitslice_swaps(&mut t); + + // Final bitsliced bit index, as desired: + // p2 p1 p0 r1 r0 c1 c0 + output[..8].copy_from_slice(&t); + } + + /// Un-bitslice a 128-bit internal state into one 128-bit block. + fn inv_bitslice(input: &[u16]) -> Array { + debug_assert_eq!(input.len(), 8); + + // Unbitslicing is a bit index manipulation. 128 bits of data means each bit is positioned + // at a 7-bit index. AES data is a single 4x4 column-major matrix of bytes, so the desired + // index for the output is ([c]olumn, [r]ow, [p]osition): + // c1 c0 r1 r0 p2 p1 p0 + // + // The initially bitsliced data groups first by bit position, then row, then column: + // p2 p1 p0 r1 r0 c1 c0 + + let mut t = [ + input[0], input[1], input[2], input[3], input[4], input[5], input[6], input[7], + ]; + + bitslice_swaps(&mut t); + + fn write_reordered(rows: u16, output: &mut [u8]) { + output[0x0] = rows as u8; + output[0x2] = (rows >> 8) as u8; + } + + let mut output = Array::::default(); + // Reorder by relabeling (note the order of output) + // r0 c1 c0 __ __ __ __ => c1 c0 r0 __ __ __ __ + // Reorder each block's bytes on output + // c1 c0 r0 r1 __ __ __ => c1 c0 r1 r0 __ __ __ + write_reordered(t[0], &mut output[0][0x00..0x03]); + write_reordered(t[1], &mut output[0][0x04..0x07]); + write_reordered(t[2], &mut output[0][0x08..0x0b]); + write_reordered(t[3], &mut output[0][0x0c..0x0f]); + write_reordered(t[4], &mut output[0][0x01..0x04]); + write_reordered(t[5], &mut output[0][0x05..0x08]); + write_reordered(t[6], &mut output[0][0x09..0x0c]); + write_reordered(t[7], &mut output[0][0x0d..0x10]); + + // Final AES bit index, as desired: + // c1 c0 r1 r0 p2 p1 p0 + output + } +} + /// Expand an 8-bit row pattern to a 16-bit row pattern by doubling each bit: /// input bit `i` becomes output bits `2i` and `2i+1`. Branchless SWAR so LLVM /// folds it to a single 16-bit immediate when `b` is a constant. From 02c5d4862a50878e6f3d9b0b7519fca0080a767f Mon Sep 17 00:00:00 2001 From: Tom Dohrmann Date: Fri, 11 Sep 2026 11:56:18 +0200 Subject: [PATCH 4/7] separate soft AES type and backend Previously, we could use the same type for both of these because they contained the same data, but in the future, this will no longer be the case. This matches the VAES256 and VAES512 backends. --- aes/src/backends/fixslice/mod.rs | 2 +- aes/src/backends/soft.rs | 55 +++++++++++++++++++++++--------- aes/src/lib.rs | 8 ++--- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/aes/src/backends/fixslice/mod.rs b/aes/src/backends/fixslice/mod.rs index dfd340f51..93ea715d8 100644 --- a/aes/src/backends/fixslice/mod.rs +++ b/aes/src/backends/fixslice/mod.rs @@ -26,7 +26,7 @@ mod sbox; mod utils; mod word; -use word::Word; +pub(super) use word::Word; type State = [W; 8]; diff --git a/aes/src/backends/soft.rs b/aes/src/backends/soft.rs index 07c35a654..ba9cce2fd 100644 --- a/aes/src/backends/soft.rs +++ b/aes/src/backends/soft.rs @@ -1,8 +1,8 @@ #![deny(unsafe_code)] -use crate::Block; +use crate::{Block, backends::soft::fixslice::BatchBlocks}; use cipher::{ - BlockCipherDecBackend, BlockCipherEncBackend, BlockSizeUser, ParBlocks, ParBlocksSizeUser, - consts::U16, inout::InOut, + BlockCipherDecBackend, BlockCipherDecClosure, BlockCipherEncBackend, BlockCipherEncClosure, + BlockSizeUser, ParBlocks, ParBlocksSizeUser, consts::U16, inout::InOut, }; #[path = "fixslice/mod.rs"] @@ -11,11 +11,12 @@ pub(crate) mod fixslice; #[cfg(feature = "hazmat")] pub(crate) use fixslice::hazmat; -use fixslice::{BatchBlocks, NativeBatchSize, NativeWord}; +use fixslice::{NativeBatchSize, NativeWord}; macro_rules! impl_backend { ( name = $name:tt, + backend = $backend:tt, key_size = $key_size:literal, module = $module:ident, doc = $doc:expr, @@ -24,53 +25,74 @@ macro_rules! impl_backend { #[doc = "block cipher"] #[derive(Clone, Copy)] pub(crate) struct $name { - keys: fixslice::$module::RoundKeys, + rk: fixslice::$module::RoundKeys, } impl $name { #[inline] pub(crate) fn new(key: &[u8; $key_size]) -> Self { - let keys = fixslice::$module::key_schedule(key); - Self { keys } + let rk = fixslice::$module::key_schedule(key); + Self { rk } } + + #[inline] + pub(crate) fn encrypt(&self, f: impl BlockCipherEncClosure) { + let rk = &self.rk; + let backend = $backend { rk }; + f.call(&backend) + } + + #[inline] + pub(crate) fn decrypt(&self, f: impl BlockCipherDecClosure) { + let rk = &self.rk; + let backend = $backend { rk }; + f.call(&backend) + } + } + + #[doc=$doc] + #[doc = "block cipher"] + #[derive(Clone, Copy)] + pub(crate) struct $backend<'a> { + rk: &'a fixslice::$module::RoundKeys, } - impl BlockSizeUser for $name { + impl BlockSizeUser for $backend<'_> { type BlockSize = U16; } - impl ParBlocksSizeUser for $name { + impl ParBlocksSizeUser for $backend<'_> { type ParBlocksSize = NativeBatchSize; } - impl BlockCipherEncBackend for $name { + impl BlockCipherEncBackend for $backend<'_> { #[inline(always)] fn encrypt_block(&self, mut block: InOut<'_, '_, Block>) { let mut blocks = BatchBlocks::::default(); blocks[0] = block.clone_in().into(); - let res = fixslice::$module::encrypt(&self.keys, &blocks); + let res = fixslice::$module::encrypt(&self.rk, &blocks); *block.get_out() = res[0].into(); } #[inline(always)] fn encrypt_par_blocks(&self, mut blocks: InOut<'_, '_, ParBlocks>) { - let res = fixslice::$module::encrypt(&self.keys, blocks.get_in()); + let res = fixslice::$module::encrypt::(&self.rk, blocks.get_in()); *blocks.get_out() = res; } } - impl BlockCipherDecBackend for $name { + impl BlockCipherDecBackend for $backend<'_> { #[inline(always)] fn decrypt_block(&self, mut block: InOut<'_, '_, Block>) { let mut blocks = BatchBlocks::::default(); blocks[0] = block.clone_in(); - let res = fixslice::$module::decrypt(&self.keys, &blocks); + let res = fixslice::$module::decrypt(&self.rk, &blocks); *block.get_out() = res[0]; } #[inline(always)] fn decrypt_par_blocks(&self, mut blocks: InOut<'_, '_, ParBlocks>) { - let res = fixslice::$module::decrypt(&self.keys, blocks.get_in()); + let res = fixslice::$module::decrypt::(&self.rk, blocks.get_in()); *blocks.get_out() = res; } } @@ -79,18 +101,21 @@ macro_rules! impl_backend { impl_backend!( name = Aes128, + backend = Aes128Backend, key_size = 16, module = aes128, doc = "AES-128", ); impl_backend!( name = Aes192, + backend = Aes192Backend, key_size = 24, module = aes192, doc = "AES-192", ); impl_backend!( name = Aes256, + backend = Aes256Backend, key_size = 32, module = aes256, doc = "AES-256", diff --git a/aes/src/lib.rs b/aes/src/lib.rs index 2ee6820a3..fc9959224 100644 --- a/aes/src/lib.rs +++ b/aes/src/lib.rs @@ -298,8 +298,8 @@ macro_rules! impl_encrypt { } // SAFETY: we access correct union variant - let backend = unsafe { &self.inner.soft }; - f.call(backend); + let aes = unsafe { &self.inner.soft }; + aes.encrypt(f); } } }; @@ -348,8 +348,8 @@ macro_rules! impl_decrypt { } // SAFETY: we access correct union variant - let backend = unsafe { &self.inner.soft }; - f.call(backend); + let aes = unsafe { &self.inner.soft }; + aes.decrypt(f); } } }; From a07e3accb9502aae18d3b1f806152a13ca5e5197 Mon Sep 17 00:00:00 2001 From: Tom Dohrmann Date: Fri, 11 Sep 2026 11:59:17 +0200 Subject: [PATCH 5/7] store round keys as u16 for soft implementation The round keys are expanded to the native word size when multiple blocks are processed in parallel. --- aes/src/backends/fixslice/mod.rs | 1 + aes/src/backends/fixslice/word.rs | 18 ++++++++++++++++-- aes/src/backends/soft.rs | 21 ++++++++++++--------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/aes/src/backends/fixslice/mod.rs b/aes/src/backends/fixslice/mod.rs index 93ea715d8..b9ace40ab 100644 --- a/aes/src/backends/fixslice/mod.rs +++ b/aes/src/backends/fixslice/mod.rs @@ -44,3 +44,4 @@ cpubits::cpubits! { pub(super) type NativeBatchSize = ::Blocks; pub(super) type BatchBlocks = Array::Blocks>; +pub(super) type MinWord = u16; diff --git a/aes/src/backends/fixslice/word.rs b/aes/src/backends/fixslice/word.rs index c90e7f4e1..696edc6cb 100644 --- a/aes/src/backends/fixslice/word.rs +++ b/aes/src/backends/fixslice/word.rs @@ -1,4 +1,4 @@ -use crate::Block; +use crate::{Block, backends::soft::MinWord}; use cipher::{ Array, array::ArraySize, @@ -57,6 +57,9 @@ pub(crate) trait Word: /// Unpack a bitsliced 8-row state slice into `Self::Blocks` output blocks. fn inv_bitslice(input: &[Self]) -> Array; + + /// Broadcast the round key into all lanes. + fn broadcast(rkey: MinWord) -> Self; } impl Word for u16 { @@ -162,6 +165,10 @@ impl Word for u16 { // c1 c0 r1 r0 p2 p1 p0 output } + + fn broadcast(rkey: MinWord) -> u16 { + rkey + } } /// Expand an 8-bit row pattern to a 16-bit row pattern by doubling each bit: @@ -182,7 +189,6 @@ const fn double_bits_8_to_16(b: u8) -> u16 { /// input bit `i` becomes output bits `2i` and `2i+1`. Branchless SWAR so LLVM /// folds it to a single 32-bit immediate when `b` is a constant. #[inline(always)] -#[expect(dead_code)] // Will be used in a future commit. const fn double_bits_16_to_32(b: u16) -> u32 { let x = b as u32; // Spread the 16 bits of x to even positions 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30. @@ -304,6 +310,10 @@ impl Word for u32 { // b0 c1 c0 r1 r0 p2 p1 p0 output } + + fn broadcast(rkey: MinWord) -> Self { + double_bits_16_to_32(rkey) + } } impl Word for u64 { @@ -426,6 +436,10 @@ impl Word for u64 { // b1 b0 c1 c0 r1 r0 p2 p1 p0 output } + + fn broadcast(rkey: MinWord) -> Self { + quad_bits_16_to_64(rkey) + } } /// Width-generic delta-swap pipeline shared by `bitslice` and `inv_bitslice` diff --git a/aes/src/backends/soft.rs b/aes/src/backends/soft.rs index ba9cce2fd..7487ad458 100644 --- a/aes/src/backends/soft.rs +++ b/aes/src/backends/soft.rs @@ -11,7 +11,7 @@ pub(crate) mod fixslice; #[cfg(feature = "hazmat")] pub(crate) use fixslice::hazmat; -use fixslice::{NativeBatchSize, NativeWord}; +use fixslice::{MinWord, NativeBatchSize, NativeWord, Word}; macro_rules! impl_backend { ( @@ -25,7 +25,7 @@ macro_rules! impl_backend { #[doc = "block cipher"] #[derive(Clone, Copy)] pub(crate) struct $name { - rk: fixslice::$module::RoundKeys, + rk: fixslice::$module::RoundKeys, } impl $name { @@ -38,14 +38,16 @@ macro_rules! impl_backend { #[inline] pub(crate) fn encrypt(&self, f: impl BlockCipherEncClosure) { let rk = &self.rk; - let backend = $backend { rk }; + let rk_native = self.rk.map(Word::broadcast); + let backend = $backend { rk, rk_native }; f.call(&backend) } #[inline] pub(crate) fn decrypt(&self, f: impl BlockCipherDecClosure) { let rk = &self.rk; - let backend = $backend { rk }; + let rk_native = self.rk.map(Word::broadcast); + let backend = $backend { rk, rk_native }; f.call(&backend) } } @@ -54,7 +56,8 @@ macro_rules! impl_backend { #[doc = "block cipher"] #[derive(Clone, Copy)] pub(crate) struct $backend<'a> { - rk: &'a fixslice::$module::RoundKeys, + rk: &'a fixslice::$module::RoundKeys, + rk_native: fixslice::$module::RoundKeys, } impl BlockSizeUser for $backend<'_> { @@ -68,7 +71,7 @@ macro_rules! impl_backend { impl BlockCipherEncBackend for $backend<'_> { #[inline(always)] fn encrypt_block(&self, mut block: InOut<'_, '_, Block>) { - let mut blocks = BatchBlocks::::default(); + let mut blocks = BatchBlocks::::default(); blocks[0] = block.clone_in().into(); let res = fixslice::$module::encrypt(&self.rk, &blocks); *block.get_out() = res[0].into(); @@ -76,7 +79,7 @@ macro_rules! impl_backend { #[inline(always)] fn encrypt_par_blocks(&self, mut blocks: InOut<'_, '_, ParBlocks>) { - let res = fixslice::$module::encrypt::(&self.rk, blocks.get_in()); + let res = fixslice::$module::encrypt(&self.rk_native, blocks.get_in()); *blocks.get_out() = res; } } @@ -84,7 +87,7 @@ macro_rules! impl_backend { impl BlockCipherDecBackend for $backend<'_> { #[inline(always)] fn decrypt_block(&self, mut block: InOut<'_, '_, Block>) { - let mut blocks = BatchBlocks::::default(); + let mut blocks = BatchBlocks::::default(); blocks[0] = block.clone_in(); let res = fixslice::$module::decrypt(&self.rk, &blocks); *block.get_out() = res[0]; @@ -92,7 +95,7 @@ macro_rules! impl_backend { #[inline(always)] fn decrypt_par_blocks(&self, mut blocks: InOut<'_, '_, ParBlocks>) { - let res = fixslice::$module::decrypt::(&self.rk, blocks.get_in()); + let res = fixslice::$module::decrypt(&self.rk_native, blocks.get_in()); *blocks.get_out() = res; } } From 3bc62dc6d2f64bcc3310464a87ac667e1e1d2879 Mon Sep 17 00:00:00 2001 From: Artyom Pavlov Date: Mon, 14 Sep 2026 13:24:06 +0300 Subject: [PATCH 6/7] tweak CI job --- .github/workflows/aes.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/aes.yml b/.github/workflows/aes.yml index dcb3a5e69..a65c7c0e7 100644 --- a/.github/workflows/aes.yml +++ b/.github/workflows/aes.yml @@ -248,7 +248,6 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - - run: ${{ matrix.deps }} - run: cargo test - run: cargo test --all-features From 63685b3716d4616fd177f4a4bc272168b3a28e90 Mon Sep 17 00:00:00 2001 From: Artyom Pavlov Date: Mon, 14 Sep 2026 13:27:06 +0300 Subject: [PATCH 7/7] Add changelog entry --- aes/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aes/CHANGELOG.md b/aes/CHANGELOG.md index c161d6730..802c03925 100644 --- a/aes/CHANGELOG.md +++ b/aes/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.9.4 (UNRELEASED) +### Changed +- Reduce size of `Aes128/192/256` structs by using compact round key representation + for software backend ([#594]) + +[#594]: https://github.com/RustCrypto/block-ciphers/pull/594 + ## 0.9.3 (2026-08-28) ### Changed - MSRV bumped to 1.89 ([#580])