From 50fe78546b3bf64e21dc1da6035c0ca4f6a396a3 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 2 Sep 2026 10:24:52 +1000 Subject: [PATCH 1/7] aes-lowmemory: add AES_CBC_128 / AES_CBC_192 / AES_CBC_256 type aliases One alias per AES key length over bouncycastle_modes::Cbc, generic in the direction marker, so callers never spell out the KEY_LEN / BLOCK_LEN const parameters. They live in the AES crate (new cbc.rs module) because the modes crate is deliberately cipher-agnostic; this adds bouncycastle-modes as a dependency of aes-lowmemory, leaving modes' dev-dependency on aes-lowmemory as a Cargo-permitted dev-dep cycle. Each alias carries a doctest exercising both directions, since Rust only checks an alias's bounds at a use site, and the crate docs gain a CBC usage example. Naming follows the HMAC_SHA256 / HKDF_SHA256 convention, with the same non_camel_case_types allowance. Co-Authored-By: Claude Fable 5.1 --- crypto/aes-lowmemory/Cargo.toml | 2 ++ crypto/aes-lowmemory/src/cbc.rs | 61 +++++++++++++++++++++++++++++++++ crypto/aes-lowmemory/src/lib.rs | 24 +++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 crypto/aes-lowmemory/src/cbc.rs diff --git a/crypto/aes-lowmemory/Cargo.toml b/crypto/aes-lowmemory/Cargo.toml index 93316d45..f6cbff4d 100644 --- a/crypto/aes-lowmemory/Cargo.toml +++ b/crypto/aes-lowmemory/Cargo.toml @@ -6,6 +6,8 @@ edition.workspace = true [dependencies] bouncycastle-core.workspace = true bouncycastle-utils.workspace = true +# Only for the AES-CBC type aliases in `cbc.rs`; the engine itself does not use it. +bouncycastle-modes.workspace = true [dev-dependencies] bouncycastle-core-test-framework.workspace = true diff --git a/crypto/aes-lowmemory/src/cbc.rs b/crypto/aes-lowmemory/src/cbc.rs new file mode 100644 index 00000000..2e9245b2 --- /dev/null +++ b/crypto/aes-lowmemory/src/cbc.rs @@ -0,0 +1,61 @@ +//! Type aliases for AES in CBC mode (NIST SP 800-38A Sec 6.2). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cbc` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. These aliases pin the AES values so +//! callers never spell them out. They add nothing to the engine: the permutation still implements +//! none of the data-encryption traits itself (see the crate docs), the mode does. + +use crate::{Aes128, Aes192, Aes256, BLOCK_LEN}; +use bouncycastle_modes::Cbc; + +/// AES-128 in CBC mode. `Dir` is [`bouncycastle_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// The IV is generated by encryption and returned alongside the ciphertext; it is never supplied. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CBC_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// let plaintext = [[0u8; 16]; 3]; +/// +/// let (iv, ciphertext) = AES_CBC_128::::encrypt_blocks(&key, &plaintext).unwrap(); +/// let recovered = AES_CBC_128::::decrypt_blocks(&key, &iv, &ciphertext).unwrap(); +/// assert_eq!(recovered, plaintext); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CBC_128 = Cbc; + +/// AES-192 in CBC mode. See [`AES_CBC_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CBC_192; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey).unwrap(); +/// let (iv, ct) = AES_CBC_192::::encrypt_blocks(&key, &[[0u8; 16]; 2]).unwrap(); +/// assert_eq!(AES_CBC_192::::decrypt_blocks(&key, &iv, &ct).unwrap(), [[0u8; 16]; 2]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CBC_192 = Cbc; + +/// AES-256 in CBC mode. See [`AES_CBC_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CBC_256; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +/// let (iv, ct) = AES_CBC_256::::encrypt_blocks(&key, &[[0u8; 16]; 2]).unwrap(); +/// assert_eq!(AES_CBC_256::::decrypt_blocks(&key, &iv, &ct).unwrap(), [[0u8; 16]; 2]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CBC_256 = Cbc; diff --git a/crypto/aes-lowmemory/src/lib.rs b/crypto/aes-lowmemory/src/lib.rs index 866a5167..5a879dfe 100644 --- a/crypto/aes-lowmemory/src/lib.rs +++ b/crypto/aes-lowmemory/src/lib.rs @@ -56,6 +56,28 @@ //! assert_eq!(blocks, [[0u8; 16], [1u8; 16]]); //! ``` //! +//! ## CBC mode +//! +//! To encrypt more than one block, use a mode of operation from `bouncycastle-modes`. This crate +//! provides [`AES_CBC_128`], [`AES_CBC_192`] and [`AES_CBC_256`] as aliases that fill in the const +//! parameters, with the direction left as the type parameter: +//! +//! ``` +//! use bouncycastle_aes_lowmemory::AES_CBC_256; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_modes::{Decrypting, Encrypting}; +//! +//! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let plaintext = [[0u8; 16], [1u8; 16], [2u8; 16]]; +//! +//! // The IV is generated for you and returned; there is no API for supplying one. +//! let (iv, ciphertext) = AES_CBC_256::::encrypt_blocks(&key, &plaintext).unwrap(); +//! let recovered = AES_CBC_256::::decrypt_blocks(&key, &iv, &ciphertext).unwrap(); +//! assert_eq!(recovered, plaintext); +//! ``` +//! //! There is no one-shot static on the permutation, because `Aes128::new(&key)?.encrypt_block(..)` //! already *is* the one shot. Data-level one-shots belong to the modes of operation, which take //! arbitrary-length input and generate their own initialisation data. @@ -166,10 +188,12 @@ mod aes; mod bitslice; +mod cbc; mod round; mod sbox; mod schedule; pub use aes::{Aes, Aes128, Aes192, Aes256, BLOCK_LEN}; pub use bitslice::Block; +pub use cbc::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; pub use schedule::{Aes128Params, Aes192Params, Aes256Params, AesParams}; From f6569674bd44f45742776f4a061921773ac84c3f Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 2 Sep 2026 15:01:28 +1000 Subject: [PATCH 2/7] core: block cipher one-shots take flat arrays; by-value streaming is provided The one-shots on BlockCipherEncryptor / BlockCipherDecryptor -- encrypt, encrypt_rng, encrypt_out, encrypt_out_rng and decrypt, decrypt_out -- now take a `[u8; LEN]` instead of an array of blocks. They replace the block-shaped encrypt_blocks / encrypt_blocks_rng / encrypt_blocks_out / encrypt_blocks_out_rng and decrypt_blocks / decrypt_blocks_out from #96, which nothing outside tests and docs used and which the flat form makes redundant (0.1.3 is unreleased, so nothing shipped changes). LEN must be a whole number of blocks and this is enforced at compile time: an inline `const { assert!(LEN % BLOCK_LEN == 0) }` fails at the call site that instantiates a misaligned LEN, so no runtime length check and no error variant. Inside, LEN / BLOCK_LEN is not nameable without generic_const_exprs, so the shared helpers walk the buffer with as_chunks -- pairs first, so a mode's two-block path is used, then the at-most-one remaining block. The by-value streaming methods do_{en,de}crypt_blocks are now provided in terms of their _out forms, shrinking the implementor contract to init[_rng] and do_*_blocks_out. Tests and docs updated to the flat form: the core-test-framework suite covers the single-block flat forms (all it can form generically); modes/tests/cbc_tests checks 3- and 4-block flat arrays agree byte-for-byte with the streaming API in both directions and via _out; sp800_38a_tests runs the F.2 decrypt vectors through the flat one-shot; the AES_CBC_* docs gain flat and streaming examples and a compile_fail doctest for a 47-byte array. Release note updated. Co-Authored-By: Claude Fable 5.1 --- alpha_0.1.3_release_notes.md | 12 +- crypto/aes-lowmemory/src/cbc.rs | 36 +++- crypto/aes-lowmemory/src/lib.rs | 7 +- .../src/symmetric_ciphers.rs | 29 ++-- crypto/core/src/traits.rs | 158 ++++++++++++++---- crypto/modes/src/lib.rs | 9 +- crypto/modes/tests/cbc_tests.rs | 65 ++++++- crypto/modes/tests/sp800_38a_tests.rs | 18 +- 8 files changed, 255 insertions(+), 79 deletions(-) diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index 6705ef81..e20f391f 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -139,10 +139,14 @@ Block cipher traits (PR #96): pattern. * The `do_{en,de}crypt_final[_out]` methods are removed: the traits are now strictly block-aligned, and padding of arbitrary-length data belongs to a separate `PaddedEncryptor` / `PaddedDecryptor` layer built on top. -* One-shot static APIs are provided (default) methods implemented once in the traits -- `encrypt_blocks`, - `encrypt_blocks_rng`, `encrypt_blocks_out`, `encrypt_blocks_out_rng` on `BlockCipherEncryptor` and `decrypt_blocks`, - `decrypt_blocks_out` on `BlockCipherDecryptor` -- so every block-aligned mode gets the house-standard - take-data-return-result API at no cost to implementors. +* One-shot static APIs are provided (default) methods implemented once in the traits -- `encrypt`, `encrypt_rng`, + `encrypt_out`, `encrypt_out_rng` on `BlockCipherEncryptor` and `decrypt`, `decrypt_out` on `BlockCipherDecryptor` -- + so every block-aligned mode gets the house-standard take-data-return-result API at no cost to implementors. They + take a flat `[u8; LEN]`; `LEN` must be a whole number of blocks, and this is enforced at **compile time** by an + inline `const` assertion at the instantiating call site, so there is no runtime length check and no error variant + for it. (An earlier form of these one-shots took `[[u8; BLOCK_LEN]; N]`; it was replaced before release.) +* The by-value streaming methods `do_{en,de}crypt_blocks` are provided in terms of their `_out` forms, so an + implementor writes only `do_{en,de}crypt_init[_rng]` and `do_{en,de}crypt_blocks_out`. Testing: diff --git a/crypto/aes-lowmemory/src/cbc.rs b/crypto/aes-lowmemory/src/cbc.rs index 2e9245b2..a0919c1d 100644 --- a/crypto/aes-lowmemory/src/cbc.rs +++ b/crypto/aes-lowmemory/src/cbc.rs @@ -21,11 +21,31 @@ use bouncycastle_modes::Cbc; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) /// .expect("a 16-byte symmetric cipher key"); -/// let plaintext = [[0u8; 16]; 3]; +/// // 48 bytes: three whole blocks. The length is checked at compile time. +/// let message = [0u8; 48]; +/// let (iv, ciphertext) = AES_CBC_128::::encrypt(&key, &message).unwrap(); +/// assert_eq!(AES_CBC_128::::decrypt(&key, &iv, &ciphertext).unwrap(), message); /// -/// let (iv, ciphertext) = AES_CBC_128::::encrypt_blocks(&key, &plaintext).unwrap(); -/// let recovered = AES_CBC_128::::decrypt_blocks(&key, &iv, &ciphertext).unwrap(); -/// assert_eq!(recovered, plaintext); +/// // Streaming, a few blocks at a time: +/// let (mut enc, iv) = AES_CBC_128::::do_encrypt_init(&key).unwrap(); +/// let first = enc.do_encrypt_blocks(&[[0u8; 16]]).unwrap(); +/// let rest = enc.do_encrypt_blocks(&[[1u8; 16], [2u8; 16]]).unwrap(); +/// let mut dec = AES_CBC_128::::do_decrypt_init(&key, &iv).unwrap(); +/// assert_eq!(dec.do_decrypt_blocks(&first).unwrap(), [[0u8; 16]]); +/// assert_eq!(dec.do_decrypt_blocks(&rest).unwrap(), [[1u8; 16], [2u8; 16]]); +/// ``` +/// +/// A length that is not a whole number of blocks is a **compile** error, not a runtime one: +/// +/// ```compile_fail +/// use bouncycastle_aes_lowmemory::AES_CBC_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::BlockCipherEncryptor; +/// use bouncycastle_modes::Encrypting; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// // 47 bytes is not a multiple of 16: the inline const assertion in `encrypt` fails to compile. +/// let _ = AES_CBC_128::::encrypt(&key, &[0u8; 47]); /// ``` #[allow(non_camel_case_types)] pub type AES_CBC_128 = Cbc; @@ -39,8 +59,8 @@ pub type AES_CBC_128 = Cbc; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// /// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey).unwrap(); -/// let (iv, ct) = AES_CBC_192::::encrypt_blocks(&key, &[[0u8; 16]; 2]).unwrap(); -/// assert_eq!(AES_CBC_192::::decrypt_blocks(&key, &iv, &ct).unwrap(), [[0u8; 16]; 2]); +/// let (iv, ct) = AES_CBC_192::::encrypt(&key, &[0u8; 32]).unwrap(); +/// assert_eq!(AES_CBC_192::::decrypt(&key, &iv, &ct).unwrap(), [0u8; 32]); /// ``` #[allow(non_camel_case_types)] pub type AES_CBC_192 = Cbc; @@ -54,8 +74,8 @@ pub type AES_CBC_192 = Cbc; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// /// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); -/// let (iv, ct) = AES_CBC_256::::encrypt_blocks(&key, &[[0u8; 16]; 2]).unwrap(); -/// assert_eq!(AES_CBC_256::::decrypt_blocks(&key, &iv, &ct).unwrap(), [[0u8; 16]; 2]); +/// let (iv, ct) = AES_CBC_256::::encrypt(&key, &[0u8; 32]).unwrap(); +/// assert_eq!(AES_CBC_256::::decrypt(&key, &iv, &ct).unwrap(), [0u8; 32]); /// ``` #[allow(non_camel_case_types)] pub type AES_CBC_256 = Cbc; diff --git a/crypto/aes-lowmemory/src/lib.rs b/crypto/aes-lowmemory/src/lib.rs index 5a879dfe..a39067f4 100644 --- a/crypto/aes-lowmemory/src/lib.rs +++ b/crypto/aes-lowmemory/src/lib.rs @@ -70,11 +70,12 @@ //! //! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); -//! let plaintext = [[0u8; 16], [1u8; 16], [2u8; 16]]; +//! // 48 bytes: three whole blocks. A length that is not a multiple of 16 would not compile. +//! let plaintext = [0x5Au8; 48]; //! //! // The IV is generated for you and returned; there is no API for supplying one. -//! let (iv, ciphertext) = AES_CBC_256::::encrypt_blocks(&key, &plaintext).unwrap(); -//! let recovered = AES_CBC_256::::decrypt_blocks(&key, &iv, &ciphertext).unwrap(); +//! let (iv, ciphertext) = AES_CBC_256::::encrypt(&key, &plaintext).unwrap(); +//! let recovered = AES_CBC_256::::decrypt(&key, &iv, &ciphertext).unwrap(); //! assert_eq!(recovered, plaintext); //! ``` //! diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 180e5851..4e1c57c0 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -195,20 +195,23 @@ impl TestFrameworkBlockCipher { assert_eq!(msg_pair, &pt); } - // one-shot API: must agree with the streaming API for the same key, and round-trip - let two_blocks: &[[u8; BLOCK_LEN]; 2] = - &DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0[0]; - let (iv, ct) = E::encrypt_blocks(&key, two_blocks).unwrap(); - assert_eq!(D::decrypt_blocks(&key, &iv, &ct).unwrap(), *two_blocks); + // one-shot API: a block-aligned byte array. It must round-trip and agree with the streaming + // API for the same key and init data. Only LEN = BLOCK_LEN can be formed generically here + // (`2 * BLOCK_LEN` needs generic_const_exprs); multi-block one-shots are covered by the modes + // crate's tests with a concrete BLOCK_LEN. + let one_block: &[u8; BLOCK_LEN] = &DUMMY_SEED.as_chunks::().0[0]; + let (iv, ct) = E::encrypt(&key, one_block).unwrap(); + assert_eq!(D::decrypt(&key, &iv, &ct).unwrap(), *one_block); + // ...and it must agree with the block-shaped API under the same init data. let mut streamed = D::do_decrypt_init(&key, &iv).unwrap(); - assert_eq!(streamed.do_decrypt_blocks(&ct).unwrap(), *two_blocks); - - let mut ct = [[0u8; BLOCK_LEN]; 2]; - let mut pt = [[0u8; BLOCK_LEN]; 2]; - let (iv, n) = E::encrypt_blocks_out(&key, two_blocks, &mut ct).unwrap(); - assert_eq!(n, 2 * BLOCK_LEN); - assert_eq!(D::decrypt_blocks_out(&key, &iv, &ct, &mut pt).unwrap(), 2 * BLOCK_LEN); - assert_eq!(pt, *two_blocks); + assert_eq!(streamed.do_decrypt_blocks(&[ct]).unwrap(), [*one_block]); + + let mut ct = [0u8; BLOCK_LEN]; + let mut pt = [0u8; BLOCK_LEN]; + let (iv, n) = E::encrypt_out(&key, one_block, &mut ct).unwrap(); + assert_eq!(n, BLOCK_LEN); + assert_eq!(D::decrypt_out(&key, &iv, &ct, &mut pt).unwrap(), BLOCK_LEN); + assert_eq!(pt, *one_block); // test that the iv is random (ie not the same on two runs) let (_encryptor, iv1) = E::do_encrypt_init(&key).unwrap(); diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 2092d7bd..cce737af 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -179,10 +179,17 @@ pub trait BlockCipherEncryptor< ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; /// Encrypts `N` consecutive blocks of plaintext. A sequence of calls is equivalent to one call over /// the concatenation. + /// + /// Provided in terms of [`BlockCipherEncryptor::do_encrypt_blocks_out`]; implementors need only + /// write the `_out` form. fn do_encrypt_blocks( &mut self, plaintext: &[[u8; BLOCK_LEN]; N], - ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError>; + ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { + let mut ciphertext = [[0u8; BLOCK_LEN]; N]; + self.do_encrypt_blocks_out(plaintext, &mut ciphertext)?; + Ok(ciphertext) + } /// Encrypts `N` consecutive blocks of plaintext into the provided buffer. Returns `N * BLOCK_LEN`. fn do_encrypt_blocks_out( &mut self, @@ -190,45 +197,87 @@ pub trait BlockCipherEncryptor< ciphertext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; - /// One-shot: encrypts `N` blocks under a fresh init. Returns the generated init data and the ciphertext. - fn encrypt_blocks( + /// One-shot on a flat byte array: encrypts `LEN` bytes under a fresh init. Returns the generated + /// init data and the ciphertext. + /// + /// `LEN` must be a whole number of blocks. This is checked **at compile time**: instantiating + /// this method with a `LEN` that is not a multiple of `BLOCK_LEN` is a compile error at the call + /// site, not a runtime `Err`. The check is an inline `const` assertion, so it fires when the + /// generic is instantiated (i.e. in the calling crate), which is why there is no length variant + /// of [`SymmetricCipherError`] here. Non-block-aligned data belongs to the padding layer. + fn encrypt( key: &KeyMaterial, - plaintext: &[[u8; BLOCK_LEN]; N], - ) -> Result<([u8; INIT_DATA_LEN], [[u8; BLOCK_LEN]; N]), SymmetricCipherError> { - let (mut enc, init_data) = Self::do_encrypt_init(key)?; - Ok((init_data, enc.do_encrypt_blocks(plaintext)?)) + plaintext: &[u8; LEN], + ) -> Result<([u8; INIT_DATA_LEN], [u8; LEN]), SymmetricCipherError> { + let mut ciphertext = [0u8; LEN]; + let (init_data, _) = Self::encrypt_out(key, plaintext, &mut ciphertext)?; + Ok((init_data, ciphertext)) } - /// As [`BlockCipherEncryptor::encrypt_blocks`], but sources randomness from the provided RNG. - fn encrypt_blocks_rng( + /// As [`BlockCipherEncryptor::encrypt`], but sources randomness from the provided RNG. + fn encrypt_rng( key: &KeyMaterial, rng: &mut dyn RNG, - plaintext: &[[u8; BLOCK_LEN]; N], - ) -> Result<([u8; INIT_DATA_LEN], [[u8; BLOCK_LEN]; N]), SymmetricCipherError> { - let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; - Ok((init_data, enc.do_encrypt_blocks(plaintext)?)) + plaintext: &[u8; LEN], + ) -> Result<([u8; INIT_DATA_LEN], [u8; LEN]), SymmetricCipherError> { + let mut ciphertext = [0u8; LEN]; + let (init_data, _) = Self::encrypt_out_rng(key, rng, plaintext, &mut ciphertext)?; + Ok((init_data, ciphertext)) } - /// One-shot: encrypts `N` blocks under a fresh init into the provided buffer. - /// Returns the generated init data and `N * BLOCK_LEN`. - fn encrypt_blocks_out( + /// As [`BlockCipherEncryptor::encrypt`], into the provided buffer. Returns the generated init + /// data and `LEN`. + fn encrypt_out( key: &KeyMaterial, - plaintext: &[[u8; BLOCK_LEN]; N], - ciphertext: &mut [[u8; BLOCK_LEN]; N], + plaintext: &[u8; LEN], + ciphertext: &mut [u8; LEN], ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { let (mut enc, init_data) = Self::do_encrypt_init(key)?; - Ok((init_data, enc.do_encrypt_blocks_out(plaintext, ciphertext)?)) + Ok((init_data, encrypt_flat(&mut enc, plaintext, ciphertext)?)) } - /// As [`BlockCipherEncryptor::encrypt_blocks_out`], but sources randomness from the provided RNG. - fn encrypt_blocks_out_rng( + /// As [`BlockCipherEncryptor::encrypt_out`], but sources randomness from the provided RNG. + fn encrypt_out_rng( key: &KeyMaterial, rng: &mut dyn RNG, - plaintext: &[[u8; BLOCK_LEN]; N], - ciphertext: &mut [[u8; BLOCK_LEN]; N], + plaintext: &[u8; LEN], + ciphertext: &mut [u8; LEN], ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; - Ok((init_data, enc.do_encrypt_blocks_out(plaintext, ciphertext)?)) + Ok((init_data, encrypt_flat(&mut enc, plaintext, ciphertext)?)) } } +/// Drives a constructed encryptor over flat, block-aligned byte arrays; the shared body of the flat +/// one-shots on [`BlockCipherEncryptor`]. +/// +/// `LEN % BLOCK_LEN == 0` is asserted at compile time when the caller's `LEN` is instantiated, so the +/// `as_chunks` remainders are provably empty and are ignored. Blocks are fed in pairs first, so that +/// a mode which overrides its two-block path gets to use it, then the at-most-one block left over. +/// Equivalent to a single `do_encrypt_blocks_out::<{LEN / BLOCK_LEN}>` call, which cannot be +/// written without `generic_const_exprs`. +fn encrypt_flat< + E: BlockCipherEncryptor, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, + const LEN: usize, +>( + enc: &mut E, + plaintext: &[u8; LEN], + ciphertext: &mut [u8; LEN], +) -> Result { + const { assert!(LEN % BLOCK_LEN == 0, "flat one-shot length must be a whole number of blocks") }; + let (pt_blocks, _) = plaintext.as_chunks::(); + let (ct_blocks, _) = ciphertext.as_chunks_mut::(); + let (pt_pairs, pt_tail) = pt_blocks.as_chunks::<2>(); + let (ct_pairs, ct_tail) = ct_blocks.as_chunks_mut::<2>(); + for (p, c) in pt_pairs.iter().zip(ct_pairs.iter_mut()) { + enc.do_encrypt_blocks_out(p, c)?; + } + for (p, c) in pt_tail.iter().zip(ct_tail.iter_mut()) { + enc.do_encrypt_blocks_out(core::array::from_ref(p), core::array::from_mut(c))?; + } + Ok(LEN) +} + /// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`]. pub trait BlockCipherDecryptor< const KEY_LEN: usize, @@ -243,10 +292,17 @@ pub trait BlockCipherDecryptor< ) -> Result; /// Decrypts `N` consecutive blocks of ciphertext. A sequence of calls is equivalent to one call over /// the concatenation. + /// + /// Provided in terms of [`BlockCipherDecryptor::do_decrypt_blocks_out`]; implementors need only + /// write the `_out` form. fn do_decrypt_blocks( &mut self, ciphertext: &[[u8; BLOCK_LEN]; N], - ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError>; + ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { + let mut plaintext = [[0u8; BLOCK_LEN]; N]; + self.do_decrypt_blocks_out(ciphertext, &mut plaintext)?; + Ok(plaintext) + } /// Decrypts `N` consecutive blocks of ciphertext into the provided buffer. Returns `N * BLOCK_LEN`. fn do_decrypt_blocks_out( &mut self, @@ -254,23 +310,55 @@ pub trait BlockCipherDecryptor< plaintext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; - /// One-shot: decrypts `N` blocks from the given init data. - fn decrypt_blocks( + /// One-shot on a flat byte array: decrypts `LEN` bytes from the given init data. + /// + /// `LEN` must be a whole number of blocks, checked at compile time exactly as for + /// [`BlockCipherEncryptor::encrypt`]. + fn decrypt( key: &KeyMaterial, init_data: &[u8; INIT_DATA_LEN], - ciphertext: &[[u8; BLOCK_LEN]; N], - ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { - Self::do_decrypt_init(key, init_data)?.do_decrypt_blocks(ciphertext) + ciphertext: &[u8; LEN], + ) -> Result<[u8; LEN], SymmetricCipherError> { + let mut plaintext = [0u8; LEN]; + Self::decrypt_out(key, init_data, ciphertext, &mut plaintext)?; + Ok(plaintext) } - /// One-shot: decrypts `N` blocks from the given init data into the provided buffer. Returns `N * BLOCK_LEN`. - fn decrypt_blocks_out( + /// As [`BlockCipherDecryptor::decrypt`], into the provided buffer. Returns `LEN`. + fn decrypt_out( key: &KeyMaterial, init_data: &[u8; INIT_DATA_LEN], - ciphertext: &[[u8; BLOCK_LEN]; N], - plaintext: &mut [[u8; BLOCK_LEN]; N], + ciphertext: &[u8; LEN], + plaintext: &mut [u8; LEN], ) -> Result { - Self::do_decrypt_init(key, init_data)?.do_decrypt_blocks_out(ciphertext, plaintext) + let mut dec = Self::do_decrypt_init(key, init_data)?; + decrypt_flat(&mut dec, ciphertext, plaintext) + } +} + +/// Decryption counterpart of [`encrypt_flat`]; see it for the alignment check and the pairing. +fn decrypt_flat< + D: BlockCipherDecryptor, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, + const LEN: usize, +>( + dec: &mut D, + ciphertext: &[u8; LEN], + plaintext: &mut [u8; LEN], +) -> Result { + const { assert!(LEN % BLOCK_LEN == 0, "flat one-shot length must be a whole number of blocks") }; + let (ct_blocks, _) = ciphertext.as_chunks::(); + let (pt_blocks, _) = plaintext.as_chunks_mut::(); + let (ct_pairs, ct_tail) = ct_blocks.as_chunks::<2>(); + let (pt_pairs, pt_tail) = pt_blocks.as_chunks_mut::<2>(); + for (c, p) in ct_pairs.iter().zip(pt_pairs.iter_mut()) { + dec.do_decrypt_blocks_out(c, p)?; + } + for (c, p) in ct_tail.iter().zip(pt_tail.iter_mut()) { + dec.do_decrypt_blocks_out(core::array::from_ref(c), core::array::from_mut(p))?; } + Ok(LEN) } /// A block padding scheme, used to extend arbitrary-length data to a whole number of blocks so that it diff --git a/crypto/modes/src/lib.rs b/crypto/modes/src/lib.rs index a25468aa..50d8c57a 100644 --- a/crypto/modes/src/lib.rs +++ b/crypto/modes/src/lib.rs @@ -34,14 +34,13 @@ //! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); //! -//! let plaintext = [[0u8; 16], [1u8; 16], [2u8; 16]]; +//! // 48 bytes: three whole blocks. A length that is not a multiple of 16 would not compile. +//! let plaintext: [u8; 48] = *b"The quick brown fox jumps over the lazy dog. OK!"; //! //! // One shot: encrypts under a freshly generated IV, which is returned alongside the ciphertext. -//! let (iv, ciphertext) = -//! Aes128Cbc::::encrypt_blocks(&key, &plaintext).expect("encryption"); +//! let (iv, ciphertext) = Aes128Cbc::::encrypt(&key, &plaintext).expect("encryption"); //! -//! let recovered = -//! Aes128Cbc::::decrypt_blocks(&key, &iv, &ciphertext).expect("decryption"); +//! let recovered = Aes128Cbc::::decrypt(&key, &iv, &ciphertext).expect("decryption"); //! assert_eq!(recovered, plaintext); //! ``` //! diff --git a/crypto/modes/tests/cbc_tests.rs b/crypto/modes/tests/cbc_tests.rs index b2430103..a544e7b8 100644 --- a/crypto/modes/tests/cbc_tests.rs +++ b/crypto/modes/tests/cbc_tests.rs @@ -253,15 +253,19 @@ fn each_encryption_gets_a_fresh_iv() { #[test] fn identical_plaintext_gives_different_ciphertext() { let key = toy_key(); - let plaintext = [[0x77u8; TOY_LEN], [0x77u8; TOY_LEN]]; + let plaintext = [0x77u8; 2 * TOY_LEN]; - let (_, first) = ToyCbc::::encrypt_blocks(&key, &plaintext).unwrap(); - let (_, second) = ToyCbc::::encrypt_blocks(&key, &plaintext).unwrap(); + let (_, first) = ToyCbc::::encrypt(&key, &plaintext).unwrap(); + let (_, second) = ToyCbc::::encrypt(&key, &plaintext).unwrap(); assert_ne!(first, second); // ...and, within one message, two identical plaintext blocks must not give identical // ciphertext blocks either, because the chaining value differs. - assert_ne!(first[0], first[1], "chaining should break the ECB pattern within a message"); + assert_ne!( + first[..TOY_LEN], + first[TOY_LEN..], + "chaining should break the ECB pattern within a message" + ); } // ---- key handling ------------------------------------------------------------------------ @@ -296,3 +300,56 @@ fn sizes_match_the_documented_memory_table() { // ...and the general rule the docs state. assert_eq!(size_of::>(), size_of::() + 16); } + +/// The one-shots (`encrypt` / `decrypt` on a `[u8; LEN]`) must produce exactly what the streaming +/// API produces over the same blocks, for an odd block count (pairs plus a one-block tail) and an +/// even one (pairs only), in both directions and through the `_out` variants. +#[test] +fn one_shots_agree_with_the_streaming_api() { + let key = toy_key(); + let iv: [u8; TOY_LEN] = core::array::from_fn(|i| 0x0F ^ (i as u8)); + let pinned_rng = || bouncycastle_core_test_framework::FixedSeedRNG::::new(iv); + + // 3 blocks = 48 bytes: one pair and a tail. + let flat3: [u8; 3 * TOY_LEN] = core::array::from_fn(|i| (i * 7) as u8); + let blocks3: [[u8; TOY_LEN]; 3] = + core::array::from_fn(|b| flat3[b * TOY_LEN..][..TOY_LEN].try_into().unwrap()); + let (iv_a, ct_blocks) = { + let (mut enc, iv) = + ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); + (iv, enc.do_encrypt_blocks(&blocks3).unwrap()) + }; + let (iv_b, ct_flat) = + ToyCbc::::encrypt_rng(&key, &mut pinned_rng(), &flat3).unwrap(); + assert_eq!(iv_a, iv_b); + assert_eq!(ct_flat, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); + assert_eq!(ToyCbc::::decrypt(&key, &iv, &ct_flat).unwrap(), flat3); + let mut ct_out = [0u8; 3 * TOY_LEN]; + let (_, n) = + ToyCbc::::encrypt_out_rng(&key, &mut pinned_rng(), &flat3, &mut ct_out) + .unwrap(); + assert_eq!((n, ct_out), (3 * TOY_LEN, ct_flat)); + let mut pt_out = [0u8; 3 * TOY_LEN]; + assert_eq!( + ToyCbc::::decrypt_out(&key, &iv, &ct_out, &mut pt_out).unwrap(), + 3 * TOY_LEN + ); + assert_eq!(pt_out, flat3); + + // 4 blocks = 64 bytes: pairs only, no tail. + let flat4: [u8; 4 * TOY_LEN] = core::array::from_fn(|i| (i * 13 + 1) as u8); + let blocks4: [[u8; TOY_LEN]; 4] = + core::array::from_fn(|b| flat4[b * TOY_LEN..][..TOY_LEN].try_into().unwrap()); + let (_, ct_blocks) = { + let (mut enc, iv) = + ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); + (iv, enc.do_encrypt_blocks(&blocks4).unwrap()) + }; + let (_, ct_flat) = ToyCbc::::encrypt_rng(&key, &mut pinned_rng(), &flat4).unwrap(); + assert_eq!(ct_flat, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); + assert_eq!(ToyCbc::::decrypt(&key, &iv, &ct_flat).unwrap(), flat4); + + // The OS-RNG variant round-trips too. + let (iv_fresh, ct) = ToyCbc::::encrypt(&key, &flat3).unwrap(); + assert_eq!(ToyCbc::::decrypt(&key, &iv_fresh, &ct).unwrap(), flat3); +} diff --git a/crypto/modes/tests/sp800_38a_tests.rs b/crypto/modes/tests/sp800_38a_tests.rs index 9bc24fd2..93f98aaf 100644 --- a/crypto/modes/tests/sp800_38a_tests.rs +++ b/crypto/modes/tests/sp800_38a_tests.rs @@ -197,34 +197,38 @@ fn f_2_6_cbc_aes256_decrypt() { } /// The one-shot API must agree with the vectors too, on the decrypt side where the IV is an input. +/// The one-shots take flat arrays, so the four blocks are presented as 64 contiguous bytes. #[test] fn the_one_shot_api_matches_the_vectors() { + fn flat(hex_strs: &[&str; 4]) -> [u8; 4 * BLOCK_LEN] { + blocks(hex_strs).as_flattened().try_into().expect("4 blocks = 64 bytes") + } let iv = block(IV); - let pt = blocks(&PLAINTEXTS); + let pt = flat(&PLAINTEXTS); assert_eq!( - Cbc::::decrypt_blocks( + Cbc::::decrypt( &key_material::<16>(KEY_128), &iv, - &blocks(&CIPHERTEXTS_128) + &flat(&CIPHERTEXTS_128) ) .unwrap(), pt ); assert_eq!( - Cbc::::decrypt_blocks( + Cbc::::decrypt( &key_material::<24>(KEY_192), &iv, - &blocks(&CIPHERTEXTS_192) + &flat(&CIPHERTEXTS_192) ) .unwrap(), pt ); assert_eq!( - Cbc::::decrypt_blocks( + Cbc::::decrypt( &key_material::<32>(KEY_256), &iv, - &blocks(&CIPHERTEXTS_256) + &flat(&CIPHERTEXTS_256) ) .unwrap(), pt From b8c9518a7a9f42a27827a8e042c4e4f07a56a899 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 2 Sep 2026 15:41:45 +1000 Subject: [PATCH 3/7] core: flat streaming methods replace the by-value block methods; simplify the CLI BlockCipherEncryptor / BlockCipherDecryptor gain provided flat streaming methods -- do_encrypt / do_encrypt_out and do_decrypt / do_decrypt_out over a `[u8; LEN]`, with the same compile-time LEN % BLOCK_LEN == 0 assertion as the one-shots, which now delegate to them. The by-value do_{en,de}crypt_blocks are removed. The one block-shaped method left is the implementor hook do_{en,de}crypt_blocks_out over [[u8; BLOCK_LEN]; N]: it is what guarantees an implementation never sees a partial block and that in/out lengths agree at compile time, and it serves runtime-count tails one block at a time. An implementor now writes only init[_rng] and that hook; Cbc and the padding test toy drop their by-value wrappers, and the padding adapter's two single-block calls use do_encrypt / do_decrypt. CLI: aes*-cbc stream stdin into a flat 1 KiB buffer and hand full chunks to do_*_out::<1024>, the whole-block tail to do_*::<16>; the block staging buffer and partial-block carry logic are gone (reads simply accumulate until the buffer is full). Behaviour and error messages unchanged; the 16 CLI subprocess tests still pass. Tests: framework, modes (cbc, sp800-38a, acvp) and benches moved to the flat methods; block-structured tests use two small helpers over the hook. Release notes updated. Co-Authored-By: Claude Fable 5.1 --- alpha_0.1.3_release_notes.md | 12 +- cli/src/aes_cbc_cmd.rs | 112 +++++------ crypto/aes-lowmemory/src/cbc.rs | 8 +- .../src/symmetric_ciphers.rs | 38 ++-- crypto/core/src/traits.rs | 174 +++++++++--------- crypto/modes/benches/modes_benches.rs | 44 +++-- crypto/modes/src/cbc.rs | 22 +-- crypto/modes/src/lib.rs | 8 +- crypto/modes/tests/acvp_tests.rs | 20 +- crypto/modes/tests/cbc_tests.rs | 81 ++++---- crypto/modes/tests/sp800_38a_tests.rs | 39 ++-- crypto/padding/src/padded.rs | 5 +- crypto/padding/tests/padded_tests.rs | 16 -- 13 files changed, 274 insertions(+), 305 deletions(-) diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index e20f391f..55c4137c 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -71,8 +71,9 @@ New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of op not be secret (SP 800-38A Sec 5.3), so this is sound. * Input must be a whole number of 16-byte blocks. Unaligned input is rejected with a message pointing at the missing padding layer rather than being silently padded. -* Reads do not respect block boundaries, so a block split across two reads is carried over; - verified by round-tripping 64 KiB through `dd bs=3`. +* Reads need not respect block boundaries: bytes accumulate in a 1 KiB buffer that goes through the flat + `do_*_out::<1024>` when full, and the whole-block remainder at end of input goes one block at a time; verified by + round-tripping 64 KiB through `dd bs=3`. * Verified against SP 800-38A F.2: prepending the spec's IV to the spec's ciphertext and running `decrypt` reproduces the spec's plaintext for all three key lengths. The `encrypt` direction was cross-checked against an independent CBC implementation under the IV the CLI generated. @@ -145,8 +146,11 @@ Block cipher traits (PR #96): take a flat `[u8; LEN]`; `LEN` must be a whole number of blocks, and this is enforced at **compile time** by an inline `const` assertion at the instantiating call site, so there is no runtime length check and no error variant for it. (An earlier form of these one-shots took `[[u8; BLOCK_LEN]; N]`; it was replaced before release.) -* The by-value streaming methods `do_{en,de}crypt_blocks` are provided in terms of their `_out` forms, so an - implementor writes only `do_{en,de}crypt_init[_rng]` and `do_{en,de}crypt_blocks_out`. +* The streaming API is flat as well: `do_{en,de}crypt` / `do_{en,de}crypt_out` take a `[u8; LEN]` with the + same compile-time alignment check, and are provided methods. The single block-shaped method left is the implementor + hook `do_{en,de}crypt_blocks_out` over `[[u8; BLOCK_LEN]; N]`, which is what guarantees an implementation never + sees a partial block and that input and output lengths agree at compile time; an implementor writes only + `do_{en,de}crypt_init[_rng]` and that hook. (The by-value `do_{en,de}crypt_blocks` of #96 are gone.) Testing: diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs index 40727c85..a33d1f65 100644 --- a/cli/src/aes_cbc_cmd.rs +++ b/cli/src/aes_cbc_cmd.rs @@ -49,12 +49,12 @@ use std::{fs, io}; /// The AES block length in bytes. const BLOCK_LEN: usize = 16; -/// Blocks processed per call: 64 blocks = 1 KiB, matching the other streaming commands. +/// Bytes processed per call: 1 KiB = 64 blocks, matching the other streaming commands. /// -/// A whole chunk goes through `do_*_blocks[_out]::` in one call, which for decryption -/// means 32 pairs down the `decrypt_blocks2` path. The at-most-63-block tail at end of input is -/// flushed one block at a time; it is bounded, so its cost does not scale with the input. -const CHUNK_BLOCKS: usize = 64; +/// A full chunk goes through `do_*_out::` in one call, which for decryption means 32 +/// pairs down the `decrypt_blocks2` path. The at-most-63-block tail at end of input goes one block +/// at a time; it is bounded, so its cost does not scale with the input. +const CHUNK_LEN: usize = 64 * BLOCK_LEN; #[derive(ValueEnum, Clone, Debug)] pub(crate) enum AESCBCAction { @@ -183,19 +183,18 @@ where // The IV goes out ahead of the ciphertext, so `decrypt` can pick it up. write_bytes_or_hex(&iv, output_hex); - let mut out = [[0u8; BLOCK_LEN]; CHUNK_BLOCKS]; + let mut out = [0u8; CHUNK_LEN]; - stream_blocks(|blocks| match <&[[u8; BLOCK_LEN]; CHUNK_BLOCKS]>::try_from(blocks) { - Ok(full_chunk) => { + stream_aligned(|data| match <&[u8; CHUNK_LEN]>::try_from(data) { + Ok(chunk) => { // Cannot fail: the mode's block methods are infallible for a constructed value. - enc.do_encrypt_blocks_out(full_chunk, &mut out).unwrap(); - write_blocks(&out, output_hex); + enc.do_encrypt_out(chunk, &mut out).unwrap(); + write_bytes_or_hex(&out, output_hex); } Err(_) => { - // The bounded tail at end of input. - for block in blocks.iter() { - let [c] = enc.do_encrypt_blocks(&[*block]).unwrap(); - write_bytes_or_hex(&c, output_hex); + // The bounded tail at end of input: whole blocks, fewer than a chunk. + for block in data.as_chunks::().0 { + write_bytes_or_hex(&enc.do_encrypt(block).unwrap(), output_hex); } } }); @@ -224,18 +223,17 @@ where exit(-1); }); - let mut out = [[0u8; BLOCK_LEN]; CHUNK_BLOCKS]; + let mut out = [0u8; CHUNK_LEN]; - stream_blocks(|blocks| match <&[[u8; BLOCK_LEN]; CHUNK_BLOCKS]>::try_from(blocks) { - Ok(full_chunk) => { + stream_aligned(|data| match <&[u8; CHUNK_LEN]>::try_from(data) { + Ok(chunk) => { // A full chunk is 32 pairs, so this is the `decrypt_blocks2` path. - dec.do_decrypt_blocks_out(full_chunk, &mut out).unwrap(); - write_blocks(&out, output_hex); + dec.do_decrypt_out(chunk, &mut out).unwrap(); + write_bytes_or_hex(&out, output_hex); } Err(_) => { - for block in blocks.iter() { - let [p] = dec.do_decrypt_blocks(&[*block]).unwrap(); - write_bytes_or_hex(&p, output_hex); + for block in data.as_chunks::().0 { + write_bytes_or_hex(&dec.do_decrypt(block).unwrap(), output_hex); } } }); @@ -243,71 +241,43 @@ where finish(output_hex); } -/// Reads stdin a block at a time, calling `process` with a full `CHUNK_BLOCKS` slice whenever one -/// is available and once more at end of input with whatever whole blocks remain. +/// Reads stdin and hands it to `process` in block-aligned pieces: a full `CHUNK_LEN` bytes each time +/// one has accumulated, then once more at end of input with whatever whole blocks remain (fewer +/// than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the +/// buffer until it is full -- so a block split across two reads needs no special handling. /// -/// `process` therefore sees a slice of exactly `CHUNK_BLOCKS` for every call but the last, which is -/// how the callers can hand a fixed-size array to `do_*_blocks_out::` and fall back -/// to single blocks only for the bounded tail. -/// -/// Reads do not respect block boundaries, so a block can arrive split across two reads; the -/// partial block is carried over rather than assumed complete. Input whose total length is not a -/// multiple of `BLOCK_LEN` is an error, because CBC is not defined on a partial block and there is -/// no padding layer to appeal to. -fn stream_blocks(mut process: impl FnMut(&[[u8; BLOCK_LEN]])) { - let mut staged = [[0u8; BLOCK_LEN]; CHUNK_BLOCKS]; - let mut read_buf = [0u8; BLOCK_LEN * CHUNK_BLOCKS]; - let mut partial = [0u8; BLOCK_LEN]; - let mut partial_len = 0usize; - let mut blocks = 0usize; +/// Input whose total length is not a multiple of `BLOCK_LEN` is an error, because CBC is not +/// defined on a partial block and there is no padding layer to appeal to. +fn stream_aligned(mut process: impl FnMut(&[u8])) { + let mut buf = [0u8; CHUNK_LEN]; + let mut filled = 0usize; loop { - let n = io::stdin().read(&mut read_buf).unwrap_or_else(|e| { + let n = io::stdin().read(&mut buf[filled..]).unwrap_or_else(|e| { eprintln!("Error: failed to read from stdin: {e}"); exit(-1); }); if n == 0 { break; } - - let mut src = &read_buf[..n]; - while !src.is_empty() { - let take = core::cmp::min(BLOCK_LEN - partial_len, src.len()); - partial[partial_len..partial_len + take].copy_from_slice(&src[..take]); - partial_len += take; - src = &src[take..]; - - if partial_len == BLOCK_LEN { - staged[blocks] = partial; - blocks += 1; - partial_len = 0; - - if blocks == CHUNK_BLOCKS { - process(&staged); - blocks = 0; - } - } + filled += n; + if filled == CHUNK_LEN { + process(&buf); + filled = 0; } } - if partial_len != 0 { + if filled % BLOCK_LEN != 0 { eprintln!( - "Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({partial_len} \ - trailing byte(s)). CBC is defined only on whole blocks (SP 800-38A Sec 5.2), and \ - this build has no padding layer, so the input must be padded by the caller." + "Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({} trailing byte(s)). \ + CBC is defined only on whole blocks (SP 800-38A Sec 5.2), and this build has no \ + padding layer, so the input must be padded by the caller.", + filled % BLOCK_LEN ); exit(-1); } - - if blocks != 0 { - process(&staged[..blocks]); - } -} - -/// Writes a run of whole blocks. -fn write_blocks(blocks: &[[u8; BLOCK_LEN]], output_hex: bool) { - for block in blocks.iter() { - write_bytes_or_hex(block, output_hex); + if filled != 0 { + process(&buf[..filled]); } } diff --git a/crypto/aes-lowmemory/src/cbc.rs b/crypto/aes-lowmemory/src/cbc.rs index a0919c1d..39b549f8 100644 --- a/crypto/aes-lowmemory/src/cbc.rs +++ b/crypto/aes-lowmemory/src/cbc.rs @@ -28,11 +28,11 @@ use bouncycastle_modes::Cbc; /// /// // Streaming, a few blocks at a time: /// let (mut enc, iv) = AES_CBC_128::::do_encrypt_init(&key).unwrap(); -/// let first = enc.do_encrypt_blocks(&[[0u8; 16]]).unwrap(); -/// let rest = enc.do_encrypt_blocks(&[[1u8; 16], [2u8; 16]]).unwrap(); +/// let first = enc.do_encrypt(&[0u8; 16]).unwrap(); +/// let rest = enc.do_encrypt(&[1u8; 32]).unwrap(); /// let mut dec = AES_CBC_128::::do_decrypt_init(&key, &iv).unwrap(); -/// assert_eq!(dec.do_decrypt_blocks(&first).unwrap(), [[0u8; 16]]); -/// assert_eq!(dec.do_decrypt_blocks(&rest).unwrap(), [[1u8; 16], [2u8; 16]]); +/// assert_eq!(dec.do_decrypt(&first).unwrap(), [0u8; 16]); +/// assert_eq!(dec.do_decrypt(&rest).unwrap(), [1u8; 32]); /// ``` /// /// A length that is not a whole number of blocks is a **compile** error, not a runtime one: diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 4e1c57c0..1044b037 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -140,10 +140,10 @@ impl TestFrameworkBlockCipher { let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); - // one block at a time (N = 1) + // one block at a time, through the flat streaming methods (LEN = BLOCK_LEN) for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct = encryptor.do_encrypt_blocks(&[*msg_chunk]).unwrap(); - let [pt] = decryptor.do_decrypt_blocks(&ct).unwrap(); + let ct = encryptor.do_encrypt(msg_chunk).unwrap(); + let pt = decryptor.do_decrypt(&ct).unwrap(); assert_eq!(msg_chunk, &pt); } @@ -152,43 +152,45 @@ impl TestFrameworkBlockCipher { let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); - let mut ct = [[0u8; BLOCK_LEN]; 1]; - let mut pt = [[0u8; BLOCK_LEN]; 1]; + let mut ct = [0u8; BLOCK_LEN]; + let mut pt = [0u8; BLOCK_LEN]; for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct_bytes_written = encryptor.do_encrypt_blocks_out(&[*msg_chunk], &mut ct).unwrap(); + let ct_bytes_written = encryptor.do_encrypt_out(msg_chunk, &mut ct).unwrap(); assert_eq!(ct_bytes_written, BLOCK_LEN); - let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap(); + let pt_bytes_written = decryptor.do_decrypt_out(&ct, &mut pt).unwrap(); assert_eq!(pt_bytes_written, BLOCK_LEN); - assert_eq!(msg_chunk, &pt[0]); + assert_eq!(msg_chunk, &pt); } - // multi-block (N = 2): blocks encrypted together must decrypt both together and one at a time, - // and blocks encrypted one at a time must decrypt together. + // multi-block (N = 2) through the implementor hook `do_*_blocks_out`: blocks encrypted together + // must decrypt both together and one at a time, and blocks encrypted one at a time must + // decrypt together. let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); let mut ct = [[0u8; BLOCK_LEN]; 2]; let mut pt = [[0u8; BLOCK_LEN]; 2]; for msg_pair in DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0.iter() { - // encrypt together, decrypt together (by value) - let ct_by_value = encryptor.do_encrypt_blocks(msg_pair).unwrap(); - let pt_by_value = decryptor.do_decrypt_blocks(&ct_by_value).unwrap(); - assert_eq!(msg_pair, &pt_by_value); + // encrypt together, decrypt together + let mut ct_pair = [[0u8; BLOCK_LEN]; 2]; + encryptor.do_encrypt_blocks_out(msg_pair, &mut ct_pair).unwrap(); + let mut pt_pair = [[0u8; BLOCK_LEN]; 2]; + decryptor.do_decrypt_blocks_out(&ct_pair, &mut pt_pair).unwrap(); + assert_eq!(msg_pair, &pt_pair); // encrypt together (_out), decrypt one at a time let ct_bytes_written = encryptor.do_encrypt_blocks_out(msg_pair, &mut ct).unwrap(); assert_eq!(ct_bytes_written, 2 * BLOCK_LEN); for (msg_chunk, ct_chunk) in msg_pair.iter().zip(ct.iter()) { - let [pt] = decryptor.do_decrypt_blocks(&[*ct_chunk]).unwrap(); + let pt = decryptor.do_decrypt(ct_chunk).unwrap(); assert_eq!(msg_chunk, &pt); } // encrypt one at a time, decrypt together (_out) for (msg_chunk, ct_chunk) in msg_pair.iter().zip(ct.iter_mut()) { - let [c] = encryptor.do_encrypt_blocks(&[*msg_chunk]).unwrap(); - *ct_chunk = c; + *ct_chunk = encryptor.do_encrypt(msg_chunk).unwrap(); } let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap(); assert_eq!(pt_bytes_written, 2 * BLOCK_LEN); @@ -204,7 +206,7 @@ impl TestFrameworkBlockCipher { assert_eq!(D::decrypt(&key, &iv, &ct).unwrap(), *one_block); // ...and it must agree with the block-shaped API under the same init data. let mut streamed = D::do_decrypt_init(&key, &iv).unwrap(); - assert_eq!(streamed.do_decrypt_blocks(&[ct]).unwrap(), [*one_block]); + assert_eq!(streamed.do_decrypt(&ct).unwrap(), *one_block); let mut ct = [0u8; BLOCK_LEN]; let mut pt = [0u8; BLOCK_LEN]; diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index cce737af..696dce3c 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -177,26 +177,59 @@ pub trait BlockCipherEncryptor< key: &KeyMaterial, rng: &mut dyn RNG, ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// Encrypts `N` consecutive blocks of plaintext. A sequence of calls is equivalent to one call over - /// the concatenation. + /// The implementor hook: encrypts `N` consecutive whole blocks into the provided buffer and + /// returns `N * BLOCK_LEN`. A sequence of calls is equivalent to one call over the concatenation. /// - /// Provided in terms of [`BlockCipherEncryptor::do_encrypt_blocks_out`]; implementors need only - /// write the `_out` form. - fn do_encrypt_blocks( - &mut self, - plaintext: &[[u8; BLOCK_LEN]; N], - ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { - let mut ciphertext = [[0u8; BLOCK_LEN]; N]; - self.do_encrypt_blocks_out(plaintext, &mut ciphertext)?; - Ok(ciphertext) - } - /// Encrypts `N` consecutive blocks of plaintext into the provided buffer. Returns `N * BLOCK_LEN`. + /// This is the only method an implementor writes besides the two `_init` constructors; the + /// block shape is what guarantees it never sees a partial block and that input and output + /// lengths agree at compile time. Callers should normally use the flat + /// [`BlockCipherEncryptor::do_encrypt`] / [`BlockCipherEncryptor::do_encrypt_out`] instead. fn do_encrypt_blocks_out( &mut self, plaintext: &[[u8; BLOCK_LEN]; N], ciphertext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; + /// Streaming: encrypts `LEN` bytes, a whole number of blocks. A sequence of calls is + /// equivalent to one call over the concatenation. + /// + /// `LEN % BLOCK_LEN == 0` is checked **at compile time**: instantiating this with a misaligned + /// `LEN` is a compile error at the call site (an inline `const` assertion), not a runtime `Err`. + /// Non-block-aligned data belongs to the padding layer. + fn do_encrypt( + &mut self, + plaintext: &[u8; LEN], + ) -> Result<[u8; LEN], SymmetricCipherError> { + let mut ciphertext = [0u8; LEN]; + self.do_encrypt_out(plaintext, &mut ciphertext)?; + Ok(ciphertext) + } + /// As [`BlockCipherEncryptor::do_encrypt`], into the provided buffer. Returns `LEN`. + /// + /// Blocks are fed to [`BlockCipherEncryptor::do_encrypt_blocks_out`] in pairs first, so a mode + /// that overrides its two-block path gets to use it, then the at-most-one block left over. This + /// is equivalent to a single `do_encrypt_blocks_out::<{LEN / BLOCK_LEN}>` call, which cannot be + /// written without `generic_const_exprs`. + fn do_encrypt_out( + &mut self, + plaintext: &[u8; LEN], + ciphertext: &mut [u8; LEN], + ) -> Result { + const { assert!(LEN % BLOCK_LEN == 0, "length must be a whole number of BLOCK_LEN-byte blocks") }; + // The remainders are provably empty (asserted above) and ignored. + let (pt_blocks, _) = plaintext.as_chunks::(); + let (ct_blocks, _) = ciphertext.as_chunks_mut::(); + let (pt_pairs, pt_tail) = pt_blocks.as_chunks::<2>(); + let (ct_pairs, ct_tail) = ct_blocks.as_chunks_mut::<2>(); + for (p, c) in pt_pairs.iter().zip(ct_pairs.iter_mut()) { + self.do_encrypt_blocks_out(p, c)?; + } + for (p, c) in pt_tail.iter().zip(ct_tail.iter_mut()) { + self.do_encrypt_blocks_out(core::array::from_ref(p), core::array::from_mut(c))?; + } + Ok(LEN) + } + /// One-shot on a flat byte array: encrypts `LEN` bytes under a fresh init. Returns the generated /// init data and the ciphertext. /// @@ -231,7 +264,7 @@ pub trait BlockCipherEncryptor< ciphertext: &mut [u8; LEN], ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { let (mut enc, init_data) = Self::do_encrypt_init(key)?; - Ok((init_data, encrypt_flat(&mut enc, plaintext, ciphertext)?)) + Ok((init_data, enc.do_encrypt_out(plaintext, ciphertext)?)) } /// As [`BlockCipherEncryptor::encrypt_out`], but sources randomness from the provided RNG. fn encrypt_out_rng( @@ -241,41 +274,8 @@ pub trait BlockCipherEncryptor< ciphertext: &mut [u8; LEN], ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; - Ok((init_data, encrypt_flat(&mut enc, plaintext, ciphertext)?)) - } -} - -/// Drives a constructed encryptor over flat, block-aligned byte arrays; the shared body of the flat -/// one-shots on [`BlockCipherEncryptor`]. -/// -/// `LEN % BLOCK_LEN == 0` is asserted at compile time when the caller's `LEN` is instantiated, so the -/// `as_chunks` remainders are provably empty and are ignored. Blocks are fed in pairs first, so that -/// a mode which overrides its two-block path gets to use it, then the at-most-one block left over. -/// Equivalent to a single `do_encrypt_blocks_out::<{LEN / BLOCK_LEN}>` call, which cannot be -/// written without `generic_const_exprs`. -fn encrypt_flat< - E: BlockCipherEncryptor, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, - const LEN: usize, ->( - enc: &mut E, - plaintext: &[u8; LEN], - ciphertext: &mut [u8; LEN], -) -> Result { - const { assert!(LEN % BLOCK_LEN == 0, "flat one-shot length must be a whole number of blocks") }; - let (pt_blocks, _) = plaintext.as_chunks::(); - let (ct_blocks, _) = ciphertext.as_chunks_mut::(); - let (pt_pairs, pt_tail) = pt_blocks.as_chunks::<2>(); - let (ct_pairs, ct_tail) = ct_blocks.as_chunks_mut::<2>(); - for (p, c) in pt_pairs.iter().zip(ct_pairs.iter_mut()) { - enc.do_encrypt_blocks_out(p, c)?; + Ok((init_data, enc.do_encrypt_out(plaintext, ciphertext)?)) } - for (p, c) in pt_tail.iter().zip(ct_tail.iter_mut()) { - enc.do_encrypt_blocks_out(core::array::from_ref(p), core::array::from_mut(c))?; - } - Ok(LEN) } /// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`]. @@ -290,26 +290,47 @@ pub trait BlockCipherDecryptor< key: &KeyMaterial, init_data: &[u8; INIT_DATA_LEN], ) -> Result; - /// Decrypts `N` consecutive blocks of ciphertext. A sequence of calls is equivalent to one call over - /// the concatenation. - /// - /// Provided in terms of [`BlockCipherDecryptor::do_decrypt_blocks_out`]; implementors need only - /// write the `_out` form. - fn do_decrypt_blocks( - &mut self, - ciphertext: &[[u8; BLOCK_LEN]; N], - ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { - let mut plaintext = [[0u8; BLOCK_LEN]; N]; - self.do_decrypt_blocks_out(ciphertext, &mut plaintext)?; - Ok(plaintext) - } - /// Decrypts `N` consecutive blocks of ciphertext into the provided buffer. Returns `N * BLOCK_LEN`. + /// The implementor hook: decrypts `N` consecutive whole blocks into the provided buffer and + /// returns `N * BLOCK_LEN`. See [`BlockCipherEncryptor::do_encrypt_blocks_out`]; callers should + /// normally use the flat [`BlockCipherDecryptor::do_decrypt`] / + /// [`BlockCipherDecryptor::do_decrypt_out`] instead. fn do_decrypt_blocks_out( &mut self, ciphertext: &[[u8; BLOCK_LEN]; N], plaintext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; + /// Streaming: decrypts `LEN` bytes, a whole number of blocks; `LEN % BLOCK_LEN == 0` is checked + /// at compile time exactly as for [`BlockCipherEncryptor::do_encrypt`]. + fn do_decrypt( + &mut self, + ciphertext: &[u8; LEN], + ) -> Result<[u8; LEN], SymmetricCipherError> { + let mut plaintext = [0u8; LEN]; + self.do_decrypt_out(ciphertext, &mut plaintext)?; + Ok(plaintext) + } + /// As [`BlockCipherDecryptor::do_decrypt`], into the provided buffer. Returns `LEN`. Pairs first, + /// then the tail, as [`BlockCipherEncryptor::do_encrypt_out`]. + fn do_decrypt_out( + &mut self, + ciphertext: &[u8; LEN], + plaintext: &mut [u8; LEN], + ) -> Result { + const { assert!(LEN % BLOCK_LEN == 0, "length must be a whole number of BLOCK_LEN-byte blocks") }; + let (ct_blocks, _) = ciphertext.as_chunks::(); + let (pt_blocks, _) = plaintext.as_chunks_mut::(); + let (ct_pairs, ct_tail) = ct_blocks.as_chunks::<2>(); + let (pt_pairs, pt_tail) = pt_blocks.as_chunks_mut::<2>(); + for (c, p) in ct_pairs.iter().zip(pt_pairs.iter_mut()) { + self.do_decrypt_blocks_out(c, p)?; + } + for (c, p) in ct_tail.iter().zip(pt_tail.iter_mut()) { + self.do_decrypt_blocks_out(core::array::from_ref(c), core::array::from_mut(p))?; + } + Ok(LEN) + } + /// One-shot on a flat byte array: decrypts `LEN` bytes from the given init data. /// /// `LEN` must be a whole number of blocks, checked at compile time exactly as for @@ -330,35 +351,8 @@ pub trait BlockCipherDecryptor< ciphertext: &[u8; LEN], plaintext: &mut [u8; LEN], ) -> Result { - let mut dec = Self::do_decrypt_init(key, init_data)?; - decrypt_flat(&mut dec, ciphertext, plaintext) - } -} - -/// Decryption counterpart of [`encrypt_flat`]; see it for the alignment check and the pairing. -fn decrypt_flat< - D: BlockCipherDecryptor, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, - const LEN: usize, ->( - dec: &mut D, - ciphertext: &[u8; LEN], - plaintext: &mut [u8; LEN], -) -> Result { - const { assert!(LEN % BLOCK_LEN == 0, "flat one-shot length must be a whole number of blocks") }; - let (ct_blocks, _) = ciphertext.as_chunks::(); - let (pt_blocks, _) = plaintext.as_chunks_mut::(); - let (ct_pairs, ct_tail) = ct_blocks.as_chunks::<2>(); - let (pt_pairs, pt_tail) = pt_blocks.as_chunks_mut::<2>(); - for (c, p) in ct_pairs.iter().zip(pt_pairs.iter_mut()) { - dec.do_decrypt_blocks_out(c, p)?; - } - for (c, p) in ct_tail.iter().zip(pt_tail.iter_mut()) { - dec.do_decrypt_blocks_out(core::array::from_ref(c), core::array::from_mut(p))?; + Self::do_decrypt_init(key, init_data)?.do_decrypt_out(ciphertext, plaintext) } - Ok(LEN) } /// A block padding scheme, used to extend arbitrary-length data to a whole number of blocks so that it diff --git a/crypto/modes/benches/modes_benches.rs b/crypto/modes/benches/modes_benches.rs index 66cdaea8..1863cc5a 100644 --- a/crypto/modes/benches/modes_benches.rs +++ b/crypto/modes/benches/modes_benches.rs @@ -83,7 +83,7 @@ fn bench_aes128(c: &mut Criterion) { b.iter(|| { let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); for block in blocks.iter() { - black_box(enc.do_encrypt_blocks(&[*block]).unwrap()); + black_box(enc.do_encrypt(block).unwrap()); } }) }); @@ -92,8 +92,8 @@ fn bench_aes128(c: &mut Criterion) { b.iter(|| { let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); for chunk in blocks.chunks_exact(8) { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - black_box(enc.do_encrypt_blocks(arr).unwrap()); + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(enc.do_encrypt(arr).unwrap()); } }) }); @@ -104,7 +104,9 @@ fn bench_aes128(c: &mut Criterion) { .chunks_exact(8) .flat_map(|chunk| { let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - enc.do_encrypt_blocks(arr).unwrap() + let mut out = [[0u8; BLOCK_LEN]; 8]; + enc.do_encrypt_blocks_out(arr, &mut out).unwrap(); + out }) .collect(); @@ -114,7 +116,7 @@ fn bench_aes128(c: &mut Criterion) { b.iter(|| { let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); for block in ciphertext.iter() { - black_box(dec.do_decrypt_blocks(&[*block]).unwrap()); + black_box(dec.do_decrypt(block).unwrap()); } }) }); @@ -124,8 +126,8 @@ fn bench_aes128(c: &mut Criterion) { b.iter(|| { let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); for chunk in ciphertext.chunks_exact(2) { - let arr: &[[u8; BLOCK_LEN]; 2] = chunk.try_into().unwrap(); - black_box(dec.do_decrypt_blocks(arr).unwrap()); + let arr: &[u8; 2 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); } }) }); @@ -134,8 +136,8 @@ fn bench_aes128(c: &mut Criterion) { b.iter(|| { let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); for chunk in ciphertext.chunks_exact(8) { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - black_box(dec.do_decrypt_blocks(arr).unwrap()); + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); } }) }); @@ -145,8 +147,8 @@ fn bench_aes128(c: &mut Criterion) { b.iter(|| { let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); for chunk in ciphertext.chunks_exact(9) { - let arr: &[[u8; BLOCK_LEN]; 9] = chunk.try_into().unwrap(); - black_box(dec.do_decrypt_blocks(arr).unwrap()); + let arr: &[u8; 9 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); } }) }); @@ -157,8 +159,8 @@ fn bench_aes128(c: &mut Criterion) { b.iter(|| { let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); for chunk in ciphertext.chunks_exact(8) { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - black_box(dec.do_decrypt_blocks(arr).unwrap()); + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); } }) }); @@ -167,8 +169,8 @@ fn bench_aes128(c: &mut Criterion) { b.iter(|| { let mut dec = UnpairedAes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); for chunk in ciphertext.chunks_exact(8) { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - black_box(dec.do_decrypt_blocks(arr).unwrap()); + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); } }) }); @@ -187,8 +189,8 @@ fn bench_aes256(c: &mut Criterion) { b.iter(|| { let (mut enc, _) = Aes256Cbc::::do_encrypt_init(&k).unwrap(); for chunk in blocks.chunks_exact(8) { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - black_box(enc.do_encrypt_blocks(arr).unwrap()); + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(enc.do_encrypt(arr).unwrap()); } }) }); @@ -198,7 +200,9 @@ fn bench_aes256(c: &mut Criterion) { .chunks_exact(8) .flat_map(|chunk| { let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - enc.do_encrypt_blocks(arr).unwrap() + let mut out = [[0u8; BLOCK_LEN]; 8]; + enc.do_encrypt_blocks_out(arr, &mut out).unwrap(); + out }) .collect(); @@ -206,8 +210,8 @@ fn bench_aes256(c: &mut Criterion) { b.iter(|| { let mut dec = Aes256Cbc::::do_decrypt_init(&k, &iv).unwrap(); for chunk in ciphertext.chunks_exact(8) { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - black_box(dec.do_decrypt_blocks(arr).unwrap()); + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); } }) }); diff --git a/crypto/modes/src/cbc.rs b/crypto/modes/src/cbc.rs index 996c441d..61518817 100644 --- a/crypto/modes/src/cbc.rs +++ b/crypto/modes/src/cbc.rs @@ -157,16 +157,7 @@ where Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) } - fn do_encrypt_blocks( - &mut self, - plaintext: &[[u8; BLOCK_LEN]; N], - ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { - let mut ciphertext = [[0u8; BLOCK_LEN]; N]; - self.do_encrypt_blocks_out(plaintext, &mut ciphertext)?; - Ok(ciphertext) - } - - /// The real implementation; the by-value variant above is a wrapper over it. + /// The implementor hook (the flat `do_encrypt[_out]` are provided over it). /// /// Strictly serial: `Cj` is the input to block `j + 1`, so there is no pair path here. See the /// module docs. @@ -197,16 +188,7 @@ where Ok(Self { perm, chain: *init_data, _dir: PhantomData }) } - fn do_decrypt_blocks( - &mut self, - ciphertext: &[[u8; BLOCK_LEN]; N], - ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { - let mut plaintext = [[0u8; BLOCK_LEN]; N]; - self.do_decrypt_blocks_out(ciphertext, &mut plaintext)?; - Ok(plaintext) - } - - /// The real implementation; the by-value variant above is a wrapper over it. + /// The implementor hook (the flat `do_decrypt[_out]` are provided over it). /// /// Walks the input in pairs so the permutation's two-block path is used, with an at-most-one /// block remainder for odd `N`. `as_chunks` splits into exactly that shape with no runtime diff --git a/crypto/modes/src/lib.rs b/crypto/modes/src/lib.rs index 50d8c57a..5bc2f45f 100644 --- a/crypto/modes/src/lib.rs +++ b/crypto/modes/src/lib.rs @@ -60,12 +60,12 @@ //! //! let (mut encryptor, iv) = //! Aes256Cbc::::do_encrypt_init(&key).expect("encrypt init"); -//! let first = encryptor.do_encrypt_blocks(&[[0xAAu8; 16]]).expect("block 1"); -//! let rest = encryptor.do_encrypt_blocks(&[[0xBBu8; 16], [0xCCu8; 16]]).expect("blocks 2-3"); +//! let first = encryptor.do_encrypt(&[0xAAu8; 16]).expect("block 1"); +//! let rest = encryptor.do_encrypt(&[0xBBu8; 32]).expect("blocks 2-3"); //! //! let mut decryptor = Aes256Cbc::::do_decrypt_init(&key, &iv).expect("decrypt init"); -//! assert_eq!(decryptor.do_decrypt_blocks(&first).unwrap(), [[0xAAu8; 16]]); -//! assert_eq!(decryptor.do_decrypt_blocks(&rest).unwrap(), [[0xBBu8; 16], [0xCCu8; 16]]); +//! assert_eq!(decryptor.do_decrypt(&first).unwrap(), [0xAAu8; 16]); +//! assert_eq!(decryptor.do_decrypt(&rest).unwrap(), [0xBBu8; 32]); //! ``` //! //! Using the wrong direction does not compile: diff --git a/crypto/modes/tests/acvp_tests.rs b/crypto/modes/tests/acvp_tests.rs index 47f8b504..0d18d1ea 100644 --- a/crypto/modes/tests/acvp_tests.rs +++ b/crypto/modes/tests/acvp_tests.rs @@ -127,18 +127,18 @@ where match grouping { Grouping::Single => { for block in input { - let [c] = enc.do_encrypt_blocks(&[*block]).unwrap(); - out.push(c); + out.push(enc.do_encrypt(block).unwrap()); } } Grouping::Pairs => { let (pairs, tail) = input.as_chunks::<2>(); for pair in pairs { - out.extend_from_slice(&enc.do_encrypt_blocks(pair).unwrap()); + let mut c = [[0u8; BLOCK_LEN]; 2]; + enc.do_encrypt_blocks_out(pair, &mut c).unwrap(); + out.extend_from_slice(&c); } for block in tail { - let [c] = enc.do_encrypt_blocks(&[*block]).unwrap(); - out.push(c); + out.push(enc.do_encrypt(block).unwrap()); } } } @@ -149,18 +149,18 @@ where match grouping { Grouping::Single => { for block in input { - let [p] = dec.do_decrypt_blocks(&[*block]).unwrap(); - out.push(p); + out.push(dec.do_decrypt(block).unwrap()); } } Grouping::Pairs => { let (pairs, tail) = input.as_chunks::<2>(); for pair in pairs { - out.extend_from_slice(&dec.do_decrypt_blocks(pair).unwrap()); + let mut p = [[0u8; BLOCK_LEN]; 2]; + dec.do_decrypt_blocks_out(pair, &mut p).unwrap(); + out.extend_from_slice(&p); } for block in tail { - let [p] = dec.do_decrypt_blocks(&[*block]).unwrap(); - out.push(p); + out.push(dec.do_decrypt(block).unwrap()); } } } diff --git a/crypto/modes/tests/cbc_tests.rs b/crypto/modes/tests/cbc_tests.rs index a544e7b8..05912c81 100644 --- a/crypto/modes/tests/cbc_tests.rs +++ b/crypto/modes/tests/cbc_tests.rs @@ -17,6 +17,26 @@ use common::{SwappedPairToy, TOY_LEN, Toy, toy_key}; type ToyCbc = Cbc; type SwappedCbc = Cbc; +/// The implementor hook `do_encrypt_blocks_out`, by value, for tests whose data is block-shaped. +fn enc_blocks( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[[u8; TOY_LEN]; N], +) -> [[u8; TOY_LEN]; N] { + let mut ct = [[0u8; TOY_LEN]; N]; + enc.do_encrypt_blocks_out(plaintext, &mut ct).unwrap(); + ct +} + +/// The implementor hook `do_decrypt_blocks_out`, by value. +fn dec_blocks( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[[u8; TOY_LEN]; N], +) -> [[u8; TOY_LEN]; N] { + let mut pt = [[0u8; TOY_LEN]; N]; + dec.do_decrypt_blocks_out(ciphertext, &mut pt).unwrap(); + pt +} + // ---- the toy itself, and the mode, against the shared frameworks ------------------------- /// The toy must be a real permutation before any conclusion drawn from it is worth anything. @@ -55,16 +75,16 @@ fn call_grouping_does_not_change_the_result() { let (mut enc, got_iv) = ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); assert_eq!(got_iv, iv, "the pinned RNG should reproduce the IV"); - let reference = enc.do_encrypt_blocks(&plaintext).unwrap(); + let reference = enc_blocks(&mut enc, &plaintext); // The same eight blocks, grouped every way that exercises a different code path. let (mut enc, _) = ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); let mut got = [[0u8; TOY_LEN]; 8]; - let a = enc.do_encrypt_blocks(&[plaintext[0]]).unwrap(); // N = 1 - let b = enc.do_encrypt_blocks(&[plaintext[1], plaintext[2]]).unwrap(); // N = 2 - let c = enc.do_encrypt_blocks(&[plaintext[3], plaintext[4], plaintext[5]]).unwrap(); // N = 3 - let d = enc.do_encrypt_blocks(&[plaintext[6], plaintext[7]]).unwrap(); // N = 2 - got[0] = a[0]; + let a = enc.do_encrypt(&plaintext[0]).unwrap(); // one block, flat + let b = enc_blocks(&mut enc, &[plaintext[1], plaintext[2]]); // N = 2 + let c = enc_blocks(&mut enc, &[plaintext[3], plaintext[4], plaintext[5]]); // N = 3 + let d = enc_blocks(&mut enc, &[plaintext[6], plaintext[7]]); // N = 2 + got[0] = a; got[1..3].copy_from_slice(&b); got[3..6].copy_from_slice(&c); got[6..8].copy_from_slice(&d); @@ -75,7 +95,7 @@ fn call_grouping_does_not_change_the_result() { let ct = reference; let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); - let all_at_once = dec.do_decrypt_blocks(&ct).unwrap(); + let all_at_once = dec_blocks(&mut dec, &ct); assert_eq!(all_at_once, plaintext); for grouping in [1usize, 2, 4] { @@ -85,17 +105,14 @@ fn call_grouping_does_not_change_the_result() { while at < 8 { match grouping { 1 => { - let [p] = dec.do_decrypt_blocks(&[ct[at]]).unwrap(); - out[at] = p; + out[at] = dec.do_decrypt(&ct[at]).unwrap(); } 2 => { - let p = dec.do_decrypt_blocks(&[ct[at], ct[at + 1]]).unwrap(); + let p = dec_blocks(&mut dec, &[ct[at], ct[at + 1]]); out[at..at + 2].copy_from_slice(&p); } _ => { - let p = dec - .do_decrypt_blocks(&[ct[at], ct[at + 1], ct[at + 2], ct[at + 3]]) - .unwrap(); + let p = dec_blocks(&mut dec, &[ct[at], ct[at + 1], ct[at + 2], ct[at + 3]]); out[at..at + 4].copy_from_slice(&p); } } @@ -106,8 +123,8 @@ fn call_grouping_does_not_change_the_result() { // N = 3 and N = 5 both leave a one-block remainder after the pair loop. let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); - let three = dec.do_decrypt_blocks(&[ct[0], ct[1], ct[2]]).unwrap(); - let five = dec.do_decrypt_blocks(&[ct[3], ct[4], ct[5], ct[6], ct[7]]).unwrap(); + let three = dec_blocks(&mut dec, &[ct[0], ct[1], ct[2]]); + let five = dec_blocks(&mut dec, &[ct[3], ct[4], ct[5], ct[6], ct[7]]); assert_eq!(three, [plaintext[0], plaintext[1], plaintext[2]]); assert_eq!(five, [plaintext[3], plaintext[4], plaintext[5], plaintext[6], plaintext[7]]); } @@ -125,37 +142,39 @@ fn the_pair_path_is_really_used() { // The correct toy round-trips. let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); - let ct = enc.do_encrypt_blocks(&plaintext).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); - assert_eq!(dec.do_decrypt_blocks(&ct).unwrap(), plaintext); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext); // The swapped-pair toy encrypts identically (encryption is serial and never pairs)... let (mut enc, iv) = SwappedCbc::::do_encrypt_init(&key).unwrap(); - let ct = enc.do_encrypt_blocks(&plaintext).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); // ...but decrypting the pair together must now be wrong, because the pair path is used. let mut dec = SwappedCbc::::do_decrypt_init(&key, &iv).unwrap(); assert_ne!( - dec.do_decrypt_blocks(&ct).unwrap(), + dec_blocks(&mut dec, &ct), plaintext, "decrypting a pair must go through decrypt_blocks2" ); // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. let mut dec = SwappedCbc::::do_decrypt_init(&key, &iv).unwrap(); - let [p0] = dec.do_decrypt_blocks(&[ct[0]]).unwrap(); - let [p1] = dec.do_decrypt_blocks(&[ct[1]]).unwrap(); + let p0 = dec.do_decrypt(&ct[0]).unwrap(); + let p1 = dec.do_decrypt(&ct[1]).unwrap(); assert_eq!([p0, p1], plaintext, "the single-block path must not pair"); } -/// The `_out` variants must agree with the by-value ones and report the byte count. +/// The flat streaming method must agree with the block-shaped implementor hook and report the +/// byte count. #[test] -fn out_variants_agree_with_by_value() { +fn flat_streaming_agrees_with_the_block_hook() { let key = toy_key(); let plaintext = [[0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + let flat_plaintext: [u8; 3 * TOY_LEN] = plaintext.as_flattened().try_into().unwrap(); let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); - let by_value = enc.do_encrypt_blocks(&plaintext).unwrap(); + let by_value = enc.do_encrypt(&flat_plaintext).unwrap(); let (mut enc, iv2) = ToyCbc::::do_encrypt_init_rng( &key, @@ -166,7 +185,7 @@ fn out_variants_agree_with_by_value() { let mut out = [[0u8; TOY_LEN]; 3]; let n = enc.do_encrypt_blocks_out(&plaintext, &mut out).unwrap(); assert_eq!(n, 3 * TOY_LEN); - assert_eq!(out, by_value); + assert_eq!(*out.as_flattened(), by_value, "flat streaming must equal the block hook"); let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); let mut back = [[0u8; TOY_LEN]; 3]; @@ -189,7 +208,7 @@ fn an_iv_bit_error_flips_exactly_that_bit_of_the_first_block() { let plaintext = [[0x00u8; TOY_LEN], [0x11u8; TOY_LEN], [0x22u8; TOY_LEN]]; let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); - let ct = enc.do_encrypt_blocks(&plaintext).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); for byte in 0..TOY_LEN { for bit in 0..8 { @@ -197,7 +216,7 @@ fn an_iv_bit_error_flips_exactly_that_bit_of_the_first_block() { corrupt_iv[byte] ^= 1 << bit; let mut dec = ToyCbc::::do_decrypt_init(&key, &corrupt_iv).unwrap(); - let got = dec.do_decrypt_blocks(&ct).unwrap(); + let got = dec_blocks(&mut dec, &ct); let mut expected = plaintext; expected[0][byte] ^= 1 << bit; @@ -217,13 +236,13 @@ fn a_ciphertext_bit_error_affects_only_two_blocks() { let plaintext = [[0x00u8; TOY_LEN], [0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); - let ct = enc.do_encrypt_blocks(&plaintext).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); let mut corrupt = ct; corrupt[1][3] ^= 0b0010_0000; let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); - let got = dec.do_decrypt_blocks(&corrupt).unwrap(); + let got = dec_blocks(&mut dec, &corrupt); assert_eq!(got[0], plaintext[0], "P1 depends only on C1 and the IV"); assert_ne!(got[1], plaintext[1], "P2 comes from the corrupted C2"); @@ -317,7 +336,7 @@ fn one_shots_agree_with_the_streaming_api() { let (iv_a, ct_blocks) = { let (mut enc, iv) = ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); - (iv, enc.do_encrypt_blocks(&blocks3).unwrap()) + (iv, enc_blocks(&mut enc, &blocks3)) }; let (iv_b, ct_flat) = ToyCbc::::encrypt_rng(&key, &mut pinned_rng(), &flat3).unwrap(); @@ -343,7 +362,7 @@ fn one_shots_agree_with_the_streaming_api() { let (_, ct_blocks) = { let (mut enc, iv) = ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); - (iv, enc.do_encrypt_blocks(&blocks4).unwrap()) + (iv, enc_blocks(&mut enc, &blocks4)) }; let (_, ct_flat) = ToyCbc::::encrypt_rng(&key, &mut pinned_rng(), &flat4).unwrap(); assert_eq!(ct_flat, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); diff --git a/crypto/modes/tests/sp800_38a_tests.rs b/crypto/modes/tests/sp800_38a_tests.rs index 93f98aaf..6bf1f55a 100644 --- a/crypto/modes/tests/sp800_38a_tests.rs +++ b/crypto/modes/tests/sp800_38a_tests.rs @@ -73,6 +73,11 @@ fn blocks(hex_strs: &[&str; 4]) -> [[u8; BLOCK_LEN]; 4] { core::array::from_fn(|i| block(hex_strs[i])) } +/// The same four blocks as 64 contiguous bytes, for the flat streaming and one-shot methods. +fn flat(hex_strs: &[&str; 4]) -> [u8; 4 * BLOCK_LEN] { + blocks(hex_strs).as_flattened().try_into().expect("4 blocks = 64 bytes") +} + fn key_material(hex_str: &str) -> KeyMaterial { let bytes = hex::decode(hex_str).expect("valid hex"); assert_eq!(bytes.len(), N, "key length"); @@ -100,7 +105,11 @@ where ) .unwrap(); assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); - assert_eq!(enc.do_encrypt_blocks(&pt).unwrap(), ct, "{section}: four blocks in one call"); + assert_eq!( + enc.do_encrypt(&flat(&PLAINTEXTS)).unwrap(), + flat(expected), + "{section}: four blocks in one call" + ); // One block at a time. let (mut enc, _) = Cbc::::do_encrypt_init_rng( @@ -109,11 +118,11 @@ where ) .unwrap(); for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { - let [got] = enc.do_encrypt_blocks(&[*p]).unwrap(); + let got = enc.do_encrypt(p).unwrap(); assert_eq!(&got, c, "{section}: block #{}", i + 1); } - // Through the `_out` variant. + // Through the implementor hook, `do_*_blocks_out`. let (mut enc, _) = Cbc::::do_encrypt_init_rng( &key, &mut FixedSeedRNG::::new(iv), @@ -142,23 +151,28 @@ where // All four blocks in one call (two pairs, no remainder). let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); - assert_eq!(dec.do_decrypt_blocks(&ct).unwrap(), pt, "{section}: four blocks in one call"); + assert_eq!( + dec.do_decrypt(&flat(ciphertext)).unwrap(), + flat(&PLAINTEXTS), + "{section}: four blocks in one call" + ); // One block at a time (never takes the pair path). let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { - let [got] = dec.do_decrypt_blocks(&[*c]).unwrap(); + let got = dec.do_decrypt(c).unwrap(); assert_eq!(&got, p, "{section}: block #{}", i + 1); } // 3 + 1: one pair plus a remainder, then a lone block. let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); - let three = dec.do_decrypt_blocks(&[ct[0], ct[1], ct[2]]).unwrap(); - let one = dec.do_decrypt_blocks(&[ct[3]]).unwrap(); - assert_eq!(three, [pt[0], pt[1], pt[2]], "{section}: blocks 1-3"); - assert_eq!(one, [pt[3]], "{section}: block 4"); + let first_three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); + let three = dec.do_decrypt(&first_three).unwrap(); + let one = dec.do_decrypt(&ct[3]).unwrap(); + assert_eq!(&three[..], pt[..3].as_flattened(), "{section}: blocks 1-3"); + assert_eq!(one, pt[3], "{section}: block 4"); - // Through the `_out` variant. + // Through the implementor hook, `do_*_blocks_out`. let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); let mut out = [[0u8; BLOCK_LEN]; 4]; let n = dec.do_decrypt_blocks_out(&ct, &mut out).unwrap(); @@ -200,9 +214,6 @@ fn f_2_6_cbc_aes256_decrypt() { /// The one-shots take flat arrays, so the four blocks are presented as 64 contiguous bytes. #[test] fn the_one_shot_api_matches_the_vectors() { - fn flat(hex_strs: &[&str; 4]) -> [u8; 4 * BLOCK_LEN] { - blocks(hex_strs).as_flattened().try_into().expect("4 blocks = 64 bytes") - } let iv = block(IV); let pt = flat(&PLAINTEXTS); @@ -259,7 +270,7 @@ fn cbc_differs_from_ecb_by_the_iv() { &mut FixedSeedRNG::<16>::new(iv), ) .unwrap(); - let [cbc] = enc.do_encrypt_blocks(&[block(PLAINTEXTS[0])]).unwrap(); + let cbc = enc.do_encrypt(&block(PLAINTEXTS[0])).unwrap(); assert_eq!(cbc, block(CIPHERTEXTS_128[0]), "F.2.1 block #1"); assert_ne!(cbc, ecb); } diff --git a/crypto/padding/src/padded.rs b/crypto/padding/src/padded.rs index cf4bc8f1..7ae45fc1 100644 --- a/crypto/padding/src/padded.rs +++ b/crypto/padding/src/padded.rs @@ -121,8 +121,7 @@ where let Self { mut inner, mut buf, buf_len, .. } = self; // buf_len < BLOCK_LEN is an invariant of this type, so pad() cannot fail here. P::pad(&mut buf, buf_len)?; - let [ct] = inner.do_encrypt_blocks(from_ref(&*buf))?; - Ok(ct) + inner.do_encrypt(&*buf) } /// As [`do_final`](Self::do_final), writing the final block into `ciphertext`. Returns `BLOCK_LEN`. @@ -306,7 +305,7 @@ where let Some(last) = held else { return Err(SymmetricCipherError::DecryptionFailed); }; - let [pt] = inner.do_decrypt_blocks(from_ref(&last))?; + let pt = inner.do_decrypt(&last)?; let data_len = P::unpad(&pt)?; Ok((pt, data_len)) } diff --git a/crypto/padding/tests/padded_tests.rs b/crypto/padding/tests/padded_tests.rs index 8bd27941..23b38cf0 100644 --- a/crypto/padding/tests/padded_tests.rs +++ b/crypto/padding/tests/padded_tests.rs @@ -54,14 +54,6 @@ impl BlockCipherEncryptor for ToyCbc { rng.next_bytes_out(&mut iv)?; Ok((Self { key, chain: iv }, iv)) } - fn do_encrypt_blocks( - &mut self, - plaintext: &[[u8; B]; N], - ) -> Result<[[u8; B]; N], SymmetricCipherError> { - let mut ct = [[0u8; B]; N]; - self.do_encrypt_blocks_out(plaintext, &mut ct)?; - Ok(ct) - } fn do_encrypt_blocks_out( &mut self, plaintext: &[[u8; B]; N], @@ -81,14 +73,6 @@ impl BlockCipherDecryptor for ToyCbc { fn do_decrypt_init(key: &KeyMaterial, iv: &[u8; B]) -> Result { Ok(Self { key: Self::check_key(key)?, chain: *iv }) } - fn do_decrypt_blocks( - &mut self, - ciphertext: &[[u8; B]; N], - ) -> Result<[[u8; B]; N], SymmetricCipherError> { - let mut pt = [[0u8; B]; N]; - self.do_decrypt_blocks_out(ciphertext, &mut pt)?; - Ok(pt) - } fn do_decrypt_blocks_out( &mut self, ciphertext: &[[u8; B]; N], From 60e0cc07e1b2f6bf26d453a3f867206c36e1a832 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 3 Sep 2026 17:12:33 +1000 Subject: [PATCH 4/7] core: fold BlockCipher into Algorithm BlockCipher declared only MAX_SECURITY_STRENGTH, which Algorithm already has, so every implementor of BlockPermutation (which also implemented Algorithm) had two copies of the same constant and had to qualify every use of it. BlockPermutation, BlockCipherEncryptor and BlockCipherDecryptor are now bounded on Algorithm instead, and Cbc implements Algorithm with its permutation's ALG_NAME and MAX_SECURITY_STRENGTH. Raised in review of PR #107. Co-Authored-By: Claude Fable 5.1 --- alpha_0.1.3_release_notes.md | 11 +++++---- crypto/aes-lowmemory/src/aes.rs | 24 +------------------ .../src/block_permutation.rs | 10 ++++---- crypto/core/src/traits.rs | 15 ++++-------- crypto/modes/benches/modes_benches.rs | 5 ++-- crypto/modes/src/cbc.rs | 10 ++++---- crypto/modes/tests/common/mod.rs | 8 ++++--- crypto/padding/tests/padded_tests.rs | 5 ++-- 8 files changed, 34 insertions(+), 54 deletions(-) diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index 55c4137c..8e869608 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -87,9 +87,8 @@ keyed permutation -- `CIPH_K` / `CIPH^-1_K` of SP 800-38A Sec 5.1 -- that a mode `new`, `encrypt_block`, `decrypt_block`, plus provided `encrypt_blocks2` / `decrypt_blocks2` that default to two single-block calls and which bit-sliced implementations override. The block methods are infallible; only `new` can fail, and only on the key. `bouncycastle-aes-lowmemory` implements -it for all three key lengths (and `BlockCipher`, which is metadata only and is -`BlockPermutation`'s supertrait; the data-encryption traits are still deliberately not -implemented there). +it for all three key lengths (the data-encryption traits are still deliberately not implemented +there). Testing: @@ -131,8 +130,10 @@ Testing: Block cipher traits (PR #96): * The single `BlockCipher` streaming trait is split into `BlockCipherEncryptor` and `BlockCipherDecryptor` (mirroring - `KEMEncapsulator` / `KEMDecapsulator`) so the direction is encoded in the implementing type. A minimal `BlockCipher` - supertrait carries the shared `MAX_SECURITY_STRENGTH`; the `SymmetricCipher` one-shot API is no longer a supertrait. + `KEMEncapsulator` / `KEMDecapsulator`) so the direction is encoded in the implementing type. Both, and + `BlockPermutation`, are bounded on `Algorithm`, whose `MAX_SECURITY_STRENGTH` is the strength the `_init` + constructors enforce (a mode reports its permutation's name and strength); the `SymmetricCipher` one-shot API is no + longer a supertrait. * The single-block `do_{en,de}crypt_block[_out]` methods are replaced by multi-block `do_{en,de}crypt_blocks[_out]`, taking `&[[u8; BLOCK_LEN]; N]` so the block count is compile-time and input/output lengths cannot disagree. diff --git a/crypto/aes-lowmemory/src/aes.rs b/crypto/aes-lowmemory/src/aes.rs index 1b889ab7..9b25fe4e 100644 --- a/crypto/aes-lowmemory/src/aes.rs +++ b/crypto/aes-lowmemory/src/aes.rs @@ -6,7 +6,7 @@ use crate::sbox::{inv_sbox, sbox}; use crate::schedule::{Aes128Params, Aes192Params, Aes256Params, AesParams, expand, round_key}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Algorithm, BlockCipher, BlockPermutation, SecurityStrength}; +use bouncycastle_core::traits::{Algorithm, BlockPermutation, SecurityStrength}; use bouncycastle_utils::secret::Secret; /// The AES block length in bytes: 16 (FIPS 197 Sec 3.4, `Nb` = 4 words). @@ -221,28 +221,6 @@ impl Algorithm for Aes256 { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; } -// `BlockCipher` here is metadata only -- it declares `MAX_SECURITY_STRENGTH` and nothing else, and -// it is the supertrait `BlockPermutation` requires. It is *not* one of the data-encryption traits -// (`SymmetricCipher`, `BlockCipherEncryptor`, `BlockCipherDecryptor`, `AEADCipher`), which this -// crate still deliberately does not implement: those are mode-of-operation concerns. See the crate -// docs. -// -// Both `Algorithm` and `BlockCipher` declare `MAX_SECURITY_STRENGTH`, so a bare -// `Aes128::MAX_SECURITY_STRENGTH` is ambiguous; qualify it as `::...` or -// `::...` at the use site. - -impl BlockCipher for Aes128 { - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} - -impl BlockCipher for Aes192 { - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; -} - -impl BlockCipher for Aes256 { - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; -} - // The three `BlockPermutation` impls are one-line delegations to the inherent methods above. They // are written out longhand rather than generated, for the `cargo mutants` reason given above. // diff --git a/crypto/core-test-framework/src/block_permutation.rs b/crypto/core-test-framework/src/block_permutation.rs index 6eed66fe..7f37c51e 100644 --- a/crypto/core-test-framework/src/block_permutation.rs +++ b/crypto/core-test-framework/src/block_permutation.rs @@ -5,7 +5,7 @@ use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; -use bouncycastle_core::traits::{BlockCipher, BlockPermutation, SecurityStrength}; +use bouncycastle_core::traits::{BlockPermutation, SecurityStrength}; /// Instance of the test framework. pub struct TestFrameworkBlockPermutation { @@ -35,7 +35,9 @@ impl TestFrameworkBlockPermutation { /// semantics, and it is the reason the pair methods are worth having in the trait at all; /// * the pair methods round-trip each other; /// * a key of the wrong [`KeyType`] is rejected; - /// * the security-strength policy matches [`BlockCipher::MAX_SECURITY_STRENGTH`]. + /// * the security-strength policy matches [`Algorithm::MAX_SECURITY_STRENGTH`]. + /// + /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH pub fn test< const KEY_LEN: usize, const BLOCK_LEN: usize, @@ -152,11 +154,11 @@ impl TestFrameworkBlockPermutation { match P::new(&key) { Ok(_) => assert!( - ss >= &

::MAX_SECURITY_STRENGTH, + ss >= &P::MAX_SECURITY_STRENGTH, "should have required a key at least as strong as the algorithm" ), Err(SymmetricCipherError::KeyMaterialError(_)) => assert!( - ss < &

::MAX_SECURITY_STRENGTH, + ss < &P::MAX_SECURITY_STRENGTH, "should not have rejected a key strong enough for the algorithm" ), _ => panic!("Unexpected error"), diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 696dce3c..b66e3f97 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -80,13 +80,6 @@ pub trait SymmetricCipher: Alg ) -> Result; } -/// Metadata shared by [`BlockCipherEncryptor`] and [`BlockCipherDecryptor`]. -pub trait BlockCipher { - /// Maximum security strength supported by the algorithm; keys tagged with a lower strength are - /// rejected by the `_init` constructors. - const MAX_SECURITY_STRENGTH: SecurityStrength; -} - /// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1. /// /// This is the raw primitive a mode of operation is built on, not something to encrypt data with. @@ -103,13 +96,13 @@ pub trait BlockCipher { /// is nothing a caller can get wrong once [`BlockPermutation::new`] has returned. Only `new` can /// fail, and only because of the key. pub trait BlockPermutation: - BlockCipher + Sized + Algorithm + Sized { /// Expands the key. /// /// # Errors /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose - /// security strength is below [`BlockCipher::MAX_SECURITY_STRENGTH`], both as a + /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a /// [`SymmetricCipherError::KeyMaterialError`]. fn new(key: &KeyMaterial) -> Result; @@ -165,7 +158,7 @@ pub trait BlockCipherEncryptor< const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, ->: BlockCipher + Sized +>: Algorithm + Sized { /// Begins a streaming encryption flow, returning the generated init data (e.g. IV). /// Sources randomness from the library's default OS-backed RNG. @@ -283,7 +276,7 @@ pub trait BlockCipherDecryptor< const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, ->: BlockCipher + Sized +>: Algorithm + Sized { /// Begins a streaming decryption flow from the init data returned by [`BlockCipherEncryptor::do_encrypt_init`]. fn do_decrypt_init( diff --git a/crypto/modes/benches/modes_benches.rs b/crypto/modes/benches/modes_benches.rs index 1863cc5a..191c8e16 100644 --- a/crypto/modes/benches/modes_benches.rs +++ b/crypto/modes/benches/modes_benches.rs @@ -15,7 +15,7 @@ use bouncycastle_aes_lowmemory::{Aes128, Aes256}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{ - BlockCipher, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, }; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; use criterion::{Criterion, Throughput, criterion_group, criterion_main}; @@ -41,7 +41,8 @@ type Aes256Cbc

= Cbc; /// speeds up substantially between those two, so call granularity dominates that comparison. struct UnpairedAes128(Aes128); -impl BlockCipher for UnpairedAes128 { +impl Algorithm for UnpairedAes128 { + const ALG_NAME: &'static str = "AES-128 (unpaired)"; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } diff --git a/crypto/modes/src/cbc.rs b/crypto/modes/src/cbc.rs index 61518817..51b49ff1 100644 --- a/crypto/modes/src/cbc.rs +++ b/crypto/modes/src/cbc.rs @@ -35,8 +35,7 @@ use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ - BlockCipher, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, RNG, - SecurityStrength, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, RNG, SecurityStrength, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -125,13 +124,16 @@ where } } -impl BlockCipher +impl Algorithm for Cbc where P: BlockPermutation, { + /// The underlying permutation's name. The mode is not appended: `&'static str`s cannot be + /// concatenated in a `const`, and the mode is already in the type. + const ALG_NAME: &'static str = P::ALG_NAME; /// A mode does not change the strength of the underlying cipher. - const MAX_SECURITY_STRENGTH: SecurityStrength =

::MAX_SECURITY_STRENGTH; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } impl diff --git a/crypto/modes/tests/common/mod.rs b/crypto/modes/tests/common/mod.rs index fcb52c5b..6bd5dcd4 100644 --- a/crypto/modes/tests/common/mod.rs +++ b/crypto/modes/tests/common/mod.rs @@ -15,7 +15,7 @@ use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{BlockCipher, BlockPermutation, SecurityStrength}; +use bouncycastle_core::traits::{Algorithm, BlockPermutation, SecurityStrength}; /// Block and key length of the toy ciphers, chosen to match AES so the tests exercise the same /// shapes the real thing will. @@ -46,7 +46,8 @@ pub struct Toy { key: [u8; TOY_LEN], } -impl BlockCipher for Toy { +impl Algorithm for Toy { + const ALG_NAME: &'static str = "Toy"; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } @@ -83,7 +84,8 @@ pub struct SwappedPairToy { inner: Toy, } -impl BlockCipher for SwappedPairToy { +impl Algorithm for SwappedPairToy { + const ALG_NAME: &'static str = "SwappedPairToy"; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } diff --git a/crypto/padding/tests/padded_tests.rs b/crypto/padding/tests/padded_tests.rs index 23b38cf0..21a6c181 100644 --- a/crypto/padding/tests/padded_tests.rs +++ b/crypto/padding/tests/padded_tests.rs @@ -8,7 +8,7 @@ use bouncycastle_core::errors::{KeyMaterialError, PaddingError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{ - BlockCipher, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SecurityStrength, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SecurityStrength, }; use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkBlockCipher; use bouncycastle_padding::{PKCS7, PaddedDecryptor, PaddedEncryptor}; @@ -36,7 +36,8 @@ impl ToyCbc { } } -impl BlockCipher for ToyCbc { +impl Algorithm for ToyCbc { + const ALG_NAME: &'static str = "ToyCbc"; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; } From ede3b9ee1f5aa6e165ba743e0b11101ae0b7ed61 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 3 Sep 2026 17:20:21 +1000 Subject: [PATCH 5/7] core: block cipher data methods work in place A block cipher mode never changes the length of its data, so a separate output buffer was only ever a copy, and a copy of plaintext is one more thing to scrub. The one-shots, the flat streaming methods and the implementor hook now all take a single `&mut [u8; LEN]` / `&mut [[u8; BLOCK_LEN]; N]` and transform it in place; `encrypt` and `encrypt_rng` return just the init data. The `_out` variants and the `usize` byte counts (always LEN) are gone. The compile-time `LEN % BLOCK_LEN == 0` check is unchanged. The data methods keep a `Result` for modes with a per-initialization data limit (counter-based modes); CBC never fails them, and the docs say so. Cbc, PaddedEncryptor/PaddedDecryptor, the test framework, the modes tests and benches, the doc examples and the CLI follow. The padding layer pads and encrypts the final block inside its `Secret`, so only ciphertext is ever copied out of it. Raised in review of PR #107. Co-Authored-By: Claude Fable 5.1 --- alpha_0.1.3_release_notes.md | 24 +- cli/src/aes_cbc_cmd.rs | 57 ++--- crypto/aes-lowmemory/src/cbc.rs | 36 ++- crypto/aes-lowmemory/src/lib.rs | 11 +- .../src/symmetric_ciphers.rs | 107 ++++----- crypto/core/src/traits.rs | 220 +++++++---------- crypto/modes/benches/modes_benches.rs | 227 +++++++++++------- crypto/modes/src/cbc.rs | 101 ++++---- crypto/modes/src/lib.rs | 26 +- crypto/modes/tests/acvp_tests.rs | 24 +- crypto/modes/tests/cbc_tests.rs | 117 +++++---- crypto/modes/tests/sp800_38a_tests.rs | 98 ++++---- crypto/padding/src/padded.rs | 69 +++--- crypto/padding/tests/padded_tests.rs | 35 ++- 14 files changed, 590 insertions(+), 562 deletions(-) diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index 8e869608..d74c766d 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -141,17 +141,19 @@ Block cipher traits (PR #96): pattern. * The `do_{en,de}crypt_final[_out]` methods are removed: the traits are now strictly block-aligned, and padding of arbitrary-length data belongs to a separate `PaddedEncryptor` / `PaddedDecryptor` layer built on top. -* One-shot static APIs are provided (default) methods implemented once in the traits -- `encrypt`, `encrypt_rng`, - `encrypt_out`, `encrypt_out_rng` on `BlockCipherEncryptor` and `decrypt`, `decrypt_out` on `BlockCipherDecryptor` -- - so every block-aligned mode gets the house-standard take-data-return-result API at no cost to implementors. They - take a flat `[u8; LEN]`; `LEN` must be a whole number of blocks, and this is enforced at **compile time** by an - inline `const` assertion at the instantiating call site, so there is no runtime length check and no error variant - for it. (An earlier form of these one-shots took `[[u8; BLOCK_LEN]; N]`; it was replaced before release.) -* The streaming API is flat as well: `do_{en,de}crypt` / `do_{en,de}crypt_out` take a `[u8; LEN]` with the - same compile-time alignment check, and are provided methods. The single block-shaped method left is the implementor - hook `do_{en,de}crypt_blocks_out` over `[[u8; BLOCK_LEN]; N]`, which is what guarantees an implementation never - sees a partial block and that input and output lengths agree at compile time; an implementor writes only - `do_{en,de}crypt_init[_rng]` and that hook. (The by-value `do_{en,de}crypt_blocks` of #96 are gone.) +* One-shot static APIs are provided (default) methods implemented once in the traits -- `encrypt`, `encrypt_rng` on + `BlockCipherEncryptor` and `decrypt` on `BlockCipherDecryptor` -- so every block-aligned mode gets the + house-standard one-shot API at no cost to implementors. They take a flat `&mut [u8; LEN]` and work **in place** + (plaintext in, ciphertext out in the same bytes; `encrypt` returns the generated init data). `LEN` must be a whole + number of blocks, and this is enforced at **compile time** by an inline `const` assertion at the instantiating call + site, so there is no runtime length check and no error variant for it. Data whose length is only known at run + time goes block by block or through the padding layer. (Earlier forms took `[[u8; BLOCK_LEN]; N]`, then separate + input and output arrays; both were replaced before release.) +* The streaming API is flat and in place as well: `do_{en,de}crypt(&mut [u8; LEN])`, with the same compile-time + alignment check, are provided methods. The single block-shaped method left is the implementor hook + `do_{en,de}crypt_blocks(&mut [[u8; BLOCK_LEN]; N])`, which is what guarantees an implementation never sees a + partial block; an implementor writes only `do_{en,de}crypt_init[_rng]` and that hook. The data methods keep a + `Result` only for modes with a per-initialization data limit (counter-based modes); CBC never fails them. Testing: diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs index a33d1f65..d532a31a 100644 --- a/cli/src/aes_cbc_cmd.rs +++ b/cli/src/aes_cbc_cmd.rs @@ -51,9 +51,9 @@ const BLOCK_LEN: usize = 16; /// Bytes processed per call: 1 KiB = 64 blocks, matching the other streaming commands. /// -/// A full chunk goes through `do_*_out::` in one call, which for decryption means 32 -/// pairs down the `decrypt_blocks2` path. The at-most-63-block tail at end of input goes one block -/// at a time; it is bounded, so its cost does not scale with the input. +/// A full chunk goes through `do_*::` in one call, in place, which for decryption means +/// 32 pairs down the `decrypt_blocks2` path. The at-most-63-block tail at end of input goes one +/// block at a time; it is bounded, so its cost does not scale with the input. const CHUNK_LEN: usize = 64 * BLOCK_LEN; #[derive(ValueEnum, Clone, Debug)] @@ -183,20 +183,18 @@ where // The IV goes out ahead of the ciphertext, so `decrypt` can pick it up. write_bytes_or_hex(&iv, output_hex); - let mut out = [0u8; CHUNK_LEN]; - - stream_aligned(|data| match <&[u8; CHUNK_LEN]>::try_from(data) { - Ok(chunk) => { - // Cannot fail: the mode's block methods are infallible for a constructed value. - enc.do_encrypt_out(chunk, &mut out).unwrap(); - write_bytes_or_hex(&out, output_hex); - } - Err(_) => { + // The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out. + stream_aligned(|data| { + if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) { + // Cannot fail: CBC has no per-IV data limit. + enc.do_encrypt(chunk).unwrap(); + } else { // The bounded tail at end of input: whole blocks, fewer than a chunk. - for block in data.as_chunks::().0 { - write_bytes_or_hex(&enc.do_encrypt(block).unwrap(), output_hex); + for block in data.as_chunks_mut::().0 { + enc.do_encrypt(block).unwrap(); } } + write_bytes_or_hex(data, output_hex); }); finish(output_hex); @@ -223,32 +221,29 @@ where exit(-1); }); - let mut out = [0u8; CHUNK_LEN]; - - stream_aligned(|data| match <&[u8; CHUNK_LEN]>::try_from(data) { - Ok(chunk) => { + stream_aligned(|data| { + if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) { // A full chunk is 32 pairs, so this is the `decrypt_blocks2` path. - dec.do_decrypt_out(chunk, &mut out).unwrap(); - write_bytes_or_hex(&out, output_hex); - } - Err(_) => { - for block in data.as_chunks::().0 { - write_bytes_or_hex(&dec.do_decrypt(block).unwrap(), output_hex); + dec.do_decrypt(chunk).unwrap(); + } else { + for block in data.as_chunks_mut::().0 { + dec.do_decrypt(block).unwrap(); } } + write_bytes_or_hex(data, output_hex); }); finish(output_hex); } -/// Reads stdin and hands it to `process` in block-aligned pieces: a full `CHUNK_LEN` bytes each time -/// one has accumulated, then once more at end of input with whatever whole blocks remain (fewer -/// than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the +/// Reads stdin and hands it to `process` in block-aligned pieces, mutably so it can be transformed +/// in place: a full `CHUNK_LEN` bytes each time one has accumulated, then once more at end of input +/// with whatever whole blocks remain (fewer than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the /// buffer until it is full -- so a block split across two reads needs no special handling. /// /// Input whose total length is not a multiple of `BLOCK_LEN` is an error, because CBC is not /// defined on a partial block and there is no padding layer to appeal to. -fn stream_aligned(mut process: impl FnMut(&[u8])) { +fn stream_aligned(mut process: impl FnMut(&mut [u8])) { let mut buf = [0u8; CHUNK_LEN]; let mut filled = 0usize; @@ -262,12 +257,12 @@ fn stream_aligned(mut process: impl FnMut(&[u8])) { } filled += n; if filled == CHUNK_LEN { - process(&buf); + process(&mut buf); filled = 0; } } - if filled % BLOCK_LEN != 0 { + if !filled.is_multiple_of(BLOCK_LEN) { eprintln!( "Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({} trailing byte(s)). \ CBC is defined only on whole blocks (SP 800-38A Sec 5.2), and this build has no \ @@ -277,7 +272,7 @@ fn stream_aligned(mut process: impl FnMut(&[u8])) { exit(-1); } if filled != 0 { - process(&buf[..filled]); + process(&mut buf[..filled]); } } diff --git a/crypto/aes-lowmemory/src/cbc.rs b/crypto/aes-lowmemory/src/cbc.rs index 39b549f8..d68f6e2a 100644 --- a/crypto/aes-lowmemory/src/cbc.rs +++ b/crypto/aes-lowmemory/src/cbc.rs @@ -11,7 +11,8 @@ use bouncycastle_modes::Cbc; /// AES-128 in CBC mode. `Dir` is [`bouncycastle_modes::Encrypting`] or /// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. /// -/// The IV is generated by encryption and returned alongside the ciphertext; it is never supplied. +/// The IV is generated by encryption and returned; it is never supplied. Encryption and decryption +/// work in place. /// /// ``` /// use bouncycastle_aes_lowmemory::AES_CBC_128; @@ -23,16 +24,23 @@ use bouncycastle_modes::Cbc; /// .expect("a 16-byte symmetric cipher key"); /// // 48 bytes: three whole blocks. The length is checked at compile time. /// let message = [0u8; 48]; -/// let (iv, ciphertext) = AES_CBC_128::::encrypt(&key, &message).unwrap(); -/// assert_eq!(AES_CBC_128::::decrypt(&key, &iv, &ciphertext).unwrap(), message); +/// let mut data = message; +/// let iv = AES_CBC_128::::encrypt(&key, &mut data).unwrap(); +/// assert_ne!(data, message); +/// AES_CBC_128::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, message); /// /// // Streaming, a few blocks at a time: /// let (mut enc, iv) = AES_CBC_128::::do_encrypt_init(&key).unwrap(); -/// let first = enc.do_encrypt(&[0u8; 16]).unwrap(); -/// let rest = enc.do_encrypt(&[1u8; 32]).unwrap(); +/// let mut first = [0u8; 16]; +/// let mut rest = [1u8; 32]; +/// enc.do_encrypt(&mut first).unwrap(); +/// enc.do_encrypt(&mut rest).unwrap(); /// let mut dec = AES_CBC_128::::do_decrypt_init(&key, &iv).unwrap(); -/// assert_eq!(dec.do_decrypt(&first).unwrap(), [0u8; 16]); -/// assert_eq!(dec.do_decrypt(&rest).unwrap(), [1u8; 32]); +/// dec.do_decrypt(&mut first).unwrap(); +/// dec.do_decrypt(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 16]); +/// assert_eq!(rest, [1u8; 32]); /// ``` /// /// A length that is not a whole number of blocks is a **compile** error, not a runtime one: @@ -45,7 +53,7 @@ use bouncycastle_modes::Cbc; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); /// // 47 bytes is not a multiple of 16: the inline const assertion in `encrypt` fails to compile. -/// let _ = AES_CBC_128::::encrypt(&key, &[0u8; 47]); +/// let _ = AES_CBC_128::::encrypt(&key, &mut [0u8; 47]); /// ``` #[allow(non_camel_case_types)] pub type AES_CBC_128

= Cbc; @@ -59,8 +67,10 @@ pub type AES_CBC_128 = Cbc; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// /// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey).unwrap(); -/// let (iv, ct) = AES_CBC_192::::encrypt(&key, &[0u8; 32]).unwrap(); -/// assert_eq!(AES_CBC_192::::decrypt(&key, &iv, &ct).unwrap(), [0u8; 32]); +/// let mut data = [0u8; 32]; +/// let iv = AES_CBC_192::::encrypt(&key, &mut data).unwrap(); +/// AES_CBC_192::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); /// ``` #[allow(non_camel_case_types)] pub type AES_CBC_192 = Cbc; @@ -74,8 +84,10 @@ pub type AES_CBC_192 = Cbc; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// /// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); -/// let (iv, ct) = AES_CBC_256::::encrypt(&key, &[0u8; 32]).unwrap(); -/// assert_eq!(AES_CBC_256::::decrypt(&key, &iv, &ct).unwrap(), [0u8; 32]); +/// let mut data = [0u8; 32]; +/// let iv = AES_CBC_256::::encrypt(&key, &mut data).unwrap(); +/// AES_CBC_256::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); /// ``` #[allow(non_camel_case_types)] pub type AES_CBC_256 = Cbc; diff --git a/crypto/aes-lowmemory/src/lib.rs b/crypto/aes-lowmemory/src/lib.rs index a39067f4..c7ede6c5 100644 --- a/crypto/aes-lowmemory/src/lib.rs +++ b/crypto/aes-lowmemory/src/lib.rs @@ -73,10 +73,13 @@ //! // 48 bytes: three whole blocks. A length that is not a multiple of 16 would not compile. //! let plaintext = [0x5Au8; 48]; //! -//! // The IV is generated for you and returned; there is no API for supplying one. -//! let (iv, ciphertext) = AES_CBC_256::::encrypt(&key, &plaintext).unwrap(); -//! let recovered = AES_CBC_256::::decrypt(&key, &iv, &ciphertext).unwrap(); -//! assert_eq!(recovered, plaintext); +//! // Encryption is in place. The IV is generated for you and returned; there is no API for +//! // supplying one. +//! let mut data = plaintext; +//! let iv = AES_CBC_256::::encrypt(&key, &mut data).unwrap(); +//! assert_ne!(data, plaintext); +//! AES_CBC_256::::decrypt(&key, &iv, &mut data).unwrap(); +//! assert_eq!(data, plaintext); //! ``` //! //! There is no one-shot static on the permutation, because `Aes128::new(&key)?.encrypt_block(..)` diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 1044b037..07c0584c 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -1,6 +1,6 @@ //! Generic behaviour tests for the symmetric cipher traits. -use crate::DUMMY_SEED; +use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, @@ -140,80 +140,71 @@ impl TestFrameworkBlockCipher { let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); - // one block at a time, through the flat streaming methods (LEN = BLOCK_LEN) + // one block at a time, through the flat streaming methods (LEN = BLOCK_LEN), in place for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct = encryptor.do_encrypt(msg_chunk).unwrap(); - let pt = decryptor.do_decrypt(&ct).unwrap(); - assert_eq!(msg_chunk, &pt); + let mut buf = *msg_chunk; + encryptor.do_encrypt(&mut buf).unwrap(); + decryptor.do_decrypt(&mut buf).unwrap(); + assert_eq!(msg_chunk, &buf); } - // do it again using the _out versions - - let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); - let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); - - let mut ct = [0u8; BLOCK_LEN]; - let mut pt = [0u8; BLOCK_LEN]; - for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct_bytes_written = encryptor.do_encrypt_out(msg_chunk, &mut ct).unwrap(); - assert_eq!(ct_bytes_written, BLOCK_LEN); - - let pt_bytes_written = decryptor.do_decrypt_out(&ct, &mut pt).unwrap(); - assert_eq!(pt_bytes_written, BLOCK_LEN); - - assert_eq!(msg_chunk, &pt); - } - - // multi-block (N = 2) through the implementor hook `do_*_blocks_out`: blocks encrypted together + // multi-block (N = 2) through the implementor hook `do_*_blocks`: blocks encrypted together // must decrypt both together and one at a time, and blocks encrypted one at a time must // decrypt together. let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); - let mut ct = [[0u8; BLOCK_LEN]; 2]; - let mut pt = [[0u8; BLOCK_LEN]; 2]; for msg_pair in DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0.iter() { // encrypt together, decrypt together - let mut ct_pair = [[0u8; BLOCK_LEN]; 2]; - encryptor.do_encrypt_blocks_out(msg_pair, &mut ct_pair).unwrap(); - let mut pt_pair = [[0u8; BLOCK_LEN]; 2]; - decryptor.do_decrypt_blocks_out(&ct_pair, &mut pt_pair).unwrap(); - assert_eq!(msg_pair, &pt_pair); - - // encrypt together (_out), decrypt one at a time - let ct_bytes_written = encryptor.do_encrypt_blocks_out(msg_pair, &mut ct).unwrap(); - assert_eq!(ct_bytes_written, 2 * BLOCK_LEN); - for (msg_chunk, ct_chunk) in msg_pair.iter().zip(ct.iter()) { - let pt = decryptor.do_decrypt(ct_chunk).unwrap(); - assert_eq!(msg_chunk, &pt); + let mut buf = *msg_pair; + encryptor.do_encrypt_blocks(&mut buf).unwrap(); + decryptor.do_decrypt_blocks(&mut buf).unwrap(); + assert_eq!(msg_pair, &buf); + + // encrypt together, decrypt one at a time + let mut buf = *msg_pair; + encryptor.do_encrypt_blocks(&mut buf).unwrap(); + for (msg_chunk, block) in msg_pair.iter().zip(buf.iter_mut()) { + decryptor.do_decrypt(block).unwrap(); + assert_eq!(msg_chunk, block); } - // encrypt one at a time, decrypt together (_out) - for (msg_chunk, ct_chunk) in msg_pair.iter().zip(ct.iter_mut()) { - *ct_chunk = encryptor.do_encrypt(msg_chunk).unwrap(); + // encrypt one at a time, decrypt together + let mut buf = *msg_pair; + for block in buf.iter_mut() { + encryptor.do_encrypt(block).unwrap(); } - let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap(); - assert_eq!(pt_bytes_written, 2 * BLOCK_LEN); - assert_eq!(msg_pair, &pt); + decryptor.do_decrypt_blocks(&mut buf).unwrap(); + assert_eq!(msg_pair, &buf); } - // one-shot API: a block-aligned byte array. It must round-trip and agree with the streaming - // API for the same key and init data. Only LEN = BLOCK_LEN can be formed generically here - // (`2 * BLOCK_LEN` needs generic_const_exprs); multi-block one-shots are covered by the modes - // crate's tests with a concrete BLOCK_LEN. + // one-shot API: a block-aligned byte array, in place. It must round-trip and agree with the + // streaming API for the same key and init data. Only LEN = BLOCK_LEN can be formed + // generically here (`2 * BLOCK_LEN` needs generic_const_exprs); multi-block one-shots are + // covered by the modes crate's tests with a concrete BLOCK_LEN. let one_block: &[u8; BLOCK_LEN] = &DUMMY_SEED.as_chunks::().0[0]; - let (iv, ct) = E::encrypt(&key, one_block).unwrap(); - assert_eq!(D::decrypt(&key, &iv, &ct).unwrap(), *one_block); - // ...and it must agree with the block-shaped API under the same init data. + let mut buf = *one_block; + let iv = E::encrypt(&key, &mut buf).unwrap(); + let ct = buf; + D::decrypt(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, *one_block); + // ...and it must agree with the streaming API under the same init data. let mut streamed = D::do_decrypt_init(&key, &iv).unwrap(); - assert_eq!(streamed.do_decrypt(&ct).unwrap(), *one_block); - - let mut ct = [0u8; BLOCK_LEN]; - let mut pt = [0u8; BLOCK_LEN]; - let (iv, n) = E::encrypt_out(&key, one_block, &mut ct).unwrap(); - assert_eq!(n, BLOCK_LEN); - assert_eq!(D::decrypt_out(&key, &iv, &ct, &mut pt).unwrap(), BLOCK_LEN); - assert_eq!(pt, *one_block); + let mut buf = ct; + streamed.do_decrypt(&mut buf).unwrap(); + assert_eq!(buf, *one_block); + + // the RNG-taking one-shot must give the streaming API's answer for the same RNG stream + let pinned = [0xA5u8; INIT_DATA_LEN]; + let mut expected = *one_block; + let (mut streamed, iv_streamed) = + E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)).unwrap(); + streamed.do_encrypt(&mut expected).unwrap(); + let mut buf = *one_block; + let iv = E::encrypt_rng(&key, &mut FixedSeedRNG::::new(pinned), &mut buf) + .unwrap(); + assert_eq!(iv, iv_streamed); + assert_eq!(buf, expected); // test that the iv is random (ie not the same on two runs) let (_encryptor, iv1) = E::do_encrypt_init(&key).unwrap(); diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index b66e3f97..05d29188 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -154,6 +154,28 @@ pub trait BlockPermutation: /// In order for these APIs to be usable securely in all contexts, the init data will be generated /// securely by the block cipher implementation and returned along with the ciphertext, and there is no API for the /// user to provide the init data. If you require this functionality, see the documentation for the underlying implementation. +/// +/// # Everything is in place +/// +/// Every data method here transforms its buffer in place: the plaintext goes in, the ciphertext +/// comes out in the same bytes. A block cipher mode never changes the length of its data, so a +/// separate output buffer would only ever be a copy, and a copy of plaintext is one more thing to +/// scrub. Callers that need to keep the plaintext copy it first. +/// +/// # Lengths are checked at compile time +/// +/// Every buffer is a `[u8; LEN]`, and `LEN % BLOCK_LEN == 0` is checked by an inline `const` +/// assertion when the method is instantiated: a misaligned length is a compile error at the call +/// site, not a runtime `Err`, which is why there is no length variant of [`SymmetricCipherError`] +/// here. Data whose length is only known at run time is fed in block by block, or through the +/// padding layer. +/// +/// # Why the data methods still return `Result` +/// +/// Nothing about the buffer can go wrong, and a constructed value is always ready to use, so a +/// mode like CBC never returns `Err` from them. The `Result` is for modes with a per-initialization +/// data limit -- a counter-based mode must refuse to encrypt past the point where its counter would +/// repeat -- which a streaming API cannot check any earlier than the call that would cross it. pub trait BlockCipherEncryptor< const KEY_LEN: usize, const INIT_DATA_LEN: usize, @@ -170,108 +192,72 @@ pub trait BlockCipherEncryptor< key: &KeyMaterial, rng: &mut dyn RNG, ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// The implementor hook: encrypts `N` consecutive whole blocks into the provided buffer and - /// returns `N * BLOCK_LEN`. A sequence of calls is equivalent to one call over the concatenation. + /// The implementor hook: encrypts `N` consecutive whole blocks in place. A sequence of calls + /// is equivalent to one call over the concatenation. /// /// This is the only method an implementor writes besides the two `_init` constructors; the - /// block shape is what guarantees it never sees a partial block and that input and output - /// lengths agree at compile time. Callers should normally use the flat - /// [`BlockCipherEncryptor::do_encrypt`] / [`BlockCipherEncryptor::do_encrypt_out`] instead. - fn do_encrypt_blocks_out( + /// block shape is what guarantees it never sees a partial block. Callers should normally use + /// the flat [`BlockCipherEncryptor::do_encrypt`] instead. + fn do_encrypt_blocks( &mut self, - plaintext: &[[u8; BLOCK_LEN]; N], - ciphertext: &mut [[u8; BLOCK_LEN]; N], - ) -> Result; + blocks: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<(), SymmetricCipherError>; - /// Streaming: encrypts `LEN` bytes, a whole number of blocks. A sequence of calls is - /// equivalent to one call over the concatenation. + /// Streaming: encrypts `LEN` bytes, a whole number of blocks, in place. A sequence of calls + /// is equivalent to one call over the concatenation. /// - /// `LEN % BLOCK_LEN == 0` is checked **at compile time**: instantiating this with a misaligned - /// `LEN` is a compile error at the call site (an inline `const` assertion), not a runtime `Err`. - /// Non-block-aligned data belongs to the padding layer. - fn do_encrypt( - &mut self, - plaintext: &[u8; LEN], - ) -> Result<[u8; LEN], SymmetricCipherError> { - let mut ciphertext = [0u8; LEN]; - self.do_encrypt_out(plaintext, &mut ciphertext)?; - Ok(ciphertext) - } - /// As [`BlockCipherEncryptor::do_encrypt`], into the provided buffer. Returns `LEN`. + /// `LEN % BLOCK_LEN == 0` is checked **at compile time**; see the trait docs. /// - /// Blocks are fed to [`BlockCipherEncryptor::do_encrypt_blocks_out`] in pairs first, so a mode + /// Blocks are fed to [`BlockCipherEncryptor::do_encrypt_blocks`] in pairs first, so a mode /// that overrides its two-block path gets to use it, then the at-most-one block left over. This - /// is equivalent to a single `do_encrypt_blocks_out::<{LEN / BLOCK_LEN}>` call, which cannot be + /// is equivalent to a single `do_encrypt_blocks::<{LEN / BLOCK_LEN}>` call, which cannot be /// written without `generic_const_exprs`. - fn do_encrypt_out( + fn do_encrypt( &mut self, - plaintext: &[u8; LEN], - ciphertext: &mut [u8; LEN], - ) -> Result { - const { assert!(LEN % BLOCK_LEN == 0, "length must be a whole number of BLOCK_LEN-byte blocks") }; + data: &mut [u8; LEN], + ) -> Result<(), SymmetricCipherError> { + const { + assert!( + LEN.is_multiple_of(BLOCK_LEN), + "length must be a whole number of BLOCK_LEN-byte blocks" + ) + }; // The remainders are provably empty (asserted above) and ignored. - let (pt_blocks, _) = plaintext.as_chunks::(); - let (ct_blocks, _) = ciphertext.as_chunks_mut::(); - let (pt_pairs, pt_tail) = pt_blocks.as_chunks::<2>(); - let (ct_pairs, ct_tail) = ct_blocks.as_chunks_mut::<2>(); - for (p, c) in pt_pairs.iter().zip(ct_pairs.iter_mut()) { - self.do_encrypt_blocks_out(p, c)?; + let (blocks, _) = data.as_chunks_mut::(); + let (pairs, tail) = blocks.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.do_encrypt_blocks(pair)?; } - for (p, c) in pt_tail.iter().zip(ct_tail.iter_mut()) { - self.do_encrypt_blocks_out(core::array::from_ref(p), core::array::from_mut(c))?; + for block in tail.iter_mut() { + self.do_encrypt_blocks(core::array::from_mut(block))?; } - Ok(LEN) + Ok(()) } - /// One-shot on a flat byte array: encrypts `LEN` bytes under a fresh init. Returns the generated - /// init data and the ciphertext. - /// - /// `LEN` must be a whole number of blocks. This is checked **at compile time**: instantiating - /// this method with a `LEN` that is not a multiple of `BLOCK_LEN` is a compile error at the call - /// site, not a runtime `Err`. The check is an inline `const` assertion, so it fires when the - /// generic is instantiated (i.e. in the calling crate), which is why there is no length variant - /// of [`SymmetricCipherError`] here. Non-block-aligned data belongs to the padding layer. + /// One-shot: encrypts `LEN` bytes in place under a fresh init, and returns the generated init + /// data. `LEN % BLOCK_LEN == 0` is checked **at compile time**; see the trait docs. fn encrypt( key: &KeyMaterial, - plaintext: &[u8; LEN], - ) -> Result<([u8; INIT_DATA_LEN], [u8; LEN]), SymmetricCipherError> { - let mut ciphertext = [0u8; LEN]; - let (init_data, _) = Self::encrypt_out(key, plaintext, &mut ciphertext)?; - Ok((init_data, ciphertext)) + data: &mut [u8; LEN], + ) -> Result<[u8; INIT_DATA_LEN], SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + enc.do_encrypt(data)?; + Ok(init_data) } /// As [`BlockCipherEncryptor::encrypt`], but sources randomness from the provided RNG. fn encrypt_rng( key: &KeyMaterial, rng: &mut dyn RNG, - plaintext: &[u8; LEN], - ) -> Result<([u8; INIT_DATA_LEN], [u8; LEN]), SymmetricCipherError> { - let mut ciphertext = [0u8; LEN]; - let (init_data, _) = Self::encrypt_out_rng(key, rng, plaintext, &mut ciphertext)?; - Ok((init_data, ciphertext)) - } - /// As [`BlockCipherEncryptor::encrypt`], into the provided buffer. Returns the generated init - /// data and `LEN`. - fn encrypt_out( - key: &KeyMaterial, - plaintext: &[u8; LEN], - ciphertext: &mut [u8; LEN], - ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { - let (mut enc, init_data) = Self::do_encrypt_init(key)?; - Ok((init_data, enc.do_encrypt_out(plaintext, ciphertext)?)) - } - /// As [`BlockCipherEncryptor::encrypt_out`], but sources randomness from the provided RNG. - fn encrypt_out_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - plaintext: &[u8; LEN], - ciphertext: &mut [u8; LEN], - ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + data: &mut [u8; LEN], + ) -> Result<[u8; INIT_DATA_LEN], SymmetricCipherError> { let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; - Ok((init_data, enc.do_encrypt_out(plaintext, ciphertext)?)) + enc.do_encrypt(data)?; + Ok(init_data) } } -/// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`]. +/// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`], whose +/// notes on in-place operation, compile-time lengths and the `Result` all apply here too. pub trait BlockCipherDecryptor< const KEY_LEN: usize, const INIT_DATA_LEN: usize, @@ -283,68 +269,46 @@ pub trait BlockCipherDecryptor< key: &KeyMaterial, init_data: &[u8; INIT_DATA_LEN], ) -> Result; - /// The implementor hook: decrypts `N` consecutive whole blocks into the provided buffer and - /// returns `N * BLOCK_LEN`. See [`BlockCipherEncryptor::do_encrypt_blocks_out`]; callers should - /// normally use the flat [`BlockCipherDecryptor::do_decrypt`] / - /// [`BlockCipherDecryptor::do_decrypt_out`] instead. - fn do_decrypt_blocks_out( + /// The implementor hook: decrypts `N` consecutive whole blocks in place. See + /// [`BlockCipherEncryptor::do_encrypt_blocks`]; callers should normally use the flat + /// [`BlockCipherDecryptor::do_decrypt`] instead. + fn do_decrypt_blocks( &mut self, - ciphertext: &[[u8; BLOCK_LEN]; N], - plaintext: &mut [[u8; BLOCK_LEN]; N], - ) -> Result; + blocks: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<(), SymmetricCipherError>; - /// Streaming: decrypts `LEN` bytes, a whole number of blocks; `LEN % BLOCK_LEN == 0` is checked - /// at compile time exactly as for [`BlockCipherEncryptor::do_encrypt`]. + /// Streaming: decrypts `LEN` bytes, a whole number of blocks, in place. `LEN % BLOCK_LEN == 0` + /// is checked at compile time, and the blocks are fed to the hook pairs first, then the tail, + /// exactly as for [`BlockCipherEncryptor::do_encrypt`]. fn do_decrypt( &mut self, - ciphertext: &[u8; LEN], - ) -> Result<[u8; LEN], SymmetricCipherError> { - let mut plaintext = [0u8; LEN]; - self.do_decrypt_out(ciphertext, &mut plaintext)?; - Ok(plaintext) - } - /// As [`BlockCipherDecryptor::do_decrypt`], into the provided buffer. Returns `LEN`. Pairs first, - /// then the tail, as [`BlockCipherEncryptor::do_encrypt_out`]. - fn do_decrypt_out( - &mut self, - ciphertext: &[u8; LEN], - plaintext: &mut [u8; LEN], - ) -> Result { - const { assert!(LEN % BLOCK_LEN == 0, "length must be a whole number of BLOCK_LEN-byte blocks") }; - let (ct_blocks, _) = ciphertext.as_chunks::(); - let (pt_blocks, _) = plaintext.as_chunks_mut::(); - let (ct_pairs, ct_tail) = ct_blocks.as_chunks::<2>(); - let (pt_pairs, pt_tail) = pt_blocks.as_chunks_mut::<2>(); - for (c, p) in ct_pairs.iter().zip(pt_pairs.iter_mut()) { - self.do_decrypt_blocks_out(c, p)?; + data: &mut [u8; LEN], + ) -> Result<(), SymmetricCipherError> { + const { + assert!( + LEN.is_multiple_of(BLOCK_LEN), + "length must be a whole number of BLOCK_LEN-byte blocks" + ) + }; + let (blocks, _) = data.as_chunks_mut::(); + let (pairs, tail) = blocks.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.do_decrypt_blocks(pair)?; } - for (c, p) in ct_tail.iter().zip(pt_tail.iter_mut()) { - self.do_decrypt_blocks_out(core::array::from_ref(c), core::array::from_mut(p))?; + for block in tail.iter_mut() { + self.do_decrypt_blocks(core::array::from_mut(block))?; } - Ok(LEN) + Ok(()) } - /// One-shot on a flat byte array: decrypts `LEN` bytes from the given init data. - /// - /// `LEN` must be a whole number of blocks, checked at compile time exactly as for - /// [`BlockCipherEncryptor::encrypt`]. + /// One-shot: decrypts `LEN` bytes in place from the given init data. `LEN % BLOCK_LEN == 0` is + /// checked at compile time exactly as for [`BlockCipherEncryptor::encrypt`]. fn decrypt( key: &KeyMaterial, init_data: &[u8; INIT_DATA_LEN], - ciphertext: &[u8; LEN], - ) -> Result<[u8; LEN], SymmetricCipherError> { - let mut plaintext = [0u8; LEN]; - Self::decrypt_out(key, init_data, ciphertext, &mut plaintext)?; - Ok(plaintext) - } - /// As [`BlockCipherDecryptor::decrypt`], into the provided buffer. Returns `LEN`. - fn decrypt_out( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ciphertext: &[u8; LEN], - plaintext: &mut [u8; LEN], - ) -> Result { - Self::do_decrypt_init(key, init_data)?.do_decrypt_out(ciphertext, plaintext) + data: &mut [u8; LEN], + ) -> Result<(), SymmetricCipherError> { + Self::do_decrypt_init(key, init_data)?.do_decrypt(data) } } diff --git a/crypto/modes/benches/modes_benches.rs b/crypto/modes/benches/modes_benches.rs index 191c8e16..e7ea635e 100644 --- a/crypto/modes/benches/modes_benches.rs +++ b/crypto/modes/benches/modes_benches.rs @@ -10,6 +10,9 @@ //! //! `N = 1` is included to show the effect vanishing: with one block there is no pair to form, so //! decryption falls back to the single-block path and the ratio should be about 1. +//! +//! 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_lowmemory::{Aes128, Aes256}; use bouncycastle_core::errors::SymmetricCipherError; @@ -18,7 +21,7 @@ use bouncycastle_core::traits::{ Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, }; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; -use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; const BLOCK_LEN: usize = 16; @@ -81,99 +84,141 @@ fn bench_aes128(c: &mut Criterion) { // ---- encryption: serial, one block at a time is all it can do ---- group.bench_function("16KiB encrypt -- N=1", |b| { - b.iter(|| { - let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); - for block in blocks.iter() { - black_box(enc.do_encrypt(block).unwrap()); - } - }) + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); + for block in scratch.iter_mut() { + enc.do_encrypt(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.bench_function("16KiB encrypt -- N=8", |b| { - b.iter(|| { - let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); - for chunk in blocks.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(enc.do_encrypt(arr).unwrap()); - } - }) + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + enc.do_encrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); // ---- decryption: parallel, uses decrypt_blocks2 for every pair ---- let (mut enc, iv) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); - let ciphertext: Vec<[u8; BLOCK_LEN]> = blocks - .chunks_exact(8) - .flat_map(|chunk| { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - let mut out = [[0u8; BLOCK_LEN]; 8]; - enc.do_encrypt_blocks_out(arr, &mut out).unwrap(); - out - }) - .collect(); + let mut ciphertext = blocks.clone(); + for chunk in ciphertext.chunks_exact_mut(8) { + let arr: &mut [[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); + enc.do_encrypt_blocks(arr).unwrap(); + } // N=1 never forms a pair, so this is the single-block path: the ratio against encrypt should // be about 1. group.bench_function("16KiB decrypt -- N=1 (no pairing)", |b| { - b.iter(|| { - let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); - for block in ciphertext.iter() { - black_box(dec.do_decrypt(block).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for block in scratch.iter_mut() { + dec.do_decrypt(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); // N=2 and N=8 are all pairs, so every block goes through decrypt_blocks2. group.bench_function("16KiB decrypt -- N=2 (all pairs)", |b| { - b.iter(|| { - let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(2) { - let arr: &[u8; 2 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(2) { + let arr: &mut [u8; 2 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.bench_function("16KiB decrypt -- N=8 (all pairs)", |b| { - b.iter(|| { - let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); // N=9 is four pairs plus a one-block remainder, so it exercises the tail path too. group.bench_function("16KiB decrypt -- N=9 (pairs + remainder)", |b| { - b.iter(|| { - let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(9) { - let arr: &[u8; 9 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(9) { + let arr: &mut [u8; 9 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); // The controlled comparison: identical N, identical cipher, pair methods overridden vs not. // This pair of numbers -- and only this pair -- measures what `decrypt_blocks2` buys. group.bench_function("16KiB decrypt -- N=8, pair path (blocks2 overridden)", |b| { - b.iter(|| { - let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.bench_function("16KiB decrypt -- N=8, no pair path (trait default)", |b| { - b.iter(|| { - let mut dec = UnpairedAes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = UnpairedAes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.finish(); @@ -187,34 +232,42 @@ fn bench_aes256(c: &mut Criterion) { group.throughput(Throughput::Bytes(DATA_LEN as u64)); group.bench_function("16KiB encrypt -- N=8", |b| { - b.iter(|| { - let (mut enc, _) = Aes256Cbc::::do_encrypt_init(&k).unwrap(); - for chunk in blocks.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(enc.do_encrypt(arr).unwrap()); - } - }) + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes256Cbc::::do_encrypt_init(&k).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + enc.do_encrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); let (mut enc, iv) = Aes256Cbc::::do_encrypt_init(&k).unwrap(); - let ciphertext: Vec<[u8; BLOCK_LEN]> = blocks - .chunks_exact(8) - .flat_map(|chunk| { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - let mut out = [[0u8; BLOCK_LEN]; 8]; - enc.do_encrypt_blocks_out(arr, &mut out).unwrap(); - out - }) - .collect(); + let mut ciphertext = blocks.clone(); + for chunk in ciphertext.chunks_exact_mut(8) { + let arr: &mut [[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); + enc.do_encrypt_blocks(arr).unwrap(); + } group.bench_function("16KiB decrypt -- N=8 (all pairs)", |b| { - b.iter(|| { - let mut dec = Aes256Cbc::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes256Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.finish(); diff --git a/crypto/modes/src/cbc.rs b/crypto/modes/src/cbc.rs index 51b49ff1..1ec2d1da 100644 --- a/crypto/modes/src/cbc.rs +++ b/crypto/modes/src/cbc.rs @@ -68,26 +68,27 @@ impl Cbc, { - /// `Cj = CIPH_K(Pj XOR Cj-1)`, then `Cj` becomes the next chaining value. + /// `Cj = CIPH_K(Pj XOR Cj-1)` in place, then `Cj` becomes the next chaining value. #[inline] - fn encrypt_one(&mut self, plaintext: &[u8; BLOCK_LEN], ciphertext: &mut [u8; BLOCK_LEN]) { - for (out, (p, chain)) in ciphertext.iter_mut().zip(plaintext.iter().zip(self.chain.iter())) - { - *out = *p ^ *chain; + fn encrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { + for (b, chain) in block.iter_mut().zip(self.chain.iter()) { + *b ^= *chain; // Pj XOR Cj-1 } - self.perm.encrypt_block(ciphertext); - self.chain = *ciphertext; + self.perm.encrypt_block(block); // Cj = CIPH_K(..) + self.chain = *block; } - /// `Pj = CIPH^-1_K(Cj) XOR Cj-1`, then `Cj` becomes the next chaining value. + /// `Pj = CIPH^-1_K(Cj) XOR Cj-1` in place, then `Cj` becomes the next chaining value. + /// + /// `Cj` is overwritten by `Pj`, so it is copied first: it is the next chaining value. #[inline] - fn decrypt_one(&mut self, ciphertext: &[u8; BLOCK_LEN], plaintext: &mut [u8; BLOCK_LEN]) { - *plaintext = *ciphertext; - self.perm.decrypt_block(plaintext); - for (out, chain) in plaintext.iter_mut().zip(self.chain.iter()) { - *out ^= *chain; + fn decrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { + let cj = *block; + self.perm.decrypt_block(block); // CIPH^-1_K(Cj) + for (b, chain) in block.iter_mut().zip(self.chain.iter()) { + *b ^= *chain; // XOR Cj-1 } - self.chain = *ciphertext; + self.chain = cj; } /// Decrypts two consecutive blocks with one [`BlockPermutation::decrypt_blocks2`] call. @@ -101,26 +102,22 @@ where /// /// Neither inverse cipher depends on the other's *output* -- only on ciphertext, which is /// already in hand -- so computing them together changes nothing. The two XOR operands do - /// differ, and the second one is `Cj`, so both are read out of `ciphertext` before the - /// chaining value is advanced to `Cj+1`. + /// differ, and the second one is `Cj`, so both ciphertext blocks are copied out before the + /// permutation overwrites them, and the chaining value is then advanced to `Cj+1`. #[inline] - fn decrypt_pair( - &mut self, - ciphertext: &[[u8; BLOCK_LEN]; 2], - plaintext: &mut [[u8; BLOCK_LEN]; 2], - ) { - *plaintext = *ciphertext; - self.perm.decrypt_blocks2(plaintext); + fn decrypt_pair(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + let [cj, cj1] = *blocks; + self.perm.decrypt_blocks2(blocks); - let (first, rest) = plaintext.split_at_mut(1); - for (out, chain) in first[0].iter_mut().zip(self.chain.iter()) { - *out ^= *chain; // XOR Cj-1 + let [pj, pj1] = blocks; + for (b, chain) in pj.iter_mut().zip(self.chain.iter()) { + *b ^= *chain; // XOR Cj-1 } - for (out, prev) in rest[0].iter_mut().zip(ciphertext[0].iter()) { - *out ^= *prev; // XOR Cj + for (b, prev) in pj1.iter_mut().zip(cj.iter()) { + *b ^= *prev; // XOR Cj } - self.chain = ciphertext[1]; + self.chain = cj1; } } @@ -159,19 +156,18 @@ where Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) } - /// The implementor hook (the flat `do_encrypt[_out]` are provided over it). + /// The implementor hook (the flat `do_encrypt` is provided over it). /// /// Strictly serial: `Cj` is the input to block `j + 1`, so there is no pair path here. See the - /// module docs. - fn do_encrypt_blocks_out( + /// module docs. Never fails: CBC has no per-IV data limit. + fn do_encrypt_blocks( &mut self, - plaintext: &[[u8; BLOCK_LEN]; N], - ciphertext: &mut [[u8; BLOCK_LEN]; N], - ) -> Result { - for (p, c) in plaintext.iter().zip(ciphertext.iter_mut()) { - self.encrypt_one(p, c); + blocks: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<(), SymmetricCipherError> { + for block in blocks.iter_mut() { + self.encrypt_one(block); } - Ok(N * BLOCK_LEN) + Ok(()) } } @@ -190,27 +186,24 @@ where Ok(Self { perm, chain: *init_data, _dir: PhantomData }) } - /// The implementor hook (the flat `do_decrypt[_out]` are provided over it). + /// The implementor hook (the flat `do_decrypt` is provided over it). /// /// Walks the input in pairs so the permutation's two-block path is used, with an at-most-one - /// block remainder for odd `N`. `as_chunks` splits into exactly that shape with no runtime + /// block remainder for odd `N`. `as_chunks_mut` splits into exactly that shape with no runtime /// length check and no indexing arithmetic; `N` is a compile-time constant, so for even `N` the - /// tail loop is empty and for `N = 1` the pair loop is. - fn do_decrypt_blocks_out( + /// tail loop is empty and for `N = 1` the pair loop is. Never fails: CBC has no per-IV data + /// limit. + fn do_decrypt_blocks( &mut self, - ciphertext: &[[u8; BLOCK_LEN]; N], - plaintext: &mut [[u8; BLOCK_LEN]; N], - ) -> Result { - let (ct_pairs, ct_tail) = ciphertext.as_chunks::<2>(); - let (pt_pairs, pt_tail) = plaintext.as_chunks_mut::<2>(); - - for (ct_pair, pt_pair) in ct_pairs.iter().zip(pt_pairs.iter_mut()) { - self.decrypt_pair(ct_pair, pt_pair); + blocks: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<(), SymmetricCipherError> { + let (pairs, tail) = blocks.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.decrypt_pair(pair); } - for (c, p) in ct_tail.iter().zip(pt_tail.iter_mut()) { - self.decrypt_one(c, p); + for block in tail.iter_mut() { + self.decrypt_one(block); } - - Ok(N * BLOCK_LEN) + Ok(()) } } diff --git a/crypto/modes/src/lib.rs b/crypto/modes/src/lib.rs index 5bc2f45f..0680ee6d 100644 --- a/crypto/modes/src/lib.rs +++ b/crypto/modes/src/lib.rs @@ -37,11 +37,13 @@ //! // 48 bytes: three whole blocks. A length that is not a multiple of 16 would not compile. //! let plaintext: [u8; 48] = *b"The quick brown fox jumps over the lazy dog. OK!"; //! -//! // One shot: encrypts under a freshly generated IV, which is returned alongside the ciphertext. -//! let (iv, ciphertext) = Aes128Cbc::::encrypt(&key, &plaintext).expect("encryption"); +//! // One shot, in place: encrypts under a freshly generated IV, which is returned. +//! let mut data = plaintext; +//! let iv = Aes128Cbc::::encrypt(&key, &mut data).expect("encryption"); +//! assert_ne!(data, plaintext); //! -//! let recovered = Aes128Cbc::::decrypt(&key, &iv, &ciphertext).expect("decryption"); -//! assert_eq!(recovered, plaintext); +//! Aes128Cbc::::decrypt(&key, &iv, &mut data).expect("decryption"); +//! assert_eq!(data, plaintext); //! ``` //! //! Streaming, for data that arrives in pieces. A sequence of calls is equivalent to one call over @@ -60,12 +62,16 @@ //! //! let (mut encryptor, iv) = //! Aes256Cbc::::do_encrypt_init(&key).expect("encrypt init"); -//! let first = encryptor.do_encrypt(&[0xAAu8; 16]).expect("block 1"); -//! let rest = encryptor.do_encrypt(&[0xBBu8; 32]).expect("blocks 2-3"); +//! let mut first = [0xAAu8; 16]; +//! let mut rest = [0xBBu8; 32]; +//! encryptor.do_encrypt(&mut first).expect("block 1"); +//! encryptor.do_encrypt(&mut rest).expect("blocks 2-3"); //! //! let mut decryptor = Aes256Cbc::::do_decrypt_init(&key, &iv).expect("decrypt init"); -//! assert_eq!(decryptor.do_decrypt(&first).unwrap(), [0xAAu8; 16]); -//! assert_eq!(decryptor.do_decrypt(&rest).unwrap(), [0xBBu8; 32]); +//! decryptor.do_decrypt(&mut first).unwrap(); +//! decryptor.do_decrypt(&mut rest).unwrap(); +//! assert_eq!(first, [0xAAu8; 16]); +//! assert_eq!(rest, [0xBBu8; 32]); //! ``` //! //! Using the wrong direction does not compile: @@ -109,8 +115,8 @@ //! | AES-192 CBC | 208 B | 16 B | 224 B | //! | AES-256 CBC | 240 B | 16 B | 256 B | //! -//! `do_*_blocks_out::` adds nothing; the by-value `do_*_blocks::` adds `N * BLOCK_LEN` of -//! stack for the returned array. [`Encrypting`] and [`Decrypting`] are zero-sized and held in a +//! The data methods work in place and add nothing beyond the copy of the two ciphertext blocks +//! `decrypt_pair` keeps for the chaining value. [`Encrypting`] and [`Decrypting`] are zero-sized and held in a //! `PhantomData`, so encoding the direction in the type is free. The table is pinned by //! `sizes_match_the_documented_memory_table` in `tests/cbc_tests.rs`. //! diff --git a/crypto/modes/tests/acvp_tests.rs b/crypto/modes/tests/acvp_tests.rs index 0d18d1ea..c74571dc 100644 --- a/crypto/modes/tests/acvp_tests.rs +++ b/crypto/modes/tests/acvp_tests.rs @@ -127,18 +127,22 @@ where match grouping { Grouping::Single => { for block in input { - out.push(enc.do_encrypt(block).unwrap()); + let mut c = *block; + enc.do_encrypt(&mut c).unwrap(); + out.push(c); } } Grouping::Pairs => { let (pairs, tail) = input.as_chunks::<2>(); for pair in pairs { - let mut c = [[0u8; BLOCK_LEN]; 2]; - enc.do_encrypt_blocks_out(pair, &mut c).unwrap(); + let mut c = *pair; + enc.do_encrypt_blocks(&mut c).unwrap(); out.extend_from_slice(&c); } for block in tail { - out.push(enc.do_encrypt(block).unwrap()); + let mut c = *block; + enc.do_encrypt(&mut c).unwrap(); + out.push(c); } } } @@ -149,18 +153,22 @@ where match grouping { Grouping::Single => { for block in input { - out.push(dec.do_decrypt(block).unwrap()); + let mut p = *block; + dec.do_decrypt(&mut p).unwrap(); + out.push(p); } } Grouping::Pairs => { let (pairs, tail) = input.as_chunks::<2>(); for pair in pairs { - let mut p = [[0u8; BLOCK_LEN]; 2]; - dec.do_decrypt_blocks_out(pair, &mut p).unwrap(); + let mut p = *pair; + dec.do_decrypt_blocks(&mut p).unwrap(); out.extend_from_slice(&p); } for block in tail { - out.push(dec.do_decrypt(block).unwrap()); + let mut p = *block; + dec.do_decrypt(&mut p).unwrap(); + out.push(p); } } } diff --git a/crypto/modes/tests/cbc_tests.rs b/crypto/modes/tests/cbc_tests.rs index 05912c81..96e6f53f 100644 --- a/crypto/modes/tests/cbc_tests.rs +++ b/crypto/modes/tests/cbc_tests.rs @@ -17,24 +17,44 @@ use common::{SwappedPairToy, TOY_LEN, Toy, toy_key}; type ToyCbc = Cbc; type SwappedCbc = Cbc; -/// The implementor hook `do_encrypt_blocks_out`, by value, for tests whose data is block-shaped. +/// The implementor hook `do_encrypt_blocks`, by value, for tests whose data is block-shaped. fn enc_blocks( enc: &mut impl BlockCipherEncryptor, plaintext: &[[u8; TOY_LEN]; N], ) -> [[u8; TOY_LEN]; N] { - let mut ct = [[0u8; TOY_LEN]; N]; - enc.do_encrypt_blocks_out(plaintext, &mut ct).unwrap(); - ct + let mut blocks = *plaintext; + enc.do_encrypt_blocks(&mut blocks).unwrap(); + blocks } -/// The implementor hook `do_decrypt_blocks_out`, by value. +/// The implementor hook `do_decrypt_blocks`, by value. fn dec_blocks( dec: &mut impl BlockCipherDecryptor, ciphertext: &[[u8; TOY_LEN]; N], ) -> [[u8; TOY_LEN]; N] { - let mut pt = [[0u8; TOY_LEN]; N]; - dec.do_decrypt_blocks_out(ciphertext, &mut pt).unwrap(); - pt + let mut blocks = *ciphertext; + dec.do_decrypt_blocks(&mut blocks).unwrap(); + blocks +} + +/// The flat streaming method `do_encrypt`, by value. +fn enc_flat( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *plaintext; + enc.do_encrypt(&mut data).unwrap(); + data +} + +/// The flat streaming method `do_decrypt`, by value. +fn dec_flat( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *ciphertext; + dec.do_decrypt(&mut data).unwrap(); + data } // ---- the toy itself, and the mode, against the shared frameworks ------------------------- @@ -80,7 +100,7 @@ fn call_grouping_does_not_change_the_result() { // The same eight blocks, grouped every way that exercises a different code path. let (mut enc, _) = ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); let mut got = [[0u8; TOY_LEN]; 8]; - let a = enc.do_encrypt(&plaintext[0]).unwrap(); // one block, flat + let a = enc_flat(&mut enc, &plaintext[0]); // one block, flat let b = enc_blocks(&mut enc, &[plaintext[1], plaintext[2]]); // N = 2 let c = enc_blocks(&mut enc, &[plaintext[3], plaintext[4], plaintext[5]]); // N = 3 let d = enc_blocks(&mut enc, &[plaintext[6], plaintext[7]]); // N = 2 @@ -105,7 +125,7 @@ fn call_grouping_does_not_change_the_result() { while at < 8 { match grouping { 1 => { - out[at] = dec.do_decrypt(&ct[at]).unwrap(); + out[at] = dec_flat(&mut dec, &ct[at]); } 2 => { let p = dec_blocks(&mut dec, &[ct[at], ct[at + 1]]); @@ -129,7 +149,7 @@ fn call_grouping_does_not_change_the_result() { assert_eq!(five, [plaintext[3], plaintext[4], plaintext[5], plaintext[6], plaintext[7]]); } -/// The pair path in `do_decrypt_blocks_out` must actually be taken. +/// The pair path in `do_decrypt_blocks` must actually be taken. /// /// [`SwappedPairToy`] returns its two pair results in the wrong order while its single-block /// methods are correct. So a CBC decryptor that uses `decrypt_blocks2` gives the wrong answer for @@ -160,13 +180,12 @@ fn the_pair_path_is_really_used() { // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. let mut dec = SwappedCbc::::do_decrypt_init(&key, &iv).unwrap(); - let p0 = dec.do_decrypt(&ct[0]).unwrap(); - let p1 = dec.do_decrypt(&ct[1]).unwrap(); + let p0 = dec_flat(&mut dec, &ct[0]); + let p1 = dec_flat(&mut dec, &ct[1]); assert_eq!([p0, p1], plaintext, "the single-block path must not pair"); } -/// The flat streaming method must agree with the block-shaped implementor hook and report the -/// byte count. +/// The flat streaming method must agree with the block-shaped implementor hook. #[test] fn flat_streaming_agrees_with_the_block_hook() { let key = toy_key(); @@ -174,7 +193,7 @@ fn flat_streaming_agrees_with_the_block_hook() { let flat_plaintext: [u8; 3 * TOY_LEN] = plaintext.as_flattened().try_into().unwrap(); let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); - let by_value = enc.do_encrypt(&flat_plaintext).unwrap(); + let flat_ct = enc_flat(&mut enc, &flat_plaintext); let (mut enc, iv2) = ToyCbc::::do_encrypt_init_rng( &key, @@ -182,16 +201,13 @@ fn flat_streaming_agrees_with_the_block_hook() { ) .unwrap(); assert_eq!(iv2, iv, "the pinned RNG should reproduce the IV"); - let mut out = [[0u8; TOY_LEN]; 3]; - let n = enc.do_encrypt_blocks_out(&plaintext, &mut out).unwrap(); - assert_eq!(n, 3 * TOY_LEN); - assert_eq!(*out.as_flattened(), by_value, "flat streaming must equal the block hook"); + let block_ct = enc_blocks(&mut enc, &plaintext); + assert_eq!(*block_ct.as_flattened(), flat_ct, "flat streaming must equal the block hook"); let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); - let mut back = [[0u8; TOY_LEN]; 3]; - let n = dec.do_decrypt_blocks_out(&out, &mut back).unwrap(); - assert_eq!(n, 3 * TOY_LEN); - assert_eq!(back, plaintext); + assert_eq!(dec_blocks(&mut dec, &block_ct), plaintext); + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_flat(&mut dec, &flat_ct), flat_plaintext); } // ---- SP 800-38A Appendix D error propagation --------------------------------------------- @@ -274,8 +290,10 @@ fn identical_plaintext_gives_different_ciphertext() { let key = toy_key(); let plaintext = [0x77u8; 2 * TOY_LEN]; - let (_, first) = ToyCbc::::encrypt(&key, &plaintext).unwrap(); - let (_, second) = ToyCbc::::encrypt(&key, &plaintext).unwrap(); + let mut first = plaintext; + ToyCbc::::encrypt(&key, &mut first).unwrap(); + let mut second = plaintext; + ToyCbc::::encrypt(&key, &mut second).unwrap(); assert_ne!(first, second); // ...and, within one message, two identical plaintext blocks must not give identical @@ -320,9 +338,9 @@ fn sizes_match_the_documented_memory_table() { assert_eq!(size_of::>(), size_of::() + 16); } -/// The one-shots (`encrypt` / `decrypt` on a `[u8; LEN]`) must produce exactly what the streaming -/// API produces over the same blocks, for an odd block count (pairs plus a one-block tail) and an -/// even one (pairs only), in both directions and through the `_out` variants. +/// The one-shots (`encrypt` / `decrypt` on a `[u8; LEN]`, in place) must produce exactly what the +/// streaming API produces over the same blocks, for an odd block count (pairs plus a one-block +/// tail) and an even one (pairs only), in both directions. #[test] fn one_shots_agree_with_the_streaming_api() { let key = toy_key(); @@ -338,37 +356,32 @@ fn one_shots_agree_with_the_streaming_api() { ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); (iv, enc_blocks(&mut enc, &blocks3)) }; - let (iv_b, ct_flat) = - ToyCbc::::encrypt_rng(&key, &mut pinned_rng(), &flat3).unwrap(); + let mut buf = flat3; + let iv_b = ToyCbc::::encrypt_rng(&key, &mut pinned_rng(), &mut buf).unwrap(); assert_eq!(iv_a, iv_b); - assert_eq!(ct_flat, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); - assert_eq!(ToyCbc::::decrypt(&key, &iv, &ct_flat).unwrap(), flat3); - let mut ct_out = [0u8; 3 * TOY_LEN]; - let (_, n) = - ToyCbc::::encrypt_out_rng(&key, &mut pinned_rng(), &flat3, &mut ct_out) - .unwrap(); - assert_eq!((n, ct_out), (3 * TOY_LEN, ct_flat)); - let mut pt_out = [0u8; 3 * TOY_LEN]; - assert_eq!( - ToyCbc::::decrypt_out(&key, &iv, &ct_out, &mut pt_out).unwrap(), - 3 * TOY_LEN - ); - assert_eq!(pt_out, flat3); + assert_eq!(buf, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); + ToyCbc::::decrypt(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, flat3); // 4 blocks = 64 bytes: pairs only, no tail. let flat4: [u8; 4 * TOY_LEN] = core::array::from_fn(|i| (i * 13 + 1) as u8); let blocks4: [[u8; TOY_LEN]; 4] = core::array::from_fn(|b| flat4[b * TOY_LEN..][..TOY_LEN].try_into().unwrap()); - let (_, ct_blocks) = { - let (mut enc, iv) = + let ct_blocks = { + let (mut enc, _) = ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); - (iv, enc_blocks(&mut enc, &blocks4)) + enc_blocks(&mut enc, &blocks4) }; - let (_, ct_flat) = ToyCbc::::encrypt_rng(&key, &mut pinned_rng(), &flat4).unwrap(); - assert_eq!(ct_flat, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); - assert_eq!(ToyCbc::::decrypt(&key, &iv, &ct_flat).unwrap(), flat4); + let mut buf = flat4; + ToyCbc::::encrypt_rng(&key, &mut pinned_rng(), &mut buf).unwrap(); + assert_eq!(buf, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); + ToyCbc::::decrypt(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, flat4); // The OS-RNG variant round-trips too. - let (iv_fresh, ct) = ToyCbc::::encrypt(&key, &flat3).unwrap(); - assert_eq!(ToyCbc::::decrypt(&key, &iv_fresh, &ct).unwrap(), flat3); + let mut buf = flat3; + let iv_fresh = ToyCbc::::encrypt(&key, &mut buf).unwrap(); + assert_ne!(buf, flat3); + ToyCbc::::decrypt(&key, &iv_fresh, &mut buf).unwrap(); + assert_eq!(buf, flat3); } diff --git a/crypto/modes/tests/sp800_38a_tests.rs b/crypto/modes/tests/sp800_38a_tests.rs index 6bf1f55a..cec9404b 100644 --- a/crypto/modes/tests/sp800_38a_tests.rs +++ b/crypto/modes/tests/sp800_38a_tests.rs @@ -88,7 +88,7 @@ fn key_material(hex_str: &str) -> KeyMaterial { /// Runs one Appendix F.2 encrypt subsection. /// /// Checks the whole message in one call, then again one block at a time, then again through the -/// `_out` variant -- the vector should not care how the calls are grouped. +/// implementor hook -- the vector should not care how the calls are grouped. fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) where P: BlockPermutation, @@ -105,11 +105,9 @@ where ) .unwrap(); assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); - assert_eq!( - enc.do_encrypt(&flat(&PLAINTEXTS)).unwrap(), - flat(expected), - "{section}: four blocks in one call" - ); + let mut data = flat(&PLAINTEXTS); + enc.do_encrypt(&mut data).unwrap(); + assert_eq!(data, flat(expected), "{section}: four blocks in one call"); // One block at a time. let (mut enc, _) = Cbc::::do_encrypt_init_rng( @@ -118,26 +116,26 @@ where ) .unwrap(); for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { - let got = enc.do_encrypt(p).unwrap(); + let mut got = *p; + enc.do_encrypt(&mut got).unwrap(); assert_eq!(&got, c, "{section}: block #{}", i + 1); } - // Through the implementor hook, `do_*_blocks_out`. + // Through the implementor hook, `do_*_blocks`. let (mut enc, _) = Cbc::::do_encrypt_init_rng( &key, &mut FixedSeedRNG::::new(iv), ) .unwrap(); - let mut out = [[0u8; BLOCK_LEN]; 4]; - let n = enc.do_encrypt_blocks_out(&pt, &mut out).unwrap(); - assert_eq!(n, 4 * BLOCK_LEN); - assert_eq!(out, ct, "{section}: _out variant"); + let mut blocks = pt; + enc.do_encrypt_blocks(&mut blocks).unwrap(); + assert_eq!(blocks, ct, "{section}: implementor hook"); } /// Runs one Appendix F.2 decrypt subsection. /// /// Checks one call, one block at a time, and the odd grouping `3 + 1` -- which is the grouping that -/// leaves a one-block remainder after the pair loop in `do_decrypt_blocks_out`. +/// leaves a one-block remainder after the pair loop in `do_decrypt_blocks`. fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) where P: BlockPermutation, @@ -151,33 +149,32 @@ where // All four blocks in one call (two pairs, no remainder). let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); - assert_eq!( - dec.do_decrypt(&flat(ciphertext)).unwrap(), - flat(&PLAINTEXTS), - "{section}: four blocks in one call" - ); + let mut data = flat(ciphertext); + dec.do_decrypt(&mut data).unwrap(); + assert_eq!(data, flat(&PLAINTEXTS), "{section}: four blocks in one call"); // One block at a time (never takes the pair path). let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { - let got = dec.do_decrypt(c).unwrap(); + let mut got = *c; + dec.do_decrypt(&mut got).unwrap(); assert_eq!(&got, p, "{section}: block #{}", i + 1); } // 3 + 1: one pair plus a remainder, then a lone block. let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); - let first_three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); - let three = dec.do_decrypt(&first_three).unwrap(); - let one = dec.do_decrypt(&ct[3]).unwrap(); + let mut three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); + dec.do_decrypt(&mut three).unwrap(); + let mut one = ct[3]; + dec.do_decrypt(&mut one).unwrap(); assert_eq!(&three[..], pt[..3].as_flattened(), "{section}: blocks 1-3"); assert_eq!(one, pt[3], "{section}: block 4"); - // Through the implementor hook, `do_*_blocks_out`. + // Through the implementor hook, `do_*_blocks`. let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); - let mut out = [[0u8; BLOCK_LEN]; 4]; - let n = dec.do_decrypt_blocks_out(&ct, &mut out).unwrap(); - assert_eq!(n, 4 * BLOCK_LEN); - assert_eq!(out, pt, "{section}: _out variant"); + let mut blocks = ct; + dec.do_decrypt_blocks(&mut blocks).unwrap(); + assert_eq!(blocks, pt, "{section}: implementor hook"); } #[test] @@ -211,39 +208,27 @@ fn f_2_6_cbc_aes256_decrypt() { } /// The one-shot API must agree with the vectors too, on the decrypt side where the IV is an input. -/// The one-shots take flat arrays, so the four blocks are presented as 64 contiguous bytes. +/// The one-shots take flat arrays and work in place, so the four ciphertext blocks are presented +/// as 64 contiguous bytes and become the four plaintext blocks. #[test] fn the_one_shot_api_matches_the_vectors() { let iv = block(IV); let pt = flat(&PLAINTEXTS); - assert_eq!( - Cbc::::decrypt( - &key_material::<16>(KEY_128), - &iv, - &flat(&CIPHERTEXTS_128) - ) - .unwrap(), - pt - ); - assert_eq!( - Cbc::::decrypt( - &key_material::<24>(KEY_192), - &iv, - &flat(&CIPHERTEXTS_192) - ) - .unwrap(), - pt - ); - assert_eq!( - Cbc::::decrypt( - &key_material::<32>(KEY_256), - &iv, - &flat(&CIPHERTEXTS_256) - ) - .unwrap(), - pt - ); + let mut data = flat(&CIPHERTEXTS_128); + Cbc::::decrypt(&key_material::<16>(KEY_128), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); + + let mut data = flat(&CIPHERTEXTS_192); + Cbc::::decrypt(&key_material::<24>(KEY_192), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); + + let mut data = flat(&CIPHERTEXTS_256); + Cbc::::decrypt(&key_material::<32>(KEY_256), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); } /// The IV really is what distinguishes CBC from ECB here: the same key and plaintext under the @@ -270,7 +255,8 @@ fn cbc_differs_from_ecb_by_the_iv() { &mut FixedSeedRNG::<16>::new(iv), ) .unwrap(); - let cbc = enc.do_encrypt(&block(PLAINTEXTS[0])).unwrap(); + let mut cbc = block(PLAINTEXTS[0]); + enc.do_encrypt(&mut cbc).unwrap(); assert_eq!(cbc, block(CIPHERTEXTS_128[0]), "F.2.1 block #1"); assert_ne!(cbc, ecb); } diff --git a/crypto/padding/src/padded.rs b/crypto/padding/src/padded.rs index 7ae45fc1..749492e5 100644 --- a/crypto/padding/src/padded.rs +++ b/crypto/padding/src/padded.rs @@ -5,7 +5,7 @@ use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, Padding, RNG}; use bouncycastle_utils::secret::Secret; -use core::array::{from_mut, from_ref}; +use core::array::from_mut; use core::marker::PhantomData; /// Blocks per inner-cipher call on the bulk path; the remainder is processed one at a time. @@ -91,23 +91,27 @@ where return Ok(0); } // Block completed. out_len >= BLOCK_LEN here, so `split_first_mut` always succeeds. + // The cipher works in place, so the block is encrypted inside the `Secret` and only + // ciphertext is copied out of it. if let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() { - self.inner.do_encrypt_blocks_out(from_ref(&*self.buf), from_mut(first))?; + self.inner.do_encrypt_blocks(from_mut(&mut *self.buf))?; + *first = *self.buf; out_blocks = rest; } self.buf_len = 0; } - // 2. Bulk path: whole blocks straight from the input, in groups of GROUP then singly. + // 2. Bulk path: whole blocks are copied into the output and encrypted there, in place, in + // groups of GROUP then singly. let (in_blocks, remainder) = plaintext.as_chunks::(); debug_assert_eq!(in_blocks.len(), out_blocks.len()); - let (in_groups, in_tail) = in_blocks.as_chunks::(); + out_blocks.copy_from_slice(in_blocks); let (out_groups, out_tail) = out_blocks.as_chunks_mut::(); - for (i, o) in in_groups.iter().zip(out_groups.iter_mut()) { - self.inner.do_encrypt_blocks_out(i, o)?; + for group in out_groups.iter_mut() { + self.inner.do_encrypt_blocks(group)?; } - for (i, o) in in_tail.iter().zip(out_tail.iter_mut()) { - self.inner.do_encrypt_blocks_out(from_ref(i), from_mut(o))?; + for block in out_tail.iter_mut() { + self.inner.do_encrypt_blocks(from_mut(block))?; } // 3. Buffer the trailing partial block (remainder.len() < BLOCK_LEN). @@ -117,11 +121,14 @@ where } /// Pads and encrypts the buffered partial block, returning the final ciphertext block. + /// + /// The block is padded and encrypted inside the `Secret`, so what is copied out is ciphertext. pub fn do_final(self) -> Result<[u8; BLOCK_LEN], SymmetricCipherError> { let Self { mut inner, mut buf, buf_len, .. } = self; // buf_len < BLOCK_LEN is an invariant of this type, so pad() cannot fail here. P::pad(&mut buf, buf_len)?; - inner.do_encrypt(&*buf) + inner.do_encrypt(&mut buf)?; + Ok(*buf) } /// As [`do_final`](Self::do_final), writing the final block into `ciphertext`. Returns `BLOCK_LEN`. @@ -129,9 +136,8 @@ where self, ciphertext: &mut [u8; BLOCK_LEN], ) -> Result { - let Self { mut inner, mut buf, buf_len, .. } = self; - P::pad(&mut buf, buf_len)?; - inner.do_encrypt_blocks_out(from_ref(&*buf), from_mut(ciphertext)) + *ciphertext = self.do_final()?; + Ok(BLOCK_LEN) } /// Ciphertext length for a `plaintext_len`-byte plaintext: `(plaintext_len / BLOCK_LEN + 1) * BLOCK_LEN`. @@ -261,7 +267,8 @@ where if let Some(prev) = self.held.replace(self.buf) && let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() { - self.inner.do_decrypt_blocks_out(from_ref(&prev), from_mut(first))?; + *first = prev; + self.inner.do_decrypt_blocks(from_mut(first))?; out_blocks = rest; } } @@ -273,18 +280,20 @@ where if let Some(prev) = self.held.replace(*last) && let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() { - self.inner.do_decrypt_blocks_out(from_ref(&prev), from_mut(first))?; + *first = prev; + self.inner.do_decrypt_blocks(from_mut(first))?; out_blocks = rest; } - // Then every block of this call except the new held one. + // Then every block of this call except the new held one: copied into the output and + // decrypted there, in place. debug_assert_eq!(release.len(), out_blocks.len()); - let (in_groups, in_tail) = release.as_chunks::(); + out_blocks.copy_from_slice(release); let (out_groups, out_tail) = out_blocks.as_chunks_mut::(); - for (i, o) in in_groups.iter().zip(out_groups.iter_mut()) { - self.inner.do_decrypt_blocks_out(i, o)?; + for group in out_groups.iter_mut() { + self.inner.do_decrypt_blocks(group)?; } - for (i, o) in in_tail.iter().zip(out_tail.iter_mut()) { - self.inner.do_decrypt_blocks_out(from_ref(i), from_mut(o))?; + for block in out_tail.iter_mut() { + self.inner.do_decrypt_blocks(from_mut(block))?; } } @@ -302,12 +311,12 @@ where if buf_len != 0 { return Err(SymmetricCipherError::DecryptionFailed); } - let Some(last) = held else { + let Some(mut block) = held else { return Err(SymmetricCipherError::DecryptionFailed); }; - let pt = inner.do_decrypt(&last)?; - let data_len = P::unpad(&pt)?; - Ok((pt, data_len)) + inner.do_decrypt(&mut block)?; + let data_len = P::unpad(&block)?; + Ok((block, data_len)) } /// As [`do_final`](Self::do_final), writing the block into `plaintext`. Returns its data length. @@ -315,15 +324,9 @@ where self, plaintext: &mut [u8; BLOCK_LEN], ) -> Result { - let Self { mut inner, buf_len, held, .. } = self; - if buf_len != 0 { - return Err(SymmetricCipherError::DecryptionFailed); - } - let Some(last) = held else { - return Err(SymmetricCipherError::DecryptionFailed); - }; - inner.do_decrypt_blocks_out(from_ref(&last), from_mut(plaintext))?; - Ok(P::unpad(plaintext)?) + let (block, data_len) = self.do_final()?; + *plaintext = block; + Ok(data_len) } /// Upper bound on the plaintext recovered from `ciphertext_len` bytes: `ciphertext_len - 1`. diff --git a/crypto/padding/tests/padded_tests.rs b/crypto/padding/tests/padded_tests.rs index 21a6c181..1e51999b 100644 --- a/crypto/padding/tests/padded_tests.rs +++ b/crypto/padding/tests/padded_tests.rs @@ -55,18 +55,17 @@ impl BlockCipherEncryptor for ToyCbc { rng.next_bytes_out(&mut iv)?; Ok((Self { key, chain: iv }, iv)) } - fn do_encrypt_blocks_out( + fn do_encrypt_blocks( &mut self, - plaintext: &[[u8; B]; N], - ciphertext: &mut [[u8; B]; N], - ) -> Result { - for (p, c) in plaintext.iter().zip(ciphertext.iter_mut()) { - for i in 0..B { - c[i] = p[i] ^ self.chain[i] ^ self.key[i]; + blocks: &mut [[u8; B]; N], + ) -> Result<(), SymmetricCipherError> { + for block in blocks.iter_mut() { + for (b, (c, k)) in block.iter_mut().zip(self.chain.iter().zip(self.key.iter())) { + *b ^= c ^ k; } - self.chain = *c; + self.chain = *block; } - Ok(N * B) + Ok(()) } } @@ -74,18 +73,18 @@ impl BlockCipherDecryptor for ToyCbc { fn do_decrypt_init(key: &KeyMaterial, iv: &[u8; B]) -> Result { Ok(Self { key: Self::check_key(key)?, chain: *iv }) } - fn do_decrypt_blocks_out( + fn do_decrypt_blocks( &mut self, - ciphertext: &[[u8; B]; N], - plaintext: &mut [[u8; B]; N], - ) -> Result { - for (c, p) in ciphertext.iter().zip(plaintext.iter_mut()) { - for i in 0..B { - p[i] = c[i] ^ self.chain[i] ^ self.key[i]; + blocks: &mut [[u8; B]; N], + ) -> Result<(), SymmetricCipherError> { + for block in blocks.iter_mut() { + let ct = *block; + for (b, (c, k)) in block.iter_mut().zip(self.chain.iter().zip(self.key.iter())) { + *b ^= c ^ k; } - self.chain = *c; + self.chain = ct; } - Ok(N * B) + Ok(()) } } From 0d06cf9584045746523aa6aea139e252c13d70cb Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 3 Sep 2026 17:21:30 +1000 Subject: [PATCH 6/7] core: order the items in traits.rs alphabetically A pure reordering (verified: the sorted non-blank lines are identical before and after). Each trait keeps its doc comment and any todo notes attached to it; SecurityStrength keeps its two impl blocks. Sorted case-insensitively by item name. Requested in review of PR #107. Co-Authored-By: Claude Fable 5.1 --- crypto/core/src/traits.rs | 1086 ++++++++++++++++++------------------- 1 file changed, 543 insertions(+), 543 deletions(-) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 05d29188..37690f7a 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -12,130 +12,142 @@ use crate::key_material::KeyMaterial; use crate::key_material::KeyType; // end of imports needed for docs -/// Metadata about a cryptographic algorithm. -pub trait Algorithm { - /// String name for the algorithm, used consistently across the library. - const ALG_NAME: &'static str; - /// Maximum security strength supported by the algorithm. - /// In other words, this algorithm can produce outputs up to this security strength, - /// but may produce outputs with lower security strength, for example, if asked to truncate. - const MAX_SECURITY_STRENGTH: SecurityStrength; -} - -/// Some algorithms have an assigned OID. -pub trait AlgorithmOID { - /// The OID in component form -- each u32 is one OID component. - const OID: &'static [u32]; - /// The OID in its DER-encoded form. - const OID_DER: &'static [u8]; -} - -// todo -- split all the SymmetricCipher traits into Encryptor and Decryptor -/// The basic one-shot encrypt and decrypt that all types of symmetric ciphers must implement. -/// These are meant to be simple, easy to use, secure, and fool-proof APIs, but they may result in -/// ciphertexts that are incompatible with other implementations as ciphers in more complex modes, such -/// as AEADs or stream ciphers may need to stick extra data either at the beginning or end of the ciphertext. -/// See the documentation of the underlying implementation for more details. -pub trait SymmetricCipher: Algorithm { +/// The basic functions of an Authenticated Encryption with Addititional Data cipher. +pub trait AEADCipher: + SymmetricCipher + Sized +{ #[cfg(feature = "std")] /// A one-shot API to encrypt some plaintext with the given key. + /// A distinguishing feature of AEAD ciphers is the ability to provide additional authenticated data (AAD) + /// that is not encrypted but is protected by the authentication tag; ie it can be sent along with the ciphertext + /// and any tampering with it will result in the decryption operation failing the tag check. /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. - /// Returns a tuple containing the initialization data and the ciphertext. - /// This is not available if building for no_std. - fn encrypt( + /// Returns a tuple containing a generated nonce, the ciphertext and the tag. + fn aead_encrypt( key: &KeyMaterial, + aad: &[u8], plaintext: &[u8], - ) -> Result<([u8; INIT_DATA_LEN], Vec), SymmetricCipherError>; + ) -> Result<([u8; NONCE_LEN], Vec, [u8; TAG_LEN]), SymmetricCipherError>; /// A one-shot API to encrypt some plaintext with the given key. - /// This function takes a reference to the output buffer for the ciphertext, and is therefore available in no_std. - /// See the documentation for the underlying implementation for details on providing a ciphertext buffer of sufficient size; - /// typically the ciphertext is the same length as the plaintext, but some ciphers may have an expansion factor or require - /// extra space for a nonce or tag. - /// Returns a tuple containing the initialization data and the number of bytes written to the ciphertext buffer. - fn encrypt_out( + /// A distinguishing feature of AEAD ciphers is the ability to provide additional authenticated data (AAD) + /// that is not encrypted but is protected by the authentication tag; ie it can be sent along with the ciphertext + /// and any tampering with it will result in the decryption operation failing the tag check. + /// Returns a tuple containing the randomly-generated nonce, number of bytes written to the ciphertext buffer, and the tag. + /// If you need a deterministic mode where you feed in the nonce, use the streaming API of [`BlockCipherEncryptor`] + /// or [`StreamCipher`] as appropriate and feed the nonce into the IV field. + fn aead_encrypt_out( key: &KeyMaterial, + aad: &[u8], plaintext: &[u8], ciphertext: &mut [u8], - ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError>; + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError>; + /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a [`StreamCipher`], and so will already + /// have a streaming API. + /// This allows you to finish either style of streaming API flow with AEAD specific do_final() + /// that computes and returns the authentication tag. + fn do_aead_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError>; #[cfg(feature = "std")] /// A one-shot API to decrypt some ciphertext with the given key. /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. - /// This is not available if building for no_std. - fn decrypt( + fn aead_decrypt( key: &KeyMaterial, - init_data: [u8; INIT_DATA_LEN], + nonce: &[u8; NONCE_LEN], + aad: &[u8], ciphertext: &[u8], + tag: &[u8; TAG_LEN], ) -> Result, SymmetricCipherError>; /// A one-shot API to decrypt some ciphertext with the given key. /// This function takes a reference to the output buffer for the plaintext, and is therefore available in no_std. /// See the documentation for the underlying implementation for details on providing a plaintext buffer of sufficient size; /// typically the ciphertext is the same length as the plaintext, but some ciphers may have an expansion factor or require /// extra space for a nonce or tag. - /// Returns a tuple containing the initialization data and the number of bytes written to the plaintext buffer. - fn decrypt_out( + /// Returns the number of bytes written to the plaintext buffer. + fn aead_decrypt_out( key: &KeyMaterial, - init_data: [u8; INIT_DATA_LEN], + nonce: &[u8; NONCE_LEN], + aad: &[u8], ciphertext: &[u8], + tag: &[u8; TAG_LEN], plaintext: &mut [u8], ) -> Result; + /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a [`StreamCipher`], and so will already + /// have a streaming API. + /// This allows you to finish either style of streaming API flow with AEAD specific do_final() + /// that computes and returns the authentication tag. + fn do_aead_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError>; } -/// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1. -/// -/// This is the raw primitive a mode of operation is built on, not something to encrypt data with. -/// It transforms exactly one block, so applying it directly to data is ECB, which is not -/// confidential. [`BlockCipherEncryptor`] and [`BlockCipherDecryptor`] are the *mode* traits -- -/// they carry initialization data and chaining state; this one carries only a key schedule. -/// -/// Implementors are expected to hold that key schedule in a zeroize-on-drop wrapper -/// (`bouncycastle_utils::secret::Secret`), so it is scrubbed when the value is dropped. -/// -/// # Why the block methods are infallible -/// -/// Every length here is fixed by a type, and a constructed value is always ready to use, so there -/// is nothing a caller can get wrong once [`BlockPermutation::new`] has returned. Only `new` can -/// fail, and only because of the key. -pub trait BlockPermutation: - Algorithm + Sized -{ - /// Expands the key. - /// - /// # Errors - /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose - /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a - /// [`SymmetricCipherError::KeyMaterialError`]. - fn new(key: &KeyMaterial) -> Result; +/// Metadata about a cryptographic algorithm. +pub trait Algorithm { + /// String name for the algorithm, used consistently across the library. + const ALG_NAME: &'static str; + /// Maximum security strength supported by the algorithm. + /// In other words, this algorithm can produce outputs up to this security strength, + /// but may produce outputs with lower security strength, for example, if asked to truncate. + const MAX_SECURITY_STRENGTH: SecurityStrength; +} - /// The forward cipher function, in place. - fn encrypt_block(&self, block: &mut [u8; BLOCK_LEN]); +/// Some algorithms have an assigned OID. +pub trait AlgorithmOID { + /// The OID in component form -- each u32 is one OID component. + const OID: &'static [u32]; + /// The OID in its DER-encoded form. + const OID_DER: &'static [u8]; +} - /// The inverse cipher function, in place. - fn decrypt_block(&self, block: &mut [u8; BLOCK_LEN]); +/// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`], whose +/// notes on in-place operation, compile-time lengths and the `Result` all apply here too. +pub trait BlockCipherDecryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: Algorithm + Sized +{ + /// Begins a streaming decryption flow from the init data returned by [`BlockCipherEncryptor::do_encrypt_init`]. + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result; + /// The implementor hook: decrypts `N` consecutive whole blocks in place. See + /// [`BlockCipherEncryptor::do_encrypt_blocks`]; callers should normally use the flat + /// [`BlockCipherDecryptor::do_decrypt`] instead. + fn do_decrypt_blocks( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<(), SymmetricCipherError>; - /// The forward cipher function on two *independent* blocks, in place. - /// - /// Provided as two [`BlockPermutation::encrypt_block`] calls. Bit-sliced implementations - /// override it, because a pair of blocks is their natural unit of work and costs barely more - /// than one; see `bouncycastle-aes-lowmemory`. - /// - /// Overrides must be indistinguishable from the default, including the order of the two - /// results. `TestFrameworkBlockPermutation` pins that. - /// - /// Modes whose structure is parallel -- CBC decryption, CFB decryption, CTR -- should prefer - /// this. CBC and CFB *encryption* cannot use it: each input block depends on the previous - /// output. - fn encrypt_blocks2(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { - let [a, b] = blocks; - self.encrypt_block(a); - self.encrypt_block(b); + /// Streaming: decrypts `LEN` bytes, a whole number of blocks, in place. `LEN % BLOCK_LEN == 0` + /// is checked at compile time, and the blocks are fed to the hook pairs first, then the tail, + /// exactly as for [`BlockCipherEncryptor::do_encrypt`]. + fn do_decrypt( + &mut self, + data: &mut [u8; LEN], + ) -> Result<(), SymmetricCipherError> { + const { + assert!( + LEN.is_multiple_of(BLOCK_LEN), + "length must be a whole number of BLOCK_LEN-byte blocks" + ) + }; + let (blocks, _) = data.as_chunks_mut::(); + let (pairs, tail) = blocks.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.do_decrypt_blocks(pair)?; + } + for block in tail.iter_mut() { + self.do_decrypt_blocks(core::array::from_mut(block))?; + } + Ok(()) } - /// The inverse cipher function on two *independent* blocks, in place. - /// See [`BlockPermutation::encrypt_blocks2`]. - fn decrypt_blocks2(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { - let [a, b] = blocks; - self.decrypt_block(a); - self.decrypt_block(b); + /// One-shot: decrypts `LEN` bytes in place from the given init data. `LEN % BLOCK_LEN == 0` is + /// checked at compile time exactly as for [`BlockCipherEncryptor::encrypt`]. + fn decrypt( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + data: &mut [u8; LEN], + ) -> Result<(), SymmetricCipherError> { + Self::do_decrypt_init(key, init_data)?.do_decrypt(data) } } @@ -256,194 +268,63 @@ pub trait BlockCipherEncryptor< } } -/// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`], whose -/// notes on in-place operation, compile-time lengths and the `Result` all apply here too. -pub trait BlockCipherDecryptor< - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, ->: Algorithm + Sized +/// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1. +/// +/// This is the raw primitive a mode of operation is built on, not something to encrypt data with. +/// It transforms exactly one block, so applying it directly to data is ECB, which is not +/// confidential. [`BlockCipherEncryptor`] and [`BlockCipherDecryptor`] are the *mode* traits -- +/// they carry initialization data and chaining state; this one carries only a key schedule. +/// +/// Implementors are expected to hold that key schedule in a zeroize-on-drop wrapper +/// (`bouncycastle_utils::secret::Secret`), so it is scrubbed when the value is dropped. +/// +/// # Why the block methods are infallible +/// +/// Every length here is fixed by a type, and a constructed value is always ready to use, so there +/// is nothing a caller can get wrong once [`BlockPermutation::new`] has returned. Only `new` can +/// fail, and only because of the key. +pub trait BlockPermutation: + Algorithm + Sized { - /// Begins a streaming decryption flow from the init data returned by [`BlockCipherEncryptor::do_encrypt_init`]. - fn do_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ) -> Result; - /// The implementor hook: decrypts `N` consecutive whole blocks in place. See - /// [`BlockCipherEncryptor::do_encrypt_blocks`]; callers should normally use the flat - /// [`BlockCipherDecryptor::do_decrypt`] instead. - fn do_decrypt_blocks( - &mut self, - blocks: &mut [[u8; BLOCK_LEN]; N], - ) -> Result<(), SymmetricCipherError>; + /// Expands the key. + /// + /// # Errors + /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose + /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a + /// [`SymmetricCipherError::KeyMaterialError`]. + fn new(key: &KeyMaterial) -> Result; - /// Streaming: decrypts `LEN` bytes, a whole number of blocks, in place. `LEN % BLOCK_LEN == 0` - /// is checked at compile time, and the blocks are fed to the hook pairs first, then the tail, - /// exactly as for [`BlockCipherEncryptor::do_encrypt`]. - fn do_decrypt( - &mut self, - data: &mut [u8; LEN], - ) -> Result<(), SymmetricCipherError> { - const { - assert!( - LEN.is_multiple_of(BLOCK_LEN), - "length must be a whole number of BLOCK_LEN-byte blocks" - ) - }; - let (blocks, _) = data.as_chunks_mut::(); - let (pairs, tail) = blocks.as_chunks_mut::<2>(); - for pair in pairs.iter_mut() { - self.do_decrypt_blocks(pair)?; - } - for block in tail.iter_mut() { - self.do_decrypt_blocks(core::array::from_mut(block))?; - } - Ok(()) - } - - /// One-shot: decrypts `LEN` bytes in place from the given init data. `LEN % BLOCK_LEN == 0` is - /// checked at compile time exactly as for [`BlockCipherEncryptor::encrypt`]. - fn decrypt( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - data: &mut [u8; LEN], - ) -> Result<(), SymmetricCipherError> { - Self::do_decrypt_init(key, init_data)?.do_decrypt(data) - } -} + /// The forward cipher function, in place. + fn encrypt_block(&self, block: &mut [u8; BLOCK_LEN]); -/// A block padding scheme, used to extend arbitrary-length data to a whole number of blocks so that it -/// can be processed by a [`BlockCipherEncryptor`]. Implementations are pure functions of the block -/// contents: no key, no state. -/// -/// Only the final, partial block of a message is ever padded; the padding layer sitting between the -/// caller and the block cipher is responsible for routing whole blocks straight through. -pub trait Padding { - /// Pads `block` in place: bytes `0..data_len` are data and are left untouched, bytes - /// `data_len..BLOCK_LEN` are overwritten with padding. `data_len` must be less than `BLOCK_LEN` - /// (a full block of data requires a whole additional block of padding, which the caller supplies - /// as `data_len = 0`). - fn pad(block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError>; - /// Returns the number of data bytes in a padded `block`, or [`PaddingError::InvalidPadding`]. - /// Implementations must run in constant time with respect to the block contents, so that a - /// decryptor built on them does not leak a padding oracle. - fn unpad(block: &[u8; BLOCK_LEN]) -> Result; -} + /// The inverse cipher function, in place. + fn decrypt_block(&self, block: &mut [u8; BLOCK_LEN]); -/// The basic functions of an Authenticated Encryption with Addititional Data cipher. -pub trait AEADCipher: - SymmetricCipher + Sized -{ - #[cfg(feature = "std")] - /// A one-shot API to encrypt some plaintext with the given key. - /// A distinguishing feature of AEAD ciphers is the ability to provide additional authenticated data (AAD) - /// that is not encrypted but is protected by the authentication tag; ie it can be sent along with the ciphertext - /// and any tampering with it will result in the decryption operation failing the tag check. - /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. - /// Returns a tuple containing a generated nonce, the ciphertext and the tag. - fn aead_encrypt( - key: &KeyMaterial, - aad: &[u8], - plaintext: &[u8], - ) -> Result<([u8; NONCE_LEN], Vec, [u8; TAG_LEN]), SymmetricCipherError>; - /// A one-shot API to encrypt some plaintext with the given key. - /// A distinguishing feature of AEAD ciphers is the ability to provide additional authenticated data (AAD) - /// that is not encrypted but is protected by the authentication tag; ie it can be sent along with the ciphertext - /// and any tampering with it will result in the decryption operation failing the tag check. - /// Returns a tuple containing the randomly-generated nonce, number of bytes written to the ciphertext buffer, and the tag. - /// If you need a deterministic mode where you feed in the nonce, use the streaming API of [`BlockCipherEncryptor`] - /// or [`StreamCipher`] as appropriate and feed the nonce into the IV field. - fn aead_encrypt_out( - key: &KeyMaterial, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError>; - /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a [`StreamCipher`], and so will already - /// have a streaming API. - /// This allows you to finish either style of streaming API flow with AEAD specific do_final() - /// that computes and returns the authentication tag. - fn do_aead_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError>; - #[cfg(feature = "std")] - /// A one-shot API to decrypt some ciphertext with the given key. - /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. - fn aead_decrypt( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - ) -> Result, SymmetricCipherError>; - /// A one-shot API to decrypt some ciphertext with the given key. - /// This function takes a reference to the output buffer for the plaintext, and is therefore available in no_std. - /// See the documentation for the underlying implementation for details on providing a plaintext buffer of sufficient size; - /// typically the ciphertext is the same length as the plaintext, but some ciphers may have an expansion factor or require - /// extra space for a nonce or tag. - /// Returns the number of bytes written to the plaintext buffer. - fn aead_decrypt_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - plaintext: &mut [u8], - ) -> Result; - /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a [`StreamCipher`], and so will already - /// have a streaming API. - /// This allows you to finish either style of streaming API flow with AEAD specific do_final() - /// that computes and returns the authentication tag. - fn do_aead_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError>; -} + /// The forward cipher function on two *independent* blocks, in place. + /// + /// Provided as two [`BlockPermutation::encrypt_block`] calls. Bit-sliced implementations + /// override it, because a pair of blocks is their natural unit of work and costs barely more + /// than one; see `bouncycastle-aes-lowmemory`. + /// + /// Overrides must be indistinguishable from the default, including the order of the two + /// results. `TestFrameworkBlockPermutation` pins that. + /// + /// Modes whose structure is parallel -- CBC decryption, CFB decryption, CTR -- should prefer + /// this. CBC and CFB *encryption* cannot use it: each input block depends on the previous + /// output. + fn encrypt_blocks2(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + let [a, b] = blocks; + self.encrypt_block(a); + self.encrypt_block(b); + } -/// The basic functions of a stream cipher, which differ from those of a block cipher only in that -/// a stream cipher is assumed to have no underlying block size tied to the implementation, and so the caller gets to specify -/// the block size for the streaming APIs. -pub trait StreamCipher: - SymmetricCipher + Sized -{ - /// Constructor that begins a flow of the streaming API for encrypting one block at a time. - /// Allows for the implementation to return init data such as an IV which is generated prior to encrypting the first block. - fn do_stream_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// Encrypts a single block of plaintext. - fn do_stream_encrypt_block( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts a single block of plaintext and writes the ciphertext to the provided buffer. - fn do_stream_encrypt_block_out( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Encrypts the final block of plaintext. - fn do_stream_encrypt_final( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts the final block of plaintext and writes the ciphertext to the provided buffer. - fn do_stream_encrypt_final_out( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Constructor that begins a flow of the streaming API for decryption one block at a time. - fn do_stream_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ) -> Result; - /// Decrypts a single block of ciphertext. - fn do_stream_decrypt_block( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Decrypts a single block of ciphertext and writes the plaintext to the provided buffer. - fn do_stream_decrypt_block_out( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], - ) -> Result; + /// The inverse cipher function on two *independent* blocks, in place. + /// See [`BlockPermutation::encrypt_blocks2`]. + fn decrypt_blocks2(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + let [a, b] = blocks; + self.decrypt_block(a); + self.decrypt_block(b); + } } /// A hash function is a cryptographic primitive that takes an input of any length and produces a fixed-size output. @@ -623,8 +504,8 @@ pub trait KDF: Default { /// A Key Encapsulation Mechanism (KEM) is defined as a set of three operations: /// key generation, encapsulation, and decapsulation. /// -/// This trait represents the encapsulation operation performed by the holder of the public key. -/// Decapsulation operations are performed by the corresponding [`KEMDecapsulator`] trait, and key +/// This trait represents the decapsulation operation performed by the holder of the private key. +/// Encapsulation operations are performed by the corresponding [`KEMEncapsulator`] trait, and key /// generation is provided as an inherent associated function directly on the algorithm struct. /// There are several reasons for this split: first is architectural; some complex algorithms may /// benefit from having the encapsulation and decapsulation implementations split into separate modules. @@ -632,35 +513,27 @@ pub trait KDF: Default { /// can no longer be created, but existing ciphertexts can still be decapsulated. Splitting the traits /// makes this policy easier to enforce. /// -/// The arrays used to encode public keys, ciphertexts, and shared secrets are statically-sized +/// The arrays used to encode private keys, ciphertexts, and shared secrets are statically-sized /// because this allows us to safely remove runtime checks for array lengths, which overall reduces /// the fallibility of the library. This design choice could make this trait complicated to apply /// to a KEM algorithm that does not have fixed sizes for the encodings of these objects. -pub trait KEMEncapsulator< - PK: KEMPublicKey, - const PK_LEN: usize, +pub trait KEMDecapsulator< + SK: KEMPrivateKey, + const SK_LEN: usize, const CT_LEN: usize, const SS_LEN: usize, >: Sized { - /// Performs an encapsulation against the given public key. - /// Sources randomness from the library's default OS-backed RNG. - /// Returns the ciphertext and derived shared secret. - fn encaps(pk: &PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; - /// Performs an encapsulation against the given public key. - /// Sources randomness from the provided RNG. - /// Returns the ciphertext and derived shared secret. - fn encaps_rng( - pk: &PK, - rng: &mut dyn RNG, - ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; + /// Performs a decapsulation of the given ciphertext. + /// Returns the derived shared secret. + fn decaps(sk: &SK, ct: &[u8]) -> Result, KEMError>; } /// A Key Encapsulation Mechanism (KEM) is defined as a set of three operations: /// key generation, encapsulation, and decapsulation. /// -/// This trait represents the decapsulation operation performed by the holder of the private key. -/// Encapsulation operations are performed by the corresponding [`KEMEncapsulator`] trait, and key +/// This trait represents the encapsulation operation performed by the holder of the public key. +/// Decapsulation operations are performed by the corresponding [`KEMDecapsulator`] trait, and key /// generation is provided as an inherent associated function directly on the algorithm struct. /// There are several reasons for this split: first is architectural; some complex algorithms may /// benefit from having the encapsulation and decapsulation implementations split into separate modules. @@ -668,20 +541,39 @@ pub trait KEMEncapsulator< /// can no longer be created, but existing ciphertexts can still be decapsulated. Splitting the traits /// makes this policy easier to enforce. /// -/// The arrays used to encode private keys, ciphertexts, and shared secrets are statically-sized +/// The arrays used to encode public keys, ciphertexts, and shared secrets are statically-sized /// because this allows us to safely remove runtime checks for array lengths, which overall reduces /// the fallibility of the library. This design choice could make this trait complicated to apply /// to a KEM algorithm that does not have fixed sizes for the encodings of these objects. -pub trait KEMDecapsulator< - SK: KEMPrivateKey, - const SK_LEN: usize, +pub trait KEMEncapsulator< + PK: KEMPublicKey, + const PK_LEN: usize, const CT_LEN: usize, const SS_LEN: usize, >: Sized { - /// Performs a decapsulation of the given ciphertext. - /// Returns the derived shared secret. - fn decaps(sk: &SK, ct: &[u8]) -> Result, KEMError>; + /// Performs an encapsulation against the given public key. + /// Sources randomness from the library's default OS-backed RNG. + /// Returns the ciphertext and derived shared secret. + fn encaps(pk: &PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; + /// Performs an encapsulation against the given public key. + /// Sources randomness from the provided RNG. + /// Returns the ciphertext and derived shared secret. + fn encaps_rng( + pk: &PK, + rng: &mut dyn RNG, + ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; +} + +/// A private key for a KEM algorithm, often denoted "sk" (for "secret key"). +pub trait KEMPrivateKey: PartialEq + Eq + Clone + Sized { + /// Write it out to bytes in its standard encoding. + fn encode(&self) -> [u8; SK_LEN]; + /// Write it out to bytes in its standard encoding. + /// The entire output buffer is zeroized before the encoding is written. + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize; + /// Read it in from bytes in its standard encoding. + fn from_bytes(bytes: &[u8]) -> Result; } // todo: could the public and private key types impl Into> and From> @@ -700,17 +592,6 @@ pub trait KEMPublicKey: fn from_bytes(bytes: &[u8]) -> Result; } -/// A private key for a KEM algorithm, often denoted "sk" (for "secret key"). -pub trait KEMPrivateKey: PartialEq + Eq + Clone + Sized { - /// Write it out to bytes in its standard encoding. - fn encode(&self) -> [u8; SK_LEN]; - /// Write it out to bytes in its standard encoding. - /// The entire output buffer is zeroized before the encoding is written. - fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize; - /// Read it in from bytes in its standard encoding. - fn from_bytes(bytes: &[u8]) -> Result; -} - /// A Message Authentication Code algorithm is a keyed hash function that behaves somewhat like a symmetric signature function. /// A MAC algorithm takes in a key and some data, and produces a MAC (message authentication code) that /// can be used to verify the integrity of data. @@ -826,11 +707,139 @@ pub trait MAC: Sized { fn max_security_strength(&self) -> SecurityStrength; } -/// A general indicator used across the library for marking the security level of a cryptographic primitive, -/// and for tracking the security level of the algorithms that interacted with a given piece of data. -/// For example, if a KDF at the 128-bit security strength is used to produce a 512-bit key, that key -/// will also be tagged as having a 128-bit security strength. -/// +/// A block padding scheme, used to extend arbitrary-length data to a whole number of blocks so that it +/// can be processed by a [`BlockCipherEncryptor`]. Implementations are pure functions of the block +/// contents: no key, no state. +/// +/// Only the final, partial block of a message is ever padded; the padding layer sitting between the +/// caller and the block cipher is responsible for routing whole blocks straight through. +pub trait Padding { + /// Pads `block` in place: bytes `0..data_len` are data and are left untouched, bytes + /// `data_len..BLOCK_LEN` are overwritten with padding. `data_len` must be less than `BLOCK_LEN` + /// (a full block of data requires a whole additional block of padding, which the caller supplies + /// as `data_len = 0`). + fn pad(block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError>; + /// Returns the number of data bytes in a padded `block`, or [`PaddingError::InvalidPadding`]. + /// Implementations must run in constant time with respect to the block contents, so that a + /// decryptor built on them does not leak a padding oracle. + fn unpad(block: &[u8; BLOCK_LEN]) -> Result; +} + +/// Pre-Hashed Signature Verifier is an extension to [`SignatureVerifier`] that adds functionality specific to signature +/// primatives that can operate on a pre-hashed message instead of the full message. +pub trait PHSignatureVerifier< + PK: SignaturePublicKey, + const PK_LEN: usize, + const SIG_LEN: usize, + const PH_LEN: usize, +>: SignatureVerifier +{ + /// On success, returns Ok(()) + /// On failure, returns Err([`SignatureError::SignatureVerificationFailed`]); may also return other types of [`SignatureError`] as appropriate (such as for invalid-length inputs). + fn verify_ph( + pk: &PK, + ph: &[u8; PH_LEN], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError>; +} + +/// Pre-Hashed Signer is an extension to [`Signer`] that adds functionality specific to signature +/// primatives that can operate on a pre-hashed message instead of the full message. +pub trait PHSigner< + PK: SignaturePublicKey, + SK: SignaturePrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, + const PH_LEN: usize, +>: Signer +{ + /// Produce a signature for the provided pre-hashed message and context. + /// + /// `ctx` accepts a zero-length byte array. + /// + /// A note about the `ctx` context parameter: + /// This is a newer addition to cryptographic signature primitives. It allows for binding the + /// signature to some external property of the application so that a signature will fail to validate + /// if removed from its intended context. + /// This is particularly useful at preventing content confusion attacks between data formats that + /// have very similar data structures, for example S/MIME emails, signed PDFs, and signed executables + /// that all use the Cryptographic Message Syntax (CMS) data format, or multiple data objects that + /// all use the JWS data format. + /// To be properly effective, the ctx value must not be under the control of the attacker, which generally + /// means that it needs to be a value that is never transmitted over the wire, but rather is something + /// known to the application by context. + /// For example, "email" vs "pdf" would be a good choice since the application should know what it is + /// attempting to sign or verify. + /// The `ctx` param can also be used to bind the signed content to a transaction ID or a username, + /// but care should be taken to ensure that an attacker attempting a + /// content confusion attack not also cause the signed / verifier to use an incorrect transaction ID or username. + /// + /// Not all signature primitives will support a context value, so you may need to consult the + /// documentation for the underlying primitive for how it handles a ctx in that case, for example, it + /// might throw an error, ignore the provided ctx value, or append the ctx to the msg in a non-standard way. + fn sign_ph( + sk: &SK, + ph: &[u8; PH_LEN], + ctx: Option<&[u8]>, + ) -> Result<[u8; SIG_LEN], SignatureError>; + /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. + /// The entire output buffer is zeroized before the signature is written. + fn sign_ph_out( + sk: &SK, + ph: &[u8; PH_LEN], + ctx: Option<&[u8]>, + output: &mut [u8; SIG_LEN], + ) -> Result; +} + +/// An interface for random number generation. +/// This interface is meant to be simpler and more ergonomic than the interfaces provided by the +/// `rng` crate, but that one should +/// be used by applications that intend to submit to FIPS certification as it more closely aligns with the +/// requirements of SP 800-90A. +/// Note: this interface produces bytes. If you want a [`KeyMaterialTrait`], then use [`KeyMaterial::from_rng`]. +/// +/// Implementors are expected to also implement [`Default`] (default-construction should produce a +/// securely OS-seeded instance), but this is intentionally *not* a supertrait bound: requiring +/// `Default` would make `RNG` not dyn-compatible, and `&mut dyn RNG` is needed so RNG instances +/// can be handed around as trait objects. +pub trait RNG { + // TODO: add back once we figure out streaming interaction with entropy sources. + // fn add_seed_bytes(&mut self, additional_seed: &[u8]) -> Result<(), RNGError>; + + /// Provide additional key material to be mixed in to the existing RNG instance. + /// The exact behaviour will be implementation-specific, but this is intended for injecting + /// additional entropy, not as the primary method of seeding the RNG. + fn add_seed_keymaterial( + &mut self, + additional_seed: &dyn KeyMaterialTrait, + ) -> Result<(), RNGError>; + /// Returns the next random 32-bit integer. + fn next_int(&mut self) -> Result; + + /// Returns the number of requested bytes. + fn next_bytes(&mut self, len: usize) -> Result, RNGError>; + + /// Returns the number of bytes written. + /// The entire output buffer is zeroized before the random bytes are written. + fn next_bytes_out(&mut self, out: &mut [u8]) -> Result; + + /// Fill the provided [`KeyMaterial`] with random bytes. + fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result; + + /// Returns the Security Strength of this RNG. + // todo: we should do a refactor to make [Algorithm] be a `security_strength()` function instead of constant, + // then have `RNG: Algorithm`, then delete this function. + fn security_strength(&self) -> SecurityStrength; +} + +/// A general indicator used across the library for marking the security level of a cryptographic primitive, +/// and for tracking the security level of the algorithms that interacted with a given piece of data. +/// For example, if a KDF at the 128-bit security strength is used to produce a 512-bit key, that key +/// will also be tagged as having a 128-bit security strength. +/// /// Some functions across the library may reject or behave differently based on the security strength /// of the inputs they are given. For example a `keygen_from_seed()` may reject a seed taged at a lower /// security strength than the one required by the algorithm, or it may proceed, but lower its own @@ -893,191 +902,29 @@ impl SecurityStrength { /// For example, 15 bytes (120-bits) is rounded down to 112-bit. pub fn from_bytes(bytes: usize) -> Self { Self::from_bits(bytes * 8) - } - - /// Outputs the security strength in bits for easier computation. - pub fn as_int(&self) -> u32 { - match self { - Self::None => 0, - Self::_112bit => 112, - Self::_128bit => 128, - Self::_192bit => 192, - Self::_256bit => 256, - } - } -} - -/// An interface for random number generation. -/// This interface is meant to be simpler and more ergonomic than the interfaces provided by the -/// `rng` crate, but that one should -/// be used by applications that intend to submit to FIPS certification as it more closely aligns with the -/// requirements of SP 800-90A. -/// Note: this interface produces bytes. If you want a [`KeyMaterialTrait`], then use [`KeyMaterial::from_rng`]. -/// -/// Implementors are expected to also implement [`Default`] (default-construction should produce a -/// securely OS-seeded instance), but this is intentionally *not* a supertrait bound: requiring -/// `Default` would make `RNG` not dyn-compatible, and `&mut dyn RNG` is needed so RNG instances -/// can be handed around as trait objects. -pub trait RNG { - // TODO: add back once we figure out streaming interaction with entropy sources. - // fn add_seed_bytes(&mut self, additional_seed: &[u8]) -> Result<(), RNGError>; - - /// Provide additional key material to be mixed in to the existing RNG instance. - /// The exact behaviour will be implementation-specific, but this is intended for injecting - /// additional entropy, not as the primary method of seeding the RNG. - fn add_seed_keymaterial( - &mut self, - additional_seed: &dyn KeyMaterialTrait, - ) -> Result<(), RNGError>; - /// Returns the next random 32-bit integer. - fn next_int(&mut self) -> Result; - - /// Returns the number of requested bytes. - fn next_bytes(&mut self, len: usize) -> Result, RNGError>; - - /// Returns the number of bytes written. - /// The entire output buffer is zeroized before the random bytes are written. - fn next_bytes_out(&mut self, out: &mut [u8]) -> Result; - - /// Fill the provided [`KeyMaterial`] with random bytes. - fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result; - - /// Returns the Security Strength of this RNG. - // todo: we should do a refactor to make [Algorithm] be a `security_strength()` function instead of constant, - // then have `RNG: Algorithm`, then delete this function. - fn security_strength(&self) -> SecurityStrength; -} - -/// Allows a stateful object to suspend its operation by serializing its state into a byte array -///so that it can be resumed later, potentially from a different host. -/// -/// This is intended for situations where an object is being used through its streaming API -/// (do_update, do_final) and the operation wants to be paused to a cache, for example while waiting -/// for network IO. -/// -/// This is not intended as a mechanism to clone the state of an object since in most cases `.clone()` -/// will be more straightforward. -/// -/// The serialized state MAY contain short-term sensitive values such as nonces or IVs, -/// but it MUST NOT include a serialized private key. -/// Keyed algorithms MUST instead impl -/// [`SuspendableKeyed`] which requires the key to be supplied independently at the time of deserialization. -pub trait Suspendable: Sized { - /// Suspend operation by serializing out the state of the object. - /// - /// Note that this consumes `self` to prevent accidentally continuing to use the object after serialization. - /// If you want to do this intentionally, then you will need to clone the object before serializing it. - /// - /// The serialized state MUST include a prefix indicating the version of the library that serialized it. - fn suspend(self) -> [u8; SERIALIZED_STATE_LEN]; - - /// Resume operation from a serialized state. - /// - /// Deserializers SHOULD check the version and reject serialized states from incompatible versions - /// (including rejecting serializations from a future version of the library). - /// For example, if a given object made a breaking change to its serialization in version 1.2.3, then its - /// deserializer should reject serialized states from that version or older. - fn from_suspended(state: [u8; SERIALIZED_STATE_LEN]) -> Result; -} - -/// Similar to [`Suspendable`] in that it allows a stateful object to suspend its operation by -/// serializing its state into a byte array so that it can be resumed later, potentially from a different host. -/// -/// The difference is that this trait is for keyed algorithms -- MACs, symmetric ciphers, signatures, etc -- -/// which require a private key in order to resume successfully. -/// For security reasons, the private key is not included in the serialized state -/// and must be provided separately as part of the deserialization process. -pub trait SuspendableKeyed: Sized { - /// The type of key that must be re-supplied to resume this object. - type Key: ?Sized; - - /// Suspend operation by serializing out the state of the object. - /// - /// Note that this consumes `self` to prevent accidentally continuing to use the object after serialization. - /// If you want to do this intentionally, then you will need to clone the object before serializing it. - /// - /// The serialized state MUST include a prefix indicating the version of the library that serialized it. - fn suspend(self) -> [u8; SERIALIZED_STATE_LEN]; - - /// Resume operation from a serialized state and the key. - /// - /// Deserializers SHOULD check the version and reject serialized states from incompatible versions - /// (including rejecting serializations from a future version of the library). - /// For example, if a given object made a breaking change to its serialization in version 1.2.3, then its - /// deserializer should reject serialized states from that version or older. - fn from_suspended( - state: [u8; SERIALIZED_STATE_LEN], - key: &Self::Key, - ) -> Result; -} - -/// Pre-Hashed Signer is an extension to [`Signer`] that adds functionality specific to signature -/// primatives that can operate on a pre-hashed message instead of the full message. -pub trait PHSigner< - PK: SignaturePublicKey, - SK: SignaturePrivateKey, - const PK_LEN: usize, - const SK_LEN: usize, - const SIG_LEN: usize, - const PH_LEN: usize, ->: Signer -{ - /// Produce a signature for the provided pre-hashed message and context. - /// - /// `ctx` accepts a zero-length byte array. - /// - /// A note about the `ctx` context parameter: - /// This is a newer addition to cryptographic signature primitives. It allows for binding the - /// signature to some external property of the application so that a signature will fail to validate - /// if removed from its intended context. - /// This is particularly useful at preventing content confusion attacks between data formats that - /// have very similar data structures, for example S/MIME emails, signed PDFs, and signed executables - /// that all use the Cryptographic Message Syntax (CMS) data format, or multiple data objects that - /// all use the JWS data format. - /// To be properly effective, the ctx value must not be under the control of the attacker, which generally - /// means that it needs to be a value that is never transmitted over the wire, but rather is something - /// known to the application by context. - /// For example, "email" vs "pdf" would be a good choice since the application should know what it is - /// attempting to sign or verify. - /// The `ctx` param can also be used to bind the signed content to a transaction ID or a username, - /// but care should be taken to ensure that an attacker attempting a - /// content confusion attack not also cause the signed / verifier to use an incorrect transaction ID or username. - /// - /// Not all signature primitives will support a context value, so you may need to consult the - /// documentation for the underlying primitive for how it handles a ctx in that case, for example, it - /// might throw an error, ignore the provided ctx value, or append the ctx to the msg in a non-standard way. - fn sign_ph( - sk: &SK, - ph: &[u8; PH_LEN], - ctx: Option<&[u8]>, - ) -> Result<[u8; SIG_LEN], SignatureError>; - /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. - /// The entire output buffer is zeroized before the signature is written. - fn sign_ph_out( - sk: &SK, - ph: &[u8; PH_LEN], - ctx: Option<&[u8]>, - output: &mut [u8; SIG_LEN], - ) -> Result; -} - -/// Pre-Hashed Signature Verifier is an extension to [`SignatureVerifier`] that adds functionality specific to signature -/// primatives that can operate on a pre-hashed message instead of the full message. -pub trait PHSignatureVerifier< - PK: SignaturePublicKey, - const PK_LEN: usize, - const SIG_LEN: usize, - const PH_LEN: usize, ->: SignatureVerifier -{ - /// On success, returns Ok(()) - /// On failure, returns Err([`SignatureError::SignatureVerificationFailed`]); may also return other types of [`SignatureError`] as appropriate (such as for invalid-length inputs). - fn verify_ph( - pk: &PK, - ph: &[u8; PH_LEN], - ctx: Option<&[u8]>, - sig: &[u8], - ) -> Result<(), SignatureError>; + } + + /// Outputs the security strength in bits for easier computation. + pub fn as_int(&self) -> u32 { + match self { + Self::None => 0, + Self::_112bit => 112, + Self::_128bit => 128, + Self::_192bit => 192, + Self::_256bit => 256, + } + } +} + +/// A private key for a signature algorithm, often denoted "sk" (for "secret key"). +pub trait SignaturePrivateKey: PartialEq + Eq + Clone + Sized { + /// Write it out to bytes in its standard encoding. + fn encode(&self) -> [u8; SK_LEN]; + /// Write it out to bytes in its standard encoding. + /// The entire output buffer is zeroized before the encoding is written. + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize; + /// Read it in from bytes in its standard encoding. + fn from_bytes(bytes: &[u8]) -> Result; } // todo: could the public and private key types impl Into> and From> @@ -1096,15 +943,42 @@ pub trait SignaturePublicKey: fn from_bytes(bytes: &[u8]) -> Result; } -/// A private key for a signature algorithm, often denoted "sk" (for "secret key"). -pub trait SignaturePrivateKey: PartialEq + Eq + Clone + Sized { - /// Write it out to bytes in its standard encoding. - fn encode(&self) -> [u8; SK_LEN]; - /// Write it out to bytes in its standard encoding. - /// The entire output buffer is zeroized before the encoding is written. - fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize; - /// Read it in from bytes in its standard encoding. - fn from_bytes(bytes: &[u8]) -> Result; +/// A digital signature algorithm is defined as a set of three operations: +/// key generation, signing, and verification. +/// +/// This trait represents the verification operations performed by the holder of the verification public key. +/// Keygen and signing operations are performed by the corresponding [`Signer`] trait. +/// There are several reasons for this split: first is architectural; some complex algorithms may +/// benefit from having the signature generation and verification implementations split into separate modules. +/// Second is for compliance: sometimes a policy soft-deprecates an algorithm so that new signatures +/// can no longer be created, but existing signatures can still be verified. Splitting the traits +/// makes this policy easier to enforce. +/// +/// Here we statically-size the arrays used to encode public keys, private keys, and signature values +/// because this allows us to safely remove runtime checks for array lengths, which overall reduces +/// the fallibility of the library. This design choice could make this trait complicated to apply +/// to a signature algorithm that do not have fixed sizes for the encodings of these objects. +pub trait SignatureVerifier< + PK: SignaturePublicKey, + const PK_LEN: usize, + const SIG_LEN: usize, +>: Sized +{ + /// On success, returns Ok(()) + /// On failure, returns Err([`SignatureError::SignatureVerificationFailed`]); may also return other types of [`SignatureError`] as appropriate (such as for invalid-length inputs). + fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError>; + + /// streaming verification API + fn verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result; + + // todo: make this a AsRef<[u8]> ? + /// Update the verifier with the next chunk of data. + /// This can be called multiple times. + fn verify_update(&mut self, msg_chunk: &[u8]); + + /// On success, returns Ok(()) + /// On failure, returns Err([`SignatureError::SignatureVerificationFailed`]); may also return other types of [`SignatureError`] as appropriate (such as for invalid-length inputs). + fn verify_final(self, sig: &[u8]) -> Result<(), SignatureError>; } /// A digital signature algorithm is defined as a set of three operations: @@ -1182,42 +1056,168 @@ pub trait Signer, const SK_LEN: usize, const SIG fn sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result; } -/// A digital signature algorithm is defined as a set of three operations: -/// key generation, signing, and verification. +/// The basic functions of a stream cipher, which differ from those of a block cipher only in that +/// a stream cipher is assumed to have no underlying block size tied to the implementation, and so the caller gets to specify +/// the block size for the streaming APIs. +pub trait StreamCipher: + SymmetricCipher + Sized +{ + /// Constructor that begins a flow of the streaming API for encrypting one block at a time. + /// Allows for the implementation to return init data such as an IV which is generated prior to encrypting the first block. + fn do_stream_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + /// Encrypts a single block of plaintext. + fn do_stream_encrypt_block( + &mut self, + plaintext: &[u8; BLOCK_LEN], + ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; + /// Encrypts a single block of plaintext and writes the ciphertext to the provided buffer. + fn do_stream_encrypt_block_out( + &mut self, + plaintext: &[u8; BLOCK_LEN], + ciphertext: &mut [u8; BLOCK_LEN], + ) -> Result; + /// Encrypts the final block of plaintext. + fn do_stream_encrypt_final( + &mut self, + plaintext: &[u8; BLOCK_LEN], + ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; + /// Encrypts the final block of plaintext and writes the ciphertext to the provided buffer. + fn do_stream_encrypt_final_out( + &mut self, + plaintext: &[u8; BLOCK_LEN], + ciphertext: &mut [u8; BLOCK_LEN], + ) -> Result; + /// Constructor that begins a flow of the streaming API for decryption one block at a time. + fn do_stream_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result; + /// Decrypts a single block of ciphertext. + fn do_stream_decrypt_block( + &mut self, + ciphertext: &[u8; BLOCK_LEN], + ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; + /// Decrypts a single block of ciphertext and writes the plaintext to the provided buffer. + fn do_stream_decrypt_block_out( + &mut self, + ciphertext: &[u8; BLOCK_LEN], + plaintext: &mut [u8; BLOCK_LEN], + ) -> Result; +} + +/// Allows a stateful object to suspend its operation by serializing its state into a byte array +///so that it can be resumed later, potentially from a different host. /// -/// This trait represents the verification operations performed by the holder of the verification public key. -/// Keygen and signing operations are performed by the corresponding [`Signer`] trait. -/// There are several reasons for this split: first is architectural; some complex algorithms may -/// benefit from having the signature generation and verification implementations split into separate modules. -/// Second is for compliance: sometimes a policy soft-deprecates an algorithm so that new signatures -/// can no longer be created, but existing signatures can still be verified. Splitting the traits -/// makes this policy easier to enforce. +/// This is intended for situations where an object is being used through its streaming API +/// (do_update, do_final) and the operation wants to be paused to a cache, for example while waiting +/// for network IO. /// -/// Here we statically-size the arrays used to encode public keys, private keys, and signature values -/// because this allows us to safely remove runtime checks for array lengths, which overall reduces -/// the fallibility of the library. This design choice could make this trait complicated to apply -/// to a signature algorithm that do not have fixed sizes for the encodings of these objects. -pub trait SignatureVerifier< - PK: SignaturePublicKey, - const PK_LEN: usize, - const SIG_LEN: usize, ->: Sized -{ - /// On success, returns Ok(()) - /// On failure, returns Err([`SignatureError::SignatureVerificationFailed`]); may also return other types of [`SignatureError`] as appropriate (such as for invalid-length inputs). - fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError>; +/// This is not intended as a mechanism to clone the state of an object since in most cases `.clone()` +/// will be more straightforward. +/// +/// The serialized state MAY contain short-term sensitive values such as nonces or IVs, +/// but it MUST NOT include a serialized private key. +/// Keyed algorithms MUST instead impl +/// [`SuspendableKeyed`] which requires the key to be supplied independently at the time of deserialization. +pub trait Suspendable: Sized { + /// Suspend operation by serializing out the state of the object. + /// + /// Note that this consumes `self` to prevent accidentally continuing to use the object after serialization. + /// If you want to do this intentionally, then you will need to clone the object before serializing it. + /// + /// The serialized state MUST include a prefix indicating the version of the library that serialized it. + fn suspend(self) -> [u8; SERIALIZED_STATE_LEN]; - /// streaming verification API - fn verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result; + /// Resume operation from a serialized state. + /// + /// Deserializers SHOULD check the version and reject serialized states from incompatible versions + /// (including rejecting serializations from a future version of the library). + /// For example, if a given object made a breaking change to its serialization in version 1.2.3, then its + /// deserializer should reject serialized states from that version or older. + fn from_suspended(state: [u8; SERIALIZED_STATE_LEN]) -> Result; +} - // todo: make this a AsRef<[u8]> ? - /// Update the verifier with the next chunk of data. - /// This can be called multiple times. - fn verify_update(&mut self, msg_chunk: &[u8]); +/// Similar to [`Suspendable`] in that it allows a stateful object to suspend its operation by +/// serializing its state into a byte array so that it can be resumed later, potentially from a different host. +/// +/// The difference is that this trait is for keyed algorithms -- MACs, symmetric ciphers, signatures, etc -- +/// which require a private key in order to resume successfully. +/// For security reasons, the private key is not included in the serialized state +/// and must be provided separately as part of the deserialization process. +pub trait SuspendableKeyed: Sized { + /// The type of key that must be re-supplied to resume this object. + type Key: ?Sized; - /// On success, returns Ok(()) - /// On failure, returns Err([`SignatureError::SignatureVerificationFailed`]); may also return other types of [`SignatureError`] as appropriate (such as for invalid-length inputs). - fn verify_final(self, sig: &[u8]) -> Result<(), SignatureError>; + /// Suspend operation by serializing out the state of the object. + /// + /// Note that this consumes `self` to prevent accidentally continuing to use the object after serialization. + /// If you want to do this intentionally, then you will need to clone the object before serializing it. + /// + /// The serialized state MUST include a prefix indicating the version of the library that serialized it. + fn suspend(self) -> [u8; SERIALIZED_STATE_LEN]; + + /// Resume operation from a serialized state and the key. + /// + /// Deserializers SHOULD check the version and reject serialized states from incompatible versions + /// (including rejecting serializations from a future version of the library). + /// For example, if a given object made a breaking change to its serialization in version 1.2.3, then its + /// deserializer should reject serialized states from that version or older. + fn from_suspended( + state: [u8; SERIALIZED_STATE_LEN], + key: &Self::Key, + ) -> Result; +} + +// todo -- split all the SymmetricCipher traits into Encryptor and Decryptor +/// The basic one-shot encrypt and decrypt that all types of symmetric ciphers must implement. +/// These are meant to be simple, easy to use, secure, and fool-proof APIs, but they may result in +/// ciphertexts that are incompatible with other implementations as ciphers in more complex modes, such +/// as AEADs or stream ciphers may need to stick extra data either at the beginning or end of the ciphertext. +/// See the documentation of the underlying implementation for more details. +pub trait SymmetricCipher: Algorithm { + #[cfg(feature = "std")] + /// A one-shot API to encrypt some plaintext with the given key. + /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. + /// Returns a tuple containing the initialization data and the ciphertext. + /// This is not available if building for no_std. + fn encrypt( + key: &KeyMaterial, + plaintext: &[u8], + ) -> Result<([u8; INIT_DATA_LEN], Vec), SymmetricCipherError>; + /// A one-shot API to encrypt some plaintext with the given key. + /// This function takes a reference to the output buffer for the ciphertext, and is therefore available in no_std. + /// See the documentation for the underlying implementation for details on providing a ciphertext buffer of sufficient size; + /// typically the ciphertext is the same length as the plaintext, but some ciphers may have an expansion factor or require + /// extra space for a nonce or tag. + /// Returns a tuple containing the initialization data and the number of bytes written to the ciphertext buffer. + fn encrypt_out( + key: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError>; + #[cfg(feature = "std")] + /// A one-shot API to decrypt some ciphertext with the given key. + /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. + /// This is not available if building for no_std. + fn decrypt( + key: &KeyMaterial, + init_data: [u8; INIT_DATA_LEN], + ciphertext: &[u8], + ) -> Result, SymmetricCipherError>; + /// A one-shot API to decrypt some ciphertext with the given key. + /// This function takes a reference to the output buffer for the plaintext, and is therefore available in no_std. + /// See the documentation for the underlying implementation for details on providing a plaintext buffer of sufficient size; + /// typically the ciphertext is the same length as the plaintext, but some ciphers may have an expansion factor or require + /// extra space for a nonce or tag. + /// Returns a tuple containing the initialization data and the number of bytes written to the plaintext buffer. + fn decrypt_out( + key: &KeyMaterial, + init_data: [u8; INIT_DATA_LEN], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result; } /// Extensible Output Functions (XOFs) are similar to hash functions, except that they can produce output of arbitrary length. From 2a9461dc7fc737eaa6f65975667882ae79019a23 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 7 Sep 2026 07:01:18 +1000 Subject: [PATCH 7/7] core: rename BlockPermutation to ElectronicCodeBook Adopts the name the trait carries on release/0.1.3alpha, where it arrived in 2c0567e ("core: ElectronicCodeBook (was BlockPermutation), slice block hooks, blocks8, SymmetricCipherEncryptor/Decryptor (from feature/sm4)"). Only the rename is taken; the other three items in that commit are not, so the trait keeps the method set this branch already had -- `new`, the two single-block methods and the pair methods. `encrypt_blocks8` / `decrypt_blocks8` remain upstream-only. The trait is named for the mode it *is* when applied directly to data: one block at a time under a fixed key is ECB (SP 800-38A Sec 6.1), which is not confidential, and the doc comment now says so as a reminder. That sentence is taken from upstream too. Mechanical throughout: `BlockPermutation` -> `ElectronicCodeBook` and `block_permutation` -> `electronic_code_book`, which also carries `TestFrameworkBlockPermutation` and the two file names (`core-test-framework/src/block_permutation.rs` and `aes-lowmemory/tests/block_permutation_tests.rs`) to the names upstream uses. Prose "block permutation" is left alone: it still describes what the primitive is, as it does upstream. No alphabetical move was needed -- `ElectronicCodeBook` sorts into the same slot `BlockPermutation` held, between `BlockCipherEncryptor` and `Hash`. rustfmt reflowed one import in `modes/src/cbc.rs` that the longer name pushed over the width. 586 tests pass, unchanged from before the rename. Co-Authored-By: Claude Fable 5.1 --- alpha_0.1.3_release_notes.md | 10 +++++----- cli/src/aes_cbc_cmd.rs | 6 +++--- crypto/aes-lowmemory/src/aes.rs | 10 +++++----- crypto/aes-lowmemory/summary.md | 4 ++-- ...tests.rs => electronic_code_book_tests.rs} | 16 +++++++-------- ...permutation.rs => electronic_code_book.rs} | 12 +++++------ crypto/core-test-framework/src/lib.rs | 2 +- crypto/core-test-framework/summary.md | 20 +++++++++---------- crypto/core/src/traits.rs | 15 +++++++------- crypto/modes/benches/modes_benches.rs | 12 +++++------ crypto/modes/src/cbc.rs | 19 +++++++++--------- crypto/modes/src/lib.rs | 4 ++-- crypto/modes/tests/acvp_tests.rs | 6 +++--- crypto/modes/tests/cbc_tests.rs | 4 ++-- crypto/modes/tests/common/mod.rs | 8 ++++---- crypto/modes/tests/sp800_38a_tests.rs | 10 +++++----- 16 files changed, 80 insertions(+), 78 deletions(-) rename crypto/aes-lowmemory/tests/{block_permutation_tests.rs => electronic_code_book_tests.rs} (50%) rename crypto/core-test-framework/src/{block_permutation.rs => electronic_code_book.rs} (95%) diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index d74c766d..540406f8 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -30,7 +30,7 @@ permutation (NIST FIPS 197), re-exported from the umbrella crate. New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of operation (NIST SP 800-38A), currently **CBC** (Sec 6.2). Re-exported from the umbrella crate. -* `Cbc` over any `BlockPermutation`, so the crate depends on no +* `Cbc` over any `ElectronicCodeBook`, so the crate depends on no concrete cipher. The direction is a type parameter: `BlockCipherEncryptor` is implemented only for `Cbc<_, Encrypting, _, _>` and `BlockCipherDecryptor` only for `Cbc<_, Decrypting, _, _>`, making a wrong-direction call a compile error rather than a runtime check. @@ -40,7 +40,7 @@ New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of op supplying your own. Known-answer tests drive `do_encrypt_init_rng` with a fixed-output test RNG. * **Parallel decryption.** Sec 6.2 notes CBC decryption's inverse cipher calls can run in parallel, so `do_decrypt_blocks[_out]` walks the ciphertext in pairs through - `BlockPermutation::decrypt_blocks2`, with a one-block remainder for odd `N`. Measured against an + `ElectronicCodeBook::decrypt_blocks2`, with a one-block remainder for odd `N`. Measured against an otherwise identical permutation that does not override the pair methods, this is **1.83x** the decryption throughput (67.9 vs 37.1 MiB/s, AES-128, 16 KiB, N=8). CBC encryption is serial by construction and does not use it. @@ -82,7 +82,7 @@ New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of op the F.2 vectors, round trips across the chunk boundary, a fresh IV per invocation, hex/binary agreement, `--key-file` in both hex and binary, and every error path with its message. -`core`: new `BlockPermutation` trait (`crypto/core/src/traits.rs`), the raw +`core`: new `ElectronicCodeBook` trait (`crypto/core/src/traits.rs`), the raw keyed permutation -- `CIPH_K` / `CIPH^-1_K` of SP 800-38A Sec 5.1 -- that a mode is built on. `new`, `encrypt_block`, `decrypt_block`, plus provided `encrypt_blocks2` / `decrypt_blocks2` that default to two single-block calls and which bit-sliced implementations override. The block methods @@ -92,7 +92,7 @@ there). Testing: -* `core-test-framework` gains `TestFrameworkBlockPermutation`, which pins the trait contract: +* `core-test-framework` gains `TestFrameworkElectronicCodeBook`, which pins the trait contract: both directions are inverses either way round, the permutation is injective, and the pair methods are indistinguishable from two single-block calls **including their order** -- the check that makes an override safe. @@ -131,7 +131,7 @@ Block cipher traits (PR #96): * The single `BlockCipher` streaming trait is split into `BlockCipherEncryptor` and `BlockCipherDecryptor` (mirroring `KEMEncapsulator` / `KEMDecapsulator`) so the direction is encoded in the implementing type. Both, and - `BlockPermutation`, are bounded on `Algorithm`, whose `MAX_SECURITY_STRENGTH` is the strength the `_init` + `ElectronicCodeBook`, are bounded on `Algorithm`, whose `MAX_SECURITY_STRENGTH` is the strength the `_init` constructors enforce (a mode reports its permutation's name and strength); the `SymmetricCipher` one-shot API is no longer a supertrait. * The single-block `do_{en,de}crypt_block[_out]` methods are replaced by multi-block diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs index d532a31a..c338b0d3 100644 --- a/cli/src/aes_cbc_cmd.rs +++ b/cli/src/aes_cbc_cmd.rs @@ -37,7 +37,7 @@ use bouncycastle::core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle::core::traits::{ - BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, + BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SecurityStrength, }; use bouncycastle::hex; use bouncycastle::modes::{Cbc, Decrypting, Encrypting}; @@ -172,7 +172,7 @@ fn load_key( /// Encrypts stdin to stdout, writing the generated IV first. fn encrypt_stream(key: &KeyMaterial, output_hex: bool) where - P: BlockPermutation, + P: ElectronicCodeBook, { let (mut enc, iv) = Cbc::::do_encrypt_init(key) .unwrap_or_else(|e| { @@ -203,7 +203,7 @@ where /// Decrypts stdin to stdout, taking the IV from the first block of input. fn decrypt_stream(key: &KeyMaterial, output_hex: bool) where - P: BlockPermutation, + P: ElectronicCodeBook, { // The leading block is the IV, not ciphertext. let mut iv = [0u8; BLOCK_LEN]; diff --git a/crypto/aes-lowmemory/src/aes.rs b/crypto/aes-lowmemory/src/aes.rs index 9b25fe4e..08198459 100644 --- a/crypto/aes-lowmemory/src/aes.rs +++ b/crypto/aes-lowmemory/src/aes.rs @@ -6,7 +6,7 @@ use crate::sbox::{inv_sbox, sbox}; use crate::schedule::{Aes128Params, Aes192Params, Aes256Params, AesParams, expand, round_key}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Algorithm, BlockPermutation, SecurityStrength}; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; use bouncycastle_utils::secret::Secret; /// The AES block length in bytes: 16 (FIPS 197 Sec 3.4, `Nb` = 4 words). @@ -221,14 +221,14 @@ impl Algorithm for Aes256 { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; } -// The three `BlockPermutation` impls are one-line delegations to the inherent methods above. They +// The three `ElectronicCodeBook` impls are one-line delegations to the inherent methods above. They // are written out longhand rather than generated, for the `cargo mutants` reason given above. // // Each overrides `encrypt_blocks2` / `decrypt_blocks2`, because a pair of blocks is exactly what // the bit-sliced state holds: the pair form costs barely more than one block, where the default // (two single-block calls) would do four blocks' worth of work. -impl BlockPermutation<16, BLOCK_LEN> for Aes128 { +impl ElectronicCodeBook<16, BLOCK_LEN> for Aes128 { fn new(key: &KeyMaterial<16>) -> Result { Aes128::new(key) } @@ -246,7 +246,7 @@ impl BlockPermutation<16, BLOCK_LEN> for Aes128 { } } -impl BlockPermutation<24, BLOCK_LEN> for Aes192 { +impl ElectronicCodeBook<24, BLOCK_LEN> for Aes192 { fn new(key: &KeyMaterial<24>) -> Result { Aes192::new(key) } @@ -264,7 +264,7 @@ impl BlockPermutation<24, BLOCK_LEN> for Aes192 { } } -impl BlockPermutation<32, BLOCK_LEN> for Aes256 { +impl ElectronicCodeBook<32, BLOCK_LEN> for Aes256 { fn new(key: &KeyMaterial<32>) -> Result { Aes256::new(key) } diff --git a/crypto/aes-lowmemory/summary.md b/crypto/aes-lowmemory/summary.md index 20ab8fca..cf300350 100644 --- a/crypto/aes-lowmemory/summary.md +++ b/crypto/aes-lowmemory/summary.md @@ -408,7 +408,7 @@ files (see the ML-KEM and ML-DSA suites). | Item | Why | |---|---| -| `BlockPermutation` trait impls, and `encrypt_blocks2`/`decrypt_blocks2` as trait methods | The trait does not exist in `crypto/core`, which has the mode-level `BlockCipher` / `BlockCipherEncryptor` / `BlockCipherDecryptor`. Introducing it is the plan's separate "PR A". The two-block entry points are inherent methods for now; promoting them to provided trait methods is a one-line delegation once the trait lands. | +| `ElectronicCodeBook` trait impls, and `encrypt_blocks2`/`decrypt_blocks2` as trait methods | The trait does not exist in `crypto/core`, which has the mode-level `BlockCipher` / `BlockCipherEncryptor` / `BlockCipherDecryptor`. Introducing it is the plan's separate "PR A". The two-block entry points are inherent methods for now; promoting them to provided trait methods is a one-line delegation once the trait lands. | | `core-test-framework` conformance test | Follows from the above — there is no test suite for a raw permutation yet. | | ACVP MCT (Monte Carlo) groups — 6 cases | Their expected `resultsArray` comes from a chained key/plaintext update rule defined in the ACVP AES specification, not in FIPS 197. Implementing it from anything other than that specification would be guesswork. The test reports the skip count so the gap is visible rather than silent. | | CLI subcommand | A bare permutation only does ECB. `aes128-cbc-*` / `-cfb-*` belong with the modes crate. | @@ -477,7 +477,7 @@ they print a warning and pass. than a technical one. 2. **Confirm the PR base branch.** The plan specifies `release/0.1.3alpha`, set explicitly — GitHub defaults to `main`. -3. Decide whether `BlockPermutation` (plan PR A) lands before or after this crate, since it +3. Decide whether `ElectronicCodeBook` (plan PR A) lands before or after this crate, since it determines whether the two-block entry points become trait methods now or later (§6). 4. Note in the PR description that the plan's layout claim (§5.1) and PR B (§5.3) are superseded, so the plan document does not mislead the next reader. diff --git a/crypto/aes-lowmemory/tests/block_permutation_tests.rs b/crypto/aes-lowmemory/tests/electronic_code_book_tests.rs similarity index 50% rename from crypto/aes-lowmemory/tests/block_permutation_tests.rs rename to crypto/aes-lowmemory/tests/electronic_code_book_tests.rs index d6119d97..2098315e 100644 --- a/crypto/aes-lowmemory/tests/block_permutation_tests.rs +++ b/crypto/aes-lowmemory/tests/electronic_code_book_tests.rs @@ -1,4 +1,4 @@ -//! `BlockPermutation` trait conformance, via the shared test framework. +//! `ElectronicCodeBook` trait conformance, via the shared test framework. //! //! The framework checks the properties every implementor must have -- both directions are //! inverses, the permutation is injective, the pair methods are indistinguishable from two @@ -7,19 +7,19 @@ //! `decrypt_blocks2`, so the default implementation is not what runs. use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256, BLOCK_LEN}; -use bouncycastle_core_test_framework::block_permutation::TestFrameworkBlockPermutation; +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; #[test] -fn aes128_conforms_to_block_permutation() { - TestFrameworkBlockPermutation::new().test::<16, BLOCK_LEN, Aes128>(); +fn aes128_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<16, BLOCK_LEN, Aes128>(); } #[test] -fn aes192_conforms_to_block_permutation() { - TestFrameworkBlockPermutation::new().test::<24, BLOCK_LEN, Aes192>(); +fn aes192_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<24, BLOCK_LEN, Aes192>(); } #[test] -fn aes256_conforms_to_block_permutation() { - TestFrameworkBlockPermutation::new().test::<32, BLOCK_LEN, Aes256>(); +fn aes256_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<32, BLOCK_LEN, Aes256>(); } diff --git a/crypto/core-test-framework/src/block_permutation.rs b/crypto/core-test-framework/src/electronic_code_book.rs similarity index 95% rename from crypto/core-test-framework/src/block_permutation.rs rename to crypto/core-test-framework/src/electronic_code_book.rs index 7f37c51e..1c41a35d 100644 --- a/crypto/core-test-framework/src/block_permutation.rs +++ b/crypto/core-test-framework/src/electronic_code_book.rs @@ -1,24 +1,24 @@ -//! Shared conformance tests for [`BlockPermutation`] implementors. +//! Shared conformance tests for [`ElectronicCodeBook`] implementors. use crate::DUMMY_SEED; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; -use bouncycastle_core::traits::{BlockPermutation, SecurityStrength}; +use bouncycastle_core::traits::{ElectronicCodeBook, SecurityStrength}; /// Instance of the test framework. -pub struct TestFrameworkBlockPermutation { +pub struct TestFrameworkElectronicCodeBook { // Put any config options here } -impl Default for TestFrameworkBlockPermutation { +impl Default for TestFrameworkElectronicCodeBook { fn default() -> Self { Self::new() } } -impl TestFrameworkBlockPermutation { +impl TestFrameworkElectronicCodeBook { /// pub fn new() -> Self { Self {} @@ -41,7 +41,7 @@ impl TestFrameworkBlockPermutation { pub fn test< const KEY_LEN: usize, const BLOCK_LEN: usize, - P: BlockPermutation, + P: ElectronicCodeBook, >( &self, ) { diff --git a/crypto/core-test-framework/src/lib.rs b/crypto/core-test-framework/src/lib.rs index f5519d95..45d922e4 100644 --- a/crypto/core-test-framework/src/lib.rs +++ b/crypto/core-test-framework/src/lib.rs @@ -14,7 +14,7 @@ // properly document everything. #![forbid(missing_docs)] -pub mod block_permutation; +pub mod electronic_code_book; pub mod hash; pub mod kdf; pub mod kem; diff --git a/crypto/core-test-framework/summary.md b/crypto/core-test-framework/summary.md index dcd404e7..738de37a 100644 --- a/crypto/core-test-framework/summary.md +++ b/crypto/core-test-framework/summary.md @@ -1,8 +1,8 @@ -# `crypto/core-test-framework` — changes for `BlockPermutation` and CBC +# `crypto/core-test-framework` — changes for `ElectronicCodeBook` and CBC Changes made on branch `feature/officialfrancismendoza/100-AES-lightengine-CBC-mode` while adding `crypto/aes-lowmemory` and `crypto/modes`. Two things: a **new** per-trait suite for -`core::traits::BlockPermutation`, and a **bug fix** to the existing `TestFrameworkBlockCipher`. +`core::traits::ElectronicCodeBook`, and a **bug fix** to the existing `TestFrameworkBlockCipher`. For what this crate is for in general, see its [`src/lib.rs`](src/lib.rs) docs: one KAT-style harness per `core` trait, so that behaviour which should be consistent across implementations of a @@ -11,17 +11,17 @@ here rather than re-written per implementation. --- -## 1. New: `TestFrameworkBlockPermutation` +## 1. New: `TestFrameworkElectronicCodeBook` -[`src/block_permutation.rs`](src/block_permutation.rs), registered as `pub mod block_permutation;` +[`src/electronic_code_book.rs`](src/electronic_code_book.rs), registered as `pub mod electronic_code_book;` in [`src/lib.rs`](src/lib.rs). -`core::traits::BlockPermutation` is new in this branch: the raw keyed +`core::traits::ElectronicCodeBook` is new in this branch: the raw keyed permutation (`CIPH_K` / `CIPH^-1_K` of SP 800-38A Sec 5.1) that a mode of operation is built on. It needed a conformance suite like every other `core` trait. ```rust -TestFrameworkBlockPermutation::new().test::(); +TestFrameworkElectronicCodeBook::new().test::(); ``` ### What it checks, and why each check exists @@ -39,7 +39,7 @@ TestFrameworkBlockPermutation::new().test::(); ### The order check is the load-bearing one -`BlockPermutation::encrypt_blocks2` and `decrypt_blocks2` are *provided* methods: the default is +`ElectronicCodeBook::encrypt_blocks2` and `decrypt_blocks2` are *provided* methods: the default is two single-block calls, and implementations are free to override them. `bouncycastle-aes-lowmemory` does, because a pair of blocks is exactly what its bit-sliced state holds, so the pair form costs barely more than one block. @@ -56,7 +56,7 @@ takes the pair path. ### Current implementors -* `crypto/aes-lowmemory/tests/block_permutation_tests.rs` — AES-128, AES-192, AES-256. +* `crypto/aes-lowmemory/tests/electronic_code_book_tests.rs` — AES-128, AES-192, AES-256. * `crypto/modes/tests/cbc_tests.rs` — the toy permutation, checked before anything is concluded from it. @@ -169,7 +169,7 @@ cargo fmt --all -- --check This crate has no tests of its own — it *is* tests — so it is verified by its consumers. The two new suites are exercised by: -* `cargo test -p bouncycastle-aes-lowmemory --test block_permutation_tests` (3 tests) +* `cargo test -p bouncycastle-aes-lowmemory --test electronic_code_book_tests` (3 tests) * `cargo test -p bouncycastle-modes --test cbc_tests` (11 tests, including `cbc_conforms_to_the_block_cipher_framework`, which is what the §2 fix unblocked, and `the_toy_permutation_conforms_to_the_trait`) @@ -180,7 +180,7 @@ new suites are exercised by: 1. **Fix the same loop in `TestFrameworkSymmetricCipher` and `TestFrameworkAEADCipher`** (§3). Three lines each, and the next implementor of either trait will otherwise hit the panic. -2. **Decide whether the `Default` impl added to `TestFrameworkBlockPermutation` should be added to +2. **Decide whether the `Default` impl added to `TestFrameworkElectronicCodeBook` should be added to the other suites** for consistency — they all have `new()` and no `Default`, which clippy flags on new code but not on existing code. 3. When `crypto/padding` (PR #97) merges, its toy XOR-CBC cipher becomes a second diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 37690f7a..28402454 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -271,8 +271,9 @@ pub trait BlockCipherEncryptor< /// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1. /// /// This is the raw primitive a mode of operation is built on, not something to encrypt data with. -/// It transforms exactly one block, so applying it directly to data is ECB, which is not -/// confidential. [`BlockCipherEncryptor`] and [`BlockCipherDecryptor`] are the *mode* traits -- +/// It transforms exactly one block, so applying it directly to data is ECB (Sec 6.1), which is not +/// confidential -- the trait is named for the mode it *is* when used that way, as a reminder. +/// [`BlockCipherEncryptor`] and [`BlockCipherDecryptor`] are the *mode* traits -- /// they carry initialization data and chaining state; this one carries only a key schedule. /// /// Implementors are expected to hold that key schedule in a zeroize-on-drop wrapper @@ -281,9 +282,9 @@ pub trait BlockCipherEncryptor< /// # Why the block methods are infallible /// /// Every length here is fixed by a type, and a constructed value is always ready to use, so there -/// is nothing a caller can get wrong once [`BlockPermutation::new`] has returned. Only `new` can +/// is nothing a caller can get wrong once [`ElectronicCodeBook::new`] has returned. Only `new` can /// fail, and only because of the key. -pub trait BlockPermutation: +pub trait ElectronicCodeBook: Algorithm + Sized { /// Expands the key. @@ -302,12 +303,12 @@ pub trait BlockPermutation: /// The forward cipher function on two *independent* blocks, in place. /// - /// Provided as two [`BlockPermutation::encrypt_block`] calls. Bit-sliced implementations + /// Provided as two [`ElectronicCodeBook::encrypt_block`] calls. Bit-sliced implementations /// override it, because a pair of blocks is their natural unit of work and costs barely more /// than one; see `bouncycastle-aes-lowmemory`. /// /// Overrides must be indistinguishable from the default, including the order of the two - /// results. `TestFrameworkBlockPermutation` pins that. + /// results. `TestFrameworkElectronicCodeBook` pins that. /// /// Modes whose structure is parallel -- CBC decryption, CFB decryption, CTR -- should prefer /// this. CBC and CFB *encryption* cannot use it: each input block depends on the previous @@ -319,7 +320,7 @@ pub trait BlockPermutation: } /// The inverse cipher function on two *independent* blocks, in place. - /// See [`BlockPermutation::encrypt_blocks2`]. + /// See [`ElectronicCodeBook::encrypt_blocks2`]. fn decrypt_blocks2(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { let [a, b] = blocks; self.decrypt_block(a); diff --git a/crypto/modes/benches/modes_benches.rs b/crypto/modes/benches/modes_benches.rs index e7ea635e..44d1315f 100644 --- a/crypto/modes/benches/modes_benches.rs +++ b/crypto/modes/benches/modes_benches.rs @@ -6,7 +6,7 @@ //! implementation hands blocks to `decrypt_blocks2` in pairs. With the bit-sliced AES, whose //! two-block path costs barely more than one block, decryption should therefore run at roughly //! twice the throughput of encryption. That gap is the entire justification for the pair methods -//! on `BlockPermutation`, so if it disappears, something has stopped taking the pair path. +//! on `ElectronicCodeBook`, so if it disappears, something has stopped taking the pair path. //! //! `N = 1` is included to show the effect vanishing: with one block there is no pair to form, so //! decryption falls back to the single-block path and the ratio should be about 1. @@ -18,7 +18,7 @@ use bouncycastle_aes_lowmemory::{Aes128, Aes256}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{ - Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SecurityStrength, }; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; @@ -49,15 +49,15 @@ impl Algorithm for UnpairedAes128 { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } -impl BlockPermutation<16, BLOCK_LEN> for UnpairedAes128 { +impl ElectronicCodeBook<16, BLOCK_LEN> for UnpairedAes128 { fn new(key: &KeyMaterial<16>) -> Result { - Ok(Self(>::new(key)?)) + Ok(Self(>::new(key)?)) } fn encrypt_block(&self, block: &mut [u8; BLOCK_LEN]) { - >::encrypt_block(&self.0, block) + >::encrypt_block(&self.0, block) } fn decrypt_block(&self, block: &mut [u8; BLOCK_LEN]) { - >::decrypt_block(&self.0, block) + >::decrypt_block(&self.0, block) } // encrypt_blocks2 / decrypt_blocks2 deliberately left as the trait defaults. } diff --git a/crypto/modes/src/cbc.rs b/crypto/modes/src/cbc.rs index 1ec2d1da..20795a2b 100644 --- a/crypto/modes/src/cbc.rs +++ b/crypto/modes/src/cbc.rs @@ -27,7 +27,7 @@ //! the forward cipher operations cannot be performed in parallel". //! //! This implementation uses that: decryption walks the ciphertext two blocks at a time and hands -//! both to [`BlockPermutation::decrypt_blocks2`], which a bit-sliced engine computes for barely +//! both to [`ElectronicCodeBook::decrypt_blocks2`], which a bit-sliced engine computes for barely //! more than the cost of one block. Encryption cannot, and does not. use crate::iv::random_iv; @@ -35,12 +35,13 @@ use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ - Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, RNG, SecurityStrength, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, RNG, + SecurityStrength, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; -/// CBC mode over any [`BlockPermutation`], with the direction encoded in the type. +/// CBC mode over any [`ElectronicCodeBook`], with the direction encoded in the type. /// /// `Dir` is [`Encrypting`] or [`Decrypting`]. [`BlockCipherEncryptor`] is implemented only for the /// former and [`BlockCipherDecryptor`] only for the latter, so a `Cbc<_, Encrypting, _, _>` has no @@ -56,7 +57,7 @@ use core::marker::PhantomData; /// ciphertext block, both of which are public, so it is deliberately not wrapped in a `Secret`. pub struct Cbc where - P: BlockPermutation, + P: ElectronicCodeBook, { perm: P, /// `Cj-1`, initialised to the IV. See the module docs on why there is only one field for both. @@ -66,7 +67,7 @@ where impl Cbc where - P: BlockPermutation, + P: ElectronicCodeBook, { /// `Cj = CIPH_K(Pj XOR Cj-1)` in place, then `Cj` becomes the next chaining value. #[inline] @@ -91,7 +92,7 @@ where self.chain = cj; } - /// Decrypts two consecutive blocks with one [`BlockPermutation::decrypt_blocks2`] call. + /// Decrypts two consecutive blocks with one [`ElectronicCodeBook::decrypt_blocks2`] call. /// /// Writing the pair as `Cj, Cj+1` with `Cj-1` the incoming chaining value, Sec 6.2 gives /// @@ -124,7 +125,7 @@ where impl Algorithm for Cbc where - P: BlockPermutation, + P: ElectronicCodeBook, { /// The underlying permutation's name. The mode is not appended: `&'static str`s cannot be /// concatenated in a `const`, and the mode is already in the type. @@ -136,7 +137,7 @@ where impl BlockCipherEncryptor for Cbc where - P: BlockPermutation, + P: ElectronicCodeBook, { /// Begins an encryption flow, generating the IV from the library's default OS-backed DRBG. fn do_encrypt_init( @@ -174,7 +175,7 @@ where impl BlockCipherDecryptor for Cbc where - P: BlockPermutation, + P: ElectronicCodeBook, { /// Begins a decryption flow from the IV returned by /// [`BlockCipherEncryptor::do_encrypt_init`]. diff --git a/crypto/modes/src/lib.rs b/crypto/modes/src/lib.rs index 0680ee6d..f5d9b417 100644 --- a/crypto/modes/src/lib.rs +++ b/crypto/modes/src/lib.rs @@ -1,7 +1,7 @@ //! Block cipher modes of operation (NIST SP 800-38A). //! //! A mode turns a keyed block permutation -- `bouncycastle-aes-lowmemory`'s `Aes128` and friends, -//! or anything else implementing [`BlockPermutation`] -- into something that can encrypt more than +//! or anything else implementing [`ElectronicCodeBook`] -- into something that can encrypt more than //! one block. This crate currently provides **CBC** ([`Cbc`], SP 800-38A Sec 6.2). //! //! The crate is deliberately cipher-agnostic: it depends on no concrete block cipher, only on the @@ -189,7 +189,7 @@ pub use cbc::Cbc; // Imports needed for docs #[allow(unused_imports)] -use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook}; // end of imports needed for docs /// Direction marker for a mode that encrypts. See [`Cbc`]. diff --git a/crypto/modes/tests/acvp_tests.rs b/crypto/modes/tests/acvp_tests.rs index c74571dc..37b48d96 100644 --- a/crypto/modes/tests/acvp_tests.rs +++ b/crypto/modes/tests/acvp_tests.rs @@ -21,7 +21,7 @@ //! 2150 AFT (Algorithm Functional Test) cases across all three key lengths and both directions, //! including 60 whose payload spans 2 to 10 blocks. Every case is run **twice**: once block by //! block, and once in pairs with a one-block remainder for odd lengths. The second pass is what -//! puts the multi-block cases through `BlockPermutation::decrypt_blocks2`, so the pair path is +//! puts the multi-block cases through `ElectronicCodeBook::decrypt_blocks2`, so the pair path is //! exercised against real vectors and not only against the toy in `cbc_tests.rs`. //! //! The 6 MCT (Monte Carlo Test) groups are **not** implemented: their expected output is a @@ -34,7 +34,7 @@ use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle_core::traits::{ - BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, + BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SecurityStrength, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -111,7 +111,7 @@ fn run_case( grouping: Grouping, ) -> Vec<[u8; BLOCK_LEN]> where - P: BlockPermutation, + P: ElectronicCodeBook, { let key = cipher_key::(key_bytes); let mut out: Vec<[u8; BLOCK_LEN]> = Vec::with_capacity(input.len()); diff --git a/crypto/modes/tests/cbc_tests.rs b/crypto/modes/tests/cbc_tests.rs index 96e6f53f..c4308d33 100644 --- a/crypto/modes/tests/cbc_tests.rs +++ b/crypto/modes/tests/cbc_tests.rs @@ -9,7 +9,7 @@ mod common; use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; -use bouncycastle_core_test_framework::block_permutation::TestFrameworkBlockPermutation; +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkBlockCipher; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; use common::{SwappedPairToy, TOY_LEN, Toy, toy_key}; @@ -62,7 +62,7 @@ fn dec_flat( /// The toy must be a real permutation before any conclusion drawn from it is worth anything. #[test] fn the_toy_permutation_conforms_to_the_trait() { - TestFrameworkBlockPermutation::new().test::(); + TestFrameworkElectronicCodeBook::new().test::(); } #[test] diff --git a/crypto/modes/tests/common/mod.rs b/crypto/modes/tests/common/mod.rs index 6bd5dcd4..bead2aa7 100644 --- a/crypto/modes/tests/common/mod.rs +++ b/crypto/modes/tests/common/mod.rs @@ -1,4 +1,4 @@ -//! Toy [`BlockPermutation`] implementations, for testing the mode independently of any real cipher. +//! Toy [`ElectronicCodeBook`] implementations, for testing the mode independently of any real cipher. //! //! These are **not** cryptography. They exist so the structural properties of a mode -- chaining, //! sequencing, the pair/remainder split, direction typing -- can be tested without an AES @@ -15,7 +15,7 @@ use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Algorithm, BlockPermutation, SecurityStrength}; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; /// Block and key length of the toy ciphers, chosen to match AES so the tests exercise the same /// shapes the real thing will. @@ -51,7 +51,7 @@ impl Algorithm for Toy { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } -impl BlockPermutation for Toy { +impl ElectronicCodeBook for Toy { fn new(key: &KeyMaterial) -> Result { validate(key)?; let mut bytes = [0u8; TOY_LEN]; @@ -89,7 +89,7 @@ impl Algorithm for SwappedPairToy { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } -impl BlockPermutation for SwappedPairToy { +impl ElectronicCodeBook for SwappedPairToy { fn new(key: &KeyMaterial) -> Result { Ok(Self { inner: Toy::new(key)? }) } diff --git a/crypto/modes/tests/sp800_38a_tests.rs b/crypto/modes/tests/sp800_38a_tests.rs index cec9404b..1dee9ac7 100644 --- a/crypto/modes/tests/sp800_38a_tests.rs +++ b/crypto/modes/tests/sp800_38a_tests.rs @@ -17,7 +17,7 @@ use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; @@ -91,7 +91,7 @@ fn key_material(hex_str: &str) -> KeyMaterial { /// implementor hook -- the vector should not care how the calls are grouped. fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) where - P: BlockPermutation, + P: ElectronicCodeBook, { let key = key_material::(key_hex); let iv = block(IV); @@ -138,7 +138,7 @@ where /// leaves a one-block remainder after the pair loop in `do_decrypt_blocks`. fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) where - P: BlockPermutation, + P: ElectronicCodeBook, { let key = key_material::(key_hex); let iv = block(IV); @@ -243,8 +243,8 @@ fn cbc_differs_from_ecb_by_the_iv() { // The raw permutation on P1 alone is the ECB answer from F.1.1. let mut ecb = block(PLAINTEXTS[0]); - >::encrypt_block( - &>::new(&key).unwrap(), + >::encrypt_block( + &>::new(&key).unwrap(), &mut ecb, ); assert_eq!(ecb, block("3ad77bb40d7a3660a89ecaf32466ef97"), "F.1.1 block #1");