diff --git a/CLAUDE.md b/CLAUDE.md
index 4d26b7b15..a5583b749 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -106,7 +106,7 @@ The workspace has four top-level kinds of member:
- `crypto/core` defines the abstract traits (`Hash`, `KDF`, `MAC`, `KEM`, `Signature`, `RNG`, `Algorithm`, `HashAlgParams`, …), error enums (`HashError`, `KDFError`, `KEMError`, `MACError`, `RNGError`, `SignatureError`), and the `KeyMaterial` / `KeyType` wrapper that all sensitive byte buffers are required to use (see `Secret` super-trait requirement in QUALITY_AND_STYLE.md).
- `crypto/core-test-framework` contains the shared per-trait test suite (`hash.rs`, `kdf.rs`, `kem.rs`, `mac.rs`, `signature.rs`). New implementations of a core trait must be exercised through this framework — it's how trait conformance and error-condition coverage stay consistent across implementations.
-- `crypto/factory` provides enum-based string-name factories (`HashFactory`, `KDFFactory`, `MACFactory`, `RNGFactory`, `XOFFactory`). Each factory enum impls the underlying trait so it can be used transparently as that primitive, and each impls `AlgorithmFactory` (`new(name)`, `default_128_bit()`, `default_256_bit()`). When adding a new primitive that fits an existing trait, register it in the corresponding factory.
+- `crypto/factory` provides enum-based string-name factories (`HashFactory`, `KDFFactory`, `MACFactory`, `RNGFactory`, `XOFFactory`). Each factory enum impls the underlying trait so it can be used transparently as that primitive, except `MACFactory`, which is a key enum implementing the MAC role traits and handing out a `MACFactoryWorker` that impls `MAC`; and each impls `AlgorithmFactory` (`new(name)`, `default_128_bit()`, `default_256_bit()`). When adding a new primitive that fits an existing trait, register it in the corresponding factory.
Trait/factory/CLI is the standard layering: a new algorithm typically requires (a) the primitive crate, (b) implementing the relevant `core` trait, (c) wiring it into the matching factory, (d) a CLI subcommand in `cli/src/*_cmd.rs` registered in `cli/src/main.rs`.
@@ -173,10 +173,12 @@ taught, as mechanics to apply next time a cipher type is shaped or reshaped:
struct under the spec-spelled name (`Ascon_AEAD128
`, `#[allow(non_camel_case_types)]`)
is the whole public type, with no CamelCase twin and no alias to it.
- **The trait pair is the API.** Do not keep an inherent streaming or one-shot API beside the
- `AEADCipher*` / `SymmetricCipher*` impls; everything a caller needs, including a supplied nonce
- (`do_encrypt_init_nonce`, `encrypt_with_aad_nonce_out`), is on the traits. The only inherent
- methods a cipher adds are for what the traits lack, in-place streaming (`do_*_inplace`), and
- their decrypt form keeps the same tag hold-back as `do_decrypt_out` so the two interleave.
+ `AEADCipher*` / `SymmetricCipher*` impls; everything a caller needs is on the traits, and
+ everything that takes a key or draws a nonce, including a supplied nonce
+ (`create_encryptor_nonce`, `encrypt_with_aad_nonce_out`), is on the key object. The only
+ inherent methods a cipher adds are for what the traits lack, in-place streaming
+ (`do_*_inplace`), and their decrypt form keeps the same tag hold-back as `do_decrypt_out` so
+ the two interleave.
- **Private helpers must not share a trait method's name.** An inherent `do_encrypt_final` with a
different signature from the trait's is what forced the wrapper newtypes; name the core
helpers for what they do (`absorb_aad`, `encrypt_bytes`, `finish_tag`, `verify_tag`).
@@ -194,9 +196,57 @@ taught, as mechanics to apply next time a cipher type is shaped or reshaped:
a CLI path, run it against a known-answer vector (`--nonce` on `ascon-aead128` reproduces the
LWC KAT tag).
- **A rename stays in the crate it was asked for.** `do_*_update` to `do_*_inplace` was done for
- Ascon; the CCM packet type still has its namesake. Note the follow-up rather than widening the
+ Ascon; the CCM type still has its namesake. Note the follow-up rather than widening the
change.
+## Lessons from the key-object rework (October 2026)
+
+The symmetric cipher and MAC traits lost their constructors and key-taking one-shots; keys took them.
+What that taught, as mechanics for the next trait that sheds a constructor:
+
+- **The trait describes a worker that exists.** `SymmetricCipherEncryptor`, `AEADCipherEncryptor`,
+ `BlockCipherEncryptor`, `StreamCipherEncryptor`, their decrypting halves and `MAC` carry only
+ streaming methods and length helpers. Construction is a key object's (`create_encryptor(rng)`,
+ `create_decryptor(init)`, `create_generator()`), and the one-shots are provided on the role
+ traits in `core::symmetric_key` over those. `KEY_LEN` is on the role and mode traits, never on
+ a worker trait.
+- **Mode types carry the role.** A key is `AESKey`; which role pairs it implements is
+ decided by `M`'s mode-trait impls in `core::hazmat::mode` (`SymmetricEncryptMode`,
+ `BlockEncryptMode`, `EcbEncryptMode`, `AeadEncryptMode`, `AeadDeclaredEncryptMode`, `MacMode`).
+ Padding and frame parameters live in the mode type (`CBC`, `CBC`,
+ `ECB`, `CTR<12>`, `CCM_Packet<12, 16, 64, 1024>`), not on a worker alias.
+- **The key length stops at the key.** `BlockPermutation` carries the block methods
+ and `ElectronicCodeBook` adds only `new`, so a worker and a mode's
+ `Mode` impl bound on the former and never name a key length; the GCM, GMAC and CMAC impls
+ are generic in `cipher` for that reason. `KEY_LEN` stays on the mode traits' worker-producing
+ halves as the tie between a key's length and its permutation's, through
+ `P: ElectronicCodeBook`, which is what keeps a key object's role impls to the one
+ length. A mode that must know the block length (`CMAC`, `CTR`) takes
+ it as a parameter, since stable Rust cannot read it off the permutation inside the impl.
+- **Test the key, not the worker pair.** The framework runners are `test_key` over a role pair
+ and the construction suites in `core_test_framework::from_key_material`; a test of "a key of the
+ wrong type is rejected" belongs to the construction suite, once, not to each mode's tests.
+ Known-answer suites keep pinning their IV through `create_encryptor` and a `FixedSeedRNG`.
+- **Workers borrow the key.** The role and mode traits hand out workers through a generic
+ associated type with a lifetime, `type Encryptor<'k>`, and the cipher-crate workers hold
+ `&'k` to the mode's key state, so `create_encryptor` copies no schedule and a worker cannot
+ outlive its key. Spell the mode traits' GAT bound `where P: 'k`, not `where Self::KeyState: 'k`:
+ rustc rejects a generic impl whose key state is a struct over `P` (E0276) under the latter. A
+ function handed `key: &'k K` that names the worker is generic over `'k`; one that builds its
+ key locally bounds `for<'k> K::Encryptor<'k>: ..` with `K: 'static`. A test binds the key
+ before the worker, since `key_fn().create_encryptor(..)` borrows a temporary.
+- **Resume takes the key object.** `SuspendableKeyed<'k, N>` and `SuspendableComponent<'k>` take
+ the key by `&'k`: a cipher-mode worker's `Key` is the mode's `KeyState`, reached through
+ `hazmat::ModeKey::state`, HMAC's is `HmacKey`, Ascon's is `AsconAEAD128Key`, and HKDF's stays
+ the salt material, since HKDF is not a key object. The suspended layouts did not change. A
+ worker that owns a copy of its key (Ascon, KMAC) implements the traits for every `'k` and
+ ignores it. A struct cannot hold a worker that borrows a key it also owns: HKDF runs its
+ extract-phase HMAC as the two halves `HmacKey::inner_state` / `finish_out`, and the MAC
+ factory is a key enum that hands out a worker enum.
+- **What has no key-path twin is dropped, and said so.** `new_allow_weak_key` on a cipher-based
+ MAC, a wrong-length `KeyMaterial512` into a fixed-length key, GCM's verify-before-decrypt
+ one-shot override: each went with a line in the commit message rather than a shim.
+
## Lessons from the signature key rework (November 2026)
The signature traits moved construction from the algorithm type onto the key objects, as the
@@ -276,4 +326,16 @@ external vector suites — is specified in QUALITY_AND_STYLE.md and CONTRIBUTING
## CI
-The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job — local `cargo test --workspace` is the gate, and nothing but a developer running it stands between a broken test and `main`.
\ No newline at end of file
+Five workflows under `.github/workflows/`, all triggered by pull requests:
+
+- `rust-build.yml`: `cargo build --workspace --all-targets --all-features` on stable.
+- `rust-test.yml`: `cargo test --all` on stable.
+- `rust-style.yml`: `cargo fmt --all --check` on **nightly** rustfmt, so format before every commit.
+- `rust-docs.yml`: `cargo doc --all` on stable.
+- `publish_doc_benches_to_ghpages.yaml`: builds rustdoc and runs `quality_stats.sh`; on `main` it also
+ publishes docs and code stats to GitHub Pages (`https://bcgit.github.io/bc-rust/`). Its bench step is
+ commented out because the benches are too slow on the hosted runner, so benchmark figures come from a
+ developer's machine, not CI.
+
+`cargo test --all` is the workspace run, so a failing test does block a PR; what CI does not run is
+`cargo mutants`, the CLI shell tests under `cli/tests/`, or rustdoc with warnings denied.
\ No newline at end of file
diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md
index 007c761fe..6d3591a51 100644
--- a/QUALITY_AND_STYLE.md
+++ b/QUALITY_AND_STYLE.md
@@ -12,9 +12,13 @@ lib.rs for all crates needs to contain: `#![forbid(missing_docs)]`, `#![no_std]`
All primitives must be accompanied by a CLI in `/cli`.
-All algorithms with a state that is exercised across multiple API calls -- typically through a `do_update()`,
-`do_final()` pattern -- should implement SerializableState so that the user can pause the execution of this algorithm to
-a cache and resume later.
+Hashes, XOFs and the MACs and KDFs built on them (KMAC, HMAC, HKDF) implement `Suspendable` or `SuspendableKeyed`, so
+that a computation in progress can be written to a cache and resumed later. Cipher and MAC workers -- the encryptors,
+decryptors, generators and verifiers a key object hands out -- do not: a suspended or cloned worker is a second copy of
+a nonce, counter or keystream position, and two copies that both go on to encrypt reuse keystream (#173). Those workers
+implement neither suspend trait nor `Clone`; a message is paused by holding its worker, and the worker traits are kept
+`dyn`-compatible so it can be held as `&mut dyn` or, where allocation is acceptable, `Box`. The suspend and `Clone`
+impls the cipher workers still carry come out in the #173 sweep.
`Clone` belongs on the types, not on the `core` traits. Every hash and XOF type is `Clone`, so a computation can be
forked mid-stream and each copy finished on its own; `Hash` itself carries no `Clone` (and no `Default`), because both
@@ -96,7 +100,13 @@ In addition, some library-specific naming conventions:
## APIs
Where possible, primitives should expose "one-shot APIs" that simply take data and return a result as a static member
-function that does not require object instantiation.
+function that does not require object instantiation. The exception is anything keyed: a symmetric cipher or MAC is
+reached through a key object (`AES128Key`, `HmacKey`, `KMAC128Key`, `AsconAEAD128Key`), built once from
+`KeyMaterial` by `FromKeyMaterial` / `FromAnyKeyMaterial`, and its one-shots and `create_*` constructors are methods on
+that key. The cipher and MAC traits in `core::traits` describe a worker that already exists and carry no constructor
+and no key-taking method, so a key's validation and expansion happen once, where the key is made, rather than per call.
+A worker borrows the key it came from, for the lifetime on the role trait's associated worker type, so making one
+copies no schedule and a worker cannot outlive its key.
Other version of Bouncy Castle have a design pattern where stateful objects follow a pattern of new () -> init () ->
do_update () -> do_final (), and then optionally reset () that sets the object back to an unitialized state. Instead,
@@ -131,7 +141,10 @@ Take, for example a one-shot API `fn encrypt(plaintext: &[u8]) -> Result
The following prefixes can be applied:
* `do_`: this implies that it is part of a stateful streaming API, will typically take `&mut self`, and is likely
- accompanied by a `do_encrypt_init()` and `do_encrypt_final()`.
+ accompanied by a `do_encrypt_final()`; the object comes from a key's `create_encryptor()`.
+* `create_`: a key object's constructor for a worker -- `create_encryptor(&self, rng)`, `create_decryptor(&self,
+ init_data)`, `create_generator(&self)`, `create_verifier(&self)`. Whatever draws randomness takes a `&mut dyn RNG`
+ explicitly, so a key has no `_rng` twins and no method that reaches for a default RNG.
The following suffixes can be applied
@@ -142,8 +155,10 @@ The following suffixes can be applied
`_final` indicates that this function call renders the object unusable afterwards by consuming `self` via a move:
`_final(self, ..)`.
* `_rng`: indicates that this version of the function sources its random numbers from a provided `&mut dyn RNG` instead
- of
- from the default library RNG. `_rng(.., &mut dyn RNG)`.
+ of from the default library RNG. `_rng(.., &mut dyn RNG)`. Keyed primitives do not have it: their constructors and
+ one-shots take the RNG, full stop.
+* `_nonce`: on an AEAD key, the twin that takes the nonce from the caller instead of drawing it from the RNG, and so
+ does not return one.
* `_out / _inplace`: indicates that the function works in the provided buffer. `_out` indicates that the function takes
an output buffer, which may be oversized, and returns the number of bytes written to it:
`_out(.., out: &mut [u8] -> Result`. `_inplace` indicates that the input and output are
diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs
index 8f2d8bd2e..f5026eb86 100644
--- a/cli/src/aes_cbc_cmd.rs
+++ b/cli/src/aes_cbc_cmd.rs
@@ -10,13 +10,13 @@
//! separately.
use crate::helpers::block_mode_helpers::{
- BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key,
+ BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key_object,
+ start_encryptor,
};
-use bouncycastle::aes::{AES_CBC_128, AES_CBC_192, AES_CBC_256};
+use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
+use bouncycastle::cipher::modes::CBC;
use bouncycastle::cipher::padding::PKCS7;
-use bouncycastle::cipher::{Decrypting, Encrypting};
-use bouncycastle::core::key_material::KeyMaterial;
-use bouncycastle::core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor};
+use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey};
pub(crate) fn aes128_cbc_cmd(
action: &CipherDirection,
@@ -26,9 +26,9 @@ pub(crate) fn aes128_cbc_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run::, AES_CBC_128, 16>(
+ run::<_, 16>(
action,
- &load_key::<16>(key, key_file, "AES-128"),
+ &load_key_object::>, 16>(key, key_file, "AES-128"),
load_iv::(iv, iv_file),
output_hex,
);
@@ -42,9 +42,9 @@ pub(crate) fn aes192_cbc_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run::, AES_CBC_192, 24>(
+ run::<_, 24>(
action,
- &load_key::<24>(key, key_file, "AES-192"),
+ &load_key_object::>, 24>(key, key_file, "AES-192"),
load_iv::(iv, iv_file),
output_hex,
);
@@ -58,30 +58,31 @@ pub(crate) fn aes256_cbc_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run::, AES_CBC_256, 32>(
+ run::<_, 32>(
action,
- &load_key::<32>(key, key_file, "AES-256"),
+ &load_key_object::>, 32>(key, key_file, "AES-256"),
load_iv::(iv, iv_file),
output_hex,
);
}
-/// Dispatches to the shared streaming loops.
-fn run(
+/// Dispatches to the shared streaming loops under the key's padded CBC mode.
+fn run(
action: &CipherDirection,
- key: &KeyMaterial,
+ key: &K,
iv: Option<[u8; BLOCK_LEN]>,
output_hex: bool,
) where
- E: SymmetricCipherEncryptor,
- D: SymmetricCipherDecryptor,
+ K: SymmetricEncryptionKey
+ + SymmetricDecryptionKey,
{
match action {
CipherDirection::Encrypt => {
- encrypt_stream::(key, iv, output_hex, None)
+ let (enc, generated_iv) = start_encryptor(iv, |rng| key.create_encryptor(rng));
+ encrypt_stream::<_, BLOCK_LEN>(enc, generated_iv.as_ref(), output_hex, None)
}
CipherDirection::Decrypt => {
- decrypt_stream::(key, iv, output_hex, None)
+ decrypt_stream::<_, BLOCK_LEN>(iv, |iv| key.create_decryptor(iv), output_hex, None)
}
}
}
diff --git a/cli/src/aes_ccm_cmd.rs b/cli/src/aes_ccm_cmd.rs
index 94a140a90..d2592dc08 100644
--- a/cli/src/aes_ccm_cmd.rs
+++ b/cli/src/aes_ccm_cmd.rs
@@ -20,8 +20,8 @@
//! `ascon-aead128`, are the streaming alternatives.
//!
//! The AAD is read whole too: the key's one-shot takes it as a slice. (A.2.2 needs its length
-//! before its first byte, so streaming it would need the declared-length constructor the packet
-//! type keeps for itself.)
+//! before its first byte, so streaming it would need the declared-length constructor the type
+//! keeps for itself.)
//!
//! # The nonce is supplied, not generated
//!
@@ -53,7 +53,7 @@ use bouncycastle::cipher::modes::CCM;
use bouncycastle::core::errors::SymmetricCipherError;
use bouncycastle::core::key_material::KeyMaterial;
use bouncycastle::core::symmetric_key::{
- AeadPacketDecryptionKey, AeadPacketEncryptionKey, FromKeyMaterial,
+ AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, FromKeyMaterial,
};
use bouncycastle::hex;
@@ -68,8 +68,8 @@ use crate::helpers::block_mode_helpers::{CipherDirection, load_key};
trait CcmKeys {
/// The key type at nonce length `N` and tag length `T`.
type Key: FromKeyMaterial
- + AeadPacketEncryptionKey
- + AeadPacketDecryptionKey;
+ + AeadDeclaredEncryptionKey
+ + AeadDeclaredDecryptionKey;
}
struct Aes128;
diff --git a/cli/src/aes_cfb8_cmd.rs b/cli/src/aes_cfb8_cmd.rs
index ee6690cdb..450b8eb1c 100644
--- a/cli/src/aes_cfb8_cmd.rs
+++ b/cli/src/aes_cfb8_cmd.rs
@@ -25,10 +25,12 @@
//! plaintext byte, corrupts the following 16 bytes, and then decryption resynchronises. Do not
//! decrypt data you have not authenticated separately.
-use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key};
+use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key_object};
use crate::helpers::stream_mode_helpers::run_stream_mode;
-use bouncycastle::aes::{AES_CFB8_128, AES_CFB8_192, AES_CFB8_256};
-use bouncycastle::cipher::{Decrypting, Encrypting};
+use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
+use bouncycastle::cipher::modes::CFB8;
+use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey};
+use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor};
pub(crate) fn aes128_cfb8_cmd(
action: &CipherDirection,
@@ -38,9 +40,9 @@ pub(crate) fn aes128_cfb8_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CFB8_128, 16, BLOCK_LEN>(
+ run::<_, 16>(
action,
- &load_key::<16>(key, key_file, "AES-128"),
+ &load_key_object::, 16>(key, key_file, "AES-128"),
load_iv::(iv, iv_file),
output_hex,
);
@@ -54,9 +56,9 @@ pub(crate) fn aes192_cfb8_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CFB8_192, 24, BLOCK_LEN>(
+ run::<_, 24>(
action,
- &load_key::<24>(key, key_file, "AES-192"),
+ &load_key_object::, 24>(key, key_file, "AES-192"),
load_iv::(iv, iv_file),
output_hex,
);
@@ -70,10 +72,25 @@ pub(crate) fn aes256_cfb8_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CFB8_256, 32, BLOCK_LEN>(
+ run::<_, 32>(
action,
- &load_key::<32>(key, key_file, "AES-256"),
+ &load_key_object::, 32>(key, key_file, "AES-256"),
load_iv::(iv, iv_file),
output_hex,
);
}
+
+/// Dispatches to the shared streaming loops with `Cfb8` filled in as the mode.
+fn run<'k, K, const KEY_LEN: usize>(
+ action: &CipherDirection,
+ key: &'k K,
+ iv: Option<[u8; BLOCK_LEN]>,
+ output_hex: bool,
+) where
+ K: SymmetricEncryptionKey
+ + SymmetricDecryptionKey,
+ K::Encryptor<'k>: StreamCipherEncryptor,
+ K::Decryptor<'k>: StreamCipherDecryptor,
+{
+ run_stream_mode::(action, key, iv, output_hex)
+}
diff --git a/cli/src/aes_cfb_cmd.rs b/cli/src/aes_cfb_cmd.rs
index 9cc88a6e2..d59398684 100644
--- a/cli/src/aes_cfb_cmd.rs
+++ b/cli/src/aes_cfb_cmd.rs
@@ -26,10 +26,12 @@
//! of the plaintext in the *same* block, so an attacker edits the block they aimed at, at the cost
//! of randomising the next one. Do not decrypt data you have not authenticated separately.
-use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key};
+use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key_object};
use crate::helpers::stream_mode_helpers::run_stream_mode;
-use bouncycastle::aes::{AES_CFB_128, AES_CFB_192, AES_CFB_256};
-use bouncycastle::cipher::{Decrypting, Encrypting};
+use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
+use bouncycastle::cipher::modes::CFB;
+use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey};
+use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor};
pub(crate) fn aes128_cfb_cmd(
action: &CipherDirection,
@@ -39,9 +41,9 @@ pub(crate) fn aes128_cfb_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CFB_128, 16, BLOCK_LEN>(
+ run::<_, 16>(
action,
- &load_key::<16>(key, key_file, "AES-128"),
+ &load_key_object::, 16>(key, key_file, "AES-128"),
load_iv::(iv, iv_file),
output_hex,
);
@@ -55,9 +57,9 @@ pub(crate) fn aes192_cfb_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CFB_192, 24, BLOCK_LEN>(
+ run::<_, 24>(
action,
- &load_key::<24>(key, key_file, "AES-192"),
+ &load_key_object::, 24>(key, key_file, "AES-192"),
load_iv::(iv, iv_file),
output_hex,
);
@@ -71,10 +73,25 @@ pub(crate) fn aes256_cfb_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CFB_256, 32, BLOCK_LEN>(
+ run::<_, 32>(
action,
- &load_key::<32>(key, key_file, "AES-256"),
+ &load_key_object::, 32>(key, key_file, "AES-256"),
load_iv::(iv, iv_file),
output_hex,
);
}
+
+/// Dispatches to the shared streaming loops with `Cfb` filled in as the mode.
+fn run<'k, K, const KEY_LEN: usize>(
+ action: &CipherDirection,
+ key: &'k K,
+ iv: Option<[u8; BLOCK_LEN]>,
+ output_hex: bool,
+) where
+ K: SymmetricEncryptionKey
+ + SymmetricDecryptionKey,
+ K::Encryptor<'k>: StreamCipherEncryptor,
+ K::Decryptor<'k>: StreamCipherDecryptor,
+{
+ run_stream_mode::(action, key, iv, output_hex)
+}
diff --git a/cli/src/aes_ctr_cmd.rs b/cli/src/aes_ctr_cmd.rs
index 742f333fc..66ac5be35 100644
--- a/cli/src/aes_ctr_cmd.rs
+++ b/cli/src/aes_ctr_cmd.rs
@@ -30,13 +30,17 @@
//!
//! A repeated nonce is fatal here rather than merely unwise: the same nonce under the same key
//! gives the same keystream, and two messages XORed with the same keystream leak their XOR. The
-//! nonce is drawn from the OS-backed DRBG for exactly that reason, and there is no way to supply
-//! one.
+//! nonce is drawn from the OS-backed DRBG for exactly that reason. `--iv` / `--iv-file` exist for
+//! reproducing published vectors and for formats that carry the nonce themselves; with them, its
+//! uniqueness under the key is the caller's to keep.
-use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key};
+use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object};
use crate::helpers::stream_mode_helpers::run_stream_mode;
-use bouncycastle::aes::{AES_CTR_128, AES_CTR_192, AES_CTR_256, CTR_NONCE_LEN};
-use bouncycastle::cipher::{Decrypting, Encrypting};
+use bouncycastle::aes::CTR_NONCE_LEN;
+use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
+use bouncycastle::cipher::modes::CTR;
+use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey};
+use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor};
pub(crate) fn aes128_ctr_cmd(
action: &CipherDirection,
@@ -46,9 +50,9 @@ pub(crate) fn aes128_ctr_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CTR_128, 16, CTR_NONCE_LEN>(
+ run::<_, 16>(
action,
- &load_key::<16>(key, key_file, "AES-128"),
+ &load_key_object::>, 16>(key, key_file, "AES-128"),
load_iv::(iv, iv_file),
output_hex,
);
@@ -62,9 +66,9 @@ pub(crate) fn aes192_ctr_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CTR_192, 24, CTR_NONCE_LEN>(
+ run::<_, 24>(
action,
- &load_key::<24>(key, key_file, "AES-192"),
+ &load_key_object::>, 24>(key, key_file, "AES-192"),
load_iv::(iv, iv_file),
output_hex,
);
@@ -78,10 +82,25 @@ pub(crate) fn aes256_ctr_cmd(
iv_file: &Option,
output_hex: bool,
) {
- run_stream_mode::, AES_CTR_256, 32, CTR_NONCE_LEN>(
+ run::<_, 32>(
action,
- &load_key::<32>(key, key_file, "AES-256"),
+ &load_key_object::>, 32>(key, key_file, "AES-256"),
load_iv::(iv, iv_file),
output_hex,
);
}
+
+/// Dispatches to the shared streaming loops with `Ctr` filled in as the mode.
+fn run<'k, K, const KEY_LEN: usize>(
+ action: &CipherDirection,
+ key: &'k K,
+ iv: Option<[u8; CTR_NONCE_LEN]>,
+ output_hex: bool,
+) where
+ K: SymmetricEncryptionKey
+ + SymmetricDecryptionKey,
+ K::Encryptor<'k>: StreamCipherEncryptor,
+ K::Decryptor<'k>: StreamCipherDecryptor,
+{
+ run_stream_mode::(action, key, iv, output_hex)
+}
diff --git a/cli/src/aes_ecb_cmd.rs b/cli/src/aes_ecb_cmd.rs
index 63b7d43f5..4451abc35 100644
--- a/cli/src/aes_ecb_cmd.rs
+++ b/cli/src/aes_ecb_cmd.rs
@@ -16,13 +16,12 @@
//! `aes*-cbc` or `aes*-cfb` under separate authentication, or better an AEAD.
use crate::helpers::block_mode_helpers::{
- BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_key,
+ BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_key_object, start_encryptor,
};
-use bouncycastle::aes::hazmat::{AES_ECB_128, AES_ECB_192, AES_ECB_256};
+use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
+use bouncycastle::cipher::modes::hazmat::ecb::ECB;
use bouncycastle::cipher::padding::NoPadding;
-use bouncycastle::cipher::{Decrypting, Encrypting};
-use bouncycastle::core::key_material::KeyMaterial;
-use bouncycastle::core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor};
+use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey};
/// Names the mode in error messages.
const MODE: &str = "ECB";
@@ -33,9 +32,9 @@ pub(crate) fn aes128_ecb_cmd(
key_file: &Option,
output_hex: bool,
) {
- run::, AES_ECB_128, 16>(
+ run::<_, 16>(
action,
- &load_key::<16>(key, key_file, "AES-128"),
+ &load_key_object::>, 16>(key, key_file, "AES-128"),
output_hex,
);
}
@@ -46,9 +45,9 @@ pub(crate) fn aes192_ecb_cmd(
key_file: &Option,
output_hex: bool,
) {
- run::, AES_ECB_192, 24>(
+ run::<_, 24>(
action,
- &load_key::<24>(key, key_file, "AES-192"),
+ &load_key_object::>, 24>(key, key_file, "AES-192"),
output_hex,
);
}
@@ -59,29 +58,31 @@ pub(crate) fn aes256_ecb_cmd(
key_file: &Option,
output_hex: bool,
) {
- run::, AES_ECB_256, 32>(
+ run::<_, 32>(
action,
- &load_key::<32>(key, key_file, "AES-256"),
+ &load_key_object::>, 32>(key, key_file, "AES-256"),
output_hex,
);
}
-/// Dispatches to the shared streaming loops. `INIT_DATA_LEN` is 0, so the loops write and read no
-/// IV.
-fn run(
- action: &CipherDirection,
- key: &KeyMaterial,
- output_hex: bool,
-) where
- E: SymmetricCipherEncryptor,
- D: SymmetricCipherDecryptor,
+/// Dispatches to the shared streaming loops under the key's ECB mode. `INIT_DATA_LEN` is 0, so
+/// the loops write and read no IV, and `NoPadding` makes them refuse a partial final block.
+fn run(action: &CipherDirection, key: &K, output_hex: bool)
+where
+ K: SymmetricEncryptionKey
+ + SymmetricDecryptionKey,
{
match action {
CipherDirection::Encrypt => {
- encrypt_stream::(key, None, output_hex, Some(MODE))
- }
- CipherDirection::Decrypt => {
- decrypt_stream::(key, None, output_hex, Some(MODE))
+ // ECB has no IV: the constructor's RNG draws nothing, and nothing is written.
+ let (enc, _) = start_encryptor(None, |rng| key.create_encryptor(rng));
+ encrypt_stream::<_, 0>(enc, None, output_hex, Some(MODE))
}
+ CipherDirection::Decrypt => decrypt_stream::<_, 0>(
+ None,
+ |no_iv| key.create_decryptor(no_iv),
+ output_hex,
+ Some(MODE),
+ ),
}
}
diff --git a/cli/src/aes_gcm_cmd.rs b/cli/src/aes_gcm_cmd.rs
index c6907aebd..5575c8d6d 100644
--- a/cli/src/aes_gcm_cmd.rs
+++ b/cli/src/aes_gcm_cmd.rs
@@ -14,11 +14,10 @@
//! recover the hash subkey (Appendix A).
use crate::helpers::aead_cipher_helpers::{decrypt_gcm, encrypt_gcm, load_aad};
-use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key};
-use bouncycastle::aes::{AES_GCM_128, AES_GCM_192, AES_GCM_256};
-use bouncycastle::cipher::{Decrypting, Encrypting};
-use bouncycastle::core::key_material::KeyMaterial;
-use bouncycastle::core::traits::{AEADCipherDecryptor, AEADCipherEncryptor};
+use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object};
+use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
+use bouncycastle::cipher::modes::GCM;
+use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey};
pub(crate) fn aes128_gcm_cmd(
action: &CipherDirection,
@@ -30,9 +29,9 @@ pub(crate) fn aes128_gcm_cmd(
aad_file: &Option,
output_hex: bool,
) {
- run::, AES_GCM_128, 16>(
+ run::, 16>(
action,
- &load_key::<16>(key, key_file, "AES-128"),
+ &load_key_object::<_, 16>(key, key_file, "AES-128"),
load_iv::<12>(iv, iv_file),
&load_aad(aad, aad_file),
output_hex,
@@ -49,9 +48,9 @@ pub(crate) fn aes192_gcm_cmd(
aad_file: &Option,
output_hex: bool,
) {
- run::, AES_GCM_192, 24>(
+ run::, 24>(
action,
- &load_key::<24>(key, key_file, "AES-192"),
+ &load_key_object::<_, 24>(key, key_file, "AES-192"),
load_iv::<12>(iv, iv_file),
&load_aad(aad, aad_file),
output_hex,
@@ -68,28 +67,27 @@ pub(crate) fn aes256_gcm_cmd(
aad_file: &Option,
output_hex: bool,
) {
- run::, AES_GCM_256, 32>(
+ run::, 32>(
action,
- &load_key::<32>(key, key_file, "AES-256"),
+ &load_key_object::<_, 32>(key, key_file, "AES-256"),
load_iv::<12>(iv, iv_file),
&load_aad(aad, aad_file),
output_hex,
);
}
-/// Dispatches to the shared AEAD streaming loops with GCM's 128-bit tag.
-fn run(
+/// Dispatches to the shared AEAD streaming loops with `Gcm`'s 128-bit tag.
+fn run(
action: &CipherDirection,
- key: &KeyMaterial,
+ key: &K,
nonce: Option<[u8; 12]>,
aad: &[u8],
output_hex: bool,
) where
- E: AEADCipherEncryptor,
- D: AEADCipherDecryptor,
+ K: AeadEncryptionKey + AeadDecryptionKey,
{
match action {
- CipherDirection::Encrypt => encrypt_gcm::(key, nonce, aad, output_hex),
- CipherDirection::Decrypt => decrypt_gcm::(key, nonce, aad, output_hex),
+ CipherDirection::Encrypt => encrypt_gcm::(key, nonce, aad, output_hex),
+ CipherDirection::Decrypt => decrypt_gcm::(key, nonce, aad, output_hex),
}
}
diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs
index 7656b1f4c..a51ba01b5 100644
--- a/cli/src/ascon_cmd.rs
+++ b/cli/src/ascon_cmd.rs
@@ -1,15 +1,15 @@
use std::io::{self, Read};
use std::process::exit;
-use bouncycastle::ascon::Ascon_AEAD128;
+use bouncycastle::ascon::AsconAEAD128Key;
use bouncycastle::ascon::AsconCXof128;
use bouncycastle::ascon::AsconHash256;
use bouncycastle::ascon::AsconXof128;
-use bouncycastle::cipher::{Decrypting, Encrypting};
use bouncycastle::core::errors::SymmetricCipherError;
use bouncycastle::core::hazmat::do_hazardous_operations;
use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType};
use bouncycastle::core::security_strength::SecurityStrength;
+use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial};
use bouncycastle::core::traits::{
AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor,
};
@@ -56,9 +56,9 @@ fn require_16(bytes: Vec, label: &str) -> [u8; 16] {
})
}
-/// Build a `KeyMaterial<16>` for the AEAD key, warning (and forcing usable metadata) only if the
-/// key turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does.
-fn load_key_material(key_bytes: &[u8; 16]) -> KeyMaterial<16> {
+/// Build the AEAD key, warning (and forcing usable metadata on the material) only if the key
+/// turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does.
+fn load_aead_key(key_bytes: &[u8; 16]) -> AsconAEAD128Key {
let mut key =
KeyMaterial::<16>::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey).unwrap();
if key.key_type() == KeyType::Zeroized || key.security_strength() < SecurityStrength::_128bit {
@@ -71,7 +71,10 @@ fn load_key_material(key_bytes: &[u8; 16]) -> KeyMaterial<16> {
})
.unwrap();
}
- key
+ AsconAEAD128Key::from_key_material(&key).unwrap_or_else(|e| {
+ eprintln!("Error: couldn't load the key: {e:?}");
+ exit(-1);
+ })
}
/// Ascon-Hash256 of stdin. Streaming update; 256-bit digest.
@@ -123,7 +126,7 @@ pub(crate) fn aead128_cmd(
decrypt: bool,
output_hex: bool,
) {
- let key = load_key_material(&require_16(load_bytes(key, key_file, "key"), "key"));
+ let key = load_aead_key(&require_16(load_bytes(key, key_file, "key"), "key"));
let nonce =
load_optional_bytes(nonce, nonce_file, "nonce").map(|bytes| require_16(bytes, "nonce"));
let ad_bytes = match ad {
@@ -142,25 +145,26 @@ pub(crate) fn aead128_cmd(
}
}
-/// Drives [`Ascon_AEAD128`] in the inline `ciphertext || tag` layout (the inherited
-/// [`SymmetricCipherEncryptor::do_encrypt_final_out`]). With no nonce given one is generated and
-/// written ahead of the stream; a caller-supplied nonce is not written, since the caller has it.
+/// Drives the key's encryptor in the inline `ciphertext || tag` layout (the inherited
+/// [`SymmetricCipherEncryptor::do_encrypt_final_out`]). With no nonce given one is drawn from the
+/// OS-backed DRBG and written ahead of the stream; a caller-supplied nonce is not written, since
+/// the caller has it.
fn aead128_encrypt_stream(
- key: &KeyMaterial<16>,
+ key: &AsconAEAD128Key,
nonce: Option<&[u8; 16]>,
ad_opt: Option<&[u8]>,
output_hex: bool,
) {
- let start = match nonce {
- Some(nonce) => {
- Ascon_AEAD128::::do_encrypt_init_nonce(key, nonce).map(|c| (c, None))
- }
- None => Ascon_AEAD128::::do_encrypt_init(key).map(|(c, n)| (c, Some(n))),
+ let (mut cipher, generated_nonce) = match nonce {
+ Some(nonce) => (key.create_encryptor_nonce(nonce), None),
+ None => key
+ .create_encryptor(&mut bouncycastle::rng::HashDRBG_SHA512::new_from_os())
+ .map(|(c, n)| (c, Some(n)))
+ .unwrap_or_else(|e| {
+ eprintln!("Error: couldn't start encryption: {e:?}");
+ exit(-1);
+ }),
};
- let (mut cipher, generated_nonce) = start.unwrap_or_else(|e| {
- eprintln!("Error: couldn't start encryption: {e:?}");
- exit(-1);
- });
if let Some(ad) = ad_opt {
// infallible: `do_update_aad` only refuses AAD once plaintext has been fed in, and none
// has been yet.
@@ -193,11 +197,11 @@ fn aead128_encrypt_stream(
}
/// Decrypts a stream whose final 16 bytes are the tag, which is only known once EOF is reached.
-/// [`Ascon_AEAD128`] holds the last 16 bytes it has seen back itself, releasing everything
+/// The key's decryptor holds the last 16 bytes it has seen back itself, releasing everything
/// before them as soon as it is known not to be part of the tag; at EOF
/// [`SymmetricCipherDecryptor::do_decrypt_final`] checks what it held back as the tag.
fn aead128_decrypt_stream(
- key: &KeyMaterial<16>,
+ key: &AsconAEAD128Key,
nonce: Option<&[u8; 16]>,
ad_opt: Option<&[u8]>,
output_hex: bool,
@@ -218,11 +222,7 @@ fn aead128_decrypt_stream(
}
};
- let mut cipher =
- Ascon_AEAD128::::do_decrypt_init(key, &nonce).unwrap_or_else(|e| {
- eprintln!("Error: couldn't start decryption: {e:?}");
- exit(-1);
- });
+ let mut cipher = key.create_decryptor(&nonce);
if let Some(ad) = ad_opt {
// infallible: as on the encrypt side, no ciphertext has been fed in yet.
cipher.do_update_aad(ad).unwrap();
diff --git a/cli/src/helpers/aead_cipher_helpers.rs b/cli/src/helpers/aead_cipher_helpers.rs
index e54b36fb8..3574f6aa4 100644
--- a/cli/src/helpers/aead_cipher_helpers.rs
+++ b/cli/src/helpers/aead_cipher_helpers.rs
@@ -1,10 +1,10 @@
//! Shared plumbing for the AEAD subcommands: `aes{128,192,256}-gcm`.
//!
-//! Parallel to [`crate::helpers::stream_mode_helpers`], but for [`bouncycastle::cipher::modes::Gcm`] rather than a
-//! [`StreamCipherEncryptor`](bouncycastle::core::traits::StreamCipherEncryptor) mode: GCM carries
-//! additional authenticated data and a tag, neither of which that trait has room for, so this
-//! module drives it through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] instead, which add
-//! `do_update_aad` to the symmetric-cipher streaming methods.
+//! Parallel to [`crate::helpers::stream_mode_helpers`], but for a key bound to
+//! [`bouncycastle::cipher::modes::GCM`] rather than a stream mode: GCM carries additional
+//! authenticated data and a tag, neither of which the stream traits have room for, so this module
+//! drives the key's AEAD workers through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], which
+//! add `do_update_aad` to the symmetric-cipher streaming methods.
//!
//! # On-the-wire format: `IV || ciphertext || tag`
//!
@@ -13,7 +13,7 @@
//! rest of stdin through the inline decryptor. With `--iv`, the IV is the caller's and is
//! neither written nor read: the format is `ciphertext || tag` (see
//! [`crate::helpers::block_mode_helpers`] for the flag). The decryptor, per
-//! [`SymmetricCipherDecryptor`](bouncycastle::core::traits::SymmetricCipherDecryptor)'s contract,
+//! [`SymmetricCipherDecryptor`]'s contract,
//! holds back the last 16 bytes it has seen because they might be the tag -- and checks the tag on
//! `do_decrypt_final`.
//!
@@ -39,8 +39,10 @@
//! other GCM implementation given the same file.
use crate::helpers::{flush_stdout, read_from_file_raw, write_bytes_or_hex, write_stdout};
-use bouncycastle::core::key_material::KeyMaterial;
-use bouncycastle::core::traits::{AEADCipherDecryptor, AEADCipherEncryptor};
+use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey};
+use bouncycastle::core::traits::{
+ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor,
+};
use bouncycastle::hex;
use std::io;
use std::io::{Read, Write};
@@ -72,25 +74,27 @@ pub fn load_aad(aad: &Option, aad_file: &Option) -> Vec {
/// Encrypts stdin to stdout under GCM: writes the generated IV (unless `nonce` supplies one),
/// then the ciphertext as it streams, then the tag.
-pub fn encrypt_gcm(
- key: &KeyMaterial,
+pub fn encrypt_gcm(
+ key: &K,
nonce: Option<[u8; GCM_NONCE_LEN]>,
aad: &[u8],
output_hex: bool,
) where
- E: AEADCipherEncryptor,
+ K: AeadEncryptionKey,
{
let mut enc = match nonce {
- Some(nonce) => E::do_encrypt_init_nonce(key, &nonce),
- None => E::do_encrypt_init(key).map(|(enc, nonce)| {
+ Some(nonce) => key.create_encryptor_nonce(&nonce),
+ None => {
+ let (enc, nonce) = key
+ .create_encryptor(&mut bouncycastle::rng::HashDRBG_SHA512::new_from_os())
+ .unwrap_or_else(|e| {
+ eprintln!("Error: couldn't start encryption: {e:?}");
+ exit(-1);
+ });
write_bytes_or_hex(&nonce, output_hex);
enc
- }),
- }
- .unwrap_or_else(|e| {
- eprintln!("Error: couldn't start encryption: {e:?}");
- exit(-1);
- });
+ }
+ };
enc.do_update_aad(aad).unwrap_or_else(|e| {
eprintln!("Error: couldn't absorb the additional authenticated data: {e:?}");
@@ -116,7 +120,7 @@ pub fn encrypt_gcm(
}
// The detached final flushes nothing for GCM and returns the tag, written last.
- let (_, _, tag) = enc.do_encrypt_final_detached().unwrap_or_else(|e| {
+ let (_, tag) = enc.do_encrypt_final_detached_out(&mut [0u8; TAG_LEN]).unwrap_or_else(|e| {
eprintln!("Error: encryption failed: {e:?}");
exit(-1);
});
@@ -128,13 +132,13 @@ pub fn encrypt_gcm(
/// first, streams the rest through the inline decryptor, and checks the tag on
/// `do_decrypt_final`. See the module docs for why plaintext may already be written to stdout by
/// the time a tag failure is reported.
-pub fn decrypt_gcm(
- key: &KeyMaterial,
+pub fn decrypt_gcm(
+ key: &K,
nonce: Option<[u8; GCM_NONCE_LEN]>,
aad: &[u8],
output_hex: bool,
) where
- D: AEADCipherDecryptor,
+ K: AeadDecryptionKey,
{
let nonce = nonce.unwrap_or_else(|| {
let mut nonce = [0u8; GCM_NONCE_LEN];
@@ -148,10 +152,7 @@ pub fn decrypt_gcm(
nonce
});
- let mut dec = D::do_decrypt_init(key, &nonce).unwrap_or_else(|e| {
- eprintln!("Error: couldn't start decryption: {e:?}");
- exit(-1);
- });
+ let mut dec = key.create_decryptor(&nonce);
dec.do_update_aad(aad).unwrap_or_else(|e| {
eprintln!("Error: couldn't absorb the additional authenticated data: {e:?}");
exit(-1);
diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs
index c68dcc85b..cc3b9ec71 100644
--- a/cli/src/helpers/block_mode_helpers.rs
+++ b/cli/src/helpers/block_mode_helpers.rs
@@ -3,8 +3,8 @@
//! Everything here is mode-independent -- key loading, stdin framing, block-alignment enforcement,
//! output formatting -- and is generic over the mode via [`SymmetricCipherEncryptor`] /
//! [`SymmetricCipherDecryptor`]. `aes_cbc_cmd` and `aes_ecb_cmd` are thin dispatchers over it,
-//! instantiating it with the `AES_CBC_*` types under `PKCS7` and the `AES_ECB_*` types under
-//! `NoPadding`, so the commands cannot drift apart on the parts that matter for correctness.
+//! driving it with the workers their keys hand out, `CBC` and `ECB`, so the
+//! commands cannot drift apart on the parts that matter for correctness.
//!
//! The CFB and CTR commands are stream ciphers and live in [`crate::helpers::stream_mode_helpers`] instead;
//! they share
@@ -28,9 +28,9 @@
//! Every mode with an IV or nonce (CBC, CFB, CFB8, CTR, GCM) also takes `--iv` / `--iv-file`,
//! loaded by [`load_iv`], for a format that carries the IV itself and for reproducing published
//! vectors. With it, `encrypt` uses the supplied value and writes **only the ciphertext**, and
-//! `decrypt` takes it from the flag and reads **only ciphertext** from stdin. The modes have no
-//! IV-taking constructor, so it reaches them through `do_encrypt_init_rng` and a [`FixedIv`]
-//! that hands out exactly those bytes (GCM has `do_encrypt_init_nonce` and uses that).
+//! `decrypt` takes it from the flag and reads **only ciphertext** from stdin. The keys' constructors
+//! draw the IV from an RNG, so it reaches them through [`start_encryptor`] and a [`FixedIv`] that
+//! hands out exactly those bytes (GCM keys have `create_encryptor_nonce` and use that).
//! Unpredictability, and for CTR and GCM uniqueness, is then the caller's responsibility. CCM
//! keeps its own `--nonce`, which is variable-length and was never generated.
//!
@@ -54,16 +54,18 @@
//! cat cipher.hex | bc-rust hex-decode | bc-rust aes256-cbc decrypt --key-file k.bin
//! ```
-use crate::helpers;
use crate::helpers::{
- flush_stdout, read_from_file, strip_trailing_newline, write_bytes_or_hex, write_stdout,
+ flush_stdout, read_from_file, read_from_file_raw, strip_trailing_newline, write_bytes_or_hex,
+ write_stdout,
};
-use bouncycastle::core::errors::RNGError;
+use bouncycastle::core::errors::{RNGError, SymmetricCipherError};
use bouncycastle::core::hazmat::do_hazardous_operations;
use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType};
use bouncycastle::core::security_strength::SecurityStrength;
+use bouncycastle::core::symmetric_key::FromKeyMaterial;
use bouncycastle::core::traits::{RNG, SymmetricCipherDecryptor, SymmetricCipherEncryptor};
use bouncycastle::hex;
+use bouncycastle::rng::HashDRBG_SHA512;
use clap::ValueEnum;
use std::io;
use std::io::{Read, Write};
@@ -147,8 +149,21 @@ pub(crate) fn load_key(
key
}
-/// An [`RNG`] that yields the caller's IV once, so `--iv` can reach a mode whose only
-/// constructors are `do_encrypt_init` and `do_encrypt_init_rng`. A second draw, or one of the
+/// The key object for a command: the material from `--key` / `--key-file`, checked and expanded
+/// once. `K` is the key type the command's mode wants, `AES128Key>` and the like.
+pub(crate) fn load_key_object, const KEY_LEN: usize>(
+ key: &Option,
+ key_file: &Option,
+ alg: &str,
+) -> K {
+ K::from_key_material(&load_key::(key, key_file, alg)).unwrap_or_else(|e| {
+ eprintln!("Error: couldn't load the key: {e:?}");
+ exit(-1);
+ })
+}
+
+/// An [`RNG`] that yields the caller's IV once, so `--iv` can reach a key whose only
+/// constructor draws the IV from an RNG. A second draw, or one of the
/// wrong length, is an error rather than a repeat: the IV must be consumed exactly as given.
pub(crate) struct FixedIv {
iv: [u8; N],
@@ -205,7 +220,7 @@ pub(crate) fn load_iv(
iv_file: &Option,
) -> Option<[u8; INIT_DATA_LEN]> {
let bytes = if let Some(file) = iv_file {
- let bytes = helpers::read_from_file_raw(file);
+ let bytes = read_from_file_raw(file);
if bytes.last() == Some(&b'\n') {
eprintln!(
"Warning: IV file '{file}' ends with a newline byte (0x0a), which is used as part \
@@ -230,33 +245,44 @@ pub(crate) fn load_iv(
}))
}
-/// Encrypts stdin to stdout under the mode `E`. With no `iv`, the init data (the IV) is generated
-/// and written first; with one, it is used and the output is ciphertext only.
-///
-/// `INIT_DATA_LEN` is the mode's: one block for CBC, 0 for ECB, in which case nothing is written
-/// ahead of the ciphertext. `aligned_mode` is `Some(mode name)` for a mode that pads nothing, whose
-/// input must therefore be a whole number of blocks; the name appears in the error message.
-pub(crate) fn encrypt_stream(
- key: &KeyMaterial,
+/// Starts an encryptor from `create`, the key's constructor: under `iv` when one was supplied,
+/// through a [`FixedIv`], and otherwise under a fresh IV from the OS-backed DRBG. A generated IV
+/// comes back for the caller to write ahead of the ciphertext; a supplied one does not, since it
+/// is the caller's to carry.
+pub(crate) fn start_encryptor(
iv: Option<[u8; INIT_DATA_LEN]>,
- output_hex: bool,
- aligned_mode: Option<&str>,
-) where
- E: SymmetricCipherEncryptor,
-{
- let init = match iv {
- Some(iv) => E::do_encrypt_init_rng(key, &mut FixedIv::new(iv)),
- None => E::do_encrypt_init(key),
+ create: impl FnOnce(&mut dyn RNG) -> Result<(E, [u8; INIT_DATA_LEN]), SymmetricCipherError>,
+) -> (E, Option<[u8; INIT_DATA_LEN]>) {
+ let started = match iv {
+ Some(iv) => create(&mut FixedIv::new(iv)).map(|(enc, _)| (enc, None)),
+ None => create(&mut HashDRBG_SHA512::new_from_os()).map(|(enc, iv)| (enc, Some(iv))),
};
- let (mut enc, generated_iv) = init.unwrap_or_else(|e| {
+ started.unwrap_or_else(|e| {
eprintln!("Error: couldn't start encryption: {e:?}");
exit(-1);
- });
+ })
+}
- // A generated IV goes out ahead of the ciphertext, so `decrypt` can pick it up. (Empty for
- // ECB.) A supplied one is the caller's to carry.
- if INIT_DATA_LEN > 0 && iv.is_none() {
- write_bytes_or_hex(&generated_iv, output_hex);
+/// Encrypts stdin to stdout through `enc`, a worker the command's key has already made, writing
+/// `generated_iv` first when there is one.
+///
+/// `INIT_DATA_LEN` is the mode's: one block for CBC, 0 for ECB. `generated_iv` is `None` when the
+/// IV was supplied with `--iv`, or when the mode has none, and then nothing is written ahead of
+/// the ciphertext; see [`start_encryptor`]. `aligned_mode` names a mode whose input must be a
+/// whole number of blocks, for the error that reports a partial one (ECB under `NoPadding`); it is
+/// `None` for a padded mode, which takes any length.
+pub(crate) fn encrypt_stream(
+ mut enc: E,
+ generated_iv: Option<&[u8; INIT_DATA_LEN]>,
+ output_hex: bool,
+ aligned_mode: Option<&str>,
+) where
+ E: SymmetricCipherEncryptor,
+{
+ // A generated IV goes out ahead of the ciphertext, so `decrypt` can pick it up. A supplied
+ // one is the caller's to carry. (Nothing for ECB.)
+ if let Some(iv) = generated_iv {
+ write_bytes_or_hex(iv, output_hex);
}
let mut out = [0u8; CHUNK_LEN];
@@ -281,16 +307,16 @@ pub(crate) fn encrypt_stream(
- key: &KeyMaterial,
+/// Decrypts stdin to stdout through the worker `make_decryptor` gives for the init data (the IV):
+/// `iv` if it was supplied, else the first `INIT_DATA_LEN` bytes of input -- one block for CBC,
+/// nothing for ECB. `aligned_mode` is as for [`encrypt_stream`].
+pub(crate) fn decrypt_stream(
iv: Option<[u8; INIT_DATA_LEN]>,
+ make_decryptor: impl FnOnce(&[u8; INIT_DATA_LEN]) -> D,
output_hex: bool,
aligned_mode: Option<&str>,
) where
- D: SymmetricCipherDecryptor,
+ D: SymmetricCipherDecryptor,
{
let iv = iv.unwrap_or_else(|| {
// The leading bytes are the IV, not ciphertext. (None for ECB: the read is skipped.)
@@ -307,10 +333,7 @@ pub(crate) fn decrypt_stream(
- key: &KeyMaterial,
+pub(crate) fn encrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>(
+ key: &'k K,
iv: Option<[u8; INIT_DATA_LEN]>,
output_hex: bool,
) where
- E: StreamCipherEncryptor,
+ K: SymmetricEncryptionKey,
+ K::Encryptor<'k>: StreamCipherEncryptor,
{
- let init = match iv {
- Some(iv) => E::do_encrypt_init_rng(key, &mut FixedIv::new(iv)),
- None => E::do_encrypt_init(key),
- };
- let (mut enc, generated_iv) = init.unwrap_or_else(|e| {
- eprintln!("Error: couldn't start encryption: {e:?}");
- exit(-1);
- });
+ let (mut enc, generated_iv) = start_encryptor(iv, |rng| key.create_encryptor(rng));
// A generated IV goes out ahead of the ciphertext, so `decrypt` can pick it up. A supplied
// one is the caller's to carry.
- if iv.is_none() {
- write_bytes_or_hex(&generated_iv, output_hex);
+ if let Some(iv) = generated_iv {
+ write_bytes_or_hex(&iv, output_hex);
}
// The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out.
@@ -77,14 +71,15 @@ pub(crate) fn encrypt_stream(
- key: &KeyMaterial,
+pub(crate) fn decrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>(
+ key: &'k K,
iv: Option<[u8; INIT_DATA_LEN]>,
output_hex: bool,
) where
- D: StreamCipherDecryptor,
+ K: SymmetricDecryptionKey,
+ K::Decryptor<'k>: StreamCipherDecryptor,
{
let iv = iv.unwrap_or_else(|| {
// The leading bytes are the IV, not ciphertext.
@@ -99,10 +94,7 @@ pub(crate) fn decrypt_stream(
+/// Runs one direction of a stream mode under its key. The `run` dispatchers in `aes_cfb_cmd`,
+/// `aes_cfb8_cmd` and `aes_ctr_cmd` differ only in which key type they load, so the match lives
+/// here.
+pub(crate) fn run_stream_mode<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>(
action: &CipherDirection,
- key: &KeyMaterial,
+ key: &'k K,
iv: Option<[u8; INIT_DATA_LEN]>,
output_hex: bool,
) where
- E: StreamCipherEncryptor,
- D: StreamCipherDecryptor,
+ K: SymmetricEncryptionKey
+ + SymmetricDecryptionKey,
+ K::Encryptor<'k>: StreamCipherEncryptor,
+ K::Decryptor<'k>: StreamCipherDecryptor,
{
match action {
CipherDirection::Encrypt => {
- encrypt_stream::(key, iv, output_hex)
+ encrypt_stream::(key, iv, output_hex)
}
CipherDirection::Decrypt => {
- decrypt_stream::(key, iv, output_hex)
+ decrypt_stream::(key, iv, output_hex)
}
}
}
diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs
index f5b207c06..6cd942a39 100644
--- a/cli/src/mac_cmd.rs
+++ b/cli/src/mac_cmd.rs
@@ -1,14 +1,18 @@
+use std::io;
use std::io::Read;
use std::process::exit;
-use std::{fs, io};
+use crate::helpers::read_from_file_raw;
+use bouncycastle::core::errors::MACError;
use bouncycastle::core::hazmat::do_hazardous_operations;
use bouncycastle::core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType};
+use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey};
use bouncycastle::core::traits::MAC;
use bouncycastle::hex;
-use bouncycastle::sha2::hmac::{HMAC_SHA256, HMAC_SHA512, HMAC_SHA512_224, HMAC_SHA512_256};
-use bouncycastle::sha3::kmac::{KMAC128, KMAC256};
-use bouncycastle::sm3::hmac::HMAC_SM3;
+use bouncycastle::hmac::HmacKey;
+use bouncycastle::sha2::{SHA256, SHA512, SHA512_224, SHA512_256};
+use bouncycastle::sha3::kmac::{KMAC128Key, KMAC256Key};
+use bouncycastle::sm3::SM3;
#[allow(non_camel_case_types)]
pub(crate) enum HMACVariant {
@@ -22,10 +26,13 @@ pub(crate) enum HMACVariant {
/// Loads a MAC key from `--key-file` (raw) or `--key` (hex), tagged as a MAC key. The file wins
/// when both are given, as the help text says and the other commands' loaders do.
fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial512 {
- let key_bytes: Vec = if let Some(file) = key_file {
- fs::read(file).unwrap()
- } else if let Some(hex_str) = key {
- hex::decode(hex_str).unwrap()
+ let key_bytes: Vec = if let Some(key_file) = key_file {
+ read_from_file_raw(key_file)
+ } else if let Some(key) = key {
+ hex::decode(key).unwrap_or_else(|_| {
+ eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes.");
+ exit(-1);
+ })
} else {
eprintln!("Error: either `key` or `key-file` must be supplied.");
exit(-1)
@@ -35,8 +42,16 @@ fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial5
eprintln!("Error: CLI only supports MAC keys 64 bytes.");
exit(-1);
}
- let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap();
- do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap();
+ let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap_or_else(|e| {
+ eprintln!("Error: couldn't load the key: {e:?}");
+ exit(-1);
+ });
+ do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap_or_else(
+ |e| {
+ eprintln!("Error: couldn't load the key: {e:?}");
+ exit(-1);
+ },
+ );
key
}
@@ -52,24 +67,24 @@ pub(crate) fn mac_cmd(
// instantiate the MAC object and call do_mac()
match hmac_variant {
HMACVariant::SHA256 => {
- let mac = HMAC_SHA256::new_allow_weak_key(&key).unwrap();
- do_mac(mac, verify_val, output_hex);
+ let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap();
+ do_mac(hmac_key.create_generator(), verify_val, output_hex);
}
HMACVariant::SHA512 => {
- let mac = HMAC_SHA512::new_allow_weak_key(&key).unwrap();
- do_mac(mac, verify_val, output_hex);
+ let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap();
+ do_mac(hmac_key.create_generator(), verify_val, output_hex);
}
HMACVariant::SHA512_224 => {
- let mac = HMAC_SHA512_224::new_allow_weak_key(&key).unwrap();
- do_mac(mac, verify_val, output_hex);
+ let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap();
+ do_mac(hmac_key.create_generator(), verify_val, output_hex);
}
HMACVariant::SHA512_256 => {
- let mac = HMAC_SHA512_256::new_allow_weak_key(&key).unwrap();
- do_mac(mac, verify_val, output_hex);
+ let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap();
+ do_mac(hmac_key.create_generator(), verify_val, output_hex);
}
HMACVariant::SM3 => {
- let mac = HMAC_SM3::new_allow_weak_key(&key).unwrap();
- do_mac(mac, verify_val, output_hex);
+ let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap();
+ do_mac(hmac_key.create_generator(), verify_val, output_hex);
}
}
}
@@ -87,23 +102,30 @@ pub(crate) fn kmac_cmd(
) {
let key = load_mac_key(key, key_file);
let s = customization.as_deref().unwrap_or("").as_bytes();
- // new_allow_weak_key, as the HMAC commands do: a CLI is used for test vectors and scripting,
- // where a short or all-zero key is a legitimate thing to want.
+ // The allow-weak form, as the HMAC commands use: a CLI is used for test vectors and
+ // scripting, where a short or all-zero key is a legitimate thing to want.
match bit_len {
- 128 => do_mac(
- KMAC128::new_with_params(&key, s, length, true).expect("a valid MAC key"),
- verify_val,
- output_hex,
- ),
- 256 => do_mac(
- KMAC256::new_with_params(&key, s, length, true).expect("a valid MAC key"),
- verify_val,
- output_hex,
- ),
+ 128 => {
+ let kmac_key = KMAC128Key::with_params_allow_weak(&key, s, length)
+ .unwrap_or_else(|e| kmac_key_error(e));
+ do_mac(kmac_key.create_generator(), verify_val, output_hex)
+ }
+ 256 => {
+ let kmac_key = KMAC256Key::with_params_allow_weak(&key, s, length)
+ .unwrap_or_else(|e| kmac_key_error(e));
+ do_mac(kmac_key.create_generator(), verify_val, output_hex)
+ }
_ => panic!("Unsupported algorithm: KMAC-{bit_len}"),
}
}
+/// The key is already a MAC key, so what fails here is the tag length, which came from the
+/// command line: report it the way the key errors are reported rather than through a panic.
+fn kmac_key_error(e: MACError) -> ! {
+ eprintln!("Error: couldn't build the KMAC key: {e:?}");
+ exit(-1)
+}
+
fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) {
// read the content to be MAC'd from stdin
let mut buf: [u8; 1024] = [0u8; 1024];
@@ -121,10 +143,10 @@ fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) {
crate::helpers::write_stdout(b"\n");
} else {
// verify a MAC
- if mac.do_verify_final(&hex::decode(verify_val.as_ref().unwrap()).unwrap()) {
- exit(0)
- } else {
- exit(-1)
- }
+ let expected = hex::decode(verify_val.as_ref().unwrap()).unwrap_or_else(|_| {
+ eprintln!("Error: the MAC to verify must be hex.");
+ exit(-1);
+ });
+ if mac.do_verify_final(&expected) { exit(0) } else { exit(-1) }
}
}
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 5ab6c44a2..c443f6fd3 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -1347,7 +1347,7 @@ enum Subcommands {
/// The tag length and customization string are bound into the computation, so the verifier
/// must use the same values.
KMAC128 {
- /// Length of the tag in bytes.
+ /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2).
length: usize,
#[arg(short = 's', long)]
@@ -1374,7 +1374,7 @@ enum Subcommands {
/// Compute or verify a KMAC256 (NIST SP 800-185 Sec 4) over the content provided on stdin.
/// See kmac128.
KMAC256 {
- /// Length of the tag in bytes.
+ /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2).
length: usize,
#[arg(short = 's', long)]
diff --git a/cli/tests/test_kmac.sh b/cli/tests/test_kmac.sh
new file mode 100755
index 000000000..4c95e26c9
--- /dev/null
+++ b/cli/tests/test_kmac.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+# The kmac128 / kmac256 subcommands, end to end through the binary: a MAC computed over random
+# data under a random key must verify with `-v`, must stop verifying when the tag, the message,
+# the key, the customization string or the tag length changes, and a tag length the key refuses
+# must be reported as an error rather than through a panic.
+
+source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
+
+# `kmac_round_trip SUBCOMMAND` runs the round trip for one variant at a 32-byte tag.
+kmac_round_trip() {
+ local sub=$1 tag
+ rng 32 >"$TMP/key"
+ rng 32 >"$TMP/other_key"
+ rng 100 >"$TMP/msg"
+ cp "$TMP/msg" "$TMP/other_msg"
+ flip_byte "$TMP/other_msg" 50
+
+ tag=$(hex_out "$BC_RUST" "$sub" 32 --key-file "$TMP/key" -x <"$TMP/msg")
+ assert_eq "${#tag}" 64 "$sub tag length"
+
+ expect_ok "the tag must verify" "$BC_RUST" "$sub" 32 --key-file "$TMP/key" -v "$tag" <"$TMP/msg"
+ expect_fail "a modified tag must not verify" \
+ "$BC_RUST" "$sub" 32 --key-file "$TMP/key" -v "$(flip_hex "$tag" 0 1)" <"$TMP/msg"
+ expect_fail "a modified message must not verify" \
+ "$BC_RUST" "$sub" 32 --key-file "$TMP/key" -v "$tag" <"$TMP/other_msg"
+ expect_fail "another key must not verify" \
+ "$BC_RUST" "$sub" 32 --key-file "$TMP/other_key" -v "$tag" <"$TMP/msg"
+ expect_fail "another customization string must not verify" \
+ "$BC_RUST" "$sub" 32 -s other --key-file "$TMP/key" -v "$tag" <"$TMP/msg"
+ expect_fail "another tag length must not verify" \
+ "$BC_RUST" "$sub" 16 --key-file "$TMP/key" -v "${tag:0:32}" <"$TMP/msg"
+}
+
+# `kmac_refused_length SUBCOMMAND LENGTH` checks that LENGTH is refused with an error message,
+# not a panic trace, whether computing or verifying.
+kmac_refused_length() {
+ local sub=$1 length=$2
+ rng 32 >"$TMP/key"
+ rng 100 >"$TMP/msg"
+
+ expect_fail "tag length $length must be refused" \
+ "$BC_RUST" "$sub" "$length" --key-file "$TMP/key" -x <"$TMP/msg"
+ assert_stderr_has "Error: couldn't build the KMAC key"
+ ! grep -q panicked "$LAST_STDERR" || fail "a refused tag length must not panic"
+
+ expect_fail "tag length $length must be refused on verify" \
+ "$BC_RUST" "$sub" "$length" --key-file "$TMP/key" -v "" <"$TMP/msg"
+ assert_stderr_has "Error: couldn't build the KMAC key"
+ ! grep -q panicked "$LAST_STDERR" || fail "a refused tag length must not panic"
+}
+
+test_kmac128_round_trip() {
+ kmac_round_trip kmac128
+}
+
+test_kmac256_round_trip() {
+ kmac_round_trip kmac256
+}
+
+test_kmac128_refuses_a_short_tag() {
+ kmac_refused_length kmac128 0
+}
+
+test_kmac256_refuses_a_short_tag() {
+ kmac_refused_length kmac256 3
+}
+
+test_kmac128_refuses_a_tag_that_does_not_fit() {
+ kmac_refused_length kmac128 9223372036854775808
+}
+
+test_kmac256_refuses_a_tag_that_does_not_fit() {
+ kmac_refused_length kmac256 9223372036854775808
+}
+
+run_all
diff --git a/crypto/aes/benches/aes_gcm_packet_benches.rs b/crypto/aes/benches/aes_gcm_packet_benches.rs
index 9c8aab7f5..ed7e0e113 100644
--- a/crypto/aes/benches/aes_gcm_packet_benches.rs
+++ b/crypto/aes/benches/aes_gcm_packet_benches.rs
@@ -1,15 +1,15 @@
-//! AES-256-GCM over 16 KiB packets: the per-packet cost of starting from raw key material,
-//! which validates and expands the key for every packet, against starting from an
-//! `AES256Key`, which did that once (and derived GCM's hash subkey once) and hands the
-//! stored state to each packet's encryptor.
+//! AES-256-GCM over 16 KiB packets: the per-packet cost of building the `AES256Key` from raw
+//! key material for every packet, which validates and expands the key and derives GCM's hash
+//! subkey each time, against building it once and handing the stored state to each packet's
+//! encryptor.
//!
-//! Both arms stream one packet through the same `Gcm` type -- init, one `do_encrypt_out` /
-//! `do_decrypt_out` over the whole packet, final -- so the only difference between them is the
-//! key expansion, validation and `H`. The nonce comes from a fixed test RNG so that the RNG is not
-//! part of the comparison, except in the `OS DRBG per packet` arms, which instantiate a fresh
-//! `HashDRBG_SHA512` from the OS for every packet: the cost of the default-RNG constructors the
-//! key path does not offer. The `init` group isolates the construction cost with no data at all,
-//! and includes the one-off cost of building the key object.
+//! Both arms stream one packet through the same `Gcm` type -- `create_encryptor` /
+//! `create_decryptor`, one `do_encrypt_out` / `do_decrypt_out` over the whole packet, final -- so
+//! the only difference between them is where `from_key_material` is paid. The nonce comes from a
+//! fixed test RNG so that the RNG is not part of the comparison, except in the `OS DRBG per
+//! packet` arms, which instantiate a fresh `HashDRBG_SHA512` from the OS for every packet: what a
+//! per-packet default RNG would add. The `init` group isolates the construction cost with no data
+//! at all.
//!
//! The cipher works in place, so each measurement runs on a fresh output buffer made in
//! criterion's untimed setup (`iter_batched_ref`); the copy is not part of the timing.
@@ -32,8 +32,8 @@ const PACKET_LEN: usize = 16 * 1024;
const NONCE_LEN: usize = 12;
const TAG_LEN: usize = 16;
-type Enc = AES_GCM_256;
-type Dec = AES_GCM_256;
+type Enc<'k> = AES_GCM_256<'k, Encrypting>;
+type Dec<'k> = AES_GCM_256<'k, Decrypting>;
type Key = AES256Key;
fn material() -> KeyMaterial<32> {
@@ -77,11 +77,12 @@ fn bench_encrypt_packets(c: &mut Criterion) {
let mut group = c.benchmark_group("gcm::AES_256 16KiB packet encrypt");
group.throughput(Throughput::Bytes(PACKET_LEN as u64));
- group.bench_function("KeyMaterial: do_encrypt_init_rng (expand per packet)", |b| {
+ group.bench_function("AES256Key: from_key_material per packet (expand per packet)", |b| {
b.iter_batched_ref(
|| [0u8; PACKET_LEN],
|out| {
- let (enc, nonce) = Enc::do_encrypt_init_rng(black_box(&km), &mut rng).unwrap();
+ let per_packet = Key::from_key_material(black_box(&km)).unwrap();
+ let (enc, nonce) = per_packet.create_encryptor(&mut rng).unwrap();
black_box((nonce, encrypt_packet(enc, black_box(&packet), out)))
},
BatchSize::LargeInput,
@@ -98,12 +99,14 @@ fn bench_encrypt_packets(c: &mut Criterion) {
)
});
group.bench_function(
- "KeyMaterial: do_encrypt_init (OS DRBG per packet, expand per packet)",
+ "AES256Key: from_key_material per packet (OS DRBG per packet, expand per packet)",
|b| {
b.iter_batched_ref(
|| [0u8; PACKET_LEN],
|out| {
- let (enc, nonce) = Enc::do_encrypt_init(black_box(&km)).unwrap();
+ let mut os_rng = HashDRBG_SHA512::new_from_os();
+ let per_packet = Key::from_key_material(black_box(&km)).unwrap();
+ let (enc, nonce) = per_packet.create_encryptor(&mut os_rng).unwrap();
black_box((nonce, encrypt_packet(enc, black_box(&packet), out)))
},
BatchSize::LargeInput,
@@ -132,17 +135,18 @@ fn bench_decrypt_packets(c: &mut Criterion) {
// One valid packet to decrypt, made outside the timing.
let mut ct = [0u8; PACKET_LEN];
- let (enc, nonce) = Enc::do_encrypt_init_rng(&km, &mut rng()).unwrap();
+ let (enc, nonce) = key.create_encryptor(&mut rng()).unwrap();
let tag = encrypt_packet(enc, &packet, &mut ct);
let mut group = c.benchmark_group("gcm::AES_256 16KiB packet decrypt");
group.throughput(Throughput::Bytes(PACKET_LEN as u64));
- group.bench_function("KeyMaterial: do_decrypt_init (expand per packet)", |b| {
+ group.bench_function("AES256Key: from_key_material per packet (expand per packet)", |b| {
b.iter_batched_ref(
|| [0u8; PACKET_LEN],
|out| {
- let dec = Dec::do_decrypt_init(black_box(&km), &nonce).unwrap();
+ let per_packet = Key::from_key_material(black_box(&km)).unwrap();
+ let dec = per_packet.create_decryptor(&nonce);
black_box(decrypt_packet(dec, black_box(&ct), &tag, out))
},
BatchSize::LargeInput,
@@ -174,21 +178,45 @@ fn bench_init(c: &mut Criterion) {
group.bench_function("AES256Key::from_key_material (once per key)", |b| {
b.iter(|| black_box(Key::from_key_material(black_box(&km)).unwrap()))
});
- group.bench_function("KeyMaterial: do_encrypt_init_rng", |b| {
- b.iter(|| black_box(Enc::do_encrypt_init_rng(black_box(&km), &mut rng).unwrap()))
- });
+ group.bench_function(
+ "AES256Key: from_key_material + create_encryptor (per packet)",
+ |b| {
+ b.iter(|| {
+ let per_packet = Key::from_key_material(black_box(&km)).unwrap();
+ let (enc, nonce) = per_packet.create_encryptor(&mut rng).unwrap();
+ // The worker borrows the key, so what a per-packet key costs is what it takes
+ // to make the worker and finish it.
+ black_box((nonce, enc.do_encrypt_final().unwrap()))
+ })
+ },
+ );
group.bench_function("AES256Key: create_encryptor", |b| {
b.iter(|| black_box(black_box(&key).create_encryptor(&mut rng).unwrap()))
});
- group.bench_function("KeyMaterial: do_decrypt_init", |b| {
- b.iter(|| black_box(Dec::do_decrypt_init(black_box(&km), &nonce).unwrap()))
- });
+ group.bench_function(
+ "AES256Key: from_key_material + create_decryptor (per packet)",
+ |b| {
+ b.iter(|| {
+ let per_packet = Key::from_key_material(black_box(&km)).unwrap();
+ let dec = per_packet.create_decryptor(&nonce);
+ black_box(dec.do_decrypt_final().is_ok())
+ })
+ },
+ );
group.bench_function("AES256Key: create_decryptor", |b| {
b.iter(|| black_box(black_box(&key).create_decryptor(&nonce)))
});
- group.bench_function("KeyMaterial: do_encrypt_init (OS DRBG per call)", |b| {
- b.iter(|| black_box(Enc::do_encrypt_init(black_box(&km)).unwrap()))
- });
+ group.bench_function(
+ "AES256Key: from_key_material + create_encryptor (OS DRBG per call)",
+ |b| {
+ b.iter(|| {
+ let mut os_rng = HashDRBG_SHA512::new_from_os();
+ let per_packet = Key::from_key_material(black_box(&km)).unwrap();
+ let (enc, nonce) = per_packet.create_encryptor(&mut os_rng).unwrap();
+ black_box((nonce, enc.do_encrypt_final().unwrap()))
+ })
+ },
+ );
group.bench_function("AES256Key: create_encryptor (OS DRBG per call)", |b| {
b.iter(|| {
let mut os_rng = HashDRBG_SHA512::new_from_os();
diff --git a/crypto/aes/benches/aes_modes_benches.rs b/crypto/aes/benches/aes_modes_benches.rs
index 38d088acd..ec0cd1ad0 100644
--- a/crypto/aes/benches/aes_modes_benches.rs
+++ b/crypto/aes/benches/aes_modes_benches.rs
@@ -23,21 +23,21 @@
//! The cipher works in place, so each measurement runs on a fresh copy of the data made in
//! criterion's untimed setup (`iter_batched`); the copy is not part of the timing.
-use bouncycastle_aes::hazmat::{AES128Internal, AES256Internal};
-use bouncycastle_aes::{
- AES_CMAC_128, AES_CMAC_256, AES_GMAC_128, AES_GMAC_256, AES128Key, AES256Key,
-};
-use bouncycastle_cipher::modes::hazmat::ecb::Ecb;
+use bouncycastle_aes::hazmat::AES128Internal;
+use bouncycastle_aes::{AES128Key, AES256Key};
+use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb};
use bouncycastle_cipher::modes::{
- CCM, CMAC, Cbc, CcmEncryptor, Cfb, Cfb8, Ctr, GCM_NONCE_LEN, GMAC, Gcm,
+ Blocks, CBC, CCM, CCM_Packet, CFB, CFB8, CMAC, CTR, Cfb, Cfb8, Ctr, GCM, GCM_NONCE_LEN, GMAC,
};
use bouncycastle_cipher::{Decrypting, Encrypting};
-use bouncycastle_core::hazmat::{AeadPacketEncryptMode, ElectronicCodeBook};
+use bouncycastle_core::hazmat::{AeadDeclaredEncryptMode, EcbEncryptionKey, ElectronicCodeBook};
use bouncycastle_core::key_material::{KeyMaterial, KeyType};
-use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey};
+use bouncycastle_core::symmetric_key::{
+ AeadEncryptionKey, BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, MacGenerationKey,
+ SymmetricDecryptionKey, SymmetricEncryptionKey,
+};
use bouncycastle_core::traits::{
- AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, MAC, StreamCipherDecryptor,
- StreamCipherEncryptor, SymmetricCipherEncryptor,
+ BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor,
};
use bouncycastle_rng::FixedSeedRNG;
use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main};
@@ -48,9 +48,8 @@ const BLOCK_LEN: usize = 16;
const NUM_BLOCKS: usize = 1024;
const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN;
-type Aes128Cbc = Cbc;
-type Aes128Cfb = Cfb;
-type Aes128Cfb8 = Cfb8;
+type Aes128Cfb<'k, Dir> = Cfb<'k, AES128Internal, Dir, BLOCK_LEN>;
+type Aes128Cfb8<'k, Dir> = Cfb8<'k, AES128Internal, Dir, BLOCK_LEN>;
/// CCM at the parameters the ACVP vectors and most protocols use: a 12-byte nonce and a full
/// 16-byte tag. The mode's in-place one-shots over the keyed permutation are the direct path;
@@ -59,32 +58,31 @@ const CCM_NONCE_LEN: usize = 12;
const CCM_TAG_LEN: usize = 16;
type Aes128CcmMode = CCM;
-/// The trait adapter takes its frame size at compile time. Its one-shots are not bound by it,
-/// but using the same 4 KiB message keeps this comparison representative of the public alias a
+/// The frame key takes its frame size at compile time. Its one-shots are not bound by it, but
+/// using the same 4 KiB message keeps this comparison representative of the public alias a
/// packet protocol would choose.
const CCM_BUFFER_LEN: usize = 4096;
const CCM_AAD_LEN: usize = 64;
-type Aes128CcmEncryptor = CcmEncryptor<
- AES128Internal,
- 16,
- BLOCK_LEN,
- CCM_NONCE_LEN,
- CCM_TAG_LEN,
- CCM_AAD_LEN,
- CCM_BUFFER_LEN,
->;
+type Aes128CcmFrameKey =
+ AES128Key>;
/// GCM with the full 16-byte tag, as the `AES_GCM_*` aliases fix it.
const GCM_TAG_LEN: usize = 16;
-type Aes128Gcm