diff --git a/CLAUDE.md b/CLAUDE.md
index 908439d61..17c5b7bdf 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.
+
## Working from specifications
QUALITY_AND_STYLE.md is where the requirement for spec-corresponding comments and justified deviations lives. This
@@ -243,4 +293,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/Cargo.toml b/Cargo.toml
index 11e1f13cc..1b5d9ecec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,8 @@ bouncycastle = { path = "./" }
bouncycastle-aes = { path = "./crypto/aes" }
bouncycastle-ascon = { path = "./crypto/ascon" }
bouncycastle-base64 = { path = "./crypto/base64" }
+bouncycastle-blake2 = { path = "./crypto/blake2" }
+bouncycastle-blake3 = { path = "./crypto/blake3" }
bouncycastle-cipher = { path = "./crypto/cipher" }
bouncycastle-core = { path = "crypto/core" }
bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" }
@@ -24,6 +26,7 @@ bouncycastle-hmac = { path = "./crypto/hmac" }
bouncycastle-mlkem = { path = "./crypto/mlkem" }
bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory" }
bouncycastle-mldsa = { path = "./crypto/mldsa" }
+bouncycastle-mldsa-common = { path = "./crypto/mldsa-common" }
bouncycastle-mldsa-lowmemory = { path = "./crypto/mldsa-lowmemory" }
bouncycastle-rng = { path = "./crypto/rng" }
bouncycastle-sha2 = { path = "./crypto/sha2" }
@@ -50,6 +53,8 @@ edition.workspace = true
bouncycastle-aes.workspace = true
bouncycastle-ascon.workspace = true
bouncycastle-base64.workspace = true
+bouncycastle-blake2.workspace = true
+bouncycastle-blake3.workspace = true
bouncycastle-cipher.workspace = true
bouncycastle-core.workspace = true
bouncycastle-factory.workspace = true
diff --git a/INTRODUCTION.md b/INTRODUCTION.md
index 270c3720a..891af1347 100644
--- a/INTRODUCTION.md
+++ b/INTRODUCTION.md
@@ -114,7 +114,7 @@ pub type SHAKE128 = SHAKE;
pub type SHAKE256 = SHAKE;
```
-so that in the end `SHA3_256::new().hash(&data)` just does what you expect. The "If it compiles, then it's safe"
+so that in the end `SHA3_256::hash(&data)` just does what you expect. The "If it compiles, then it's safe"
paradigm is, however, still somewhat aspirational and not a total _fait accompli_, and as the library matures, we will
continue to find ways to refine our type system to turn ever more runtime error conditions into compile-time conditions.
diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md
index 458177530..ad10025ef 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.
# Quality
@@ -88,7 +92,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,
@@ -123,17 +133,21 @@ 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
-* `_init / _update / _final`: indicates phase of a stateful streaming API. Other verbs can be used here as appropriate
- to the primitive, such as `absorb / squeeze`, `encrypt / decrypt`, etc. `_init` is typically a static constructor
- (though exceptions may exist), and `_final` indicates that this function call renders the object unusable afterwards
- by consuming `self` via a move: `_final(self, ..)`.
+* `_update / _final`: indicates phase of a stateful streaming API. Other verbs can be used here as appropriate to the
+ primitive, such as `absorb / squeeze`, `encrypt / decrypt`, etc. `_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 89d3c28a5..d2592dc08 100644
--- a/cli/src/aes_ccm_cmd.rs
+++ b/cli/src/aes_ccm_cmd.rs
@@ -19,11 +19,9 @@
//! command to point at a multi-gigabyte file. `aes256-ctr` piped through a separate MAC, or
//! `ascon-aead128`, are the streaming alternatives.
//!
-//! The AAD is different: `--aad-file` is streamed. CCM needs the AAD's length before its first
-//! byte (A.2.2 puts the encoding of `a` in front of `A`), and a regular file's size is known
-//! before it is read, so the file is declared by its size and fed to the MAC in 1 KiB chunks
-//! without ever being held whole. A file with no size to declare -- a pipe, `/dev/stdin` -- is
-//! read whole instead.
+//! 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 type
+//! keeps for itself.)
//!
//! # The nonce is supplied, not generated
//!
@@ -42,32 +40,53 @@
//! `--nonce` must be 7..=13 bytes and `--tag-len` one of 4, 6, 8, 10, 12, 14, 16, both from
//! Appendix A.1. The nonce length fixes the maximum payload at `2^(8 * (15 - n)) - 1` bytes, which
//! this command checks against the actual input length. Because those are const generic parameters
-//! of the mode, the runtime value is dispatched to one of the seven nonce lengths and seven tag
+//! of the key type, the runtime value is dispatched to one of the seven nonce lengths and seven tag
//! lengths below.
//!
//! The output layout is Sec 6.1 step 8's own: `ciphertext || tag`.
-use std::fs::File;
use std::io::{self, Read};
-use std::marker::PhantomData;
use std::process::exit;
-use bouncycastle::aes::{
- AES_CCM_128_Packet, AES_CCM_192_Packet, AES_CCM_256_Packet, CCM_NONCE_LEN, CCM_TAG_LEN,
-};
-use bouncycastle::cipher::modes::CcmPacket;
-use bouncycastle::cipher::modes::ModeNames;
-use bouncycastle::cipher::{Decrypting, Encrypting};
+use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
+use bouncycastle::cipher::modes::CCM;
use bouncycastle::core::errors::SymmetricCipherError;
-use bouncycastle::core::hazmat::ElectronicCodeBook;
use bouncycastle::core::key_material::KeyMaterial;
+use bouncycastle::core::symmetric_key::{
+ AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, FromKeyMaterial,
+};
use bouncycastle::hex;
use crate::helpers;
-use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_key};
+use crate::helpers::block_mode_helpers::{CipherDirection, load_key};
+
+/// The CCM key type for one AES key length, at every nonce and tag length `run` can dispatch to.
+///
+/// The one-shots this command uses are on the key's role traits, and the key type carries the
+/// nonce and tag lengths as const parameters, so a generic `run` over the key length needs a way
+/// to name the key type for an `(N, T)` it chooses at run time: this generic associated type.
+trait CcmKeys {
+ /// The key type at nonce length `N` and tag length `T`.
+ type Key: FromKeyMaterial
+ + AeadDeclaredEncryptionKey
+ + AeadDeclaredDecryptionKey;
+}
+
+struct Aes128;
+struct Aes192;
+struct Aes256;
-/// Bytes of `--aad-file` read per call, matching the other commands' streaming chunk.
-const CHUNK_LEN: usize = 1024;
+impl CcmKeys<16> for Aes128 {
+ type Key = AES128Key>;
+}
+
+impl CcmKeys<24> for Aes192 {
+ type Key = AES192Key>;
+}
+
+impl CcmKeys<32> for Aes256 {
+ type Key = AES256Key>;
+}
/// AES-128 CCM. See the module docs and the subcommand help.
pub(crate) fn aes128_ccm_cmd(
@@ -81,8 +100,7 @@ pub(crate) fn aes128_ccm_cmd(
tag_len: usize,
output_hex: bool,
) {
- run(
- PhantomData::>,
+ run::(
action,
&load_key::<16>(key, key_file, "AES-128"),
nonce,
@@ -106,8 +124,7 @@ pub(crate) fn aes192_ccm_cmd(
tag_len: usize,
output_hex: bool,
) {
- run(
- PhantomData::>,
+ run::(
action,
&load_key::<24>(key, key_file, "AES-192"),
nonce,
@@ -131,8 +148,7 @@ pub(crate) fn aes256_ccm_cmd(
tag_len: usize,
output_hex: bool,
) {
- run(
- PhantomData::>,
+ run::(
action,
&load_key::<32>(key, key_file, "AES-256"),
nonce,
@@ -196,98 +212,18 @@ fn load_nonce(nonce: &Option, nonce_file: &Option) -> Vec {
bytes
}
-/// Where the AAD comes from.
-enum Aad {
- /// `--aad` (hex), a `--aad-file` with no size to declare, or no AAD at all: held whole.
- Bytes(Vec),
- /// A `--aad-file` that is a regular file: declared by its size and read in [`CHUNK_LEN`]
- /// pieces by [`feed_aad`], never held whole.
- File { file: File, path: String, len: usize },
-}
-
-impl Aad {
- /// The AAD length to declare to [`CcmPacket::new_with_lengths`].
- fn len(&self) -> usize {
- match self {
- Aad::Bytes(bytes) => bytes.len(),
- Aad::File { len, .. } => *len,
- }
- }
-}
-
/// Loads the AAD from `--aad-file` (raw bytes, never hex-decoded) or `--aad` (hex); the file wins
/// if both are given, as for `aes*-gcm`. Empty if neither is given.
-///
-/// A regular file is only opened and sized here; [`feed_aad`] reads it. Anything else -- a pipe, a
-/// character device -- has no size to declare in advance, so it is read whole.
-fn load_aad(aad: &Option, aad_file: &Option) -> Aad {
+fn load_aad(aad: &Option, aad_file: &Option) -> Vec {
if let Some(path) = aad_file {
- let file = File::open(path).unwrap_or_else(|e| {
- eprintln!("Error: couldn't read file '{path}': {e}");
- exit(-1)
- });
- let metadata = file.metadata().unwrap_or_else(|e| {
- eprintln!("Error: couldn't read file '{path}': {e}");
- exit(-1)
- });
- if !metadata.is_file() {
- return Aad::Bytes(helpers::read_from_file_raw(path));
- }
- let Ok(len) = usize::try_from(metadata.len()) else {
- eprintln!("Error: AAD file '{path}' is too large for this platform.");
- exit(-1)
- };
- Aad::File { file, path: path.clone(), len }
+ helpers::read_from_file_raw(path)
} else if let Some(v) = aad {
- Aad::Bytes(hex::decode(v).unwrap_or_else(|_| {
+ hex::decode(v).unwrap_or_else(|_| {
eprintln!("Error: associated data is not valid hex. Use --aad-file for raw bytes.");
exit(-1)
- }))
+ })
} else {
- Aad::Bytes(Vec::new())
- }
-}
-
-/// Supplies all of the AAD declared as [`Aad::len`] to `ccm`, reading an [`Aad::File`] a chunk at
-/// a time.
-///
-/// The declared length is the file's size when it was opened. If the file changes size while it
-/// is being read, the declared length is wrong, and the tag would be computed over a length
-/// encoding that does not match the AAD; that is reported and the command exits rather than
-/// producing it.
-fn feed_aad(
- ccm: &mut CcmPacket
,
- aad: &mut Aad,
-) where
- P: ElectronicCodeBook + ModeNames,
-{
- match aad {
- Aad::Bytes(bytes) => {
- // Declared as exactly `bytes.len()`, and supplied in this one call.
- ccm.do_update_aad(bytes).expect("declared AAD length matches what was sent");
- }
- Aad::File { file, path, len } => {
- let mut buf = [0u8; CHUNK_LEN];
- let mut read = 0usize;
- loop {
- let n = file.read(&mut buf).unwrap_or_else(|e| {
- eprintln!("Error: couldn't read file '{path}': {e}");
- exit(-1)
- });
- if n == 0 {
- break;
- }
- read += n;
- if ccm.do_update_aad(&buf[..n]).is_err() {
- eprintln!("Error: AAD file '{path}' grew while it was being read.");
- exit(-1)
- }
- }
- if read != *len {
- eprintln!("Error: AAD file '{path}' shrank while it was being read.");
- exit(-1)
- }
- }
+ Vec::new()
}
}
@@ -300,16 +236,11 @@ fn read_all_stdin() -> Vec {
/// Turns the runtime nonce and tag lengths into the mode's const generic parameters.
///
-/// `_aes` is the caller's `AES_CCM_*_Packet` type at the default lengths, and serves only to fix
-/// the block cipher `P` by inference, so that each entry point names the public alias rather than
-/// the cipher inside it; the lengths actually used are dispatched on below.
-///
-/// `NONCE_LEN` and `TAG_LEN` are const parameters of `CcmPacket` -- that is what makes A.1's length
-/// conditions compile-time checks rather than runtime ones -- so a command-line value has to be
-/// matched into one of the permitted instantiations. The two nested matches are the price of that,
-/// and they are exhaustive over A.1's sets: 7 nonce lengths x 7 tag lengths.
-fn run(
- _aes: PhantomData>,
+/// `NONCE_LEN` and `TAG_LEN` are const parameters of the key type -- that is what makes A.1's
+/// length conditions compile-time checks rather than runtime ones -- so a command-line value has to
+/// be matched into one of the permitted instantiations. The two nested matches are the price of
+/// that, and they are exhaustive over A.1's sets: 7 nonce lengths x 7 tag lengths.
+fn run, const KEY_LEN: usize>(
action: &CipherDirection,
key: &KeyMaterial,
nonce: &Option,
@@ -318,9 +249,7 @@ fn run(
aad_file: &Option,
tag_len: usize,
output_hex: bool,
-) where
- P: ElectronicCodeBook + ModeNames,
-{
+) {
// Reject this before opening nonce/AAD files or waiting for stdin. Appendix A.1: "t is an
// element of {4, 6, 8, 10, 12, 14, 16}".
if !matches!(tag_len, 4 | 6 | 8 | 10 | 12 | 14 | 16) {
@@ -332,34 +261,20 @@ fn run(
}
let nonce_bytes = load_nonce(nonce, nonce_file);
- let mut aad = load_aad(aad, aad_file);
+ let aad = load_aad(aad, aad_file);
let input = read_all_stdin();
let encrypt = matches!(action, CipherDirection::Encrypt);
macro_rules! with_tag_len {
($n:literal) => {
match tag_len {
- 4 => {
- go::
(key, &nonce_bytes, &mut aad, input, encrypt, output_hex)
- }
- 6 => {
- go::
(key, &nonce_bytes, &mut aad, input, encrypt, output_hex)
- }
- 8 => {
- go::
(key, &nonce_bytes, &mut aad, input, encrypt, output_hex)
- }
- 10 => go::
(
- key, &nonce_bytes, &mut aad, input, encrypt, output_hex,
- ),
- 12 => go::
(
- key, &nonce_bytes, &mut aad, input, encrypt, output_hex,
- ),
- 14 => go::
(
- key, &nonce_bytes, &mut aad, input, encrypt, output_hex,
- ),
- 16 => go::
(
- key, &nonce_bytes, &mut aad, input, encrypt, output_hex,
- ),
+ 4 => go::(key, &nonce_bytes, &aad, input, encrypt, output_hex),
+ 6 => go::(key, &nonce_bytes, &aad, input, encrypt, output_hex),
+ 8 => go::(key, &nonce_bytes, &aad, input, encrypt, output_hex),
+ 10 => go::(key, &nonce_bytes, &aad, input, encrypt, output_hex),
+ 12 => go::(key, &nonce_bytes, &aad, input, encrypt, output_hex),
+ 14 => go::(key, &nonce_bytes, &aad, input, encrypt, output_hex),
+ 16 => go::(key, &nonce_bytes, &aad, input, encrypt, output_hex),
_ => unreachable!("tag length was validated before stdin was read"),
}
};
@@ -382,24 +297,19 @@ fn run(
}
}
-/// Reports [`CcmPacket::new_with_lengths`]'s refusal of a payload past the `q` limit and exits.
+/// Reports the one-shots' refusal of a payload past the `q` limit and exits.
///
-/// The only [`SymmetricCipherError::GenericError`] it can return is that limit: A.1's
+/// The only [`SymmetricCipherError::GenericError`] they can return is that limit: A.1's
/// `p < 2^8q`, where `q = 15 - n`. Both directions hit it -- the decrypt side on the input minus
/// its tag -- so both report it here, with the numbers, since the fix is a shorter nonce.
-fn payload_past_the_q_limit
(
- msg: &str,
- payload_len: usize,
-) -> !
-where
- P: ElectronicCodeBook + ModeNames,
-{
+fn payload_past_the_q_limit(msg: &str, payload_len: usize) -> ! {
+ let q = 15 - NONCE_LEN;
+ // `2^8q - 1`; at n = 7, q = 8 and the limit is every u64.
+ let limit = if q >= 8 { u64::MAX } else { (1u64 << (8 * q)) - 1 };
eprintln!("Error: {msg}");
eprintln!(
- " Payload is {payload_len} bytes; with a {NONCE_LEN}-byte nonce, q = {} and the \
- limit is {} bytes.",
- 15 - NONCE_LEN,
- CcmPacket::::MAX_PAYLOAD_LEN,
+ " Payload is {payload_len} bytes; with a {NONCE_LEN}-byte nonce, q = {q} and the \
+ limit is {limit} bytes."
);
eprintln!(" Use a shorter nonce for a larger payload.");
exit(-1)
@@ -407,47 +317,32 @@ where
/// One fully-instantiated CCM run.
///
-/// `input` is processed in place through [`CcmPacket`]'s own streaming API rather than through the
-/// one-shot [`CcmPacket::encrypt_out`]/[`CcmPacket::decrypt_out`], which each need a second, freshly allocated buffer
-/// the size of `input`: the declared-length constructor already has everything a one-shot needs,
-/// so there is no second buffer to allocate or copy into. The AAD goes in through
-/// [`CcmPacket::new_with_lengths`] and [`feed_aad`], so a `--aad-file` is streamed rather than loaded.
-fn go
(
+/// `input` is processed in place through the key's in-place one-shots, so there is no second
+/// buffer the size of `input` to allocate or copy into, and on the decrypt side nothing is written
+/// to stdout until the tag has verified: the one-shot zeroizes the buffer on a failed check, so
+/// Sec 6.2's "the payload P and the MAC T shall not be revealed" holds end to end.
+fn go, const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize>(
key: &KeyMaterial,
nonce_bytes: &[u8],
- aad: &mut Aad,
+ aad: &[u8],
mut input: Vec,
encrypt: bool,
output_hex: bool,
-) where
- P: ElectronicCodeBook + ModeNames,
-{
- type Enc =
- CcmPacket
;
- type Dec
=
- CcmPacket
;
-
+) {
// `run` dispatched on this exact length, so the conversion cannot fail.
let Ok(nonce) = <[u8; NONCE_LEN]>::try_from(nonce_bytes) else {
eprintln!("Error: internal nonce length mismatch.");
exit(-1)
};
+ // `load_key` has already checked the length and type, so this is the schedule expansion.
+ let key = F::Key::::from_key_material(key).unwrap_or_else(|e| {
+ eprintln!("Error: AES-CCM key is not usable: {e:?}");
+ exit(-1)
+ });
if encrypt {
- match Enc::::new_with_lengths(
- key,
- &nonce,
- aad.len(),
- input.len(),
- ) {
- Ok(mut ccm) => {
- feed_aad(&mut ccm, aad);
- // `new` already accepted this exact length as `input.len()`, and this is the one
- // and only call supplying it, so `take_owed` can never see too much and `owed`
- // can never be left nonzero: neither of these can fail on the path that reaches
- // them.
- ccm.do_encrypt_inplace(&mut input).expect("declared length matches what was sent");
- let tag = ccm.do_encrypt_final().expect("declared length was fully supplied");
+ match key.encrypt_detached_in_place_nonce(&nonce, aad, &mut input) {
+ Ok(tag) => {
helpers::write_bytes_or_hex(&input, output_hex);
helpers::write_bytes_or_hex(&tag, output_hex);
if output_hex {
@@ -455,7 +350,7 @@ fn go
(
}
}
Err(SymmetricCipherError::GenericError(msg)) => {
- payload_past_the_q_limit::
(msg, input.len())
+ payload_past_the_q_limit::(msg, input.len())
}
Err(e) => {
eprintln!("Error: AES-CCM encryption failed: {e:?}");
@@ -464,8 +359,8 @@ fn go(
}
} else {
// `split_last_chunk_mut` is `None` exactly when there is no room for a `TAG_LEN`-byte tag,
- // which is the same octet-level test (and the same allowance for an empty payload plus its
- // tag) that `CcmPacket::decrypt_out`'s own doc comment explains for Sec 6.2 step 1.
+ // the same octet-level test (and the same allowance for an empty payload plus its tag)
+ // as Sec 6.2 step 1.
let Some((data, tag)) = input.split_last_chunk_mut::() else {
eprintln!(
"Error: input is {} bytes, shorter than the {TAG_LEN}-byte tag it must end with.",
@@ -473,41 +368,19 @@ fn go(
);
exit(-1)
};
- match Dec::
::new_with_lengths(
- key,
- &nonce,
- aad.len(),
- data.len(),
- ) {
- Ok(mut ccm) => {
- feed_aad(&mut ccm, aad);
- // As the encrypt arm above: `data.len()` is exactly the length just declared, and
- // it is supplied in this one call, so this cannot fail.
- ccm.do_decrypt_inplace(data).expect("declared length matches what was sent");
- match ccm.do_decrypt_final(tag) {
- Ok(()) => {
- helpers::write_bytes_or_hex(data, output_hex);
- if output_hex {
- crate::helpers::write_stdout(b"\n");
- }
- }
- Err(SymmetricCipherError::AEADTagCheckFailed) => {
- // Nothing has been written to stdout at this point, which is what
- // processing in place still buys here: Sec 6.2's "the payload P and the
- // MAC T shall not be revealed" holds end to end.
- eprintln!(
- "Error: AES-CCM authentication failed; the input is not authentic."
- );
- exit(-1)
- }
- Err(e) => {
- eprintln!("Error: AES-CCM decryption failed: {e:?}");
- exit(-1)
- }
+ match key.decrypt_detached_in_place(&nonce, aad, data, tag) {
+ Ok(()) => {
+ helpers::write_bytes_or_hex(data, output_hex);
+ if output_hex {
+ crate::helpers::write_stdout(b"\n");
}
}
+ Err(SymmetricCipherError::AEADTagCheckFailed) => {
+ eprintln!("Error: AES-CCM authentication failed; the input is not authentic.");
+ exit(-1)
+ }
Err(SymmetricCipherError::GenericError(msg)) => {
- payload_past_the_q_limit::
(msg, data.len())
+ payload_past_the_q_limit::(msg, data.len())
}
Err(e) => {
eprintln!("Error: AES-CCM decryption failed: {e:?}");
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/blake2_cmd.rs b/cli/src/blake2_cmd.rs
new file mode 100644
index 000000000..20d4ed5f2
--- /dev/null
+++ b/cli/src/blake2_cmd.rs
@@ -0,0 +1,52 @@
+//! BLAKE2b-512 and BLAKE2s-256 (RFC 7693): the unkeyed hashes and their built-in keyed (MAC) mode.
+
+use bouncycastle::blake2::{BLAKE2b_512, BLAKE2b_512_MACKey, BLAKE2s_256, BLAKE2s_256_MACKey};
+use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey};
+
+use crate::helpers::{load_mac_key, stream_hash};
+use crate::mac_cmd::do_mac;
+
+#[allow(non_camel_case_types)]
+pub(crate) enum BLAKE2Variant {
+ BLAKE2b_512,
+ BLAKE2s_256,
+}
+
+/// BLAKE2b-512 / BLAKE2s-256 of stdin, streamed in 1 KiB pieces.
+pub(crate) fn blake2_cmd(variant: BLAKE2Variant, output_hex: bool) {
+ match variant {
+ BLAKE2Variant::BLAKE2b_512 => stream_hash(BLAKE2b_512::new(), output_hex),
+ BLAKE2Variant::BLAKE2s_256 => stream_hash(BLAKE2s_256::new(), output_hex),
+ }
+}
+
+/// Keyed BLAKE2b-512 / BLAKE2s-256 (RFC 7693 s. 3.3) of stdin: compute a tag, or with `--verify`
+/// check one and report the result in the exit status.
+pub(crate) fn blake2_mac_cmd(
+ variant: BLAKE2Variant,
+ key: &Option,
+ key_file: &Option,
+ verify_val: &Option,
+ output_hex: bool,
+) {
+ // RFC 7693 s. 2.1 table, "Key bytes": "0 <= kk <= 64" (BLAKE2b) / "0 <= kk <= 32" (BLAKE2s).
+ // kk = 0 is the unkeyed hash, which is the other command, so a MAC key is at least 1 byte.
+ //
+ // The allow-weak form, as the HMAC and KMAC 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 variant {
+ BLAKE2Variant::BLAKE2b_512 => {
+ let key = load_mac_key(key, key_file, "BLAKE2b-512-MAC", 1, 64);
+ // infallible: a 1..=64-byte MACKey (or Zeroized) key is exactly what the key object
+ // accepts once the strength check is skipped.
+ let mac_key = BLAKE2b_512_MACKey::from_key_material_allow_weak(&key).unwrap();
+ do_mac(mac_key.create_generator(), verify_val, output_hex);
+ }
+ BLAKE2Variant::BLAKE2s_256 => {
+ let key = load_mac_key(key, key_file, "BLAKE2s-256-MAC", 1, 32);
+ // infallible: as above, with the 1..=32-byte range of BLAKE2s.
+ let mac_key = BLAKE2s_256_MACKey::from_key_material_allow_weak(&key).unwrap();
+ do_mac(mac_key.create_generator(), verify_val, output_hex);
+ }
+ }
+}
diff --git a/cli/src/blake3_cmd.rs b/cli/src/blake3_cmd.rs
new file mode 100644
index 000000000..30f924115
--- /dev/null
+++ b/cli/src/blake3_cmd.rs
@@ -0,0 +1,84 @@
+//! BLAKE3 (BLAKE3 specification, version 20211102173700): the `hash`, `keyed_hash` and
+//! `derive_key` modes, each with output of any length.
+
+use std::io::{self, Read};
+use std::process::exit;
+
+use bouncycastle::blake3::{BLAKE3, BLAKE3DeriveKey, BLAKE3KeyedHashKey};
+use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey};
+use bouncycastle::core::traits::{MAC, XOFSqueezer};
+
+use crate::helpers::{load_mac_key, stream_xof, write_bytes_or_hex, write_stdout};
+use crate::mac_cmd::do_mac;
+
+/// BLAKE3 spec s. 2.3: `keyed_hash` "takes an input of any length together with a 256-bit key".
+/// The crate has no public constant for it.
+const BLAKE3_KEY_LEN: usize = 32;
+
+/// The default-length BLAKE3 output: BLAKE3 spec s. 2.1, "The chaining value of the root node,
+/// encoded as 32 bytes in little-endian order, is the default-length BLAKE3 hash of the input."
+pub(crate) const BLAKE3_DEFAULT_OUT_LEN: usize = 32;
+
+/// BLAKE3 `hash` mode of stdin, streamed in 1 KiB pieces, squeezing `output_len` bytes.
+pub(crate) fn blake3_cmd(output_len: usize, output_hex: bool) {
+ stream_xof(BLAKE3::new(), output_len, output_hex);
+}
+
+/// BLAKE3 `derive_key` mode: the key material is streamed from stdin, the context string is the
+/// argument, and `output_len` bytes of derived key are written out.
+///
+/// BLAKE3 spec s. 6.2: the context string "should be hardcoded, globally unique, and
+/// application-specific"; this command takes it as an argument only so that scripts can name the
+/// application's fixed string.
+pub(crate) fn blake3_derive_key_cmd(context: &str, output_len: usize, output_hex: bool) {
+ stream_xof(BLAKE3DeriveKey::new(context.as_bytes()), output_len, output_hex);
+}
+
+/// BLAKE3 `keyed_hash` mode of stdin: write `output_len` bytes of keyed output, or with `--verify`
+/// check a 32-byte tag and report the result in the exit status.
+pub(crate) fn blake3_keyed_hash_cmd(
+ key: &Option,
+ key_file: &Option,
+ verify_val: &Option,
+ output_len: usize,
+ output_hex: bool,
+) {
+ // Verification goes through MAC::do_verify_final, which compares the default 32-byte tag in
+ // constant time and requires an exact length match; an extended output length has no
+ // meaning there, so refuse the combination rather than silently ignore --length.
+ if verify_val.is_some() && output_len != BLAKE3_DEFAULT_OUT_LEN {
+ eprintln!(
+ "Error: --verify checks the {BLAKE3_DEFAULT_OUT_LEN}-byte tag and cannot be combined \
+ with --length."
+ );
+ exit(-1);
+ }
+
+ let key = load_mac_key(key, key_file, "BLAKE3-KeyedHash", BLAKE3_KEY_LEN, BLAKE3_KEY_LEN);
+ // The allow-weak form, as the HMAC and KMAC commands use: a CLI is used for test vectors and
+ // scripting, where a low-entropy or all-zero key is a legitimate thing to want.
+ // infallible: a 32-byte MACKey (or Zeroized) key is exactly what the key object accepts once
+ // the strength check is skipped.
+ let mac_key = BLAKE3KeyedHashKey::from_key_material_allow_weak(&key).unwrap();
+ let mut mac = mac_key.create_generator();
+
+ if verify_val.is_some() {
+ // do_mac streams stdin and, given a value to verify, exits with the result.
+ do_mac(mac, verify_val, output_hex);
+ return;
+ }
+
+ // Hand-rolled rather than a helper: stream_xof needs an XOF and do_mac finishes with
+ // MAC::do_final, but extended keyed output needs the inherent into_squeezer afterwards.
+ let mut buf: [u8; 1024] = [0u8; 1024];
+ let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
+ while bytes_read != 0 {
+ mac.do_update(&buf[..bytes_read]);
+ bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
+ }
+
+ // BLAKE3 spec s. 2.6: the keyed output stream, of which the MAC tag is the first 32 bytes.
+ let out = mac.into_squeezer().do_output_final(output_len);
+ write_bytes_or_hex(&out, output_hex);
+ write_stdout(b"\n");
+}
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