From a47083a888eec59388a4dc1fa54dcfe7c84ab2ae Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 13 Oct 2026 12:26:33 +1100 Subject: [PATCH 1/3] perf: use encrypt-only Aes128Enc for the PRF, PRNG and hash ore-rs never decrypts with AES, so Aes128 computed a decryption key schedule on every key setup for nothing. The legacy comparator keys a fresh cipher per call, so it gains most: 4-20% faster compares on aes 0.8 (Apple M4). Output is unchanged; the pinned vectors still pass. --- packages/ore-rs/src/primitives/cmac.rs | 2 +- packages/ore-rs/src/primitives/hash.rs | 12 ++++++------ packages/ore-rs/src/primitives/prf.rs | 6 +++--- packages/ore-rs/src/primitives/prp/prng.rs | 10 +++++----- packages/ore-rs/src/scheme/bit2.rs | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/ore-rs/src/primitives/cmac.rs b/packages/ore-rs/src/primitives/cmac.rs index d4c89ceb..fb9b4b98 100644 --- a/packages/ore-rs/src/primitives/cmac.rs +++ b/packages/ore-rs/src/primitives/cmac.rs @@ -203,7 +203,7 @@ mod tests { /// NIST construction without reusing the accumulator's state threading, so /// it can validate that threading for any block count (not just t∈{1,4}). fn reference_cmac(key: &[u8; 16], blocks: &[[u8; 16]]) -> [u8; 16] { - let cipher = aes::Aes128::new(GenericArray::from_slice(key)); + let cipher = aes::Aes128Enc::new(GenericArray::from_slice(key)); let enc = |mut b: [u8; 16]| { cipher.encrypt_block(GenericArray::from_mut_slice(&mut b)); b diff --git a/packages/ore-rs/src/primitives/hash.rs b/packages/ore-rs/src/primitives/hash.rs index 089dd8a3..62c63816 100644 --- a/packages/ore-rs/src/primitives/hash.rs +++ b/packages/ore-rs/src/primitives/hash.rs @@ -1,17 +1,17 @@ use crate::primitives::{AesBlock, Hash, HashKey}; use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}; -use aes::Aes128; +use aes::Aes128Enc; use zeroize::ZeroizeOnDrop; #[derive(ZeroizeOnDrop)] pub struct Aes128Z2Hash { - cipher: Aes128, + cipher: Aes128Enc, } impl Hash for Aes128Z2Hash { fn new(key: &HashKey) -> Self { let key_array = GenericArray::from_slice(key); - let cipher = Aes128::new(key_array); + let cipher = Aes128Enc::new(key_array); Self { cipher } } @@ -67,10 +67,10 @@ pub struct FixedPiZ2Hash { /// evaluate H with no key material). pub const PI_KEY: [u8; 16] = *b"ORE-rs.v2.H-pi.1"; -fn pi() -> &'static Aes128 { +fn pi() -> &'static Aes128Enc { use std::sync::OnceLock; - static PI: OnceLock = OnceLock::new(); - PI.get_or_init(|| Aes128::new(GenericArray::from_slice(&PI_KEY))) + static PI: OnceLock = OnceLock::new(); + PI.get_or_init(|| Aes128Enc::new(GenericArray::from_slice(&PI_KEY))) } /// GF(2^128) doubling `2·x` on a big-endian field element packed in a `u128` diff --git a/packages/ore-rs/src/primitives/prf.rs b/packages/ore-rs/src/primitives/prf.rs index 2edf5656..ec23b20f 100644 --- a/packages/ore-rs/src/primitives/prf.rs +++ b/packages/ore-rs/src/primitives/prf.rs @@ -1,11 +1,11 @@ use crate::primitives::{AesBlock, Prf, PrfKey}; use aes::cipher::{BlockEncrypt, KeyInit}; -use aes::Aes128; +use aes::Aes128Enc; use zeroize::ZeroizeOnDrop; #[derive(Debug, ZeroizeOnDrop)] pub struct Aes128Prf { - cipher: Aes128, + cipher: Aes128Enc, } /* @@ -15,7 +15,7 @@ pub struct Aes128Prf { impl Prf for Aes128Prf { fn new(key: &PrfKey) -> Self { //let key_array = GenericArray::from_slice(key); - let cipher = Aes128::new(key); + let cipher = Aes128Enc::new(key); Self { cipher } } diff --git a/packages/ore-rs/src/primitives/prp/prng.rs b/packages/ore-rs/src/primitives/prp/prng.rs index b16f8b88..c99cee76 100644 --- a/packages/ore-rs/src/primitives/prp/prng.rs +++ b/packages/ore-rs/src/primitives/prp/prng.rs @@ -1,9 +1,9 @@ use aes::cipher::{consts::U16, generic_array::GenericArray, BlockEncrypt, KeyInit}; -use aes::Aes128; +use aes::Aes128Enc; use zeroize::{Zeroize, ZeroizeOnDrop}; pub struct Aes128Prng { - cipher: Aes128, + cipher: Aes128Enc, data: [GenericArray; 16], ptr: usize, // flat index into the 256 buffered bytes (block = ptr >> 4) ctr: u32, // increments with each new encryption @@ -35,13 +35,13 @@ impl Drop for Aes128Prng { impl ZeroizeOnDrop for Aes128Prng {} // The drop-time wipe of `cipher` (the AES key schedule) relies entirely on -// `aes::Aes128: ZeroizeOnDrop`, which is gated behind the `aes` crate's +// `aes::Aes128Enc: ZeroizeOnDrop`, which is gated behind the `aes` crate's // `zeroize` feature. Assert it at compile time so the build fails fast if that // feature is ever lost — otherwise the key-schedule wipe would silently vanish // while this type still (falsely) claims `ZeroizeOnDrop`. const _: fn() = || { fn assert_zeroize_on_drop() {} - assert_zeroize_on_drop::(); + assert_zeroize_on_drop::(); }; /* @@ -52,7 +52,7 @@ const _: fn() = || { impl Aes128Prng { pub fn init(key: &[u8]) -> Self { let key_array = GenericArray::from_slice(key); - let cipher = Aes128::new(key_array); + let cipher = Aes128Enc::new(key_array); let mut prng = Self { cipher, data: Default::default(), diff --git a/packages/ore-rs/src/scheme/bit2.rs b/packages/ore-rs/src/scheme/bit2.rs index 3f598926..a881a4ea 100644 --- a/packages/ore-rs/src/scheme/bit2.rs +++ b/packages/ore-rs/src/scheme/bit2.rs @@ -38,7 +38,7 @@ pub struct OreAes128 { rng: RefCell, } -// Opaque Debug: never render key material. (`Aes128`'s own Debug is already +// Opaque Debug: never render key material. (`Aes128Enc`'s own Debug is already // opaque, but spell it out so the guarantee can't regress.) impl std::fmt::Debug for OreAes128 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { From a9558db364bae8d64732b5ab1b2ce4ce545a4fa6 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 16 Oct 2026 14:23:17 +1100 Subject: [PATCH 2/3] fix(primitives): opaque Debug for Aes128Prf; crate-level Aes128Enc zeroize assertion From the review of #109: - `Aes128Prf` derived `Debug`, so whether it rendered key material depended on `Aes128Enc`'s Debug staying opaque across `aes` upgrades. It now has the same hand-written opaque impl as `OreAes128`. - The compile-time `Aes128Enc: ZeroizeOnDrop` assertion lived in the PRNG module but guards every AES key schedule in the crate (PRF, hash, PRNG, CMAC, the PRPs' local ciphers). It moves to `primitives.rs` so a refactor of the PRNG cannot take it away. - Drop a redundant `aes::` path in the CMAC reference test. --- packages/ore-rs/src/primitives.rs | 13 +++++++++++++ packages/ore-rs/src/primitives/cmac.rs | 2 +- packages/ore-rs/src/primitives/prf.rs | 11 ++++++++++- packages/ore-rs/src/primitives/prp/prng.rs | 10 ++-------- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/ore-rs/src/primitives.rs b/packages/ore-rs/src/primitives.rs index 9ec6ba44..de24112e 100644 --- a/packages/ore-rs/src/primitives.rs +++ b/packages/ore-rs/src/primitives.rs @@ -8,6 +8,19 @@ pub(crate) mod stack; use aes::cipher::{consts::U16, generic_array::GenericArray}; use aes::Block; use thiserror::Error; +use zeroize::ZeroizeOnDrop; + +// Every AES key schedule in the crate is an `aes::Aes128Enc`: the PRF, the Z2 +// hash, the PRP's PRNG, the CMAC accumulator and the per-block ciphers the +// PRPs key locally. Each is wiped on drop only because `Aes128Enc: +// ZeroizeOnDrop`, which the `aes` crate gates behind its `zeroize` feature. +// Assert it here, once, so the build fails fast if that feature is ever lost; +// otherwise the key-schedule wipe would silently vanish while the types above +// still (falsely) claimed `ZeroizeOnDrop` (ZA-0001). +const _: fn() = || { + fn assert_zeroize_on_drop() {} + assert_zeroize_on_drop::(); +}; pub type AesBlock = Block; pub type PrfKey = GenericArray; pub type HashKey = GenericArray; diff --git a/packages/ore-rs/src/primitives/cmac.rs b/packages/ore-rs/src/primitives/cmac.rs index fb9b4b98..c31a0b97 100644 --- a/packages/ore-rs/src/primitives/cmac.rs +++ b/packages/ore-rs/src/primitives/cmac.rs @@ -203,7 +203,7 @@ mod tests { /// NIST construction without reusing the accumulator's state threading, so /// it can validate that threading for any block count (not just t∈{1,4}). fn reference_cmac(key: &[u8; 16], blocks: &[[u8; 16]]) -> [u8; 16] { - let cipher = aes::Aes128Enc::new(GenericArray::from_slice(key)); + let cipher = Aes128Enc::new(GenericArray::from_slice(key)); let enc = |mut b: [u8; 16]| { cipher.encrypt_block(GenericArray::from_mut_slice(&mut b)); b diff --git a/packages/ore-rs/src/primitives/prf.rs b/packages/ore-rs/src/primitives/prf.rs index ec23b20f..ee59404e 100644 --- a/packages/ore-rs/src/primitives/prf.rs +++ b/packages/ore-rs/src/primitives/prf.rs @@ -3,11 +3,20 @@ use aes::cipher::{BlockEncrypt, KeyInit}; use aes::Aes128Enc; use zeroize::ZeroizeOnDrop; -#[derive(Debug, ZeroizeOnDrop)] +#[derive(ZeroizeOnDrop)] pub struct Aes128Prf { cipher: Aes128Enc, } +// Opaque Debug: never render key material. `Aes128Enc`'s own Debug is opaque +// today, but a derive would depend on that staying true across `aes` +// upgrades, so spell it out (as `OreAes128` does). +impl std::fmt::Debug for Aes128Prf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Aes128Prf").finish_non_exhaustive() + } +} + /* * This can be made a whole lot simpler * when the AES crate supports const generics and we don't have to deal with GenericArray. diff --git a/packages/ore-rs/src/primitives/prp/prng.rs b/packages/ore-rs/src/primitives/prp/prng.rs index c99cee76..c097f4a0 100644 --- a/packages/ore-rs/src/primitives/prp/prng.rs +++ b/packages/ore-rs/src/primitives/prp/prng.rs @@ -35,14 +35,8 @@ impl Drop for Aes128Prng { impl ZeroizeOnDrop for Aes128Prng {} // The drop-time wipe of `cipher` (the AES key schedule) relies entirely on -// `aes::Aes128Enc: ZeroizeOnDrop`, which is gated behind the `aes` crate's -// `zeroize` feature. Assert it at compile time so the build fails fast if that -// feature is ever lost — otherwise the key-schedule wipe would silently vanish -// while this type still (falsely) claims `ZeroizeOnDrop`. -const _: fn() = || { - fn assert_zeroize_on_drop() {} - assert_zeroize_on_drop::(); -}; +// `aes::Aes128Enc: ZeroizeOnDrop`, asserted at compile time in +// `primitives.rs` for every type in the crate that keys one. /* * Buffers 256 AES-CTR keystream bytes at a time and regenerates a fresh batch From d06ac5d9d27634e6fd02741023ca5e7a39d12ebb Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 19 Oct 2026 10:44:09 +1100 Subject: [PATCH 3/3] docs(primitives): list the subkey derivation among the Aes128Enc users; point OreAes128's Debug note at Aes128Prf Review of #109 (nits James marked for a follow-up, done here): after the restack onto #114, `scheme::keys::aes_block` also keys a local `Aes128Enc` whose wipe depends on the crate-level ZeroizeOnDrop assertion, so it is in that comment's list. `OreAes128` holds two `Aes128Prf`s, not an `Aes128Enc`, and `Aes128Prf` now has its own opaque Debug; the comment on `OreAes128`'s Debug says so. --- packages/ore-rs/src/primitives.rs | 4 ++-- packages/ore-rs/src/scheme/bit2.rs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ore-rs/src/primitives.rs b/packages/ore-rs/src/primitives.rs index de24112e..a1fc249d 100644 --- a/packages/ore-rs/src/primitives.rs +++ b/packages/ore-rs/src/primitives.rs @@ -11,8 +11,8 @@ use thiserror::Error; use zeroize::ZeroizeOnDrop; // Every AES key schedule in the crate is an `aes::Aes128Enc`: the PRF, the Z2 -// hash, the PRP's PRNG, the CMAC accumulator and the per-block ciphers the -// PRPs key locally. Each is wiped on drop only because `Aes128Enc: +// hash, the PRP's PRNG, the CMAC accumulator, the per-block ciphers the PRPs +// key locally and the v2 subkey derivation (`scheme::keys::aes_block`). Each is wiped on drop only because `Aes128Enc: // ZeroizeOnDrop`, which the `aes` crate gates behind its `zeroize` feature. // Assert it here, once, so the build fails fast if that feature is ever lost; // otherwise the key-schedule wipe would silently vanish while the types above diff --git a/packages/ore-rs/src/scheme/bit2.rs b/packages/ore-rs/src/scheme/bit2.rs index a881a4ea..ba27c118 100644 --- a/packages/ore-rs/src/scheme/bit2.rs +++ b/packages/ore-rs/src/scheme/bit2.rs @@ -38,8 +38,9 @@ pub struct OreAes128 { rng: RefCell, } -// Opaque Debug: never render key material. (`Aes128Enc`'s own Debug is already -// opaque, but spell it out so the guarantee can't regress.) +// Opaque Debug: never render key material. (`Aes128Prf`, which holds each +// key, already has an opaque Debug, but spell it out here too so the +// guarantee can't regress.) impl std::fmt::Debug for OreAes128 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("OreAes128").finish_non_exhaustive()