Skip to content

perf: use encrypt-only Aes128Enc for the PRF, PRNG and hash - #109

Open
coderdan wants to merge 2 commits into
mainfrom
perf/aes128enc
Open

coderdan wants to merge 2 commits into
mainfrom
perf/aes128enc

Conversation

@coderdan

@coderdan coderdan commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes #112.

ore-rs only ever encrypts with AES (PRF, PRNG, Z2 hash, CMAC test reference), but it used aes::Aes128, which on the hardware backends (AES-NI, ARMv8 with aes_armv8) also builds the decryption key schedule on every key setup. This switches those to the encrypt-only aes::Aes128Enc.

The saving is hardware-backend only. On the software (fixsliced) backend, aes 0.8.4 defines Aes128Enc { inner: Aes128 } over one key schedule shared by both directions, so struct size and key-setup cost are unchanged there; a downstream aarch64 build without --cfg aes_armv8 sees none of the numbers below.

The legacy comparator (OreAes128, plus the chrono and decimal wrappers) keys a fresh cipher with the ciphertext nonce on every comparison, so it gains the most.

Benchmarks

Apple M4, rustc 1.89, aes 0.8.4 with aes_armv8. Interleaved A E A E runs; A (main) varied at most 1–2% between runs. Medians, mean of two runs:

bench main this PR
compare-4 161 ns 130 ns (−19%)
compare-8 233 ns 187 ns (−20%)
compare-date 141 ns 129 ns (−8%)
compare-datetime 257 ns 245 ns (−5%)
compare-decimal 282 ns 272 ns (−4%)
encrypt-8 17.76 µs 17.18 µs (−3%)
encrypt-left-8 12.95 µs 12.12 µs (−6%)
chained / bit6 encrypt and compare within ±1%

compare-decimal-slice reads +45% (286 → 415 ns), but that's not AES: in this build LLVM inlines compare_raw_slices into that one bench binary, and the same jump shows up with any aes version whenever it does. The non-slice compare-decimal row improves. On an M1 Max with rustc 1.78 (interleaved, two rounds) both rows improve: compare-decimal 450/455 → 425/429 ns, compare-decimal-slice 454/463 → 427/427 ns, so the slice reading above is specific to that build. The root-cause write-up for the aes 0.9 regression (docs/benchmarks/2026-10-13-aes-0.9-root-cause.md) lands with #108, not this PR.

This also matters for the aes 0.9 upgrade (#106). From 0.9.0, dropping a cipher wipes the whole struct byte by byte, so a smaller cipher is cheaper to drop. On upstream aes master, Aes128Enc takes the legacy compare cost from +21–60% over 0.8 down to +1–29%.

Safety

  • Output unchanged: these are the same AES encryptions, and all pinned ciphertext vectors pass.
  • Zeroize unchanged: Aes128Enc implements ZeroizeOnDrop under the same zeroize feature. The compile-time assertion in prng.rs now checks Aes128Enc, and the drop-zeroize tests pass.
  • Checks: clippy (-D warnings), fmt and the full test suite pass on 1.78.

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.
…roize 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.
@coderdan

Copy link
Copy Markdown
Contributor Author

Code review findings (/code-review high)

No correctness bugs. The diff is a mechanical Aes128 → Aes128Enc swap; the review confirmed no BlockDecrypt/Aes128Dec use anywhere, that aes 0.8.4's Aes128Enc implements ZeroizeOnDrop on every backend, and that cmac, prp and chained already used it. Six precision and cleanup items:

# Finding Status
1 The saving is hardware-backend only. On the software (fixsliced) backend aes 0.8.4 defines Aes128Enc { inner: Aes128 } over one shared key schedule, so a downstream aarch64 build without --cfg aes_armv8 gains nothing. PR body and #112 stated the cost as universal. Fixed in the PR body and #112: claim scoped to AES-NI and ARMv8 backends.
2 Aes128Prf derived Debug, so not rendering key material depended on Aes128Enc's Debug staying opaque across aes upgrades; OreAes128 spells out an opaque impl for exactly this reason. Fixed (f6e403b): hand-written finish_non_exhaustive() impl.
3 The compile-time Aes128Enc: ZeroizeOnDrop assertion lived in prng.rs but guards every AES key schedule in the crate (PRF, hash, PRNG, CMAC, the PRPs' local ciphers); a PRNG refactor could silently drop it. Fixed (f6e403b): hoisted to primitives.rs with a comment naming every dependent.
4 The +45% compare-decimal-slice reading (286 → 415 ns on M4, rustc 1.89) was attributed to LLVM inlining compare_raw_slices into that bench binary, with no inline attribute pinning it. Measured here, not changed. Interleaved A/B on an M1 Max with rustc 1.78 and aes_armv8, two rounds each: compare-decimal 450/455 ns → 425/429 ns, compare-decimal-slice 454/463 ns → 427/427 ns. Both improve ~5.5%; no regression on this machine, consistent with a codegen artefact on the M4 build. No #[inline]/#[inline(never)] added: pinning codegen on a hunch would change every caller's inlining, and the bench is not a public contract.
5 Touched line kept the fully-qualified aes::Aes128Enc::new though use super::* already imports it. Fixed (f6e403b).
6 PR body and #112 pointed at docs/benchmarks/2026-10-13-aes-0.9-root-cause.md, which is on #108, not this branch or main. Fixed in the PR body and #112: the document is named as landing with #108.

After the fixes: clippy clean, 112 lib tests and all integration tests green, pinned vectors unchanged.

@coderdan
coderdan requested a review from freshtonic October 16, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: key encrypt-only Aes128Enc in the PRF, PRNG and hash (no unused decryption schedule)

1 participant