From 0a0e5f3f3319750d2196ebaa731cbf61b72b2a1c Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 22:50:47 +1100 Subject: [PATCH 01/54] core: the symmetric, block and ECB key paths -- role pairs and mode pairs (prototype) Three more key paths beside the AEAD, packet and MAC ones, so that every mode in the tree can be reached from a mode-bound key: SymmetricEncryptionKey / SymmetricDecryptionKey over a SymmetricCipherEncryptor / Decryptor worker (padded CBC, CFB, CFB8, CTR), BlockEncryptionKey / BlockDecryptionKey over the whole-block pair (CBC with no padding layer), and EcbEncryptionKey / EcbDecryptionKey in hazmat, whose constructors take no RNG since ECB draws nothing. Each has its hazmat mode pair (SymmetricEncryptMode and friends) for a mode type to implement. The one-shots are provided over create_* and the worker's streaming methods; the symmetric pair's in-place forms are gated on the worker being a stream cipher. The workers are bounded on the cipher traits themselves, so KEY_LEN rides along until the constructors leave those traits. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core/src/hazmat/ecb_key.rs | 53 +++++++ crypto/core/src/hazmat/mod.rs | 6 +- crypto/core/src/hazmat/mode.rs | 108 ++++++++++++- crypto/core/src/provided.rs | 75 +++++++++ crypto/core/src/symmetric_key.rs | 246 +++++++++++++++++++++++++++++- 5 files changed, 479 insertions(+), 9 deletions(-) create mode 100644 crypto/core/src/hazmat/ecb_key.rs diff --git a/crypto/core/src/hazmat/ecb_key.rs b/crypto/core/src/hazmat/ecb_key.rs new file mode 100644 index 000000000..4812f0689 --- /dev/null +++ b/crypto/core/src/hazmat/ecb_key.rs @@ -0,0 +1,53 @@ +//! The ECB role pair: what a key bound to ECB can produce. Here rather than in +//! [`crate::symmetric_key`] because ECB is not a confidentiality mode for data, so a key for it +//! is reached through `hazmat` as the mode itself is. + +use crate::errors::SymmetricCipherError; +use crate::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; + +/// A key that can begin an ECB encryption: the permutation applied block by block, with no init +/// data and nothing to draw, so construction takes nothing and cannot fail. +/// +/// # 🚨 Security Considerations 🚨 +/// Equal plaintext blocks give equal ciphertext blocks; see [`crate::hazmat`] for the supported +/// uses. +pub trait EcbEncryptionKey { + /// The streaming encryptor this key produces. + type Encryptor: BlockCipherEncryptor; + + /// Begins an encryption. + fn create_encryptor(&self) -> Self::Encryptor; + + /// One-shot: encrypts `LEN` bytes, a whole number of blocks, in place, and returns the number + /// of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. + /// + /// # Errors + /// The encryptor's own refusals; ECB has none. + fn encrypt_inplace(&self, data: &mut [u8]) -> Result { + let (blocks, []) = data.as_chunks_mut::() else { + return Err(SymmetricCipherError::DataNotBlockAligned); + }; + self.create_encryptor().do_encrypt_blocks_inplace(blocks) + } +} + +/// A key that can begin an ECB decryption; see [`EcbEncryptionKey`]. +pub trait EcbDecryptionKey { + /// The streaming decryptor this key produces. + type Decryptor: BlockCipherDecryptor; + + /// Begins a decryption. + fn create_decryptor(&self) -> Self::Decryptor; + + /// One-shot: decrypts `LEN` bytes, a whole number of blocks, in place, and returns the number + /// of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. + /// + /// # Errors + /// The decryptor's own refusals; ECB has none. + fn decrypt_inplace(&self, data: &mut [u8]) -> Result { + let (blocks, []) = data.as_chunks_mut::() else { + return Err(SymmetricCipherError::DataNotBlockAligned); + }; + self.create_decryptor().do_decrypt_blocks_inplace(blocks) + } +} diff --git a/crypto/core/src/hazmat/mod.rs b/crypto/core/src/hazmat/mod.rs index 4d254ca6e..b8292167d 100644 --- a/crypto/core/src/hazmat/mod.rs +++ b/crypto/core/src/hazmat/mod.rs @@ -11,14 +11,18 @@ //! grep -rnE --include='*.rs' 'use .*::hazmat::' //! ``` +mod ecb_key; mod electronic_code_book; mod hazardous_operations; mod key_stream; mod mode; +pub use ecb_key::{EcbDecryptionKey, EcbEncryptionKey}; pub use electronic_code_book::ElectronicCodeBook; pub use hazardous_operations::do_hazardous_operations; pub use key_stream::KeyStream; pub use mode::{ - AeadDecryptMode, AeadEncryptMode, AeadPacketDecryptMode, AeadPacketEncryptMode, MacMode, Mode, + AeadDecryptMode, AeadEncryptMode, AeadPacketDecryptMode, AeadPacketEncryptMode, + BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbEncryptMode, MacMode, Mode, + SymmetricDecryptMode, SymmetricEncryptMode, }; diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index da4ded5f3..1c299468b 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -8,13 +8,20 @@ //! must commit to the lengths before its first byte -- CCM, whose `B0` carries the payload length //! and whose AAD is length-prefixed -- implements [`AeadPacketEncryptMode`] / //! [`AeadPacketDecryptMode`] instead, whose constructors take the AAD and payload lengths and -//! whose workers then need no buffer for either. A MAC mode -- CMAC -- implements [`MacMode`], -//! whose worker takes no nonce and serves both roles. +//! whose workers then need no buffer for either. The unauthenticated modes have the same two +//! shapes with no tag: [`SymmetricEncryptMode`] / [`SymmetricDecryptMode`] for a mode that takes +//! data of any length (padded CBC, CFB, CFB8, CTR), [`BlockEncryptMode`] / [`BlockDecryptMode`] +//! for one that takes whole blocks (CBC with no padding layer), and [`EcbEncryptMode`] / +//! [`EcbDecryptMode`] for ECB, which has no init data and so takes no RNG. A MAC mode -- CMAC -- +//! implements [`MacMode`], whose worker takes no nonce and serves both roles. use crate::errors::SymmetricCipherError; use crate::key_material::KeyType; use crate::symmetric_key::{AeadDecryptWorker, AeadEncryptWorker}; -use crate::traits::{MAC, RNG}; +use crate::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, MAC, RNG, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; /// A mode's per-key state over the permutation `P`. /// @@ -251,6 +258,101 @@ pub trait AeadPacketDecryptMode< } } +/// The encryption side of an unauthenticated mode over `P` that takes data of any length: the +/// mode's own constructor for its [`SymmetricCipherEncryptor`]. +pub trait SymmetricEncryptMode< + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, +>: Mode

+{ + /// The streaming encryptor this mode produces over `P`. + type Encryptor: SymmetricCipherEncryptor; + + /// Begins an encryption from `state` under init data drawn from `rng`. + /// + /// # Errors + /// [`SymmetricCipherError::RNGError`] if the draw fails; nothing else can. + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError>; +} + +/// The decryption side of an unauthenticated mode over `P` that takes data of any length. +pub trait SymmetricDecryptMode< + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, +>: Mode

+{ + /// The streaming decryptor this mode produces over `P`. + type Decryptor: SymmetricCipherDecryptor; + + /// Begins the decryption of a message encrypted under `init_data` from `state`. Infallible: + /// the state is already keyed and the init data length is in the type. + fn decrypt_init(state: &Self::KeyState, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; +} + +/// The encryption side of a whole-block mode over `P`: the mode's own constructor for its +/// [`BlockCipherEncryptor`]. +pub trait BlockEncryptMode< + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: Mode

+{ + /// The streaming encryptor this mode produces over `P`. + type Encryptor: BlockCipherEncryptor; + + /// Begins an encryption from `state` under init data drawn from `rng`. + /// + /// # Errors + /// [`SymmetricCipherError::RNGError`] if the draw fails; nothing else can. + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError>; +} + +/// The decryption side of a whole-block mode over `P`. +pub trait BlockDecryptMode< + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: Mode

+{ + /// The streaming decryptor this mode produces over `P`. + type Decryptor: BlockCipherDecryptor; + + /// Begins the decryption of a message encrypted under `init_data` from `state`. Infallible. + fn decrypt_init(state: &Self::KeyState, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; +} + +/// The encryption side of ECB over `P`: a whole-block mode with no init data, so its constructor +/// takes no RNG and cannot fail. Its own pair rather than [`BlockEncryptMode`] with +/// `INIT_DATA_LEN = 0`, so that nothing is handed an RNG it has no use for. +pub trait EcbEncryptMode: Mode

{ + /// The streaming encryptor this mode produces over `P`. + type Encryptor: BlockCipherEncryptor; + + /// Begins an encryption from `state`. + fn encrypt_init(state: &Self::KeyState) -> Self::Encryptor; +} + +/// The decryption side of ECB over `P`; see [`EcbEncryptMode`]. +pub trait EcbDecryptMode: Mode

{ + /// The streaming decryptor this mode produces over `P`. + type Decryptor: BlockCipherDecryptor; + + /// Begins a decryption from `state`. + fn decrypt_init(state: &Self::KeyState) -> Self::Decryptor; +} + /// A MAC mode over `P`: the mode's own constructor for its worker, which generates and verifies /// alike. The role traits [`crate::symmetric_key::MacGenerationKey`] and /// [`crate::symmetric_key::MacVerificationKey`] forward to it. diff --git a/crypto/core/src/provided.rs b/crypto/core/src/provided.rs index dfb554631..8a6f56e97 100644 --- a/crypto/core/src/provided.rs +++ b/crypto/core/src/provided.rs @@ -13,6 +13,7 @@ use crate::errors::SymmetricCipherError; use crate::symmetric_key::{AeadDecryptWorker, AeadEncryptWorker}; +use crate::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; /// Bytes copied out per step. Four 16-byte blocks keeps a block-cipher mode on its wide path. const CHUNK: usize = 64; @@ -254,3 +255,77 @@ pub(crate) fn inline_payload_len( ) -> Result { ciphertext_len.checked_sub(TAG_LEN).ok_or(SymmetricCipherError::DecryptionFailed) } + +/// Encrypts `plaintext` through `enc` into `ciphertext`, and returns the number of bytes written. +/// `ciphertext` is zeroized first and must hold `encrypt_out_len(plaintext.len())` bytes. The +/// unauthenticated counterpart of [`encrypt_inline`], for the symmetric role keys. +/// +/// # Errors +/// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, or any error of +/// the worker's streaming methods. +pub(crate) fn symmetric_encrypt_out< + E, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, +>( + mut enc: E, + plaintext: &[u8], + ciphertext: &mut [u8], +) -> Result +where + E: SymmetricCipherEncryptor, +{ + ciphertext.fill(0); + let needed = E::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let written = enc.do_encrypt_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_encrypt_final()?; + // `encrypt_out_len` is exactly `written + last_len`, so this fits in `ciphertext[..needed]`. + ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); + Ok(written + last_len) +} + +/// Decrypts `ciphertext` through `dec` into `plaintext`, and returns the number of bytes +/// written. `plaintext` is zeroized first, must hold `decrypt_out_len(ciphertext.len())` bytes, +/// and is zeroized again before an error is returned. The unauthenticated counterpart of +/// [`decrypt_inline`]. +/// +/// # Errors +/// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, or any error of +/// the worker's streaming methods. +pub(crate) fn symmetric_decrypt_out< + D, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, +>( + mut dec: D, + ciphertext: &[u8], + plaintext: &mut [u8], +) -> Result +where + D: SymmetricCipherDecryptor, +{ + plaintext.fill(0); + let needed = D::decrypt_out_len(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let written = dec.do_decrypt_out(ciphertext, plaintext)?; + match dec.do_decrypt_final() { + Ok((last, data_len)) => { + // `decrypt_out_len` bounds `written + data_len`, so this fits in `plaintext[..needed]`. + plaintext[written..written + data_len].copy_from_slice(&last[..data_len]); + Ok(written + data_len) + } + Err(e) => { + // A padding failure says the stream was not what it claimed to be; this one-shot owns + // the whole message, so it does not leave the decrypted prefix in the caller's hands. + plaintext[..written].fill(0); + Err(e) + } + } +} diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index b97c74b3d..879a13510 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -13,14 +13,20 @@ //! A key that lives in a hardware token implements neither; it has its own way in. //! * **Roles**, one trait per direction: [`AeadEncryptionKey`] / [`AeadDecryptionKey`], their //! packet forms [`AeadPacketEncryptionKey`] / [`AeadPacketDecryptionKey`] for a mode that must -//! know the lengths up front, and [`MacGenerationKey`] / [`MacVerificationKey`]. A key type +//! know the lengths up front, [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] for the +//! unauthenticated modes that take data of any length (padded CBC, CFB, CFB8, CTR), +//! [`BlockEncryptionKey`] / [`BlockDecryptionKey`] for the whole-block modes, the ECB pair in +//! [`crate::hazmat`], and [`MacGenerationKey`] / [`MacVerificationKey`]. A key type //! implements the sides it is allowed to use, so a decrypt-only or verify-only key refuses the //! other side at compile time. The one-shots are provided methods over `create_*` and the -//! worker's streaming methods, which [`AeadEncryptWorker`] / [`AeadDecryptWorker`] describe. +//! worker's streaming methods, which [`AeadEncryptWorker`] / [`AeadDecryptWorker`] describe +//! for the AEAD pair and the cipher traits in [`crate::traits`] for the rest. //! //! Which role a cipher key carries is decided by its mode type through the mode traits in -//! [`crate::hazmat`] ([`Mode`], [`AeadEncryptMode`], [`AeadDecryptMode`] and their packet -//! forms); the permutation crate connects the two with a forwarding impl per role. +//! [`crate::hazmat`] ([`Mode`], [`AeadEncryptMode`], [`AeadDecryptMode`] and their packet, +//! symmetric, block and ECB forms); the permutation crate connects the two with a forwarding +//! impl per role. A mode implements exactly one pair, so `key.create_encryptor(..)` on a key +//! bound to it is never ambiguous. //! //! ```text //! let key = AES128Key::::generate(&mut rng)?; @@ -42,7 +48,10 @@ use crate::errors::{MACError, SymmetricCipherError}; use crate::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; #[cfg(feature = "std")] use crate::traits::AEADEncryptedTuple; -use crate::traits::{MAC, RNG}; +use crate::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, MAC, RNG, StreamCipherDecryptor, + StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; /// Construction of a key object from exactly `KEY_LEN` bytes of key material. /// @@ -862,6 +871,233 @@ pub trait AeadPacketDecryptionKey< } } +/// A key that can begin an unauthenticated encryption of data of any length: the encryption +/// half of a key for padded CBC, CFB, CFB8 or CTR. The encryptor is the mode's ordinary worker, a +/// [`SymmetricCipherEncryptor`]; this trait adds where it comes from, and the one-shots are +/// provided over [`create_encryptor`](Self::create_encryptor) and the worker's streaming methods. +/// +/// The init data (an IV or nonce) is drawn from the RNG the key is given and returned; there is +/// no form that takes one, since these modes need it unpredictable (SP 800-38A Appendix C). +pub trait SymmetricEncryptionKey< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, +> +{ + /// The streaming encryptor this key produces. + type Encryptor: SymmetricCipherEncryptor; + + /// Begins an encryption under init data drawn from `rng`, which is returned alongside the + /// encryptor and must reach the recipient, who passes it to + /// [`SymmetricDecryptionKey::create_decryptor`]. + /// + /// # Errors + /// [`SymmetricCipherError::RNGError`] if the draw fails; nothing else can. + fn create_encryptor( + &self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + + /// One-shot: encrypts `plaintext` into `ciphertext`, which needs `encrypt_out_len` bytes of + /// the encryptor type, and returns the number of bytes written with the generated init data. + /// The entire output buffer is zeroized before the ciphertext is written. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, or any error + /// of [`create_encryptor`](Self::create_encryptor) and the encryptor's streaming methods. + fn encrypt_out( + &self, + rng: &mut dyn RNG, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (enc, init_data) = self.create_encryptor(rng)?; + let written = crate::provided::symmetric_encrypt_out(enc, plaintext, ciphertext)?; + Ok((written, init_data)) + } + + /// As [`encrypt_out`](Self::encrypt_out), allocating the ciphertext. + /// + /// # Errors + /// As [`encrypt_out`](Self::encrypt_out). + #[cfg(feature = "std")] + fn encrypt( + &self, + rng: &mut dyn RNG, + plaintext: &[u8], + ) -> Result<([u8; INIT_DATA_LEN], Vec), SymmetricCipherError> { + let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_out_len(plaintext.len())]; + let (written, init_data) = self.encrypt_out(rng, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((init_data, ciphertext)) + } + + /// One-shot in place, for a mode whose encryptor is a stream cipher: encrypts `data` in its + /// own buffer under init data drawn from `rng`, and returns the number of bytes written with + /// the init data. + /// + /// # Errors + /// As [`create_encryptor`](Self::create_encryptor), or + /// [`StreamCipherEncryptor::do_encrypt_inplace`]'s. + fn encrypt_inplace( + &self, + rng: &mut dyn RNG, + data: &mut [u8], + ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> + where + Self::Encryptor: StreamCipherEncryptor, + { + let (mut enc, init_data) = self.create_encryptor(rng)?; + let written = StreamCipherEncryptor::do_encrypt_inplace(&mut enc, data)?; + Ok((written, init_data)) + } +} + +/// A key that can begin an unauthenticated decryption of data of any length; see +/// [`SymmetricEncryptionKey`]. Construction is infallible: the key is already validated and the +/// init data length is in the type. +pub trait SymmetricDecryptionKey< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, +> +{ + /// The streaming decryptor this key produces. + type Decryptor: SymmetricCipherDecryptor; + + /// Begins the decryption of a message whose encryptor returned `init_data`. + fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; + + /// One-shot: decrypts `ciphertext` under `init_data` into `plaintext`, which needs + /// `decrypt_out_len(ciphertext.len())` bytes of the decryptor type, and returns the number + /// of plaintext bytes written. The entire output buffer is zeroized before the plaintext is + /// written, and again before an error is returned. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, or any error + /// of the decryptor's streaming methods: a malformed ciphertext or bad padding. + fn decrypt_out( + &self, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + crate::provided::symmetric_decrypt_out( + self.create_decryptor(init_data), + ciphertext, + plaintext, + ) + } + + /// As [`decrypt_out`](Self::decrypt_out), allocating the plaintext. + /// + /// # Errors + /// As [`decrypt_out`](Self::decrypt_out). + #[cfg(feature = "std")] + fn decrypt( + &self, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[u8], + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; Self::Decryptor::decrypt_out_len(ciphertext.len())]; + let written = self.decrypt_out(init_data, ciphertext, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } + + /// One-shot in place, for a mode whose decryptor is a stream cipher: decrypts `data` in its + /// own buffer under `init_data`, and returns the number of bytes written. + /// + /// # Errors + /// [`StreamCipherDecryptor::do_decrypt_inplace`]'s. + fn decrypt_inplace( + &self, + init_data: &[u8; INIT_DATA_LEN], + data: &mut [u8], + ) -> Result + where + Self::Decryptor: StreamCipherDecryptor, + { + let mut dec = self.create_decryptor(init_data); + StreamCipherDecryptor::do_decrypt_inplace(&mut dec, data) + } +} + +/// A key that can begin a whole-block encryption: the encryption half of a key for CBC without +/// a padding layer. The encryptor is a [`BlockCipherEncryptor`], whose buffers are +/// block-aligned arrays checked at compile time; data of arbitrary length goes through a padded +/// mode and [`SymmetricEncryptionKey`] instead. +pub trait BlockEncryptionKey< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> +{ + /// The streaming encryptor this key produces. + type Encryptor: BlockCipherEncryptor; + + /// Begins an encryption under init data drawn from `rng`, which is returned alongside the + /// encryptor and must reach the recipient, who passes it to + /// [`BlockDecryptionKey::create_decryptor`]. + /// + /// # Errors + /// [`SymmetricCipherError::RNGError`] if the draw fails; nothing else can. + fn create_encryptor( + &self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + + /// One-shot: encrypts `LEN` bytes, a whole number of blocks, in place under init data drawn + /// from `rng`, and returns the number of bytes written with the init data. `LEN % BLOCK_LEN + /// == 0` is checked at compile time, as for [`BlockCipherEncryptor::do_encrypt_inplace`]. + /// + /// # Errors + /// As [`create_encryptor`](Self::create_encryptor), or the encryptor's own refusals. + fn encrypt_inplace( + &self, + rng: &mut dyn RNG, + data: &mut [u8], + ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (blocks, []) = data.as_chunks_mut::() else { + return Err(SymmetricCipherError::DataNotBlockAligned); + }; + let (mut enc, init_data) = self.create_encryptor(rng)?; + let written = enc.do_encrypt_blocks_inplace(blocks)?; + Ok((written, init_data)) + } +} + +/// A key that can begin a whole-block decryption; see [`BlockEncryptionKey`]. Construction is +/// infallible: the key is already validated and the init data length is in the type. +pub trait BlockDecryptionKey< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> +{ + /// The streaming decryptor this key produces. + type Decryptor: BlockCipherDecryptor; + + /// Begins the decryption of a message whose encryptor returned `init_data`. + fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; + + /// One-shot: decrypts `LEN` bytes, a whole number of blocks, in place under `init_data`, and + /// returns the number of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. + /// + /// # Errors + /// The decryptor's own refusals. + fn decrypt_inplace( + &self, + init_data: &[u8; INIT_DATA_LEN], + data: &mut [u8], + ) -> Result { + let (blocks, []) = data.as_chunks_mut::() else { + return Err(SymmetricCipherError::DataNotBlockAligned); + }; + self.create_decryptor(init_data).do_decrypt_blocks_inplace(blocks) + } +} + // The MAC worker is bounded on `MAC` as a whole for now: splitting its finals into `MACGenerator` // and `MACVerifier` while `MAC` still carries them would put the same method names on two traits, // which is ambiguous at every call site that has both in scope (`do_update` is on `MAC`, `do_final` From 6772d72098e1c21c552f7c508746bd30c88610e4 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 22:50:47 +1100 Subject: [PATCH 02/54] core-test-framework: key-generic runners and the construction suites (prototype) test_key variants of the symmetric, stream, block, AEAD and tagged-layout runners and the MAC runner, each driving a role-trait key with a caller-supplied RNG, plus an ECB variant that takes none. The key-type, algorithm-binding and strength checks those runners made at every do_*_init move to TestFrameworkFromKeyMaterial and TestFrameworkFromAnyKeyMaterial, which check them once, where the key is made. The E/D-generic runners stay until the constructors leave the cipher traits. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core-test-framework/src/aead.rs | 723 +++++++++++++++++- .../core-test-framework/src/block_cipher.rs | 192 ++++- .../src/from_key_material.rs | 217 ++++++ crypto/core-test-framework/src/lib.rs | 1 + crypto/core-test-framework/src/mac.rs | 71 ++ .../src/symmetric_ciphers.rs | 457 ++++++++++- 6 files changed, 1658 insertions(+), 3 deletions(-) create mode 100644 crypto/core-test-framework/src/from_key_material.rs diff --git a/crypto/core-test-framework/src/aead.rs b/crypto/core-test-framework/src/aead.rs index 520c16d36..94054341b 100644 --- a/crypto/core-test-framework/src/aead.rs +++ b/crypto/core-test-framework/src/aead.rs @@ -19,7 +19,10 @@ use crate::symmetric_ciphers::TestFrameworkSymmetricCipher; use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle_core::symmetric_key::{ + AeadDecryptWorker, AeadDecryptionKey, AeadEncryptWorker, AeadEncryptionKey, +}; +use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor, RNG}; /// Instance of the test framework. pub struct TestFrameworkAEADCipher { @@ -764,6 +767,557 @@ impl TestFrameworkAEADCipher { // The key-type and security-strength checks on `do_encrypt_init` / `do_decrypt_init` are // covered by the `TestFrameworkSymmetricCipher` suite run above. } + /// The [`AeadEncryptionKey`] / [`AeadDecryptionKey`] form of + /// [`test_encryptor_decryptor`](Self::test_encryptor_decryptor): the same AAD and tag + /// contract reached through a key object, with `rng` supplying every nonce draw that is not + /// pinned. The key-policy checks are `TestFrameworkFromKeyMaterial`'s, and the no-AAD + /// symmetric contract is reached here as the AEAD one with an empty AAD. + /// + /// A key whose workers take one message length ([`fixed_message_len`](Self::fixed_message_len)) + /// takes it through the one-shots too, since those are written over the workers; every other + /// length is checked to be refused. + pub fn test_key< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + K: AeadEncryptionKey + + AeadDecryptionKey, + >( + &self, + key: &K, + rng: &mut dyn RNG, + ) { + assert!( + FINAL_LEN >= TAG_LEN, + "FINAL_LEN must have room for the inline tag the decryptor holds back" + ); + let encrypt_out_len = |len: usize| K::Encryptor::encrypt_out_len(len); + let encrypt_detached_out_len = |len: usize| K::Encryptor::encrypt_detached_out_len(len); + let decrypt_out_len = |len: usize| K::Decryptor::decrypt_out_len(len); + let decrypt_detached_out_len = |len: usize| K::Decryptor::decrypt_detached_out_len(len); + let aad: &[u8] = b"some associated data"; + let pinned = [0xA5u8; NONCE_LEN]; + let fixed_rng = || FixedSeedRNG::::new(pinned); + + // one-shot round trip, every length up to a few times the tag length (and up to the fixed + // length, if there is one) + let max_len = (3 * TAG_LEN.max(1) + 5).max(self.fixed_message_len.unwrap_or(0)); + assert!(max_len <= DUMMY_SEED.len(), "the fixed message length must fit the seed buffer"); + for len in 0..=max_len { + let msg = &DUMMY_SEED[..len]; + // a fixed-length key takes only that length, one-shot or streamed + if let Some(fixed) = self.fixed_message_len + && fixed != len + { + let mut ct = vec![0u8; encrypt_detached_out_len(len)]; + assert!( + key.encrypt_detached_out(rng, aad, msg, &mut ct).is_err(), + "fixed length: a {len}-byte detached one-shot must be refused" + ); + let mut inline = vec![0u8; encrypt_out_len(len)]; + assert!( + key.encrypt_with_aad_out(rng, aad, msg, &mut inline).is_err(), + "fixed length: a {len}-byte inline one-shot must be refused" + ); + let (mut enc, _) = key.create_encryptor(rng).unwrap(); + enc.do_update_aad(aad).unwrap(); + let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; + assert!( + enc.do_encrypt_out(msg, &mut buf).is_err() || enc.do_encrypt_final().is_err(), + "fixed length: a {len}-byte stream must be refused" + ); + continue; + } + let mut ct = vec![0u8; encrypt_detached_out_len(len)]; + let (ct_len, nonce, tag) = key.encrypt_detached_out(rng, aad, msg, &mut ct).unwrap(); + ct.truncate(ct_len); + assert_ne!(tag, [0u8; TAG_LEN], "len {len}: the tag must not be all zeros"); + if len >= 8 { + assert_ne!(&ct[..], msg, "len {len}: the ciphertext must not be the plaintext"); + } + let mut pt = vec![0u8; decrypt_detached_out_len(ct.len())]; + let pt_len = key.decrypt_detached_out(&nonce, aad, &ct, &tag, &mut pt).unwrap(); + pt.truncate(pt_len); + assert_eq!(&pt[..], msg, "one-shot round trip, len {len}"); + + // the std one-shots agree with the _out ones for the same nonce + let (nonce2, ct2, tag2) = key.encrypt_detached(rng, aad, msg).unwrap(); + assert_eq!(ct2.len(), ct_len, "encrypt_detached must return exactly the bytes written"); + let pt2 = key.decrypt_detached(&nonce2, aad, &ct2, &tag2).unwrap(); + assert_eq!(pt2, msg, "std round trip, len {len}"); + let pt3 = key.decrypt_detached(&nonce, aad, &ct, &tag).unwrap(); + assert_eq!(pt3, msg, "decrypt_detached must agree with decrypt_detached_out"); + + // the inline `ciphertext || tag` layout with AAD must be exactly the detached + // ciphertext with the tag appended -- the same bytes under the same nonce -- and both + // the one-shot and the streaming finalizer must round trip it. + let mut detached = vec![0u8; encrypt_detached_out_len(len)]; + let (detached_len, pinned_nonce, detached_tag) = + key.encrypt_detached_out(&mut fixed_rng(), aad, msg, &mut detached).unwrap(); + assert_eq!(pinned_nonce, pinned, "a fixed RNG must yield its stream as the nonce"); + detached.truncate(detached_len); + detached.extend_from_slice(&detached_tag); + + let mut inline = vec![0u8; encrypt_out_len(len)]; + let (inline_len, inline_nonce) = + key.encrypt_with_aad_out(rng, aad, msg, &mut inline).unwrap(); + assert_eq!( + inline_len, + encrypt_detached_out_len(len) + TAG_LEN, + "encrypt_with_aad_out must write the ciphertext plus the tag, len {len}" + ); + let mut pt4 = vec![0u8; decrypt_out_len(inline_len)]; + let pt4_len = key + .decrypt_with_aad_out(&inline_nonce, aad, &inline[..inline_len], &mut pt4) + .unwrap(); + assert_eq!(&pt4[..pt4_len], msg, "tagged one-shot round trip, len {len}"); + + // the pinned inline one-shot, into a roomy buffer and an exact one + let mut inline_rng = vec![0u8; encrypt_out_len(len) + 3]; + let (rng_len, rng_nonce) = + key.encrypt_with_aad_out(&mut fixed_rng(), aad, msg, &mut inline_rng).unwrap(); + assert_eq!(rng_nonce, pinned_nonce, "the same RNG stream must give the same nonce"); + assert_eq!( + &inline_rng[..rng_len], + &detached[..], + "len {len}: the inline layout must be the detached ciphertext and its tag" + ); + let mut exact = vec![0u8; encrypt_out_len(len)]; + let (exact_len, _) = + key.encrypt_with_aad_out(&mut fixed_rng(), aad, msg, &mut exact).unwrap(); + assert_eq!(&exact[..exact_len], &detached[..], "len {len}: exact-size buffer"); + + // the supplied-nonce forms: the nonce the pinned RNG produced, supplied directly, + // must give the same bytes from the one-shots, the allocating one-shots, the in-place + // one-shots and the streaming constructor alike + let mut by_nonce = vec![0u8; encrypt_detached_out_len(len)]; + let (n_len, n_tag) = + key.encrypt_detached_nonce_out(&pinned_nonce, aad, msg, &mut by_nonce).unwrap(); + by_nonce.truncate(n_len); + by_nonce.extend_from_slice(&n_tag); + assert_eq!(by_nonce, detached, "len {len}: encrypt_detached_nonce_out under the nonce"); + let mut by_nonce = vec![0u8; encrypt_out_len(len)]; + let n_len = + key.encrypt_with_aad_nonce_out(&pinned_nonce, aad, msg, &mut by_nonce).unwrap(); + assert_eq!(&by_nonce[..n_len], &detached[..], "len {len}: encrypt_with_aad_nonce_out"); + let mut short = vec![0u8; encrypt_out_len(len) - 1]; + match key.encrypt_with_aad_nonce_out(&pinned_nonce, aad, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, encrypt_out_len(len)) + } + other => panic!("encrypt_with_aad_nonce_out into a short buffer: {other:?}"), + } + let (mut alloc_ct, alloc_tag) = + key.encrypt_detached_nonce(&pinned_nonce, aad, msg).unwrap(); + alloc_ct.extend_from_slice(&alloc_tag); + assert_eq!(alloc_ct, detached, "len {len}: encrypt_detached_nonce"); + assert_eq!( + key.encrypt_with_aad_nonce(&pinned_nonce, aad, msg).unwrap(), + detached, + "len {len}: encrypt_with_aad_nonce" + ); + let mut enc_n = key.create_encryptor_nonce(&pinned_nonce); + enc_n.do_update_aad(aad).unwrap(); + let mut streamed = vec![0u8; enc_n.do_encrypt_out_len(len)]; + let w = enc_n.do_encrypt_out(msg, &mut streamed).unwrap(); + streamed.truncate(w); + let (last_n, last_n_len) = enc_n.do_encrypt_final().unwrap(); + streamed.extend_from_slice(&last_n[..last_n_len]); + assert_eq!(streamed, detached, "len {len}: create_encryptor_nonce streaming"); + let mut short = vec![0u8; encrypt_out_len(len) - 1]; + match key.encrypt_with_aad_out(&mut fixed_rng(), aad, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, encrypt_out_len(len)) + } + other => panic!("encrypt_with_aad_out into a short buffer: {other:?}"), + } + let (alloc_nonce, alloc_ct) = key.encrypt_with_aad(rng, aad, msg).unwrap(); + assert_eq!( + alloc_ct.len(), + inline_len, + "encrypt_with_aad must return the bytes written" + ); + let alloc_pt = key.decrypt_with_aad(&alloc_nonce, aad, &alloc_ct).unwrap(); + assert_eq!(alloc_pt, msg, "allocating inline-with-AAD round trip, len {len}"); + + // the in-place one-shots, drawing and supplied-nonce + let mut buf = msg.to_vec(); + let (ip_nonce, ip_tag) = + key.encrypt_detached_in_place(&mut fixed_rng(), aad, &mut buf).unwrap(); + assert_eq!(ip_nonce, pinned_nonce, "len {len}: in place, the pinned nonce"); + assert_eq!((&buf[..], ip_tag), (&detached[..len], detached_tag), "len {len}: in place"); + key.decrypt_detached_in_place(&ip_nonce, aad, &mut buf, &ip_tag).unwrap(); + assert_eq!(&buf[..], msg, "len {len}: in-place round trip"); + let ip_tag = key.encrypt_detached_in_place_nonce(&pinned_nonce, aad, &mut buf).unwrap(); + assert_eq!((&buf[..], ip_tag), (&detached[..len], detached_tag), "len {len}: in place"); + key.decrypt_detached_in_place(&pinned_nonce, aad, &mut buf, &ip_tag).unwrap(); + assert_eq!(&buf[..], msg, "len {len}: in-place round trip under the nonce"); + + // the streaming constructor under the pinned RNG draws the pinned nonce, and the + // inline stream decrypts + let (mut enc5, nonce5) = key.create_encryptor(&mut fixed_rng()).unwrap(); + assert_eq!(nonce5, pinned_nonce, "the same RNG stream must give the same nonce"); + enc5.do_update_aad(aad).unwrap(); + let mut inline5 = vec![0u8; enc5.do_encrypt_out_len(len)]; + let written5 = enc5.do_encrypt_out(msg, &mut inline5).unwrap(); + inline5.truncate(written5); + let (last5, last5_len) = enc5.do_encrypt_final().unwrap(); + inline5.extend_from_slice(&last5[..last5_len]); + assert_eq!( + inline5.len(), + inline_len, + "tagged streaming must write as much as the one-shot" + ); + assert_eq!( + inline5, detached, + "len {len}: the inline layout must be the detached ciphertext followed by its tag" + ); + let mut dec5 = key.create_decryptor(&nonce5); + dec5.do_update_aad(aad).unwrap(); + let mut pt5 = vec![0u8; dec5.do_decrypt_out_len(inline5.len())]; + let got5 = dec5.do_decrypt_out(&inline5, &mut pt5).unwrap(); + pt5.truncate(got5); + let (last, data_len) = dec5.do_decrypt_final().unwrap(); + pt5.extend_from_slice(&last[..data_len]); + assert_eq!(pt5, msg, "tagged streaming round trip, len {len}"); + + // a stream that ends before a whole tag has been seen is a failed decryption + if TAG_LEN > 0 { + let mut dec6 = key.create_decryptor(&nonce5); + dec6.do_update_aad(aad).unwrap(); + let short = &inline5[..TAG_LEN - 1]; + let mut scratch = vec![0u8; dec6.do_decrypt_out_len(short.len())]; + dec6.do_decrypt_out(short, &mut scratch).unwrap(); + assert!( + matches!(dec6.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), + "a stream shorter than the tag must be DecryptionFailed, len {len}" + ); + } + + // too-short output buffers on the one-shots are refused with the required length, + // before any work is done + let need = encrypt_detached_out_len(len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match key.encrypt_detached_out(rng, aad, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("encrypt_detached_out into a short buffer: {other:?}"), + } + let mut roomy = vec![0u8; need + 3]; + let (n, _, _) = key.encrypt_detached_out(rng, aad, msg, &mut roomy).unwrap(); + assert_eq!(n, need, "encrypt_detached_out into a roomy buffer"); + } + let need = encrypt_out_len(len); + let mut short = vec![0u8; need - 1]; + match key.encrypt_with_aad_out(rng, aad, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("encrypt_with_aad_out into a short buffer: {other:?}"), + } + let need = decrypt_detached_out_len(ct.len()); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match key.decrypt_detached_out(&nonce, aad, &ct, &tag, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("decrypt_detached_out into a short buffer: {other:?}"), + } + } + let need = decrypt_out_len(inline_len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match key.decrypt_with_aad_out(&inline_nonce, aad, &inline, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("decrypt_with_aad_out into a short buffer: {other:?}"), + } + } + } + + // streaming in every chunking agrees with the one-shot, for both the AAD and the data. + let msg = &DUMMY_SEED[..self.fixed_message_len.unwrap_or(max_len.max(17))]; + let mut ct_ref = vec![0u8; encrypt_detached_out_len(msg.len())]; + let (ct_ref_len, nonce_ref, tag_ref) = + key.encrypt_detached_out(&mut fixed_rng(), aad, msg, &mut ct_ref).unwrap(); + ct_ref.truncate(ct_ref_len); + + for chunk in [1usize, 2, 3, 7, TAG_LEN.max(1), TAG_LEN + 1, msg.len().max(1)] { + let (mut enc, nonce) = key.create_encryptor(&mut fixed_rng()).unwrap(); + assert_eq!(nonce, nonce_ref, "the same RNG stream must give the same nonce"); + for piece in aad.chunks(chunk) { + enc.do_update_aad(piece).unwrap(); + } + let mut ct = Vec::new(); + for piece in msg.chunks(chunk) { + let expect = enc.do_encrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}: update_out_len must be exact (encrypt)"); + ct.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf).unwrap(); + assert!( + final_len + TAG_LEN <= FINAL_LEN, + "chunk {chunk}: the detached flush must leave FINAL_LEN room for the tag" + ); + ct.extend_from_slice(&final_buf[..final_len]); + assert_eq!(ct, ct_ref, "chunk {chunk}: streaming must give the one-shot ciphertext"); + assert_eq!(tag, tag_ref, "chunk {chunk}: streaming must give the one-shot tag"); + + let mut dec = key.create_decryptor(&nonce); + for piece in aad.chunks(chunk) { + dec.do_update_aad(piece).unwrap(); + } + let mut pt = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}: update_out_len must be exact (decrypt)"); + pt.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_decrypt_final_detached_out(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(pt, msg, "chunk {chunk}: streaming round trip"); + } + + // a wrong detached tag is refused at the final + let mut wrong_tag = tag_ref; + wrong_tag[0] ^= 0xFF; + let mut dec = key.create_decryptor(&nonce_ref); + dec.do_update_aad(aad).unwrap(); + let mut pt = vec![0u8; dec.do_decrypt_out_len(ct_ref.len())]; + dec.do_decrypt_out(&ct_ref, &mut pt).unwrap(); + let mut last = [0u8; FINAL_LEN]; + assert!( + matches!( + dec.do_decrypt_final_detached_out(&wrong_tag, &mut last), + Err(SymmetricCipherError::AEADTagCheckFailed) + ), + "do_decrypt_final_detached_out must check the tag" + ); + assert!(last.iter().all(|&b| b == 0), "a failed detached final must leave zeros"); + + // an empty AAD is a no-op: it must give exactly what absorbing no AAD at all gives, and + // the inline layout with an empty AAD is the no-AAD symmetric contract + let mut with_empty = vec![0u8; encrypt_detached_out_len(msg.len())]; + let (len_empty, nonce_empty, tag_empty) = + key.encrypt_detached_out(&mut fixed_rng(), b"", msg, &mut with_empty).unwrap(); + with_empty.truncate(len_empty); + let mut without = vec![0u8; encrypt_detached_out_len(msg.len())]; + let (len_none, nonce_none, tag_none) = + key.encrypt_detached_out(&mut fixed_rng(), &[], msg, &mut without).unwrap(); + without.truncate(len_none); + assert_eq!(nonce_empty, nonce_none); + assert_eq!(tag_empty, tag_none, "an empty AAD must be a no-op"); + assert_eq!(with_empty, without, "an empty AAD must be a no-op"); + let mut plain = vec![0u8; encrypt_out_len(msg.len())]; + let (len_plain, nonce_plain) = + key.encrypt_with_aad_out(&mut fixed_rng(), &[], msg, &mut plain).unwrap(); + assert_eq!(nonce_plain, nonce_none); + assert_eq!(&plain[..len_plain - TAG_LEN], &without[..], "no-AAD inline ciphertext"); + assert_eq!(&plain[len_plain - TAG_LEN..len_plain], &tag_none, "no-AAD inline tag"); + + // a message with no data at all still authenticates its AAD + if self.fixed_message_len.is_none_or(|fixed| fixed == 0) { + let (_ct_len, nonce, tag) = key.encrypt_detached_out(rng, aad, &[], &mut []).unwrap(); + key.decrypt_detached_out(&nonce, aad, &[], &tag, &mut []).unwrap(); + match key.decrypt_detached_out(&nonce, b"different associated data", &[], &tag, &mut []) + { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("an empty message must still authenticate its AAD, got {other:?}"), + }; + } + + // the AAD phase is over once data has been fed in -- on both sides, and on the decrypting + // side even when all of it is still being held back as a possible tag. + if !msg.is_empty() { + let (mut enc, nonce) = key.create_encryptor(rng).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(msg.len())]; + enc.do_encrypt_out(msg, &mut ct).unwrap(); + match enc.do_update_aad(aad) { + Err(SymmetricCipherError::StateError(_)) => { /* good */ } + other => panic!("AAD after data must be refused, got {other:?}"), + }; + enc.do_update_aad(b"").unwrap(); + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf).unwrap(); + ct.extend_from_slice(&final_buf[..final_len]); + + let mut dec = key.create_decryptor(&nonce); + let mut pt = vec![0u8; dec.do_decrypt_out_len(1)]; + let mut got = dec.do_decrypt_out(&ct[..1], &mut pt).unwrap(); + pt.truncate(got); + match dec.do_update_aad(aad) { + Err(SymmetricCipherError::StateError(_)) => { /* good */ } + other => panic!("AAD after data must be refused, got {other:?}"), + }; + dec.do_update_aad(b"").unwrap(); + let mut rest = vec![0u8; dec.do_decrypt_out_len(ct.len() - 1)]; + got = dec.do_decrypt_out(&ct[1..], &mut rest).unwrap(); + pt.extend_from_slice(&rest[..got]); + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_decrypt_final_detached_out(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(&pt[..], msg, "a refused do_update_aad must not disturb the state"); + } + + // tampering: every one of these must fail the tag check, and the one-shots must leave no + // plaintext behind when they do. + let msg = &DUMMY_SEED[..self.fixed_message_len.unwrap_or(max_len.max(17))]; + let mut ct = vec![0u8; encrypt_detached_out_len(msg.len())]; + let (ct_len, nonce, tag) = key.encrypt_detached_out(rng, aad, msg, &mut ct).unwrap(); + ct.truncate(ct_len); + + if ct.len() > 3 { + let mut tampered = ct.clone(); + tampered[3] ^= 0xFF; + let mut buf = vec![0u8; decrypt_detached_out_len(tampered.len())]; + match key.decrypt_detached_out(&nonce, aad, &tampered, &tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified ciphertext must fail the tag check, got {other:?}"), + }; + assert!( + buf.iter().all(|&b| b == 0), + "the one-shot decrypt must zeroize the buffer when the tag check fails" + ); + let mut buf = tampered.clone(); + match key.decrypt_detached_in_place(&nonce, aad, &mut buf, &tag) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified ciphertext must fail in place too, got {other:?}"), + }; + assert!( + buf.iter().all(|&b| b == 0), + "the in-place decrypt must zeroize the buffer when the tag check fails" + ); + } + + let mut tampered_inline = ct.clone(); + tampered_inline.extend_from_slice(&tag); + tampered_inline[3] ^= 0xFF; + for with_aad in [false, true] { + let mut buf = vec![0u8; decrypt_out_len(tampered_inline.len())]; + let aad_used: &[u8] = if with_aad { aad } else { &[] }; + // Without the AAD the tag was never going to verify; either way what matters is the + // failure and the zeroized buffer. + match key.decrypt_with_aad_out(&nonce, aad_used, &tampered_inline, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified inline ciphertext must fail, got {other:?}"), + }; + assert!( + buf.iter().all(|&b| b == 0), + "the inline one-shot (aad {with_aad}) must zeroize the buffer on a failed check" + ); + } + match key.decrypt_with_aad(&nonce, aad, &tampered_inline) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("decrypt_with_aad of a modified ciphertext must fail, got {other:?}"), + }; + + let mut wrong_tag = tag; + wrong_tag[0] ^= 0xFF; + let mut buf = vec![0u8; decrypt_detached_out_len(ct.len())]; + match key.decrypt_detached_out(&nonce, aad, &ct, &wrong_tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified tag must fail the tag check, got {other:?}"), + }; + + let mut buf = vec![0u8; decrypt_detached_out_len(ct.len())]; + match key.decrypt_detached_out( + &nonce, + b"not the right associated data", + &ct, + &tag, + &mut buf, + ) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified AAD must fail the tag check, got {other:?}"), + }; + + if NONCE_LEN > 0 { + let mut wrong_nonce = nonce; + wrong_nonce[0] ^= 0xFF; + let mut buf = vec![0u8; decrypt_detached_out_len(ct.len())]; + match key.decrypt_detached_out(&wrong_nonce, aad, &ct, &tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified nonce must fail the tag check, got {other:?}"), + }; + + // two encryptions under the same key must not reuse a nonce + let (_enc1, nonce1) = key.create_encryptor(rng).unwrap(); + let (_enc2, nonce2) = key.create_encryptor(rng).unwrap(); + assert_ne!(nonce1, nonce2); + } + + // Output-buffer contract for the AEAD `_out` methods: a buffer larger than needed is + // accepted, the returned count is what that call wrote, and every byte past it is zeroed. + const SENTINEL: u8 = 0xA5; + const EXTRA: usize = 7; + let assert_tail_zeroed = |buf: &[u8], n: usize, what: &str| { + assert!( + n <= buf.len(), + "{what}: claims {n} bytes written to a {}-byte buffer", + buf.len() + ); + assert!( + buf[n..].iter().all(|&b| b == 0), + "{what}: the bytes past the {n} written must be zeroed" + ); + }; + let len = self.fixed_message_len.unwrap_or(max_len); + let msg = &DUMMY_SEED[..len]; + + let need = encrypt_detached_out_len(len); + let mut ct = vec![SENTINEL; need + EXTRA]; + let (n, nonce, tag) = key.encrypt_detached_out(rng, aad, msg, &mut ct).unwrap(); + assert_eq!(n, need, "encrypt_detached_out into an oversized buffer"); + assert_tail_zeroed(&ct, n, "encrypt_detached_out"); + ct.truncate(n); + let mut pt = vec![SENTINEL; decrypt_detached_out_len(ct.len()) + EXTRA]; + let n = key.decrypt_detached_out(&nonce, aad, &ct, &tag, &mut pt).unwrap(); + assert_eq!(&pt[..n], msg, "decrypt_detached_out into an oversized buffer"); + assert_tail_zeroed(&pt, n, "decrypt_detached_out"); + + let need = encrypt_out_len(len); + let mut sealed = vec![SENTINEL; need + EXTRA]; + let (n, nonce) = key.encrypt_with_aad_out(rng, aad, msg, &mut sealed).unwrap(); + assert_eq!(n, need, "encrypt_with_aad_out into an oversized buffer"); + assert_tail_zeroed(&sealed, n, "encrypt_with_aad_out"); + sealed.truncate(n); + let mut pt = vec![SENTINEL; decrypt_out_len(sealed.len()) + EXTRA]; + let n = key.decrypt_with_aad_out(&nonce, aad, &sealed, &mut pt).unwrap(); + assert_eq!(&pt[..n], msg, "decrypt_with_aad_out into an oversized buffer"); + assert_tail_zeroed(&pt, n, "decrypt_with_aad_out"); + + // the detached finals: each reports only what it wrote itself + let (mut enc, nonce) = key.create_encryptor(rng).unwrap(); + enc.do_update_aad(aad).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(len)]; + let written = enc.do_encrypt_out(msg, &mut ct).unwrap(); + ct.truncate(written); + let mut last = [SENTINEL; FINAL_LEN]; + let (last_len, tag) = enc.do_encrypt_final_detached_out(&mut last).unwrap(); + assert_tail_zeroed(&last, last_len, "do_encrypt_final_detached_out"); + ct.extend_from_slice(&last[..last_len]); + assert_eq!( + ct.len(), + encrypt_detached_out_len(len), + "do_encrypt_out and do_encrypt_final_detached_out must report only their own bytes" + ); + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(aad).unwrap(); + let mut rec = vec![0u8; dec.do_decrypt_out_len(ct.len())]; + let released = dec.do_decrypt_out(&ct, &mut rec).unwrap(); + rec.truncate(released); + let mut last = [SENTINEL; FINAL_LEN]; + let data_len = dec.do_decrypt_final_detached_out(&tag, &mut last).unwrap(); + assert_tail_zeroed(&last, data_len, "do_decrypt_final_detached_out"); + rec.extend_from_slice(&last[..data_len]); + assert_eq!( + rec, msg, + "do_decrypt_out and do_decrypt_final_detached_out must report only their own bytes" + ); + } } /// Instance of the test framework. @@ -1039,4 +1593,171 @@ impl TestFrameworkAEADTaggedLayout { let n = D::decrypt_with_aad_out(key, &nonce, AAD, &ct, &mut exact).unwrap(); assert_eq!(&exact[..n], msg, "a buffer of exactly `needed` bytes must be enough"); } + /// The key-object form of [`test`](Self::test), over an [`AeadEncryptionKey`] / + /// [`AeadDecryptionKey`] pair; every nonce is drawn from the pinned RNG. + pub fn test_key< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + K: AeadEncryptionKey + + AeadDecryptionKey, + >( + &self, + key: &K, + ) { + let encrypt_out_len = |len: usize| K::Encryptor::encrypt_out_len(len); + let encrypt_detached_out_len = |len: usize| K::Encryptor::encrypt_detached_out_len(len); + let decrypt_out_len = |len: usize| K::Decryptor::decrypt_out_len(len); + // Encrypts `msg` into the inline layout with the one-shot, under the pinned nonce. + let tagged_ct = |msg: &[u8]| -> (Vec, [u8; NONCE_LEN]) { + let mut ct = vec![0u8; encrypt_out_len(msg.len())]; + let (written, nonce) = + key.encrypt_with_aad_out(&mut Self::rng::(), AAD, msg, &mut ct).unwrap(); + assert_eq!(written, msg.len() + TAG_LEN, "inline layout is ciphertext || tag"); + ct.truncate(written); + (ct, nonce) + }; + + // round trip at every length and chunking + for len in 0..=(4 * TAG_LEN + 5) { + let msg = &DUMMY_SEED[..len]; + let (ct, nonce) = tagged_ct(msg); + + let mut pt = vec![0u8; decrypt_out_len(ct.len())]; + let n = key.decrypt_with_aad_out(&nonce, AAD, &ct, &mut pt).unwrap(); + assert_eq!(&pt[..n], msg, "len {len}: one-shot round trip"); + + // The detached layout is the same ciphertext with the tag split off. + let mut detached = vec![0u8; encrypt_detached_out_len(len)]; + let (d_len, d_nonce, d_tag) = key + .encrypt_detached_out(&mut Self::rng::(), AAD, msg, &mut detached) + .unwrap(); + assert_eq!(d_nonce, nonce, "len {len}: the pinned RNG must give the same nonce"); + assert_eq!(&detached[..d_len], &ct[..len], "len {len}: detached ciphertext"); + assert_eq!(&d_tag[..], &ct[len..], "len {len}: detached tag"); + + for chunk in [1usize, 2, 3, TAG_LEN.max(1), len.max(1)] { + let (mut enc, stream_nonce) = + key.create_encryptor(&mut Self::rng::()).unwrap(); + assert_eq!(stream_nonce, nonce, "len {len}: streaming init draws the same nonce"); + enc.do_update_aad(AAD).unwrap(); + let mut stream_ct = vec![0u8; len + FINAL_LEN]; + let mut written = 0; + for piece in msg.chunks(chunk) { + written += enc.do_encrypt_out(piece, &mut stream_ct[written..]).unwrap(); + } + let (last, last_len) = enc.do_encrypt_final().unwrap(); + stream_ct[written..written + last_len].copy_from_slice(&last[..last_len]); + written += last_len; + stream_ct.truncate(written); + assert_eq!( + stream_ct, ct, + "len {len}, chunk {chunk}: streaming must match the one-shot" + ); + + // the decryptor holds the tag back itself + let mut dec = key.create_decryptor(&stream_nonce); + dec.do_update_aad(AAD).unwrap(); + let mut out = vec![0u8; stream_ct.len() + FINAL_LEN]; + let mut written = 0; + for piece in stream_ct.chunks(chunk) { + written += dec.do_decrypt_out(piece, &mut out[written..]).unwrap(); + } + assert!(written <= len, "len {len}, chunk {chunk}: the tag must be held back"); + let (last, data_len) = dec.do_decrypt_final().unwrap(); + assert_eq!( + written + data_len, + len, + "len {len}, chunk {chunk}: the final call releases exactly the rest" + ); + out[written..written + data_len].copy_from_slice(&last[..data_len]); + out.truncate(written + data_len); + assert_eq!(out, msg, "len {len}, chunk {chunk}: streaming round trip"); + + // The same held-back bytes are ciphertext if the tag is detached. + let mut dec = key.create_decryptor(&stream_nonce); + dec.do_update_aad(AAD).unwrap(); + let mut out = vec![0u8; len + FINAL_LEN]; + let mut written = 0; + for piece in stream_ct[..len].chunks(chunk) { + written += dec.do_decrypt_out(piece, &mut out[written..]).unwrap(); + } + let mut last = [0u8; FINAL_LEN]; + let last_len = dec.do_decrypt_final_detached_out(&d_tag, &mut last).unwrap(); + assert_eq!( + written + last_len, + len, + "len {len}, chunk {chunk}: detached final flushes the rest" + ); + out[written..written + last_len].copy_from_slice(&last[..last_len]); + out.truncate(written + last_len); + assert_eq!(out, msg, "len {len}, chunk {chunk}: detached streaming round trip"); + } + } + + // tampering and short input are rejected + let msg = &DUMMY_SEED[..10]; + let (ct, nonce) = tagged_ct(msg); + let mut tampered = ct.clone(); + tampered[0] ^= 0xFF; + let mut pt = vec![0u8; tampered.len()]; + assert!(matches!( + key.decrypt_with_aad_out(&nonce, AAD, &tampered, &mut pt), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(pt, vec![0u8; tampered.len()], "the one-shot zeroizes on a failed tag check"); + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(AAD).unwrap(); + dec.do_decrypt_out(&tampered, &mut pt).unwrap(); + assert!(matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::AEADTagCheckFailed))); + + let mut wrong_tag = [0u8; TAG_LEN]; + wrong_tag.copy_from_slice(&ct[msg.len()..]); + wrong_tag[0] ^= 0xFF; + let mut pt = vec![0u8; msg.len()]; + assert!(matches!( + key.decrypt_detached_out(&nonce, AAD, &ct[..msg.len()], &wrong_tag, &mut pt), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(pt, vec![0u8; msg.len()], "the detached one-shot zeroizes on a failed check"); + + for short_len in 0..TAG_LEN { + let mut pt = vec![0u8; TAG_LEN]; + assert!( + matches!( + key.decrypt_with_aad_out(&nonce, AAD, &ct[..short_len], &mut pt), + Err(SymmetricCipherError::DecryptionFailed) + ), + "{short_len} bytes cannot carry a {TAG_LEN}-byte tag (one-shot)" + ); + let mut dec = key.create_decryptor(&nonce); + assert_eq!(dec.do_decrypt_out(&ct[..short_len], &mut pt).unwrap(), 0); + assert!( + matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), + "{short_len} bytes cannot carry a {TAG_LEN}-byte tag (streaming)" + ); + } + + // undersized buffers are rejected, and one of exactly the length asked for is accepted + let msg = &DUMMY_SEED[..8]; + let (ct, nonce) = tagged_ct(msg); + let needed = encrypt_out_len(msg.len()); + assert_eq!(needed, msg.len() + TAG_LEN); + let mut short = vec![0u8; needed - 1]; + match key.encrypt_with_aad_out(&mut Self::rng::(), AAD, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, needed), + other => panic!("encrypt_with_aad_out into a short buffer: {other:?}"), + } + let needed = decrypt_out_len(ct.len()); + assert_eq!(needed, msg.len()); + let mut short = vec![0u8; needed - 1]; + match key.decrypt_with_aad_out(&nonce, AAD, &ct, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, needed), + other => panic!("decrypt_with_aad_out into a short buffer: {other:?}"), + } + let mut exact = vec![0u8; needed]; + let n = key.decrypt_with_aad_out(&nonce, AAD, &ct, &mut exact).unwrap(); + assert_eq!(&exact[..n], msg, "a buffer of exactly `needed` bytes must be enough"); + } } diff --git a/crypto/core-test-framework/src/block_cipher.rs b/crypto/core-test-framework/src/block_cipher.rs index 9f303d024..b1e3b4765 100644 --- a/crypto/core-test-framework/src/block_cipher.rs +++ b/crypto/core-test-framework/src/block_cipher.rs @@ -4,9 +4,11 @@ use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, RNG}; /// Instance of the test framework. pub struct TestFrameworkBlockCipher { @@ -205,4 +207,192 @@ impl TestFrameworkBlockCipher { } assert!(strengths_tested > 0, "strength sweep must not be vacuous"); } + /// The [`BlockEncryptionKey`] / [`BlockDecryptionKey`] form of [`test`](Self::test), with + /// `rng` supplying every init data draw. The key-policy checks are + /// `TestFrameworkFromKeyMaterial`'s. + pub fn test_key< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, + K: BlockEncryptionKey + + BlockDecryptionKey, + >( + &self, + key: &K, + rng: &mut dyn RNG, + ) { + let (mut encryptor, iv) = key.create_encryptor(rng).unwrap(); + let mut decryptor = key.create_decryptor(&iv); + Self::round_trip_blocks(&mut encryptor, &mut decryptor); + + let (mut encryptor, iv) = key.create_encryptor(rng).unwrap(); + let mut decryptor = key.create_decryptor(&iv); + Self::round_trip_pairs(&mut encryptor, &mut decryptor); + + // one-shot API: a block-aligned byte array, in place; see `test`. + let one_block: &[u8; BLOCK_LEN] = &DUMMY_SEED.as_chunks::().0[0]; + let mut buf = *one_block; + let (n, iv) = key.encrypt_inplace(rng, &mut buf).unwrap(); + assert_eq!(n, BLOCK_LEN, "encrypt must report the number of bytes written"); + let ct = buf; + let n = key.decrypt_inplace(&iv, &mut buf).unwrap(); + assert_eq!(n, BLOCK_LEN, "decrypt must report the number of bytes written"); + assert_eq!(buf, *one_block); + let mut streamed = key.create_decryptor(&iv); + let mut buf = ct; + streamed.do_decrypt_blocks_inplace(core::slice::from_mut(&mut buf)).unwrap(); + assert_eq!(buf, *one_block); + + // the RNG-driven 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) = + key.create_encryptor(&mut FixedSeedRNG::::new(pinned)).unwrap(); + streamed.do_encrypt_blocks_inplace(core::slice::from_mut(&mut expected)).unwrap(); + let mut buf = *one_block; + let (n, iv) = + key.encrypt_inplace(&mut FixedSeedRNG::::new(pinned), &mut buf).unwrap(); + assert_eq!(n, BLOCK_LEN, "encrypt_inplace must report the number of bytes written"); + assert_eq!(iv, iv_streamed); + assert_eq!(buf, expected); + + // The one-shots take a runtime length, so they must refuse data that is not a whole number + // of blocks, before touching it. + for len in [1, BLOCK_LEN - 1, BLOCK_LEN + 1, 2 * BLOCK_LEN + 1] { + let mut bad = DUMMY_SEED[..len].to_vec(); + assert!( + matches!( + key.encrypt_inplace(rng, &mut bad), + Err(SymmetricCipherError::DataNotBlockAligned) + ), + "len {len}: an unaligned plaintext must be refused" + ); + assert!( + matches!( + key.decrypt_inplace(&iv, &mut bad), + Err(SymmetricCipherError::DataNotBlockAligned) + ), + "len {len}: an unaligned ciphertext must be refused" + ); + assert_eq!(bad, DUMMY_SEED[..len], "len {len}: refused data must be untouched"); + } + + // the iv is random: not the same on two runs + let (_encryptor, iv1) = key.create_encryptor(rng).unwrap(); + let (_encryptor, iv2) = key.create_encryptor(rng).unwrap(); + assert_ne!(iv1, iv2); + } + + /// The [`EcbEncryptionKey`] / [`EcbDecryptionKey`] form of [`test`](Self::test): no init + /// data, so nothing is drawn and nothing can differ between two runs. + pub fn test_ecb_key< + const KEY_LEN: usize, + const BLOCK_LEN: usize, + K: EcbEncryptionKey + EcbDecryptionKey, + >( + &self, + key: &K, + ) { + let mut encryptor = key.create_encryptor(); + let mut decryptor = key.create_decryptor(); + Self::round_trip_blocks(&mut encryptor, &mut decryptor); + + let mut encryptor = key.create_encryptor(); + let mut decryptor = key.create_decryptor(); + Self::round_trip_pairs(&mut encryptor, &mut decryptor); + + let one_block: &[u8; BLOCK_LEN] = &DUMMY_SEED.as_chunks::().0[0]; + let mut buf = *one_block; + let n = key.encrypt_inplace(&mut buf).unwrap(); + assert_eq!(n, BLOCK_LEN, "encrypt must report the number of bytes written"); + let ct = buf; + assert_ne!(ct, *one_block, "encryption must change the block"); + let n = key.decrypt_inplace(&mut buf).unwrap(); + assert_eq!(n, BLOCK_LEN, "decrypt must report the number of bytes written"); + assert_eq!(buf, *one_block); + let mut streamed = key.create_decryptor(); + let mut buf = ct; + streamed.do_decrypt_blocks_inplace(core::slice::from_mut(&mut buf)).unwrap(); + assert_eq!(buf, *one_block); + + // The one-shots take a runtime length, so they must refuse data that is not a whole number + // of blocks, before touching it. + for len in [1, BLOCK_LEN - 1, BLOCK_LEN + 1] { + let mut bad = DUMMY_SEED[..len].to_vec(); + assert!( + matches!( + key.encrypt_inplace(&mut bad), + Err(SymmetricCipherError::DataNotBlockAligned) + ), + "len {len}: an unaligned plaintext must be refused" + ); + assert!( + matches!( + key.decrypt_inplace(&mut bad), + Err(SymmetricCipherError::DataNotBlockAligned) + ), + "len {len}: an unaligned ciphertext must be refused" + ); + assert_eq!(bad, DUMMY_SEED[..len], "len {len}: refused data must be untouched"); + } + + // with no init data the codebook is fixed: the same block encrypts the same way twice + let mut again = *one_block; + key.encrypt_inplace(&mut again).unwrap(); + assert_eq!(again, ct); + } + + /// One block at a time, through the flat streaming methods (LEN = BLOCK_LEN), in place. + fn round_trip_blocks< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, + E: BlockCipherEncryptor, + D: BlockCipherDecryptor, + >( + encryptor: &mut E, + decryptor: &mut D, + ) { + for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { + let mut buf = *msg_chunk; + encryptor.do_encrypt_blocks_inplace(core::slice::from_mut(&mut buf)).unwrap(); + decryptor.do_decrypt_blocks_inplace(core::slice::from_mut(&mut buf)).unwrap(); + assert_eq!(msg_chunk, &buf); + } + } + + /// Two blocks at a time through the implementor hook `do_*_blocks_inplace`: blocks encrypted + /// together must decrypt both together and one at a time, and blocks encrypted one at a time + /// must decrypt together. + fn round_trip_pairs< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, + E: BlockCipherEncryptor, + D: BlockCipherDecryptor, + >( + encryptor: &mut E, + decryptor: &mut D, + ) { + for msg_pair in DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0.iter() { + let mut buf = *msg_pair; + encryptor.do_encrypt_blocks_inplace(&mut buf).unwrap(); + decryptor.do_decrypt_blocks_inplace(&mut buf).unwrap(); + assert_eq!(msg_pair, &buf); + + let mut buf = *msg_pair; + encryptor.do_encrypt_blocks_inplace(&mut buf).unwrap(); + for (msg_chunk, block) in msg_pair.iter().zip(buf.iter_mut()) { + decryptor.do_decrypt_blocks_inplace(core::slice::from_mut(block)).unwrap(); + assert_eq!(msg_chunk, block); + } + + let mut buf = *msg_pair; + for block in buf.iter_mut() { + encryptor.do_encrypt_blocks_inplace(core::slice::from_mut(block)).unwrap(); + } + decryptor.do_decrypt_blocks_inplace(&mut buf).unwrap(); + assert_eq!(msg_pair, &buf); + } + } } diff --git a/crypto/core-test-framework/src/from_key_material.rs b/crypto/core-test-framework/src/from_key_material.rs new file mode 100644 index 000000000..1d3f33aea --- /dev/null +++ b/crypto/core-test-framework/src/from_key_material.rs @@ -0,0 +1,217 @@ +//! Generic behaviour tests for the key constructors [`FromKeyMaterial`] and +//! [`FromAnyKeyMaterial`]: the key-type and security-strength policy that the cipher and MAC +//! suites used to check at every `do_*_init`, now checked once where the key is made. + +use crate::{DUMMY_SEED, FixedSeedRNG}; +use bouncycastle_core::errors::{KeyMaterialError, MACError, SymmetricCipherError}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial512, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, FromKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::MAC; + +/// Instance of the test framework. +pub struct TestFrameworkFromKeyMaterial { + // Put any config options here +} + +impl Default for TestFrameworkFromKeyMaterial { + fn default() -> Self { + Self::new() + } +} + +impl TestFrameworkFromKeyMaterial { + /// + pub fn new() -> Self { + Self {} + } + + /// Exercises the [`FromKeyMaterial`] contract for one key type, whose workers report + /// `alg_name` and whose strength is `max_strength`. + /// + /// Checks, in order: + /// * material of the right type, unbound or bound to `alg_name`, is accepted; + /// * material of the wrong [`KeyType`] is refused with [`KeyMaterialError::InvalidKeyType`]; + /// * the security-strength policy matches `max_strength`; + /// * [`FromKeyMaterial::generate`] draws a key from an RNG of sufficient strength, tagged + /// [`FromKeyMaterial::KEY_TYPE`], and refuses one instantiated below the key's strength. + pub fn test>( + &self, + alg_name: &'static str, + max_strength: SecurityStrength, + ) { + let key = KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], K::KEY_TYPE) + .unwrap(); + K::from_key_material(&key).unwrap_or_else(|e| { + panic!("{alg_name}: material of the key's own type must be accepted: {e:?}") + }); + + // the wrong key type + let other_type = match K::KEY_TYPE { + KeyType::MACKey => KeyType::SymmetricCipherKey, + _ => KeyType::MACKey, + }; + let wrong = + KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], other_type).unwrap(); + match K::from_key_material(&wrong) { + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) => {} + Ok(_) => panic!("a key of the wrong KeyType must be refused"), + Err(e) => panic!("a key of the wrong KeyType must be refused as InvalidKeyType: {e:?}"), + } + + // security strengths too weak, and strong enough + let mut key = key; + for ss in [ + SecurityStrength::None, + SecurityStrength::_112bit, + SecurityStrength::_128bit, + SecurityStrength::_192bit, + SecurityStrength::_256bit, + ] { + // Skip the strengths a KEY_LEN-byte key cannot carry; see + // `TestFrameworkElectronicCodeBook`. + if ss > SecurityStrength::from_bytes(KEY_LEN) { + continue; + } + do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + match K::from_key_material(&key) { + Ok(_) => assert!( + ss >= max_strength, + "should have required a key at least as strong as the algorithm ({ss:?})" + ), + Err(SymmetricCipherError::KeyMaterialError(_)) => assert!( + ss < max_strength, + "should not have rejected a key strong enough for the algorithm ({ss:?})" + ), + Err(e) => panic!("unexpected error for a {ss:?} key: {e:?}"), + } + } + + // generate: from a strong enough RNG, and not from a weaker one + let mut rng = FixedSeedRNG::::new(core::array::from_fn(|i| DUMMY_SEED[200 + i])); + K::generate(&mut rng).expect("generate from a 256-bit RNG"); + let mut weak = FixedSeedRNG::::new(core::array::from_fn(|i| DUMMY_SEED[200 + i])); + weak.set_security_strength(SecurityStrength::None); + match K::generate(&mut weak) { + Ok(_) => assert!( + max_strength <= SecurityStrength::None, + "generate must refuse an RNG below the key's strength" + ), + Err(SymmetricCipherError::KeyMaterialError(_)) => {} + Err(e) => panic!("unexpected error from generate: {e:?}"), + } + } +} + +/// Instance of the test framework. +pub struct TestFrameworkFromAnyKeyMaterial { + // Put any config options here +} + +impl Default for TestFrameworkFromAnyKeyMaterial { + fn default() -> Self { + Self::new() + } +} + +impl TestFrameworkFromAnyKeyMaterial { + /// + pub fn new() -> Self { + Self {} + } + + /// Exercises the [`FromAnyKeyMaterial`] contract for one MAC key type, given a key its + /// construction accepts (the strength check is what `from_key_material_allow_weak` skips). + /// + /// Checks, in order: + /// * `from_key_material_allow_weak` accepts `key` and produces a working generator; + /// * a key tagged at no security strength is refused by `from_key_material` with + /// [`KeyMaterialError::SecurityStrength`]; + /// * a key one step below the construction's strength is refused by `from_key_material` and + /// accepted by `from_key_material_allow_weak`; + /// * material of the wrong [`KeyType`] is refused with [`KeyMaterialError::InvalidKeyType`]; + /// * [`FromAnyKeyMaterial::generate`] draws a usable key. + pub fn test(&self, key: &impl KeyMaterialTrait) { + let weak_ok = K::from_key_material_allow_weak(key).expect("allow_weak must accept the key"); + let strength = weak_ok.create_generator().max_security_strength(); + let mut generator = weak_ok.create_generator(); + generator.do_update(b"Hi There"); + assert_eq!(generator.do_final().len(), weak_ok.create_generator().output_len()); + + // a no-security (and non-zero) key is refused at any strength + let mut key_none = + KeyMaterial512::from_bytes_as_type(&DUMMY_SEED[0..64], KeyType::MACKey).unwrap(); + key_none.set_security_strength(SecurityStrength::None).unwrap(); + match K::from_key_material(&key_none) { + Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) => {} + _ => panic!("a key of no security strength must be refused as SecurityStrength"), + } + + // one step below the construction's strength: refused, unless weak keys are allowed. + // The key takes the caller's length where that fits, so a construction that accepts + // exactly one length is tested at that length. + let weak_key_len = |default_len: usize, min_len: usize| match key.key_len() { + 1..=64 => key.key_len().max(min_len), + _ => default_len, + }; + let mut low = + KeyMaterial512::from_bytes_as_type(&DUMMY_SEED[..64], KeyType::MACKey).unwrap(); + do_hazardous_operations(&mut low, |low| { + match strength { + SecurityStrength::None => { + low.set_key_len(weak_key_len(13, 0))?; + low.set_security_strength(SecurityStrength::None) + } + SecurityStrength::_112bit => { + low.set_key_len(weak_key_len(28, 0))?; + low.set_security_strength(SecurityStrength::None) + } + SecurityStrength::_128bit => { + low.set_key_len(weak_key_len(32, 14))?; + low.set_security_strength(SecurityStrength::_112bit) + } + SecurityStrength::_192bit => { + low.set_key_len(weak_key_len(48, 16))?; + low.set_security_strength(SecurityStrength::_128bit) + } + SecurityStrength::_256bit => { + low.set_key_len(weak_key_len(64, 24))?; + low.set_security_strength(SecurityStrength::_192bit) + } + // `SecurityStrength` is `#[non_exhaustive]`, so this arm is required. + _ => panic!("unhandled SecurityStrength variant -- add a case for it here"), + } + }) + .unwrap(); + assert!(low.security_strength() < strength); + match K::from_key_material(&low) { + Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) => {} + _ => panic!("a key below the construction's strength must be refused"), + } + let allowed = K::from_key_material_allow_weak(&low).expect("allow_weak must accept it"); + let mut generator = allowed.create_generator(); + generator.do_update(b"Hi There"); + generator.do_final(); + + // the wrong key type + let cipher_key = + KeyMaterial512::from_bytes_as_type(&DUMMY_SEED[..64], KeyType::SymmetricCipherKey) + .unwrap(); + match K::from_key_material(&cipher_key) { + Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) => {} + _ => panic!("a key that is not a MAC key must be refused as InvalidKeyType"), + } + match K::from_key_material_allow_weak(&cipher_key) { + Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) => {} + _ => panic!("allow_weak must still refuse a key that is not a MAC key"), + } + + // generate + let mut rng = FixedSeedRNG::<64>::new(core::array::from_fn(|i| DUMMY_SEED[300 + i])); + let generated = K::generate(&mut rng).expect("generate from a 256-bit RNG"); + let mut generator = generated.create_generator(); + generator.do_update(b"Hi There"); + assert_eq!(generator.do_final().len(), generated.create_generator().output_len()); + } +} diff --git a/crypto/core-test-framework/src/lib.rs b/crypto/core-test-framework/src/lib.rs index 5f7ffb288..08be80235 100644 --- a/crypto/core-test-framework/src/lib.rs +++ b/crypto/core-test-framework/src/lib.rs @@ -17,6 +17,7 @@ pub mod aead; pub mod block_cipher; pub mod electronic_code_book; +pub mod from_key_material; pub mod hash; pub mod kdf; pub mod kem; diff --git a/crypto/core-test-framework/src/mac.rs b/crypto/core-test-framework/src/mac.rs index 8116510ed..da1ba6b90 100644 --- a/crypto/core-test-framework/src/mac.rs +++ b/crypto/core-test-framework/src/mac.rs @@ -5,6 +5,7 @@ use bouncycastle_core::errors::{KeyMaterialError, MACError}; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::MAC; /// Instance of the test framework. @@ -151,4 +152,74 @@ impl TestFrameworkMAC { hmac.do_update(b"Hi There"); hmac.do_final(); } + /// The key-object form of [`test_mac`](Self::test_mac): the workers a + /// [`MacGenerationKey`] / [`MacVerificationKey`] hands out, against the given input-output + /// pair. The key-policy checks are `TestFrameworkFromAnyKeyMaterial`'s. + pub fn test_mac_key( + &self, + key: &K, + input: &[u8], + expected_output: &[u8], + ) { + // the key's one-shots + assert_eq!(key.mac(input), expected_output); + let mut out = vec![0u8; expected_output.len()]; + let bytes_written = key.mac_out(input, &mut out).unwrap(); + assert_eq!(bytes_written, expected_output.len()); + assert_eq!(out, expected_output); + // an output buffer that's too small truncates + let mut out = vec![0u8; expected_output.len() - 2]; + let bytes_written = key.mac_out(input, &mut out).unwrap(); + assert_eq!(bytes_written, expected_output.len() - 2); + assert_eq!(out, expected_output[..expected_output.len() - 2]); + // an output buffer that's too big gets the first L bytes filled and the rest zeroed + let mut out = vec![0xA5u8; 2 * expected_output.len()]; + let bytes_written = key.mac_out(input, &mut out).unwrap(); + assert_eq!(bytes_written, expected_output.len()); + assert_eq!(&out[..expected_output.len()], expected_output); + assert_eq!(&out[expected_output.len()..], vec![0u8; expected_output.len()]); + assert!(key.verify(input, expected_output)); + assert!(!key.verify(input, &expected_output[..expected_output.len() - 1])); + assert!(!key.verify(b"not the input", expected_output)); + + // the workers' streaming methods, and the consuming one-shots on the workers + let mut generator = key.create_generator(); + let output_len = generator.output_len(); + generator.do_update(input); + let out = generator.do_final(); + assert_eq!(out, expected_output); + assert_eq!(output_len, out.len()); + + let mut generator = key.create_generator(); + generator.do_update(input); + let mut out = vec![0u8; generator.output_len()]; + let out_len = generator.do_final_out(&mut *out).unwrap(); + assert_eq!(out, expected_output); + assert_eq!(out_len, out.len()); + + assert_eq!(key.create_generator().mac(input), expected_output); + let mut out = vec![0u8; expected_output.len()]; + assert_eq!(key.create_generator().mac_out(input, &mut out).unwrap(), out.len()); + assert_eq!(out, expected_output); + assert!(key.create_verifier().verify(input, expected_output)); + + let mut verifier = key.create_verifier(); + verifier.do_update(input); + assert!(verifier.do_verify_final(expected_output)); + let mut verifier = key.create_verifier(); + verifier.do_update(input); + assert!(!verifier.do_verify_final(&expected_output[1..])); + + // several workers in flight from one key, finished in the other order, and the key + // unchanged by use + let mut a = key.create_generator(); + let mut b = key.create_generator(); + a.do_update(input); + b.do_update(b"another message"); + let tag_b = b.do_final(); + let tag_a = a.do_final(); + assert_eq!(tag_a, expected_output); + assert_eq!(tag_b, key.mac(b"another message")); + assert_eq!(key.mac(input), expected_output); + } } diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 1f0c6336d..36be68fbb 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -7,8 +7,9 @@ use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + RNG, StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; @@ -489,6 +490,349 @@ impl TestFrameworkSymmetricCipher { }; } } + /// The [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] form of + /// [`test_encryptor_decryptor`](Self::test_encryptor_decryptor): the same contract reached + /// through a key object, with `rng` supplying every init data draw. The key-policy checks + /// are not here, since the key is already made; see `TestFrameworkFromKeyMaterial`. + pub fn test_key< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + >( + &self, + key: &K, + rng: &mut dyn RNG, + ) { + // Enough plaintext lengths to cross several final-chunk boundaries (a block, for padding). + let align = self.required_alignment.max(1); + let max_len = (3 * FINAL_LEN.max(1) + 5).next_multiple_of(align); + if let Some(fixed) = self.fixed_message_len { + assert!(fixed <= DUMMY_SEED.len(), "fixed_message_len must fit the seed buffer"); + } + let encrypt_out_len = |len: usize| K::Encryptor::encrypt_out_len(len); + let decrypt_out_len = |len: usize| K::Decryptor::decrypt_out_len(len); + + // one-shot round trip, every (accepted) length; every other length must be refused + for len in 0..=max_len { + let msg = &DUMMY_SEED[..len]; + if !len.is_multiple_of(align) { + let mut ct = vec![0u8; encrypt_out_len(len) + FINAL_LEN]; + match key.encrypt_out(rng, msg, &mut ct) { + Err(SymmetricCipherError::PaddingError(_)) => {} + other => panic!("len {len} is not aligned and must be refused, got {other:?}"), + } + let (mut enc, _) = key.create_encryptor(rng).unwrap(); + let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; + enc.do_encrypt_out(msg, &mut buf).unwrap(); + assert!( + matches!(enc.do_encrypt_final(), Err(SymmetricCipherError::PaddingError(_))), + "len {len}: streaming do_encrypt_final must refuse an unaligned message" + ); + continue; + } + // a fixed-length pair takes only that length, through the key's one-shots as well as + // its streams, since the one-shots are written over the streams + if let Some(fixed) = self.fixed_message_len + && fixed != len + { + let mut ct = vec![0u8; encrypt_out_len(len)]; + assert!( + key.encrypt_out(rng, msg, &mut ct).is_err(), + "fixed length: a {len}-byte one-shot must be refused" + ); + let (mut enc, _) = key.create_encryptor(rng).unwrap(); + let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; + assert!( + enc.do_encrypt_out(msg, &mut buf).is_err() || enc.do_encrypt_final().is_err(), + "fixed length: a {len}-byte stream must be refused" + ); + continue; + } + let mut ct = vec![0u8; encrypt_out_len(len)]; + let (ct_len, init_data) = key.encrypt_out(rng, msg, &mut ct).unwrap(); + assert_eq!(ct_len, ct.len(), "encrypt_out must write exactly encrypt_out_len bytes"); + + let mut pt = vec![0u8; decrypt_out_len(ct_len)]; + let pt_len = key.decrypt_out(&init_data, &ct[..ct_len], &mut pt).unwrap(); + assert!(pt_len <= pt.len(), "decrypt_out_len must bound the plaintext"); + assert_eq!(&pt[..pt_len], msg, "one-shot round trip, len {len}"); + + // the std one-shots agree with the _out ones for the same init data + let (init_data2, ct2) = key.encrypt(rng, msg).unwrap(); + assert_eq!(ct2.len(), ct_len, "encrypt must return exactly the bytes written"); + let pt2 = key.decrypt(&init_data2, &ct2).unwrap(); + assert_eq!(pt2, msg, "std round trip, len {len}"); + let pt3 = key.decrypt(&init_data, &ct[..ct_len]).unwrap(); + assert_eq!(pt3, msg, "decrypt must agree with decrypt_out"); + } + + // streaming in every chunking agrees with the one-shot + let len = self.fixed_message_len.unwrap_or(max_len); + let msg = &DUMMY_SEED[..len]; + let chunkings: [usize; 8] = + [1, 2, 3, 7, FINAL_LEN.max(1), FINAL_LEN + 1, 2 * FINAL_LEN + 3, len.max(1)]; + for chunk in chunkings { + // encrypt in chunks, checking update_out_len is exact each time + let (mut enc, init_data) = key.create_encryptor(rng).unwrap(); + let mut ct = Vec::new(); + for piece in msg.chunks(chunk) { + let expect = enc.do_encrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact (encrypt, chunk {chunk})"); + ct.extend_from_slice(&buf[..n]); + } + let mut last = [0u8; FINAL_LEN]; + let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); + assert!( + last_len <= FINAL_LEN, + "do_encrypt_final_out must not claim more than FINAL_LEN bytes" + ); + ct.extend_from_slice(&last[..last_len]); + assert_eq!( + ct.len(), + encrypt_out_len(len), + "streaming total must match encrypt_out_len" + ); + + // one-shot decrypt of the streamed ciphertext + let mut pt = vec![0u8; decrypt_out_len(ct.len())]; + let m = key.decrypt_out(&init_data, &ct, &mut pt).unwrap(); + assert_eq!( + &pt[..m], + msg, + "streamed ciphertext must decrypt in one shot (chunk {chunk})" + ); + + // decrypt in the same chunks, via do_decrypt_final and via do_decrypt_final_out + for use_out in [false, true] { + let mut dec = key.create_decryptor(&init_data); + let mut rec = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact (decrypt, chunk {chunk})"); + rec.extend_from_slice(&buf[..n]); + } + let (block, data_len) = if use_out { + let mut block = [0u8; FINAL_LEN]; + let data_len = dec.do_decrypt_final_out(&mut block).unwrap(); + (block, data_len) + } else { + dec.do_decrypt_final().unwrap() + }; + rec.extend_from_slice(&block[..data_len]); + assert_eq!( + rec, msg, + "streamed round trip (chunk {chunk}, do_decrypt_final_out {use_out})" + ); + } + } + + // a fixed message length is enforced on both sides: one byte more is refused at the + // update, consuming nothing, and one byte fewer is refused at the final. + if let Some(fixed) = self.fixed_message_len { + let (mut enc, init_data) = key.create_encryptor(rng).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(fixed)]; + let n = enc.do_encrypt_out(msg, &mut ct).unwrap(); + ct.truncate(n); + let mut more = vec![0u8; enc.do_encrypt_out_len(1) + 1]; + assert!( + enc.do_encrypt_out(&DUMMY_SEED[..1], &mut more).is_err(), + "fixed length: one byte more must be refused at the update" + ); + let (last, last_len) = enc.do_encrypt_final().unwrap(); + ct.extend_from_slice(&last[..last_len]); + let mut pt = vec![0u8; decrypt_out_len(ct.len())]; + let m = key.decrypt_out(&init_data, &ct, &mut pt).unwrap(); + assert_eq!(&pt[..m], msg, "fixed length: a refused update must not disturb the state"); + + if fixed > 0 { + let (mut enc, _) = key.create_encryptor(rng).unwrap(); + let mut buf = vec![0u8; enc.do_encrypt_out_len(fixed - 1)]; + enc.do_encrypt_out(&msg[..fixed - 1], &mut buf).unwrap(); + assert!( + enc.do_encrypt_final().is_err(), + "fixed length: one byte fewer must be refused at the final (encrypt)" + ); + let mut dec = key.create_decryptor(&init_data); + let mut buf = vec![0u8; dec.do_decrypt_out_len(fixed - 1)]; + dec.do_decrypt_out(&ct[..fixed - 1], &mut buf).unwrap(); + assert!( + dec.do_decrypt_final().is_err(), + "fixed length: one byte fewer must be refused at the final (decrypt)" + ); + } + let mut dec = key.create_decryptor(&init_data); + let mut rec = vec![0u8; dec.do_decrypt_out_len(ct.len())]; + let n = dec.do_decrypt_out(&ct, &mut rec).unwrap(); + rec.truncate(n); + let mut more = vec![0u8; dec.do_decrypt_out_len(1) + 1]; + assert!( + dec.do_decrypt_out(&DUMMY_SEED[..1], &mut more).is_err(), + "fixed length: one byte more must be refused at the update (decrypt)" + ); + let (last, data_len) = dec.do_decrypt_final().unwrap(); + rec.extend_from_slice(&last[..data_len]); + assert_eq!( + rec, msg, + "fixed length: a refused update must not disturb the state (decrypt)" + ); + } + + if INIT_DATA_LEN > 0 { + // a driven RNG reproduces its init data, and determines the ciphertext + let seed: [u8; INIT_DATA_LEN] = core::array::from_fn(|i| DUMMY_SEED[100 + i]); + let (mut enc, init_data) = + key.create_encryptor(&mut FixedSeedRNG::::new(seed)).unwrap(); + assert_eq!(init_data, seed, "a fixed RNG must yield its stream as the init data"); + let mut streamed = vec![0u8; enc.do_encrypt_out_len(len)]; + let n = enc.do_encrypt_out(msg, &mut streamed).unwrap(); + streamed.truncate(n); + let (last, last_len) = enc.do_encrypt_final().unwrap(); + streamed.extend_from_slice(&last[..last_len]); + let mut one_shot = vec![0u8; encrypt_out_len(len)]; + let (n2, init_data2) = key + .encrypt_out(&mut FixedSeedRNG::::new(seed), msg, &mut one_shot) + .unwrap(); + assert_eq!(init_data2, seed); + assert_eq!( + &one_shot[..n2], + &streamed[..], + "same key and init data must give the same ciphertext" + ); + + // ...and two draws from a real RNG differ + let (_, a) = key.create_encryptor(rng).unwrap(); + let (_, b) = key.create_encryptor(rng).unwrap(); + assert_ne!(a, b, "two encryptions must not share init data"); + } + + // corrupting the ciphertext does not give back the plaintext (or fails to decrypt) + let mut ct = vec![0u8; encrypt_out_len(len)]; + let (ct_len, init_data) = key.encrypt_out(rng, msg, &mut ct).unwrap(); + assert!(ct_len > 0, "the test message is non-empty, so its ciphertext must be"); + for flip in [0usize, ct_len / 2, ct_len - 1] { + let mut bad = ct[..ct_len].to_vec(); + bad[flip] ^= 0x80; + let mut pt = vec![0u8; decrypt_out_len(ct_len)]; + match key.decrypt_out(&init_data, &bad, &mut pt) { + Ok(m) => { + assert_ne!(&pt[..m], msg, "corrupted byte {flip} decrypted to the plaintext") + } + Err(SymmetricCipherError::DecryptionFailed) + | Err(SymmetricCipherError::PaddingError(_)) + | Err(SymmetricCipherError::AEADTagCheckFailed) => { /* also fine */ } + Err(e) => panic!("unexpected error for corrupted byte {flip}: {e:?}"), + } + } + + // too-short output buffers are refused with the required length, before any work is done + let need = encrypt_out_len(len); + let mut short = vec![0u8; need - 1]; + match key.encrypt_out(rng, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("encrypt_out into a short buffer: {other:?}"), + } + let need = decrypt_out_len(ct_len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match key.decrypt_out(&init_data, &ct[..ct_len], &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("decrypt_out into a short buffer: {other:?}"), + } + } + // ...and ones with room to spare are accepted + let mut roomy = vec![0u8; encrypt_out_len(len) + 3]; + let (n, _) = key.encrypt_out(rng, msg, &mut roomy).unwrap(); + assert_eq!(n, encrypt_out_len(len), "encrypt_out into a roomy buffer"); + let mut roomy = vec![0u8; need + 3]; + let n = key.decrypt_out(&init_data, &ct[..ct_len], &mut roomy).unwrap(); + assert_eq!(&roomy[..n], msg, "decrypt_out into a roomy buffer"); + let (mut enc, _) = key.create_encryptor(rng).unwrap(); + let need = enc.do_encrypt_out_len(len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match enc.do_encrypt_out(msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("do_update_out into a short buffer: {other:?}"), + } + } + + // Output-buffer contract, for every method that writes into a caller's buffer: see + // `test_encryptor_decryptor`. + const SENTINEL: u8 = 0xA5; + const EXTRA: usize = 7; + let assert_tail_zeroed = |buf: &[u8], n: usize, what: &str| { + assert!( + n <= buf.len(), + "{what}: claims {n} bytes written to a {}-byte buffer", + buf.len() + ); + assert!( + buf[n..].iter().all(|&b| b == 0), + "{what}: the bytes past the {n} written must be zeroed" + ); + }; + + let need = encrypt_out_len(len); + let mut ct = vec![SENTINEL; need + EXTRA]; + let (ct_len, init_data) = key.encrypt_out(rng, msg, &mut ct).unwrap(); + assert_eq!(ct_len, need, "encrypt_out into an oversized buffer must write encrypt_out_len"); + assert_tail_zeroed(&ct, ct_len, "encrypt_out"); + ct.truncate(ct_len); + let mut pt = vec![SENTINEL; decrypt_out_len(ct_len) + EXTRA]; + let n = key.decrypt_out(&init_data, &ct, &mut pt).unwrap(); + assert_eq!(&pt[..n], msg, "decrypt_out into an oversized buffer"); + assert_tail_zeroed(&pt, n, "decrypt_out"); + + let cuts = |total: usize| [0, 1.min(total), (FINAL_LEN + 2).min(total), total]; + let c = cuts(len); + let (mut enc, init_data) = key.create_encryptor(rng).unwrap(); + let mut streamed = Vec::new(); + for w in c.windows(2) { + let piece = &msg[w[0]..w[1]]; + let expect = enc.do_encrypt_out_len(piece.len()); + let mut buf = vec![SENTINEL; expect + EXTRA]; + let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "do_encrypt_out must report the bytes this call wrote"); + assert_tail_zeroed(&buf, n, "do_encrypt_out"); + streamed.extend_from_slice(&buf[..n]); + } + let mut last = [SENTINEL; FINAL_LEN]; + let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); + assert_tail_zeroed(&last, last_len, "do_encrypt_final_out"); + streamed.extend_from_slice(&last[..last_len]); + assert_eq!( + streamed.len(), + encrypt_out_len(len), + "the per-call counts must sum to encrypt_out_len, not overshoot it as running totals" + ); + + let c = cuts(streamed.len()); + let mut dec = key.create_decryptor(&init_data); + let mut rec = Vec::new(); + for w in c.windows(2) { + let piece = &streamed[w[0]..w[1]]; + let expect = dec.do_decrypt_out_len(piece.len()); + let mut buf = vec![SENTINEL; expect + EXTRA]; + let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "do_decrypt_out must report the bytes this call wrote"); + assert_tail_zeroed(&buf, n, "do_decrypt_out"); + rec.extend_from_slice(&buf[..n]); + } + let mut last = [SENTINEL; FINAL_LEN]; + let data_len = dec.do_decrypt_final_out(&mut last).unwrap(); + assert_tail_zeroed(&last, data_len, "do_decrypt_final_out"); + rec.extend_from_slice(&last[..data_len]); + assert_eq!( + rec, msg, + "the per-call counts must sum to the plaintext, not overshoot it as running totals" + ); + } } /// Instance of the test framework. @@ -687,4 +1031,115 @@ impl TestFrameworkStreamCipher { ); } } + /// The key-object form of [`test`](Self::test): a [`SymmetricEncryptionKey`] / + /// [`SymmetricDecryptionKey`] pair whose workers are stream ciphers, with `rng` supplying + /// every init data draw. The key-policy checks are `TestFrameworkFromKeyMaterial`'s. + pub fn test_key< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + >( + &self, + key: &K, + rng: &mut dyn RNG, + ) where + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, + { + // one-shot, in place: must round-trip, and report every byte as written. + let mut buf = *DUMMY_SEED; + let (n, iv) = key.encrypt_inplace(rng, &mut buf).unwrap(); + assert_eq!(n, buf.len(), "encrypt must report the number of bytes written"); + let reference_ct = buf; + assert_ne!(&reference_ct[..], &DUMMY_SEED[..], "encryption must change the data"); + let n = key.decrypt_inplace(&iv, &mut buf).unwrap(); + assert_eq!(n, buf.len(), "decrypt must report the number of bytes written"); + assert_eq!(&buf[..], &DUMMY_SEED[..]); + + // the streaming API under the same init data must give the one-shot's answer whatever + // the chunking, and encrypting in one chunking must decrypt in any other. + let chunkings: &[usize] = &[1, 3, 7, 16, 63, 64, 65, 250, DUMMY_SEED.len()]; + for &enc_chunk in chunkings { + let mut buf = *DUMMY_SEED; + let (mut encryptor, iv2) = key.create_encryptor(rng).unwrap(); + encryptor.do_encrypt_inplace(&mut []).unwrap(); + for chunk in buf.chunks_mut(enc_chunk) { + encryptor.do_encrypt_inplace(chunk).unwrap(); + } + encryptor.do_encrypt_inplace(&mut []).unwrap(); + let ct = buf; + + for &dec_chunk in chunkings { + let mut buf = ct; + let mut decryptor = key.create_decryptor(&iv2); + decryptor.do_decrypt_inplace(&mut []).unwrap(); + for chunk in buf.chunks_mut(dec_chunk) { + decryptor.do_decrypt_inplace(chunk).unwrap(); + } + decryptor.do_decrypt_inplace(&mut []).unwrap(); + assert_eq!( + &buf[..], + &DUMMY_SEED[..], + "enc chunk {enc_chunk}, dec chunk {dec_chunk}" + ); + } + + // and the one-shot decrypt agrees with every streaming encryption + let mut buf = ct; + key.decrypt_inplace(&iv2, &mut buf).unwrap(); + assert_eq!(&buf[..], &DUMMY_SEED[..]); + } + + // the streaming decryptor must agree with the one-shot encryptor under its init data + let mut buf = reference_ct; + let mut streamed = key.create_decryptor(&iv); + for chunk in buf.chunks_mut(5) { + streamed.do_decrypt_inplace(chunk).unwrap(); + } + assert_eq!(&buf[..], &DUMMY_SEED[..]); + + // the separate-output methods of the symmetric traits agree with the in-place ones + let mut out = vec![0u8; K::Encryptor::encrypt_out_len(DUMMY_SEED.len())]; + let (n, iv3) = key.encrypt_out(rng, DUMMY_SEED, &mut out).unwrap(); + assert_eq!(n, DUMMY_SEED.len(), "a stream cipher never changes the length"); + let mut back = out.clone(); + key.decrypt_inplace(&iv3, &mut back).unwrap(); + assert_eq!(&back[..], &DUMMY_SEED[..]); + let mut pt = vec![0u8; K::Decryptor::decrypt_out_len(n)]; + let m = key.decrypt_out(&iv3, &out[..n], &mut pt).unwrap(); + assert_eq!(&pt[..m], &DUMMY_SEED[..]); + + // the RNG-driven one-shot must give the streaming API's answer for the same RNG stream, + // and the same init data; a driven RNG determines the ciphertext. + let pinned = [0xA5u8; INIT_DATA_LEN]; + let mut expected = *DUMMY_SEED; + let (mut streamed, iv_streamed) = + key.create_encryptor(&mut FixedSeedRNG::::new(pinned)).unwrap(); + streamed.do_encrypt_inplace(&mut expected).unwrap(); + let mut buf = *DUMMY_SEED; + let (n, iv) = + key.encrypt_inplace(&mut FixedSeedRNG::::new(pinned), &mut buf).unwrap(); + assert_eq!(n, buf.len(), "encrypt_inplace must report the number of bytes written"); + assert_eq!(iv, iv_streamed); + assert_eq!(&buf[..], &expected[..]); + let mut buf2 = *DUMMY_SEED; + let (_, iv_again) = key + .encrypt_inplace(&mut FixedSeedRNG::::new(pinned), &mut buf2) + .unwrap(); + assert_eq!(iv, iv_again); + assert_eq!(&buf[..], &buf2[..]); + + // the init data is random: not the same on two runs, and different init data under the + // same key gives different ciphertext + let (_encryptor, iv1) = key.create_encryptor(rng).unwrap(); + let (_encryptor, iv2) = key.create_encryptor(rng).unwrap(); + assert_ne!(iv1, iv2); + let mut a = *DUMMY_SEED; + let mut b = *DUMMY_SEED; + let (_, iv_a) = key.encrypt_inplace(rng, &mut a).unwrap(); + let (_, iv_b) = key.encrypt_inplace(rng, &mut b).unwrap(); + assert_ne!(iv_a, iv_b); + assert_ne!(&a[..], &b[..]); + } } From 62f78a4011cc55a3700103167640385f399260a6 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 22:58:07 +1100 Subject: [PATCH 03/54] cipher: mode types for the symmetric, block and ECB paths, and CCMFrame (prototype) CBC, CFB, CFB8, CTR and hazmat::ECB implement Mode

over any ModeNames permutation, and the role-side mode traits: CBC and ECB the symmetric pair through the padding adapters, CBC the block pair, ECB the ECB pair, the stream modes the symmetric pair with no final output. CCMFrame puts the fixed-frame CCM pair on the AEAD mode traits. The worker types gain crate-private from_perm / from_inner constructors so the modes can build them from a key state; the trait constructors stay until the deletion step. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/cbc.rs | 105 ++++++++- crypto/cipher/src/modes/ccm.rs | 199 +++++++++++++++++- crypto/cipher/src/modes/cfb.rs | 49 ++++- crypto/cipher/src/modes/cfb8.rs | 49 ++++- crypto/cipher/src/modes/ctr.rs | 60 +++++- .../cipher/src/modes/hazmat/ctr_key_stream.rs | 5 +- crypto/cipher/src/modes/hazmat/ecb.rs | 105 ++++++++- crypto/cipher/src/modes/mod.rs | 27 ++- .../cipher/src/padding/padded_block_cipher.rs | 26 +++ 9 files changed, 597 insertions(+), 28 deletions(-) diff --git a/crypto/cipher/src/modes/cbc.rs b/crypto/cipher/src/modes/cbc.rs index 8cddae314..755223fda 100644 --- a/crypto/cipher/src/modes/cbc.rs +++ b/crypto/cipher/src/modes/cbc.rs @@ -116,15 +116,20 @@ //! So, while the IV need not be secret, best-practice is to authenticate it along with the ciphertext, //! or use an authenticated (AEAD) mode such as GCM. -use crate::modes::ModeNames; use crate::modes::iv::random_iv; +use crate::modes::{Blocks, ModeNames}; +use crate::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::hazmat::{ + BlockDecryptMode, BlockEncryptMode, ElectronicCodeBook, Mode, SymmetricDecryptMode, + SymmetricEncryptMode, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SuspendableKeyed, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding, RNG, + SuspendableKeyed, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_utils::suspendable_state::{ @@ -132,6 +137,93 @@ use bouncycastle_utils::suspendable_state::{ }; use core::marker::PhantomData; +/// The CBC mode type: the `M` of a mode-bound key such as `AES128Key>`. `X` is the +/// padding scheme, which puts the key on the symmetric path with the padded adapter as its +/// worker, or [`Blocks`], which puts it on the block path with [`Cbc`] itself. Its key state is +/// the keyed permutation alone: CBC derives nothing from the key. +/// +/// [`PKCS7`]: crate::padding::PKCS7 +pub struct CBC(core::marker::PhantomData); + +impl Mode

for CBC { + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl + SymmetricEncryptMode for CBC +where + P: ElectronicCodeBook + ModeNames + Clone, + Pad: BlockCipherPadding, +{ + type Encryptor = PaddedBlockCipherEncryptor< + Cbc, + Pad, + KEY_LEN, + BLOCK_LEN, + BLOCK_LEN, + >; + + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; BLOCK_LEN]), SymmetricCipherError> { + let iv = random_iv::(rng)?; + Ok((PaddedBlockCipherEncryptor::from_inner(Cbc::from_perm(state.clone(), iv)), iv)) + } +} + +impl + SymmetricDecryptMode for CBC +where + P: ElectronicCodeBook + ModeNames + Clone, + Pad: BlockCipherPadding, +{ + type Decryptor = PaddedBlockCipherDecryptor< + Cbc, + Pad, + KEY_LEN, + BLOCK_LEN, + BLOCK_LEN, + >; + + fn decrypt_init(state: &Self::KeyState, init_data: &[u8; BLOCK_LEN]) -> Self::Decryptor { + PaddedBlockCipherDecryptor::from_inner(Cbc::from_perm(state.clone(), *init_data)) + } +} + +impl + BlockEncryptMode for CBC +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Encryptor = Cbc; + + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; BLOCK_LEN]), SymmetricCipherError> { + let iv = random_iv::(rng)?; + Ok((Cbc::from_perm(state.clone(), iv), iv)) + } +} + +impl + BlockDecryptMode for CBC +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Decryptor = Cbc; + + fn decrypt_init(state: &Self::KeyState, init_data: &[u8; BLOCK_LEN]) -> Self::Decryptor { + Cbc::from_perm(state.clone(), *init_data) + } +} + /// CBC mode over any [`ElectronicCodeBook`], with the direction encoded in the type. /// /// `Dir` is [`Encrypting`] or [`Decrypting`]. [`BlockCipherEncryptor`] is implemented only for the @@ -165,6 +257,11 @@ where /// block. See [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN + BLOCK_LEN; + /// `C0 = IV`, over an already-keyed permutation: what the [`CBC`] mode impls build. + pub(crate) fn from_perm(perm: P, iv: [u8; BLOCK_LEN]) -> Self { + Self { perm, chain: iv, _dir: PhantomData } + } + /// `Cj = CIPH_K(Pj XOR Cj-1)` in place, then `Cj` becomes the next chaining value. #[inline] fn encrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index d4332d6ee..26846d6c1 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -106,7 +106,8 @@ use crate::modes::iv::random_iv; use crate::stream::StreamCipher; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ - AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook, KeyStream, Mode, + AeadDecryptMode, AeadEncryptMode, AeadPacketDecryptMode, AeadPacketEncryptMode, + ElectronicCodeBook, KeyStream, Mode, }; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; @@ -1564,7 +1565,10 @@ where ciphertext: &mut [u8], ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { ciphertext.fill(0); - let needed = Self::encrypt_out_len(plaintext.len()); + let needed = + >::encrypt_out_len( + plaintext.len(), + ); if ciphertext.len() < needed { return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); } @@ -1718,7 +1722,7 @@ where if ciphertext.is_empty() { return Ok(0); } - let release = self.do_decrypt_out_len(ciphertext.len()); + let release = SymmetricCipherDecryptor::do_decrypt_out_len(self, ciphertext.len()); if plaintext.len() < release { return Err(SymmetricCipherError::OutputBufferTooSmall(release)); } @@ -1866,7 +1870,10 @@ where plaintext: &mut [u8], ) -> Result { plaintext.fill(0); - let needed = Self::decrypt_out_len(ciphertext.len()); + let needed = + >::decrypt_out_len( + ciphertext.len(), + ); if plaintext.len() < needed { return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); } @@ -2505,6 +2512,190 @@ where } } +/// The mode type of the fixed-frame pair: the `M` of a mode-bound key such as +/// `AES128Key>`, on the AEAD key path with [`CcmEncryptor`] / +/// [`CcmDecryptor`] as its workers, which take exactly `DATA_LEN` bytes of payload and up to +/// `AAD_LEN` of AAD. The one-shots a key provides are written over those workers, so they take +/// the frame length too; a message of any length goes through [`CCM`], whose constructors take +/// the lengths. The key state is the keyed permutation alone, as for [`CCM`]. +pub struct CCMFrame< + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +>; + +impl< + P: Clone + ModeNames, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> Mode

for CCMFrame +{ + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl< + P, + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> AeadEncryptMode + for CCMFrame +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Encryptor = CcmEncryptor; + + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + CcmEncryptor::from_perm(state.clone(), rng) + } + + fn encrypt_init_nonce(state: &Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor { + CcmEncryptor::from_perm_nonce(state.clone(), nonce) + } +} + +impl< + P, + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> AeadDecryptMode + for CCMFrame +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Decryptor = CcmDecryptor; + + fn decrypt_init(state: &Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor { + CcmDecryptor::from_perm(state.clone(), nonce) + } +} + +// The key-produced worker traits for the fixed-frame pair, forwarded to the cipher traits' own +// methods while those still exist; named by path as `gcm.rs` names them. + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> AeadEncryptWorker + for CcmEncryptor +where + P: ElectronicCodeBook + ModeNames, +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + AEADCipherEncryptor::do_update_aad(self, aad) + } + + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + SymmetricCipherEncryptor::do_encrypt_out_len(self, input_len) + } + + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + SymmetricCipherEncryptor::do_encrypt_out(self, plaintext, ciphertext) + } + + fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + SymmetricCipherEncryptor::do_encrypt_final(self) + } + + fn do_encrypt_final_detached_out( + self, + ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + AEADCipherEncryptor::do_encrypt_final_detached_out(self, ciphertext) + } + + fn encrypt_out_len(plaintext_len: usize) -> usize { + >::encrypt_out_len( + plaintext_len, + ) + } + + fn encrypt_detached_out_len(plaintext_len: usize) -> usize { + >::encrypt_detached_out_len( + plaintext_len, + ) + } +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> AeadDecryptWorker + for CcmDecryptor +where + P: ElectronicCodeBook + ModeNames, +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + AEADCipherDecryptor::do_update_aad(self, aad) + } + + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + SymmetricCipherDecryptor::do_decrypt_out_len(self, input_len) + } + + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + SymmetricCipherDecryptor::do_decrypt_out(self, ciphertext, plaintext) + } + + fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + SymmetricCipherDecryptor::do_decrypt_final(self) + } + + fn do_decrypt_final_detached_out( + self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + AEADCipherDecryptor::do_decrypt_final_detached_out(self, tag, plaintext) + } + + fn decrypt_out_len(ciphertext_len: usize) -> usize { + >::decrypt_out_len( + ciphertext_len, + ) + } + + fn decrypt_detached_out_len(ciphertext_len: usize) -> usize { + >::decrypt_detached_out_len( + ciphertext_len, + ) + } +} + // ---- suspending the pair ----------------------------------------------------------------- /// The adapter's suspended state is the packet's, then the held-back AAD, its length and the diff --git a/crypto/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs index a953ef6a2..0d0dba473 100644 --- a/crypto/cipher/src/modes/cfb.rs +++ b/crypto/cipher/src/modes/cfb.rs @@ -140,8 +140,10 @@ use crate::modes::ModeNames; use crate::modes::iv::random_iv; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::hazmat::{ + ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, @@ -159,6 +161,49 @@ use core::marker::PhantomData; use crate::modes::cfb8; // End imports needed for docs +/// The CFB mode type: the `M` of a mode-bound key such as `AES128Key`, on the symmetric key +/// path with [`Cfb`] as its worker. Its key state is the keyed permutation alone. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CFB; + +impl Mode

for CFB { + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl SymmetricEncryptMode + for CFB +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Encryptor = Cfb; + + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; BLOCK_LEN]), SymmetricCipherError> { + // `I1 = IV`. + let iv = random_iv::(rng)?; + Ok((Cfb::start(state.clone(), iv), iv)) + } +} + +impl SymmetricDecryptMode + for CFB +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Decryptor = Cfb; + + fn decrypt_init(state: &Self::KeyState, init_data: &[u8; BLOCK_LEN]) -> Self::Decryptor { + Cfb::start(state.clone(), *init_data) + } +} + /// CFB mode over any [`ElectronicCodeBook`], as a stream cipher, with the direction encoded in the /// type. /// diff --git a/crypto/cipher/src/modes/cfb8.rs b/crypto/cipher/src/modes/cfb8.rs index 0d4859f6e..41dd34c62 100644 --- a/crypto/cipher/src/modes/cfb8.rs +++ b/crypto/cipher/src/modes/cfb8.rs @@ -56,8 +56,10 @@ use crate::modes::ModeNames; use crate::modes::iv::random_iv; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::hazmat::{ + ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, @@ -74,6 +76,49 @@ use core::marker::PhantomData; use crate::modes::cfb; // End imports needed for docs +/// The CFB8 mode type: the `M` of a mode-bound key such as `AES128Key`, on the symmetric +/// key path with [`Cfb8`] as its worker. Its key state is the keyed permutation alone. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CFB8; + +impl Mode

for CFB8 { + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl SymmetricEncryptMode + for CFB8 +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Encryptor = Cfb8; + + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; BLOCK_LEN]), SymmetricCipherError> { + // `I1 = IV`. + let iv = random_iv::(rng)?; + Ok((Cfb8 { perm: state.clone(), chain: iv, _dir: PhantomData }, iv)) + } +} + +impl SymmetricDecryptMode + for CFB8 +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Decryptor = Cfb8; + + fn decrypt_init(state: &Self::KeyState, init_data: &[u8; BLOCK_LEN]) -> Self::Decryptor { + Cfb8 { perm: state.clone(), chain: *init_data, _dir: PhantomData } + } +} + /// CFB8 mode over any [`ElectronicCodeBook`], with the direction encoded in the type. /// /// The segment size is one byte (`s = 8`); see the module docs, and note that this is **not** diff --git a/crypto/cipher/src/modes/ctr.rs b/crypto/cipher/src/modes/ctr.rs index 4c49449c3..9453ddd80 100644 --- a/crypto/cipher/src/modes/ctr.rs +++ b/crypto/cipher/src/modes/ctr.rs @@ -121,23 +121,75 @@ use crate::modes::ModeNames; use crate::modes::hazmat::ctr_key_stream::CtrKeyStream; +use crate::modes::iv::random_iv; use crate::stream::StreamCipher; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::{ + ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, +}; +use bouncycastle_core::key_material::KeyType; +use bouncycastle_core::traits::RNG; use bouncycastle_utils::secret::Secret; // Imports needed for docs #[allow(unused_imports)] -use bouncycastle_core::errors::SymmetricCipherError; -#[allow(unused_imports)] use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; // end of imports needed for docs +/// The CTR mode type: the `M` of a mode-bound key such as `AES128Key`, on the symmetric key +/// path with [`Ctr`] as its worker. `NONCE_LEN` is the init data length, `BLOCK_LEN - NONCE_LEN` +/// the counter width; the defaults are a 12-byte nonce on a 16-byte block, the `AES_CTR_*` +/// aliases' shape. The block length is a parameter because the nonce does not fix it, and +/// stable Rust cannot tie it to the permutation's from inside the mode impl. Its key state is the +/// keyed permutation alone. +pub struct CTR; + +impl Mode

+ for CTR +{ + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl + SymmetricEncryptMode for CTR +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Encryptor = Ctr; + + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + let nonce = random_iv::(rng)?; + Ok((StreamCipher::from_keystream(CtrKeyStream::start(state.clone(), nonce)), nonce)) + } +} + +impl + SymmetricDecryptMode for CTR +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Decryptor = Ctr; + + fn decrypt_init(state: &Self::KeyState, init_data: &[u8; NONCE_LEN]) -> Self::Decryptor { + StreamCipher::from_keystream(CtrKeyStream::start(state.clone(), *init_data)) + } +} + /// CTR mode over any [`ElectronicCodeBook`] permutation function, with the direction encoded in the type: the /// crate-private CTR keystream, `CtrKeyStream`, wrapped in a [`StreamCipher`]. /// /// The counter block is the init data (the nonce) followed by a counter filling the rest of the /// block, so `INIT_DATA_LEN` chooses the counter length; see the module docs. `Dir` is -/// [`Encrypting`](crate::Encrypting) or [`Decrypting`](crate::Decrypting). +/// [`Encrypting`] or [`Decrypting`]. /// /// # The counter width is checked at compile time /// diff --git a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs index 91f17f23b..133fc2b44 100644 --- a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs +++ b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs @@ -79,9 +79,10 @@ where }; } - /// `T1 = N | [0]m`: the nonce, then a zero counter. + /// `T1 = N | [0]m`: the nonce, then a zero counter. What the [`CTR`](crate::modes::CTR) mode + /// impls build over a key's stored permutation. #[inline] - fn start(perm: P, nonce: [u8; INIT_DATA_LEN]) -> Self { + pub(crate) fn start(perm: P, nonce: [u8; INIT_DATA_LEN]) -> Self { Self::start_at(perm, nonce, 0) } diff --git a/crypto/cipher/src/modes/hazmat/ecb.rs b/crypto/cipher/src/modes/hazmat/ecb.rs index 381164a18..f2b1c03b3 100644 --- a/crypto/cipher/src/modes/hazmat/ecb.rs +++ b/crypto/cipher/src/modes/hazmat/ecb.rs @@ -80,20 +80,110 @@ //! //! **ECB Mode should not be used in production!** -use crate::modes::ModeNames; +use crate::modes::{Blocks, ModeNames}; +use crate::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::hazmat::{ + EcbDecryptMode, EcbEncryptMode, ElectronicCodeBook, Mode, SymmetricDecryptMode, + SymmetricEncryptMode, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SuspendableKeyed, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding, RNG, + SuspendableKeyed, }; use bouncycastle_utils::suspendable_state::{ LIB_VERSION_LEN, SuspendableComponent, resume_component, suspend_component, }; use core::marker::PhantomData; +/// The ECB mode type: the `M` of a mode-bound key such as `AES128Key>`. `X` is +/// [`Blocks`], which puts the key on the ECB key path of [`bouncycastle_core::hazmat`] with +/// [`Ecb`] itself as its worker, or a padding scheme, which puts it on the symmetric path with +/// the padded adapter over `Ecb`; that path's constructor takes an RNG, which ECB, having no init +/// data, draws nothing from. Its key state is the keyed permutation alone. +/// +/// **Not a confidentiality mode for data**, whichever form; see [`Ecb`]. +pub struct ECB(core::marker::PhantomData); + +impl Mode

for ECB { + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl EcbEncryptMode + for ECB +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Encryptor = Ecb; + + fn encrypt_init(state: &Self::KeyState) -> Self::Encryptor { + Ecb::from_perm(state.clone()) + } +} + +impl EcbDecryptMode + for ECB +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Decryptor = Ecb; + + fn decrypt_init(state: &Self::KeyState) -> Self::Decryptor { + Ecb::from_perm(state.clone()) + } +} + +impl + SymmetricEncryptMode for ECB +where + P: ElectronicCodeBook + ModeNames + Clone, + Pad: BlockCipherPadding, +{ + type Encryptor = PaddedBlockCipherEncryptor< + Ecb, + Pad, + KEY_LEN, + 0, + BLOCK_LEN, + >; + + /// ECB has no init data, so nothing is drawn from `rng` and the init data is the empty + /// array. + fn encrypt_init( + state: &Self::KeyState, + _rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; 0]), SymmetricCipherError> { + Ok((PaddedBlockCipherEncryptor::from_inner(Ecb::from_perm(state.clone())), [])) + } +} + +impl + SymmetricDecryptMode for ECB +where + P: ElectronicCodeBook + ModeNames + Clone, + Pad: BlockCipherPadding, +{ + type Decryptor = PaddedBlockCipherDecryptor< + Ecb, + Pad, + KEY_LEN, + 0, + BLOCK_LEN, + >; + + fn decrypt_init(state: &Self::KeyState, _init_data: &[u8; 0]) -> Self::Decryptor { + PaddedBlockCipherDecryptor::from_inner(Ecb::from_perm(state.clone())) + } +} + /// ECB mode over any permutation that impls [`ElectronicCodeBook`], with the direction encoded in the type. /// /// **Not a confidentiality mode for data**: see the module docs and [`modes`](crate::modes)'s "Security @@ -127,7 +217,12 @@ where /// Expands the key. Both `_init` constructors are this; there is nothing else to set up. fn new(key: &KeyMaterial) -> Result { - Ok(Self { perm: P::new(key)?, _dir: PhantomData }) + Ok(Self::from_perm(P::new(key)?)) + } + + /// Over an already-keyed permutation: what the [`ECB`] mode impls build. + pub(crate) fn from_perm(perm: P) -> Self { + Self { perm, _dir: PhantomData } } } diff --git a/crypto/cipher/src/modes/mod.rs b/crypto/cipher/src/modes/mod.rs index 89201a889..aa6075b10 100644 --- a/crypto/cipher/src/modes/mod.rs +++ b/crypto/cipher/src/modes/mod.rs @@ -43,6 +43,15 @@ //! why it lives under [`hazmat`] -- see [`hazmat::ecb::Ecb`] and //! [Choosing between the modes](#choosing-between-the-modes). //! +//! Each mode is also a **mode type** -- [`CBC`], [`CCM`], [`CCMFrame`], [`CFB`], [`CFB8`], +//! [`CTR`], [`GCM`], [`hazmat::ecb::ECB`], and the MACs [`CMAC`] and [`GMAC`] -- the `M` a mode-bound +//! key such as `bouncycastle_aes::AES128Key` is parameterised by. The mode type implements the +//! hazmat mode traits of [`bouncycastle_core::hazmat`], which is how a key hands out the mode's +//! encryptors and decryptors through the role traits of `bouncycastle_core::symmetric_key`; a +//! key is bound to one mode, and which role traits it carries follow from that mode. The modes +//! with a padded and a whole-block form, CBC and ECB, take the padding scheme or [`Blocks`] as a +//! parameter, and the two forms are two key types. +//! //! [Choosing between the modes](#choosing-between-the-modes) covers when each is the right answer //! -- which, for a new design, one of them usually is. //! @@ -158,15 +167,23 @@ pub mod gmac; pub mod hazmat; mod iv; -pub use cbc::Cbc; -pub use ccm::{CCM, CcmDecryptor, CcmEncryptor, CcmPacketDecryptor, CcmPacketEncryptor}; -pub use cfb::Cfb; -pub use cfb8::Cfb8; +pub use cbc::{CBC, Cbc}; +pub use ccm::{CCM, CCMFrame, CcmDecryptor, CcmEncryptor, CcmPacketDecryptor, CcmPacketEncryptor}; +pub use cfb::{CFB, Cfb}; +pub use cfb8::{CFB8, Cfb8}; pub use cmac::{CMAC, CMAC_MIN_TAG_LEN, Cmac, CmacKeyState}; -pub use ctr::Ctr; +pub use ctr::{CTR, Ctr}; pub use gcm::{GCM, GCM_NONCE_LEN, Gcm, GcmKeyState}; pub use gmac::{GMAC, Gmac}; +/// Selects the whole-block form of a mode that also has a padded form: `CBC` is a key +/// for the block-aligned [`Cbc`] itself, on the block key path, where `CBC` is one for +/// the padded adapter over it, on the symmetric path. Likewise `ECB` under +/// [`hazmat`]. Not a padding scheme: it adds nothing, and the lengths it accepts are checked at +/// compile time rather than refused at the final as `NoPadding` does. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Blocks; + /// A table of algorithm names used to populate [`Algorithm`] for each Mode-Permutation-Size combination. /// /// For permutations that do not implement all modes, simply leave the NAME for that mode as the empty-string. diff --git a/crypto/cipher/src/padding/padded_block_cipher.rs b/crypto/cipher/src/padding/padded_block_cipher.rs index fe42b95b2..a6c43bdf1 100644 --- a/crypto/cipher/src/padding/padded_block_cipher.rs +++ b/crypto/cipher/src/padding/padded_block_cipher.rs @@ -63,6 +63,19 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = E::MAX_SECURITY_STRENGTH; } +impl + PaddedBlockCipherEncryptor +where + E: BlockCipherEncryptor, + P: BlockCipherPadding, +{ + /// The adapter over an already-constructed inner cipher, with nothing buffered: what a mode + /// type's `encrypt_init` builds. + pub(crate) fn from_inner(encryptor: E) -> Self { + Self { encryptor, _padding: PhantomData, buf: Secret::new(), buf_len: 0 } + } +} + impl SymmetricCipherEncryptor for PaddedBlockCipherEncryptor @@ -217,6 +230,19 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = D::MAX_SECURITY_STRENGTH; } +impl + PaddedBlockCipherDecryptor +where + D: BlockCipherDecryptor, + P: BlockCipherPadding, +{ + /// The adapter over an already-constructed inner cipher, with nothing buffered or held + /// back: what a mode type's `decrypt_init` builds. + pub(crate) fn from_inner(decryptor: D) -> Self { + Self { decryptor, _padding: PhantomData, buf: [0u8; BLOCK_LEN], buf_len: 0, held: None } + } +} + impl SymmetricCipherDecryptor for PaddedBlockCipherDecryptor From cb8188f741e0c19e1731d7abf3dec8ab7c0463f7 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 23:18:11 +1100 Subject: [PATCH 04/54] core: KeyMaterial::from_rng caps the strength at the key length A 16-byte key drawn from a 256-bit DRBG was tagged 256-bit, a figure set_security_strength refuses for that length, so retagging such a key (as a MAC mode's key does to hand it to the permutation) failed. The strength is now the lower of the RNG's and the one the length supports. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core/src/key_material.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crypto/core/src/key_material.rs b/crypto/core/src/key_material.rs index 7368ab2e6..ea41754e3 100644 --- a/crypto/core/src/key_material.rs +++ b/crypto/core/src/key_material.rs @@ -316,7 +316,12 @@ impl KeyMaterial { *key.key_len = KEY_LEN; key.key_type = KeyType::CryptographicRandom; - key.security_strength = rng.security_strength(); + // The RNG's strength, capped at what `KEY_LEN` bytes can carry: a 16-byte key drawn from + // a 256-bit DRBG is a 128-bit key, and `set_security_strength` would refuse the higher + // figure. + let by_len = SecurityStrength::from_bits(KEY_LEN * 8); + let by_rng = rng.security_strength(); + key.security_strength = if by_rng < by_len { by_rng } else { by_len }; Ok(key) } From 31dc2055dfe16028e54f48fc3a92d240d51d267e Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 23:18:11 +1100 Subject: [PATCH 05/54] cipher: tests onto the key path (prototype) tests/common/toy_key.rs is a ToyKey with the AES key's shape: FromKeyMaterial over any 16-byte toy permutation, the role impls forwarded to M's mode traits, and ToyGCM / ToyGMAC / ToyCMAC standing in for the modes whose key state needs the key length. The mode, padding and suspend tests build their workers from it and the mode types, the conformance runs move to the test_key runners, and each key gets the construction suite in place of the per-test wrong-key-type checks. The padded_tests toy, an 8-byte XOR cipher with its own trait constructors, is replaced by CBC / CBC over the shared toy; the ECB RNG-panic tests go, since the ECB key path has no RNG-taking entry point. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/tests/ccm_suspend_tests.rs | 17 +- crypto/cipher/tests/common/toy_key.rs | 468 ++++++++++++++++++ crypto/cipher/tests/modes/alg_name_tests.rs | 28 +- crypto/cipher/tests/modes/cbc_tests.rs | 122 ++--- crypto/cipher/tests/modes/ccm_tests.rs | 106 ++-- crypto/cipher/tests/modes/cfb8_tests.rs | 135 ++--- crypto/cipher/tests/modes/cfb_tests.rs | 134 ++--- crypto/cipher/tests/modes/cmac_tests.rs | 71 ++- crypto/cipher/tests/modes/ctr_tests.rs | 166 +++---- crypto/cipher/tests/modes/ecb_tests.rs | 149 +++--- crypto/cipher/tests/modes/gcm_tests.rs | 143 +++--- crypto/cipher/tests/modes/gmac_tests.rs | 45 +- .../tests/modes/symmetric_cipher_api_tests.rs | 135 ++--- crypto/cipher/tests/padding/padded_tests.rs | 221 ++++----- crypto/cipher/tests/suspend_tests.rs | 56 ++- 15 files changed, 1213 insertions(+), 783 deletions(-) create mode 100644 crypto/cipher/tests/common/toy_key.rs diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index 49298b392..1aa031741 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -14,9 +14,10 @@ //! the held-back tag and its length. Every integer is a little-endian `u64`. Spec references are //! to NIST SP 800-38C (May 2004, errata update 07-20-2007). -use bouncycastle_cipher::modes::{CCM, CcmDecryptor, CcmEncryptor}; +use bouncycastle_cipher::modes::{CCM, CCMFrame, CcmDecryptor, CcmEncryptor}; use bouncycastle_core::errors::SuspendableError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, @@ -25,7 +26,10 @@ use bouncycastle_utils::suspendable_state::LIB_VERSION_LEN; #[path = "common/toy_block_cipher.rs"] mod toy_block_cipher; +#[path = "common/toy_key.rs"] +mod toy_key; use toy_block_cipher::ToyBlockCipher; +use toy_key::ToyKey; /// A 12-byte nonce, so `q = 3`: the counter field is the template's last three octets and the /// payload limit is `2^24 - 1`. @@ -57,10 +61,15 @@ fn key() -> KeyMaterial<16> { KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap() } +/// The key the pair is made from; resume still takes the material, through `key()`. +fn frame_key() -> ToyKey> { + ToyKey::from_key_material(&key()).unwrap() +} + /// An encryptor holding its AAD back, before any payload: the header is not yet formatted, so /// the CBC-MAC has absorbed nothing. fn unformatted() -> [u8; N] { - let mut enc = ToyEnc::do_encrypt_init_nonce(&key(), &[0x24u8; NONCE_LEN]).unwrap(); + let mut enc = frame_key().create_encryptor_nonce(&[0x24u8; NONCE_LEN]); enc.do_update_aad(b"aad").unwrap(); enc.suspend() } @@ -68,7 +77,7 @@ fn unformatted() -> [u8; N] { /// An encryptor one block into its payload: the header is formatted, `next_ctr = 2`, the CBC-MAC /// block boundary has just been crossed, and 16 of the 32 bytes are still owed. fn mid_payload() -> [u8; N] { - let mut enc = ToyEnc::do_encrypt_init_nonce(&key(), &[0x24u8; NONCE_LEN]).unwrap(); + let mut enc = frame_key().create_encryptor_nonce(&[0x24u8; NONCE_LEN]); enc.do_update_aad(b"aad").unwrap(); let mut out = [0u8; 16]; enc.do_encrypt_out(&[0x5Au8; 16], &mut out).unwrap(); @@ -186,7 +195,7 @@ fn the_aad_length_is_held_to_its_capacity() { /// on the tag bytes it holds back. #[test] fn the_decrypting_direction_checks_the_same_bounds() { - let mut dec = ToyDec::do_decrypt_init(&key(), &[0x24u8; NONCE_LEN]).unwrap(); + let mut dec = frame_key().create_decryptor(&[0x24u8; NONCE_LEN]); dec.do_update_aad(b"aad").unwrap(); let mut out = [0u8; 16]; dec.do_decrypt_out(&[0x5Au8; 16], &mut out).unwrap(); diff --git a/crypto/cipher/tests/common/toy_key.rs b/crypto/cipher/tests/common/toy_key.rs new file mode 100644 index 000000000..603915d3f --- /dev/null +++ b/crypto/cipher/tests/common/toy_key.rs @@ -0,0 +1,468 @@ +// A key object over any 16-byte toy permutation: the shape `AESKey` has in the `aes` crate, +// written here so the modes can be driven through the key path without a real cipher. The role +// impls forward to whichever mode traits `M` implements over `P`, exactly as the AES key's do. +// +// GCM, GMAC and CMAC have no `Mode

` impl in this crate (their key state needs the permutation's +// key length, which `Mode

` cannot carry), so the marker types `ToyGCM`, `ToyGMAC` and `ToyCMAC` +// below stand in for them over the toys. +// +// Pulled in with `#[path]` from the tests, so every item is `pub` and unused ones are allowed. +#![allow(dead_code)] + +use bouncycastle_cipher::modes::{ + Cmac, CmacKeyState, GCM_NONCE_LEN, Gcm, GcmKeyState, Gmac, ModeNames, +}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{ + AeadDecryptMode, AeadEncryptMode, AeadPacketDecryptMode, AeadPacketEncryptMode, + BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbDecryptionKey, EcbEncryptMode, + EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, SymmetricDecryptMode, + SymmetricEncryptMode, do_hazardous_operations, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::symmetric_key::{ + AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, MacGenerationKey, MacVerificationKey, + SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::RNG; + +/// Key and block length of every toy permutation these keys are built over. +pub const TOY_KEY_LEN: usize = 16; + +/// A key over the permutation `P` for the mode `M`: the mode's per-key state and nothing else. +pub struct ToyKey> { + state: M::KeyState, +} + +impl> Clone for ToyKey { + fn clone(&self) -> Self { + Self { state: self.state.clone() } + } +} + +impl> ToyKey { + /// A key over an already-keyed permutation, for tests that build the permutation themselves. + pub fn from_perm(perm: P) -> Self { + Self { state: M::key_state(perm) } + } + + /// The mode's per-key state, for tests of the workers' `from_key_state` routes. + pub fn state(&self) -> &M::KeyState { + &self.state + } +} + +/// The permutation takes only a `SymmetricCipherKey`, so a MAC mode's `MACKey` is retagged for it +/// at the same strength, as the AES key does. +fn as_cipher_key( + key: &KeyMaterial, +) -> Result, SymmetricCipherError> { + let mut cipher_key = KeyMaterial::::from_bytes_as_type( + key.ref_to_bytes(), + KeyType::SymmetricCipherKey, + )?; + do_hazardous_operations(&mut cipher_key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(key.security_strength()) + })?; + Ok(cipher_key) +} + +impl FromKeyMaterial for ToyKey +where + P: ElectronicCodeBook, + M: Mode

, +{ + const KEY_TYPE: KeyType = M::KEY_TYPE; + + fn from_key_material(key: &KeyMaterial) -> Result { + if key.key_type() != M::KEY_TYPE { + return Err(KeyMaterialError::InvalidKeyType( + "The key is not of the KeyType this mode requires.", + ) + .into()); + } + let perm = match M::KEY_TYPE { + KeyType::SymmetricCipherKey => P::new(key)?, + _ => P::new(&as_cipher_key(key)?)?, + }; + Ok(Self { state: M::key_state(perm) }) + } +} + +// ---- the role impls, forwarded to the mode ----------------------------------------------------- + +impl + SymmetricEncryptionKey for ToyKey +where + M: SymmetricEncryptMode, +{ + type Encryptor = M::Encryptor; + + fn create_encryptor( + &self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + SymmetricDecryptionKey for ToyKey +where + M: SymmetricDecryptMode, +{ + type Decryptor = M::Decryptor; + + fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor { + M::decrypt_init(&self.state, init_data) + } +} + +impl + BlockEncryptionKey for ToyKey +where + M: BlockEncryptMode, +{ + type Encryptor = M::Encryptor; + + fn create_encryptor( + &self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + BlockDecryptionKey for ToyKey +where + M: BlockDecryptMode, +{ + type Decryptor = M::Decryptor; + + fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor { + M::decrypt_init(&self.state, init_data) + } +} + +impl EcbEncryptionKey + for ToyKey +where + M: EcbEncryptMode, +{ + type Encryptor = M::Encryptor; + + fn create_encryptor(&self) -> Self::Encryptor { + M::encrypt_init(&self.state) + } +} + +impl EcbDecryptionKey + for ToyKey +where + M: EcbDecryptMode, +{ + type Decryptor = M::Decryptor; + + fn create_decryptor(&self) -> Self::Decryptor { + M::decrypt_init(&self.state) + } +} + +impl< + P, + M, + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> AeadEncryptionKey for ToyKey +where + M: AeadEncryptMode, +{ + type Encryptor = M::Encryptor; + + fn create_encryptor( + &self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } + + fn create_encryptor_nonce(&self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor { + M::encrypt_init_nonce(&self.state, nonce) + } + + fn encrypt_detached_in_place( + &self, + rng: &mut dyn RNG, + aad: &[u8], + buf: &mut [u8], + ) -> Result<([u8; NONCE_LEN], [u8; TAG_LEN]), SymmetricCipherError> { + M::encrypt_detached_in_place(&self.state, rng, aad, buf) + } + + fn encrypt_detached_in_place_nonce( + &self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + buf: &mut [u8], + ) -> Result<[u8; TAG_LEN], SymmetricCipherError> { + M::encrypt_detached_in_place_nonce(&self.state, nonce, aad, buf) + } +} + +impl< + P, + M, + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> AeadDecryptionKey for ToyKey +where + M: AeadDecryptMode, +{ + type Decryptor = M::Decryptor; + + fn create_decryptor(&self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor { + M::decrypt_init(&self.state, nonce) + } + + fn decrypt_detached_in_place( + &self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + buf: &mut [u8], + tag: &[u8; TAG_LEN], + ) -> Result<(), SymmetricCipherError> { + M::decrypt_detached_in_place(&self.state, nonce, aad, buf, tag) + } +} + +impl< + P, + M, + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> AeadPacketEncryptionKey for ToyKey +where + M: AeadPacketEncryptMode, +{ + type Encryptor = M::Encryptor; + + fn create_encryptor( + &self, + rng: &mut dyn RNG, + aad_len: usize, + payload_len: usize, + ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng, aad_len, payload_len) + } + + fn create_encryptor_nonce( + &self, + nonce: &[u8; NONCE_LEN], + aad_len: usize, + payload_len: usize, + ) -> Result { + M::encrypt_init_nonce(&self.state, nonce, aad_len, payload_len) + } + + fn encrypt_detached_in_place( + &self, + rng: &mut dyn RNG, + aad: &[u8], + buf: &mut [u8], + ) -> Result<([u8; NONCE_LEN], [u8; TAG_LEN]), SymmetricCipherError> { + M::encrypt_detached_in_place(&self.state, rng, aad, buf) + } + + fn encrypt_detached_in_place_nonce( + &self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + buf: &mut [u8], + ) -> Result<[u8; TAG_LEN], SymmetricCipherError> { + M::encrypt_detached_in_place_nonce(&self.state, nonce, aad, buf) + } +} + +impl< + P, + M, + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> AeadPacketDecryptionKey for ToyKey +where + M: AeadPacketDecryptMode, +{ + type Decryptor = M::Decryptor; + + fn create_decryptor( + &self, + nonce: &[u8; NONCE_LEN], + aad_len: usize, + payload_len: usize, + ) -> Result { + M::decrypt_init(&self.state, nonce, aad_len, payload_len) + } + + fn decrypt_detached_in_place( + &self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + buf: &mut [u8], + tag: &[u8; TAG_LEN], + ) -> Result<(), SymmetricCipherError> { + M::decrypt_detached_in_place(&self.state, nonce, aad, buf, tag) + } +} + +impl> MacGenerationKey for ToyKey { + type Generator = M::Worker; + + fn create_generator(&self) -> Self::Generator { + M::mac_init(&self.state) + } +} + +impl> MacVerificationKey for ToyKey { + type Verifier = M::Worker; + + fn create_verifier(&self) -> Self::Verifier { + M::mac_init(&self.state) + } +} + +// ---- the modes whose key state needs the key length: GCM, GMAC and CMAC over a 16-byte toy ---- + +/// GCM over a 16-byte toy, as a mode type. +pub struct ToyGCM; + +impl Mode

for ToyGCM +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type KeyState = GcmKeyState

; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + GcmKeyState::new::(perm) + } +} + +impl AeadEncryptMode + for ToyGCM +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Encryptor = Gcm; + + fn encrypt_init( + state: &Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + Self::Encryptor::from_key_state(state, rng) + } + + fn encrypt_init_nonce(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Encryptor { + Self::Encryptor::from_key_state_nonce(state, nonce) + } +} + +impl AeadDecryptMode + for ToyGCM +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Decryptor = Gcm; + + fn decrypt_init(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Decryptor { + Self::Decryptor::from_key_state(state, nonce) + } +} + +/// GMAC over a 16-byte toy, as a mode type; the generator/verifier pair is inherent on the key, +/// as it is for the AES key, because a GMAC tag is bound to a nonce. +pub struct ToyGMAC; + +impl Mode

for ToyGMAC +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type KeyState = GcmKeyState

; + const KEY_TYPE: KeyType = KeyType::MACKey; + + fn key_state(perm: P) -> Self::KeyState { + GcmKeyState::new::(perm) + } +} + +impl ToyKey> +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + pub fn create_generator( + &self, + rng: &mut dyn RNG, + ) -> Result< + (Gmac, [u8; GCM_NONCE_LEN]), + SymmetricCipherError, + > { + Gmac::::from_key_state(&self.state, rng) + } + + pub fn create_generator_nonce( + &self, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Gmac { + Gmac::::from_key_state_nonce(&self.state, nonce) + } + + pub fn create_verifier( + &self, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Gmac { + Gmac::::from_key_state(&self.state, nonce) + } +} + +/// CMAC over a 16-byte toy, as a mode type. +pub struct ToyCMAC; + +impl

Mode

for ToyCMAC +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type KeyState = CmacKeyState; + const KEY_TYPE: KeyType = KeyType::MACKey; + + fn key_state(perm: P) -> Self::KeyState { + CmacKeyState::new::(perm) + } +} + +impl

MacMode

for ToyCMAC +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Worker = Cmac; + + fn mac_init(state: &Self::KeyState) -> Self::Worker { + Cmac::from_key_state(state) + } +} + +/// An RNG for the tests that need one and do not care which: the OS-seeded DRBG the trait +/// constructors used to reach for themselves. +pub fn os_rng() -> bouncycastle_rng::HashDRBG_SHA512 { + bouncycastle_rng::HashDRBG_SHA512::new_from_os() +} diff --git a/crypto/cipher/tests/modes/alg_name_tests.rs b/crypto/cipher/tests/modes/alg_name_tests.rs index 6f8b69aa8..8235837c6 100644 --- a/crypto/cipher/tests/modes/alg_name_tests.rs +++ b/crypto/cipher/tests/modes/alg_name_tests.rs @@ -6,14 +6,17 @@ mod common; -use bouncycastle_cipher::modes::hazmat::ecb::Ecb; +use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{ - Cbc, CcmDecryptor, CcmEncryptor, Cfb, Cfb8, Cmac, Ctr, Gcm, Gmac, ModeNames, + Blocks, CBC, CCM, CCMFrame, CFB, CFB8, CTR, Cbc, CcmDecryptor, CcmEncryptor, Cfb, Cfb8, Cmac, + Ctr, Gcm, Gmac, ModeNames, }; use bouncycastle_cipher::padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, }; use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::traits::Algorithm; use common::Toy; @@ -80,3 +83,24 @@ fn the_wrappers_forward_the_inner_name() { Toy::ECB_ALG_NAME, ); } + +/// A mode type's key reports the mode's name and asks for a cipher key, whatever the padding or +/// frame parameters. +#[test] +fn each_mode_type_reports_its_mode_name() { + fn check>(name: &str) { + assert_eq!(M::ALG_NAME, name); + assert_eq!(M::KEY_TYPE, KeyType::SymmetricCipherKey, "{name}"); + } + check::>(Toy::CBC_ALG_NAME); + check::>(Toy::CBC_ALG_NAME); + check::>(Toy::CBC_ALG_NAME); + check::>(Toy::ECB_ALG_NAME); + check::>(Toy::ECB_ALG_NAME); + check::(Toy::CFB_ALG_NAME); + check::(Toy::CFB8_ALG_NAME); + check::(Toy::CTR_ALG_NAME); + check::>(Toy::CTR_ALG_NAME); + check::>(Toy::CCM_ALG_NAME); + check::>(Toy::CCM_ALG_NAME); +} diff --git a/crypto/cipher/tests/modes/cbc_tests.rs b/crypto/cipher/tests/modes/cbc_tests.rs index 43ad60dcc..ce7df88eb 100644 --- a/crypto/cipher/tests/modes/cbc_tests.rs +++ b/crypto/cipher/tests/modes/cbc_tests.rs @@ -6,17 +6,32 @@ mod common; -use bouncycastle_cipher::modes::Cbc; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +#[path = "../common/toy_key.rs"] +mod toy_key; + +use bouncycastle_cipher::modes::{Blocks, CBC, ModeNames}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use common::{SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; +use toy_key::{ToyKey, os_rng}; + +type ToyCbcKey = ToyKey>; + +fn cbc_key() -> ToyCbcKey { + ToyCbcKey::from_key_material(&toy_key()).expect("the toy key") +} -type ToyCbc

= Cbc; -type SwappedCbc = Cbc; -type SwappedFourCbc = Cbc; +fn swapped_key() -> ToyKey> { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn swapped_four_key() -> ToyKey> { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} /// The implementor hook `do_encrypt_blocks_inplace`, by value, for tests whose data is /// block-shaped. @@ -50,7 +65,7 @@ fn the_toy_permutation_conforms_to_the_trait() { #[test] fn cbc_conforms_to_the_block_cipher_framework() { TestFrameworkBlockCipher::new() - .test::, ToyCbc>(); + .test_key::(&cbc_key(), &mut os_rng()); } // ---- chaining and call sequencing -------------------------------------------------------- @@ -64,23 +79,22 @@ fn cbc_conforms_to_the_block_cipher_framework() { /// remainder after the pair loop, and `N = 1` skips the pair loop altogether. #[test] fn call_grouping_does_not_change_the_result() { - let key = toy_key(); + let key = cbc_key(); let plaintext: [[u8; TOY_LEN]; 8] = core::array::from_fn(|i| core::array::from_fn(|j| (i * TOY_LEN + j) as u8)); // Both encryption runs must use the same IV to be comparable, so pin it with the fixed RNG - // rather than letting `do_encrypt_init` generate a fresh one. + // rather than letting `create_encryptor` draw a fresh one. let iv: [u8; TOY_LEN] = core::array::from_fn(|i| 0xF0 ^ (i as u8)); let pinned_rng = || bouncycastle_core_test_framework::FixedSeedRNG::::new(iv); // Reference: all eight blocks in one call (two fours). - let (mut enc, got_iv) = - ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); + let (mut enc, got_iv) = key.create_encryptor(&mut pinned_rng()).unwrap(); assert_eq!(got_iv, iv, "the pinned RNG should reproduce the IV"); 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 enc, _) = key.create_encryptor(&mut pinned_rng()).unwrap(); let mut got = [[0u8; TOY_LEN]; 8]; let a = enc_blocks(&mut enc, &[plaintext[0]])[0]; // N = 1 let b = enc_blocks(&mut enc, &[plaintext[1], plaintext[2]]); // N = 2 @@ -96,12 +110,12 @@ fn call_grouping_does_not_change_the_result() { // Now the decrypt side: one call vs several groupings, all from the same ciphertext. let ct = reference; - let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let all_at_once = dec_blocks(&mut dec, &ct); assert_eq!(all_at_once, plaintext); for grouping in [1usize, 2, 4] { - let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut out = [[0u8; TOY_LEN]; 8]; let mut at = 0; while at < 8 { @@ -124,7 +138,7 @@ 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 mut dec = key.create_decryptor(&iv); 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]]); @@ -139,21 +153,21 @@ fn call_grouping_does_not_change_the_result() { /// path would be dead code and every claim about it would be untested. #[test] fn the_pair_path_is_really_used() { - let key = toy_key(); + let key = cbc_key(); let plaintext = [[0xA5u8; TOY_LEN], [0x5Au8; TOY_LEN]]; // The correct toy round-trips. - let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let ct = enc_blocks(&mut enc, &plaintext); - let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); 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 (mut enc, iv) = swapped_key().create_encryptor(&mut os_rng()).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(); + let mut dec = swapped_key().create_decryptor(&iv); assert_ne!( dec_blocks(&mut dec, &ct), plaintext, @@ -161,7 +175,7 @@ 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 mut dec = swapped_key().create_decryptor(&iv); let p0 = dec_blocks(&mut dec, &[ct[0]])[0]; let p1 = dec_blocks(&mut dec, &[ct[1]])[0]; assert_eq!([p0, p1], plaintext, "the single-block path must not pair"); @@ -176,30 +190,30 @@ fn the_pair_path_is_really_used() { /// two pairs or one at a time. Five blocks are wrong too: four, then one. #[test] fn the_four_block_path_is_really_used() { - let key = toy_key(); + let key = cbc_key(); let plaintext: [[u8; TOY_LEN]; 5] = core::array::from_fn(|i| [0x10 * i as u8 + 1; TOY_LEN]); // The correct toy round-trips five blocks. - let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let ct = enc_blocks(&mut enc, &plaintext); - let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); assert_eq!(dec_blocks(&mut dec, &ct), plaintext); // The rotated-four toy encrypts identically (encryption is serial and never batches)... - let (mut enc, iv) = SwappedFourCbc::::do_encrypt_init(&key).unwrap(); + let (mut enc, iv) = swapped_four_key().create_encryptor(&mut os_rng()).unwrap(); let ct = enc_blocks(&mut enc, &plaintext); // ...but decrypting five together must be wrong, because the first four take the four path. - let mut dec = SwappedFourCbc::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = swapped_four_key().create_decryptor(&iv); assert_ne!(dec_blocks(&mut dec, &ct), plaintext, "four blocks must go through decrypt_4blocks"); // Exactly four together is wrong for the same reason. let four: [[u8; TOY_LEN]; 4] = ct[..4].try_into().unwrap(); - let mut dec = SwappedFourCbc::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = swapped_four_key().create_decryptor(&iv); assert_ne!(&dec_blocks(&mut dec, &four)[..], &plaintext[..4]); // Two pairs go through the pair path and are correct; so is the fifth block on its own. - let mut dec = SwappedFourCbc::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = swapped_four_key().create_decryptor(&iv); let first: [[u8; TOY_LEN]; 2] = ct[..2].try_into().unwrap(); let second: [[u8; TOY_LEN]; 2] = ct[2..4].try_into().unwrap(); assert_eq!( @@ -221,10 +235,10 @@ fn the_four_block_path_is_really_used() { /// permutation, and getting it wrong would mean the IV is not being XOR-ed where the spec says. #[test] fn an_iv_bit_error_flips_exactly_that_bit_of_the_first_block() { - let key = toy_key(); + let key = cbc_key(); let plaintext = [[0x00u8; TOY_LEN], [0x11u8; TOY_LEN], [0x22u8; TOY_LEN]]; - let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let ct = enc_blocks(&mut enc, &plaintext); for byte in 0..TOY_LEN { @@ -232,7 +246,7 @@ fn an_iv_bit_error_flips_exactly_that_bit_of_the_first_block() { let mut corrupt_iv = iv; corrupt_iv[byte] ^= 1 << bit; - let mut dec = ToyCbc::::do_decrypt_init(&key, &corrupt_iv).unwrap(); + let mut dec = key.create_decryptor(&corrupt_iv); let got = dec_blocks(&mut dec, &ct); let mut expected = plaintext; @@ -249,16 +263,16 @@ fn an_iv_bit_error_flips_exactly_that_bit_of_the_first_block() { /// the same bit positions of `Cj+1`'s decryption, leaving later blocks alone. #[test] fn a_ciphertext_bit_error_affects_only_two_blocks() { - let key = toy_key(); + let key = cbc_key(); 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 (mut enc, iv) = key.create_encryptor(&mut os_rng()).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 mut dec = key.create_decryptor(&iv); let got = dec_blocks(&mut dec, &corrupt); assert_eq!(got[0], plaintext[0], "P1 depends only on C1 and the IV"); @@ -276,10 +290,10 @@ fn a_ciphertext_bit_error_affects_only_two_blocks() { /// repeated here because for CBC it is the single most important operational requirement. #[test] fn each_encryption_gets_a_fresh_iv() { - let key = toy_key(); + let key = cbc_key(); let mut seen = std::collections::BTreeSet::new(); for _ in 0..64 { - let (_, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let (_, iv) = key.create_encryptor(&mut os_rng()).unwrap(); assert!(seen.insert(iv), "IV repeated across encryptions: {iv:02x?}"); } } @@ -288,13 +302,13 @@ fn each_encryption_gets_a_fresh_iv() { /// This is the property ECB lacks and the reason CBC needs an IV at all. #[test] fn identical_plaintext_gives_different_ciphertext() { - let key = toy_key(); + let key = cbc_key(); let plaintext = [0x77u8; 2 * TOY_LEN]; let mut first = plaintext; - ToyCbc::::encrypt_inplace(&key, &mut first).unwrap(); + key.encrypt_inplace(&mut os_rng(), &mut first).unwrap(); let mut second = plaintext; - ToyCbc::::encrypt_inplace(&key, &mut second).unwrap(); + key.encrypt_inplace(&mut os_rng(), &mut second).unwrap(); assert_ne!(first, second); // ...and, within one message, two identical plaintext blocks must not give identical @@ -308,12 +322,11 @@ fn identical_plaintext_gives_different_ciphertext() { // ---- key handling ------------------------------------------------------------------------ +/// The key's construction suite: the type, binding and strength policy of `from_key_material`. #[test] -fn a_key_of_the_wrong_type_is_rejected() { - let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); - let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); - assert!(ToyCbc::::do_encrypt_init(&seed).is_err()); - assert!(ToyCbc::::do_decrypt_init(&seed, &[0u8; TOY_LEN]).is_err()); +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::(Toy::CBC_ALG_NAME, SecurityStrength::_128bit); } /// The one-shots (`encrypt` / `decrypt` on a `[u8; LEN]`, in place) must produce exactly what the @@ -321,7 +334,7 @@ fn a_key_of_the_wrong_type_is_rejected() { /// tail) and an even one (pairs only), in both directions. #[test] fn one_shots_agree_with_the_streaming_api() { - let key = toy_key(); + let key = cbc_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); @@ -330,16 +343,14 @@ fn one_shots_agree_with_the_streaming_api() { 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(); + let (mut enc, iv) = key.create_encryptor(&mut pinned_rng()).unwrap(); (iv, enc_blocks(&mut enc, &blocks3)) }; let mut buf = flat3; - let (_, iv_b) = - ToyCbc::::encrypt_rng_inplace(&key, &mut pinned_rng(), &mut buf).unwrap(); + let (_, iv_b) = key.encrypt_inplace(&mut pinned_rng(), &mut buf).unwrap(); assert_eq!(iv_a, iv_b); assert_eq!(buf, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); - ToyCbc::::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + key.decrypt_inplace(&iv, &mut buf).unwrap(); assert_eq!(buf, flat3); // 4 blocks = 64 bytes: pairs only, no tail. @@ -347,20 +358,19 @@ fn one_shots_agree_with_the_streaming_api() { 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, _) = - ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); + let (mut enc, _) = key.create_encryptor(&mut pinned_rng()).unwrap(); enc_blocks(&mut enc, &blocks4) }; let mut buf = flat4; - ToyCbc::::encrypt_rng_inplace(&key, &mut pinned_rng(), &mut buf).unwrap(); + key.encrypt_inplace(&mut pinned_rng(), &mut buf).unwrap(); assert_eq!(buf, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); - ToyCbc::::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + key.decrypt_inplace(&iv, &mut buf).unwrap(); assert_eq!(buf, flat4); // The OS-RNG variant round-trips too. let mut buf = flat3; - let (_, iv_fresh) = ToyCbc::::encrypt_inplace(&key, &mut buf).unwrap(); + let (_, iv_fresh) = key.encrypt_inplace(&mut os_rng(), &mut buf).unwrap(); assert_ne!(buf, flat3); - ToyCbc::::decrypt_inplace(&key, &iv_fresh, &mut buf).unwrap(); + key.decrypt_inplace(&iv_fresh, &mut buf).unwrap(); assert_eq!(buf, flat3); } diff --git a/crypto/cipher/tests/modes/ccm_tests.rs b/crypto/cipher/tests/modes/ccm_tests.rs index 522107126..c0b798da7 100644 --- a/crypto/cipher/tests/modes/ccm_tests.rs +++ b/crypto/cipher/tests/modes/ccm_tests.rs @@ -15,15 +15,24 @@ //! Spec references are to NIST SP 800-38C (May 2004, errata update 07-20-2007). mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; -use bouncycastle_cipher::modes::{CCM, CcmDecryptor, CcmEncryptor, ModeNames}; +use bouncycastle_cipher::modes::{CCM, CCMFrame, ModeNames}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::{AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook}; +use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, + FromKeyMaterial, +}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; +use toy_key::ToyKey; /// The default shape under test: a 12-byte nonce, so `q = 3`, and a full 16-byte tag. const NONCE_LEN: usize = 12; @@ -31,10 +40,11 @@ const TAG_LEN: usize = 16; /// The streams' AAD capacity, above any AAD used here. const AAD_LEN: usize = 64; -type ToyEnc = - CcmEncryptor; -type ToyDec = - CcmDecryptor; +/// The toy key under the in-place pair, which takes any length. +type PacketKey

= ToyKey>; +/// The toy key under the fixed-frame streams, whose frame is `DATA_LEN`. +type FrameKey = + ToyKey>; fn pinned_nonce() -> [u8; NONCE_LEN] { core::array::from_fn(|i| 0xA0 ^ (i as u8)) @@ -44,8 +54,13 @@ fn message(len: usize) -> Vec { (0..len).map(|i| (i as u8).wrapping_mul(3).wrapping_add(1)).collect() } -fn perm>() -> P { - P::new(&toy_key()).expect("the toy key") +fn packet_key + ModeNames + Clone>() -> PacketKey

{ + PacketKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn frame_key + ModeNames + Clone, const DATA_LEN: usize>() +-> FrameKey { + FrameKey::from_key_material(&toy_key()).expect("the toy key") } /// One-shot Sec 6.1 over the toy permutation `P`, in place: the ciphertext and the tag. @@ -55,13 +70,7 @@ fn seal + ModeNames + Clone>( plaintext: &[u8], ) -> (Vec, [u8; TAG_LEN]) { let mut buf = plaintext.to_vec(); - let tag = CCM::::encrypt_detached_in_place_nonce( - &perm::

(), - nonce, - aad, - &mut buf, - ) - .unwrap(); + let tag = packet_key::

().encrypt_detached_in_place_nonce(nonce, aad, &mut buf).unwrap(); (buf, tag) } @@ -73,19 +82,22 @@ fn open + ModeNames + Clone>( tag: &[u8; TAG_LEN], ) -> Result, SymmetricCipherError> { let mut buf = ciphertext.to_vec(); - CCM::::decrypt_detached_in_place(&perm::

(), nonce, aad, &mut buf, tag)?; + packet_key::

().decrypt_detached_in_place(nonce, aad, &mut buf, tag)?; Ok(buf) } /// Sec 6.1 through the fixed-frame stream over `P`, `chunk` bytes per update. The frame is /// `DATA_LEN`, so `plaintext` must be exactly that long. -fn stream_encrypt + ModeNames, const DATA_LEN: usize>( +fn stream_encrypt< + P: ElectronicCodeBook + ModeNames + Clone, + const DATA_LEN: usize, +>( nonce: &[u8; NONCE_LEN], aad: &[u8], plaintext: &[u8], chunk: usize, ) -> (Vec, [u8; TAG_LEN]) { - let mut enc = ToyEnc::::do_encrypt_init_nonce(&toy_key(), nonce).unwrap(); + let mut enc = frame_key::().create_encryptor_nonce(nonce); enc.do_update_aad(aad).unwrap(); let mut out = vec![0u8; plaintext.len()]; let mut written = 0; @@ -98,14 +110,17 @@ fn stream_encrypt + ModeNames, const DAT } /// Sec 6.2 through the fixed-frame stream over `P`, `chunk` bytes per update, detached tag. -fn stream_decrypt + ModeNames, const DATA_LEN: usize>( +fn stream_decrypt< + P: ElectronicCodeBook + ModeNames + Clone, + const DATA_LEN: usize, +>( nonce: &[u8; NONCE_LEN], aad: &[u8], ciphertext: &[u8], tag: &[u8; TAG_LEN], chunk: usize, ) -> Result, SymmetricCipherError> { - let mut dec = ToyDec::::do_decrypt_init(&toy_key(), nonce)?; + let mut dec = frame_key::().create_decryptor(nonce); dec.do_update_aad(aad)?; let mut out = vec![0u8; ciphertext.len()]; let mut written = 0; @@ -181,7 +196,7 @@ fn the_pair_path_is_really_used_in_both_directions_and_the_mac_never_batches() { // Decryption: the swapped keystream recovers the wrong plaintext (Sec 6.2 step 5), which is // what the MAC then absorbs (step 9), so the tag check fails. The stream releases the // plaintext before the final, so the wrong recovery can be seen. - let mut dec = ToyDec::::do_decrypt_init(&toy_key(), &nonce).unwrap(); + let mut dec = frame_key::().create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut back = vec![0u8; LEN]; dec.do_decrypt_out(&ct, &mut back).unwrap(); @@ -212,7 +227,7 @@ fn the_four_block_path_is_really_used_in_both_directions() { assert_eq!(pairs_ct, ct, "pairs must not use the four path"); assert_eq!(pairs_tag, tag); - let mut dec = ToyDec::::do_decrypt_init(&toy_key(), &nonce).unwrap(); + let mut dec = frame_key::().create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut back = vec![0u8; LEN]; dec.do_decrypt_out(&ct, &mut back).unwrap(); @@ -244,7 +259,7 @@ fn every_split_agrees_with_the_one_shot_in_both_directions() { let (ct, tag) = seal::(&nonce, aad, &plaintext); for split in 0..=LEN { - let mut enc = ToyEnc::::do_encrypt_init_nonce(&toy_key(), &nonce).unwrap(); + let mut enc = frame_key::().create_encryptor_nonce(&nonce); enc.do_update_aad(aad).unwrap(); let mut streamed = vec![0u8; LEN]; enc.do_encrypt_out(&plaintext[..split], &mut streamed).unwrap(); @@ -253,7 +268,7 @@ fn every_split_agrees_with_the_one_shot_in_both_directions() { assert_eq!(streamed_tag, tag, "tag, split at {split}"); assert_eq!(streamed, ct, "ciphertext, split at {split}"); - let mut dec = ToyDec::::do_decrypt_init(&toy_key(), &nonce).unwrap(); + let mut dec = frame_key::().create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut back = vec![0u8; LEN]; dec.do_decrypt_out(&ct[..split], &mut back).unwrap(); @@ -275,7 +290,7 @@ fn both_streams_hold_to_the_frame() { let (ct, tag) = seal::(&nonce, &[], &plaintext); // Encrypting: too much is refused, and finalizing short is refused. - let mut enc = ToyEnc::::do_encrypt_init_nonce(&toy_key(), &nonce).unwrap(); + let mut enc = frame_key::().create_encryptor_nonce(&nonce); let mut out = [0u8; 9]; assert!( matches!( @@ -292,7 +307,7 @@ fn both_streams_hold_to_the_frame() { // Decrypting: the stream's bound is the frame plus an inline tag, and a final that has not // seen the frame reports a malformed ciphertext. - let mut dec = ToyDec::::do_decrypt_init(&toy_key(), &nonce).unwrap(); + let mut dec = frame_key::().create_decryptor(&nonce); let mut out = [0u8; LEN]; assert!( matches!( @@ -306,7 +321,7 @@ fn both_streams_hold_to_the_frame() { dec.do_decrypt_final_detached(&tag).unwrap(); assert_eq!(out, plaintext[..]); - let mut dec = ToyDec::::do_decrypt_init(&toy_key(), &nonce).unwrap(); + let mut dec = frame_key::().create_decryptor(&nonce); dec.do_decrypt_out(&ct[..4], &mut out).unwrap(); assert!( matches!(dec.do_decrypt_final_detached(&tag), Err(SymmetricCipherError::DecryptionFailed)), @@ -335,12 +350,11 @@ fn tag_length_changes_the_tag_but_not_the_ciphertext_and_tags_do_not_nest() { macro_rules! check_tag_len { ($t:literal) => {{ - type Mode = CCM; + let key = ToyKey::>::from_key_material(&toy_key()).unwrap(); let mut buf = plaintext.clone(); - let tag = Mode::encrypt_detached_in_place_nonce(&perm::(), &nonce, aad, &mut buf) - .unwrap(); + let tag = key.encrypt_detached_in_place_nonce(&nonce, aad, &mut buf).unwrap(); assert_eq!(buf, ct16, "ciphertext must not depend on TAG_LEN ({})", $t); - Mode::decrypt_detached_in_place(&perm::(), &nonce, aad, &mut buf, &tag).unwrap(); + key.decrypt_detached_in_place(&nonce, aad, &mut buf, &tag).unwrap(); assert_eq!(buf, plaintext, "TAG_LEN={} round trip", $t); tag.to_vec() }}; @@ -388,17 +402,16 @@ fn every_permitted_nonce_length_works() { expected_max_payload, "n = {N}: the payload limit 2^8q - 1 that q = 15 - n implies" ); - let perm = P::new(key).unwrap(); + let key = ToyKey::>::from_perm(P::new(key).unwrap()); let nonce: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(11).wrapping_add(3)); let plaintext = message(100); let mut buf = plaintext.clone(); - let tag = - Mode::::encrypt_detached_in_place_nonce(&perm, &nonce, b"aad", &mut buf).unwrap(); + let tag = key.encrypt_detached_in_place_nonce(&nonce, b"aad", &mut buf).unwrap(); assert_ne!(buf, plaintext, "nonce length {N}: must actually encrypt"); let ct = buf.clone(); - Mode::::decrypt_detached_in_place(&perm, &nonce, b"aad", &mut buf, &tag).unwrap(); + key.decrypt_detached_in_place(&nonce, b"aad", &mut buf, &tag).unwrap(); assert_eq!(buf, plaintext, "nonce length {N}: round trip"); // The last nonce octet sits right before `Q` in B0 (Table 2) and before the counter in @@ -408,7 +421,7 @@ fn every_permitted_nonce_length_works() { let mut buf = ct; assert!( matches!( - Mode::::decrypt_detached_in_place(&perm, &wrong, b"aad", &mut buf, &tag), + key.decrypt_detached_in_place(&wrong, b"aad", &mut buf, &tag), Err(SymmetricCipherError::AEADTagCheckFailed) ), "nonce length {N}: the nonce is authenticated" @@ -446,13 +459,7 @@ fn the_one_shot_releases_nothing_on_forgery_but_the_stream_does() { // The one-shot: verify-then-return, so a forged tag leaves nothing but zeros. let mut buf = ct.clone(); assert!(matches!( - CCM::::decrypt_detached_in_place( - &perm::(), - &nonce, - b"aad", - &mut buf, - &tag - ), + packet_key::().decrypt_detached_in_place(&nonce, b"aad", &mut buf, &tag), Err(SymmetricCipherError::AEADTagCheckFailed) )); assert_eq!(buf, [0u8; LEN], "the one-shot must zeroize its buffer on a forged tag"); @@ -460,7 +467,7 @@ fn the_one_shot_releases_nothing_on_forgery_but_the_stream_does() { // The stream: the payload is released by the update that brings it, the finals release // nothing, and a rejected tag cannot take it back. The detached final leaves its (unused) // buffer alone rather than zeroizing it, since there is nothing of the plaintext in it. - let mut dec = ToyDec::::do_decrypt_init(&toy_key(), &nonce).unwrap(); + let mut dec = frame_key::().create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut streamed = [0u8; LEN]; assert_eq!(dec.do_decrypt_out(&ct, &mut streamed).unwrap(), LEN, "released mid-stream"); @@ -475,7 +482,7 @@ fn the_one_shot_releases_nothing_on_forgery_but_the_stream_does() { let mut inline = ct.clone(); inline.extend_from_slice(&tag); - let mut dec = ToyDec::::do_decrypt_init(&toy_key(), &nonce).unwrap(); + let mut dec = frame_key::().create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut streamed = [0u8; LEN]; assert_eq!(dec.do_decrypt_out(&inline, &mut streamed).unwrap(), LEN); @@ -496,3 +503,12 @@ fn a_large_payload_round_trips_in_place() { assert_ne!(ct, plaintext, "must actually encrypt"); assert_eq!(open::(&nonce, aad, &ct, &tag).unwrap(), plaintext, "in-place round trip"); } + +/// The construction suite for both keys: the type, binding and strength policy of +/// `from_key_material`. +#[test] +fn the_keys_enforce_the_material_policy() { + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::>(Toy::CCM_ALG_NAME, SecurityStrength::_128bit); + framework.test::>(Toy::CCM_ALG_NAME, SecurityStrength::_128bit); +} diff --git a/crypto/cipher/tests/modes/cfb8_tests.rs b/crypto/cipher/tests/modes/cfb8_tests.rs index d3ee96f6f..df8fafe5f 100644 --- a/crypto/cipher/tests/modes/cfb8_tests.rs +++ b/crypto/cipher/tests/modes/cfb8_tests.rs @@ -12,23 +12,43 @@ //! is not re-run. mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; -use bouncycastle_cipher::modes::{Cfb, Cfb8, ModeNames}; +use bouncycastle_cipher::modes::{CFB, CFB8, Cfb8, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCipher; use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; +use toy_key::{ToyKey, os_rng}; type ToyCfb8

= Cfb8; -type SwappedCfb8 = Cfb8; -type ForwardOnlyCfb8 = Cfb8; -type SwappedFourCfb8 = Cfb8; + +type ToyCfb8Key = ToyKey; + +fn cfb8_key() -> ToyCfb8Key { + ToyCfb8Key::from_key_material(&toy_key()).expect("the toy key") +} + +fn swapped_key() -> ToyKey { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn swapped_four_key() -> ToyKey { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn forward_only_key() -> ToyKey { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} /// `do_encrypt_inplace`, by value. fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { @@ -58,7 +78,7 @@ fn dec_chunked( } /// A pinned IV, so two runs are comparable. Encryption never accepts one, so it is fed through the -/// fixed-output RNG that `do_encrypt_init_rng` takes. +/// fixed-output RNG that `create_encryptor` takes. fn pinned_iv() -> [u8; TOY_LEN] { core::array::from_fn(|i| 0xF0 ^ (i as u8)) } @@ -68,14 +88,13 @@ fn pinned_rng(iv: [u8; TOY_LEN]) -> FixedSeedRNG { } fn pinned_encryptor(iv: [u8; TOY_LEN]) -> ToyCfb8 { - let (enc, got) = ToyCfb8::::do_encrypt_init_rng(&toy_key(), &mut pinned_rng(iv)) - .expect("encrypt init"); + let (enc, got) = cfb8_key().create_encryptor(&mut pinned_rng(iv)).expect("encrypt init"); assert_eq!(got, iv, "the pinned RNG should reproduce the IV"); enc } fn pinned_decryptor(iv: [u8; TOY_LEN]) -> ToyCfb8 { - ToyCfb8::::do_decrypt_init(&toy_key(), &iv).expect("decrypt init") + cfb8_key().create_decryptor(&iv) } /// A test message of `len` bytes with no repeating structure at the block size. @@ -91,8 +110,7 @@ const CHUNKINGS: [usize; 11] = [1, 2, 3, 7, 8, 9, 15, 16, 17, 32, 100]; #[test] fn cfb8_conforms_to_the_stream_cipher_framework() { - TestFrameworkStreamCipher::new() - .test::, ToyCfb8>(); + TestFrameworkStreamCipher::new().test_key::(&cfb8_key(), &mut os_rng()); } // ---- the spec equations ------------------------------------------------------------------- @@ -137,9 +155,8 @@ fn reference_cfb8(perm: &Toy, iv: [u8; TOY_LEN], input: &[u8], encrypt: bool) -> /// mistake preserves: `C1 = P1 XOR MSB_8(CIPH_K(IV))`, and the second input block. #[test] fn the_mode_matches_the_spec_equations() { - let key = toy_key(); let iv = pinned_iv(); - let perm = >::new(&key).unwrap(); + let perm = >::new(&toy_key()).unwrap(); for len in [1, 2, TOY_LEN - 1, TOY_LEN, TOY_LEN + 1, 3 * TOY_LEN + 5] { let plaintext = message(len); @@ -201,15 +218,14 @@ fn the_mode_matches_the_spec_equations() { /// is what catches a CFB8 that has quietly become CFB128 or vice versa. #[test] fn cfb8_is_not_cfb128() { - let key = toy_key(); + let key = cfb8_key(); let iv = pinned_iv(); let plaintext = message(2 * TOY_LEN); let cfb8 = enc(&mut pinned_encryptor(iv), &plaintext); - let (mut cfb, got) = - Cfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)) - .unwrap(); + let cfb128_key = ToyKey::::from_key_material(&toy_key()).unwrap(); + let (mut cfb, got) = cfb128_key.create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(got, iv); let mut cfb128 = plaintext.clone(); cfb.do_encrypt_inplace(&mut cfb128).unwrap(); @@ -219,11 +235,11 @@ fn cfb8_is_not_cfb128() { // ...and neither can decrypt the other's ciphertext. let mut wrong = cfb128.clone(); - ToyCfb8::::decrypt_inplace(&key, &iv, &mut wrong).unwrap(); + key.decrypt_inplace(&iv, &mut wrong).unwrap(); assert_ne!(wrong, plaintext, "CFB8 must not decrypt a CFB128 ciphertext"); let mut wrong = cfb8.clone(); - Cfb::::decrypt_inplace(&key, &iv, &mut wrong).unwrap(); + cfb128_key.decrypt_inplace(&iv, &mut wrong).unwrap(); assert_ne!(wrong, plaintext, "CFB128 must not decrypt a CFB8 ciphertext"); } @@ -259,20 +275,18 @@ fn the_ciphertext_of_a_prefix_is_a_prefix_of_the_ciphertext() { /// the plain [`Toy`], otherwise the test could pass by not really encrypting anything. #[test] fn neither_direction_uses_the_inverse_cipher() { - let key = toy_key(); let iv = pinned_iv(); let plaintext = message(19); - let (mut e, _) = - ForwardOnlyCfb8::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let (mut e, _) = forward_only_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); let ct = enc(&mut e, &plaintext); // One call: four fours, then a pair, then a single byte. - let mut d = ForwardOnlyCfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = forward_only_key().create_decryptor(&iv); assert_eq!(dec(&mut d, &ct), plaintext, "all paths, forward cipher only"); // Byte by byte: the single-byte path only. - let mut d = ForwardOnlyCfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = forward_only_key().create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "single-byte path, forward cipher only"); // The forward-only toy must agree with the real one, or the above proves nothing. @@ -359,28 +373,24 @@ fn call_chunking_does_not_change_the_result() { fn chunking_matches_a_single_call_at_every_batch_remainder() { fn check(name: &str) where - P: ElectronicCodeBook + ModeNames, + P: ElectronicCodeBook + ModeNames + Clone, { let key_bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(31).wrapping_add(7)); let key = KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) .expect("a valid key"); + let key = ToyKey::::from_perm(P::new(&key).expect("a valid key")); let iv: [u8; 16] = core::array::from_fn(|i| 0xC3 ^ (i as u8)); let plaintext: Vec = (0..171).map(|i| (i * 7 + i / 16) as u8).collect(); let encryptor = || { - let (enc, got) = Cfb8::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::<16>::new(iv), - ) - .expect("encrypt init"); + let (enc, got) = + key.create_encryptor(&mut FixedSeedRNG::<16>::new(iv)).expect("encrypt init"); assert_eq!(got, iv, "{name}: the pinned RNG should reproduce the IV"); enc }; - let decryptor = || { - Cfb8::::do_decrypt_init(&key, &iv).expect("decrypt init") - }; + let decryptor = || key.create_decryptor(&iv); // The reference: the whole message in one call. let mut reference = plaintext.clone(); @@ -428,7 +438,6 @@ fn chunking_matches_a_single_call_at_every_batch_remainder() { /// bytes would also be wrong and would not distinguish the two paths. #[test] fn the_pair_path_is_really_used() { - let key = toy_key(); let iv = pinned_iv(); let plaintext = message(2); @@ -437,16 +446,15 @@ fn the_pair_path_is_really_used() { assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); // The swapped-pair toy encrypts identically -- CFB8 encryption is serial and never batches. - let (mut e, _) = - SwappedCfb8::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let (mut e, _) = swapped_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(enc(&mut e, &plaintext), ct, "CFB8 encryption must not use the pair path"); // ...but decrypting two bytes together must now be wrong, because the pair path is used. - let mut d = SwappedCfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_key().create_decryptor(&iv); assert_ne!(dec(&mut d, &ct), plaintext, "a pair must go through encrypt_2blocks"); // One byte at a time avoids the pair path, so it is correct even for this toy. - let mut d = SwappedCfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_key().create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "the single-byte path must not pair"); } @@ -458,7 +466,6 @@ fn the_pair_path_is_really_used() { /// batched, then one), while two bytes (a pair) or one at a time decrypt correctly. #[test] fn the_four_byte_path_is_really_used() { - let key = toy_key(); let iv = pinned_iv(); let plaintext = message(5); @@ -466,29 +473,28 @@ fn the_four_byte_path_is_really_used() { assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); // The rotated-four toy encrypts identically: CFB8 encryption is serial and never batches. - let (mut e, _) = - SwappedFourCfb8::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let (mut e, _) = swapped_four_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(enc(&mut e, &plaintext), ct, "CFB8 encryption must not use the four path"); // ...but five bytes together must now be wrong, because the first four go through // encrypt_4blocks. - let mut d = SwappedFourCfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_four_key().create_decryptor(&iv); assert_ne!(dec(&mut d, &ct), plaintext, "five bytes must go through encrypt_4blocks"); // Two bytes use the pair path only, so they are correct even for this toy... let two = &ct[..2]; - let mut d = SwappedFourCfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_four_key().create_decryptor(&iv); assert_eq!(dec(&mut d, two), plaintext[..2], "pairs must not use the four path"); // ...and so is one byte at a time. - let mut d = SwappedFourCfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_four_key().create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "the single-byte path must not batch"); } /// The one-shots must produce exactly what the streaming API produces. #[test] fn one_shots_agree_with_the_streaming_api() { - let key = toy_key(); + let key = cfb8_key(); let iv = pinned_iv(); for len in [1, 9, 2 * TOY_LEN + 3] { @@ -496,23 +502,21 @@ fn one_shots_agree_with_the_streaming_api() { let streamed = enc(&mut pinned_encryptor(iv), &plaintext); let mut buf = plaintext.clone(); - let (_, iv_b) = - ToyCfb8::::encrypt_rng_inplace(&key, &mut pinned_rng(iv), &mut buf) - .unwrap(); + let (_, iv_b) = key.encrypt_inplace(&mut pinned_rng(iv), &mut buf).unwrap(); assert_eq!(iv_b, iv); assert_eq!(buf, streamed, "len {len}: one-shot must equal streaming"); - ToyCfb8::::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + key.decrypt_inplace(&iv, &mut buf).unwrap(); assert_eq!(buf, plaintext); // The OS-RNG variant round-trips too. Whether the ciphertext *differs* from the plaintext // is only worth asserting once the message is long enough that coinciding with the // keystream by chance is negligible -- see `every_length_round_trips_without_padding`. let mut buf = plaintext.clone(); - let (_, iv_fresh) = ToyCfb8::::encrypt_inplace(&key, &mut buf).unwrap(); + let (_, iv_fresh) = key.encrypt_inplace(&mut os_rng(), &mut buf).unwrap(); if len >= 8 { assert_ne!(buf, plaintext); } - ToyCfb8::::decrypt_inplace(&key, &iv_fresh, &mut buf).unwrap(); + key.decrypt_inplace(&iv_fresh, &mut buf).unwrap(); assert_eq!(buf, plaintext); } } @@ -614,10 +618,10 @@ fn a_ciphertext_bit_error_flips_exactly_that_bit_of_its_own_byte() { /// Two encryption flows under the same key must not reuse an IV. #[test] fn each_encryption_gets_a_fresh_iv() { - let key = toy_key(); + let key = cfb8_key(); let mut seen = std::collections::BTreeSet::new(); for _ in 0..64 { - let (_, iv) = ToyCfb8::::do_encrypt_init(&key).unwrap(); + let (_, iv) = key.create_encryptor(&mut os_rng()).unwrap(); assert!(seen.insert(iv), "IV repeated across encryptions: {iv:02x?}"); } } @@ -625,13 +629,13 @@ fn each_encryption_gets_a_fresh_iv() { /// Identical plaintext under the same key must give different ciphertext, because the IV differs. #[test] fn identical_plaintext_gives_different_ciphertext() { - let key = toy_key(); + let key = cfb8_key(); let plaintext = [0x77u8; 2 * TOY_LEN]; let mut first = plaintext; - ToyCfb8::::encrypt_inplace(&key, &mut first).unwrap(); + key.encrypt_inplace(&mut os_rng(), &mut first).unwrap(); let mut second = plaintext; - ToyCfb8::::encrypt_inplace(&key, &mut second).unwrap(); + key.encrypt_inplace(&mut os_rng(), &mut second).unwrap(); assert_ne!(first, second); // ...and, within one message, a run of identical plaintext bytes must not give a run of @@ -647,12 +651,11 @@ fn identical_plaintext_gives_different_ciphertext() { // ---- key handling ------------------------------------------------------------------------ +/// The key's construction suite: the type, binding and strength policy of `from_key_material`. #[test] -fn a_key_of_the_wrong_type_is_rejected() { - let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); - let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); - assert!(ToyCfb8::::do_encrypt_init(&seed).is_err()); - assert!(ToyCfb8::::do_decrypt_init(&seed, &[0u8; TOY_LEN]).is_err()); +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::(Toy::CFB8_ALG_NAME, SecurityStrength::_128bit); } // ---- every length, no padding ------------------------------------------------------------ @@ -661,11 +664,11 @@ fn a_key_of_the_wrong_type_is_rejected() { /// exactly as long as the plaintext, and no padding layer is involved. #[test] fn every_length_round_trips_without_padding() { - let key = toy_key(); + let key = cfb8_key(); for len in 0..=(2 * TOY_LEN + 1) { let plaintext = message(len); let mut data = plaintext.clone(); - let (n, iv) = ToyCfb8::::encrypt_inplace(&key, &mut data).expect("encryption"); + let (n, iv) = key.encrypt_inplace(&mut os_rng(), &mut data).expect("encryption"); assert_eq!(n, len, "len {len}: encrypt must report the number of bytes written"); assert_eq!(data.len(), len, "len {len}: the ciphertext is as long as the plaintext"); // Only meaningful once the message is long enough that agreeing with the keystream by @@ -676,7 +679,7 @@ fn every_length_round_trips_without_padding() { if len >= 8 { assert_ne!(data, plaintext, "len {len}: the data must actually be encrypted"); } - ToyCfb8::::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); + key.decrypt_inplace(&iv, &mut data).expect("decryption"); assert_eq!(data, plaintext, "len {len}: round trip"); } } diff --git a/crypto/cipher/tests/modes/cfb_tests.rs b/crypto/cipher/tests/modes/cfb_tests.rs index 40349d2cd..469bf4579 100644 --- a/crypto/cipher/tests/modes/cfb_tests.rs +++ b/crypto/cipher/tests/modes/cfb_tests.rs @@ -13,23 +13,45 @@ //! is not re-run. mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; -use bouncycastle_cipher::modes::{Cbc, Cfb, ModeNames}; +use bouncycastle_cipher::modes::{Blocks, CBC, CFB, Cbc, Cfb, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{ - BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, + BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCipher; use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; +use toy_key::{ToyKey, os_rng}; type ToyCfb = Cfb; -type SwappedCfb = Cfb; -type ForwardOnlyCfb = Cfb; -type SwappedFourCfb = Cfb; + +type ToyCfbKey = ToyKey; + +fn cfb_key() -> ToyCfbKey { + ToyCfbKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn swapped_key() -> ToyKey { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn swapped_four_key() -> ToyKey { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn forward_only_key() -> ToyKey { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} /// `do_encrypt_inplace`, by value. fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { @@ -72,7 +94,7 @@ fn dec_chunked( } /// A pinned IV, so two runs are comparable. Encryption never accepts one, so it is fed through the -/// fixed-output RNG that `do_encrypt_init_rng` takes. +/// fixed-output RNG that `create_encryptor` takes. fn pinned_iv() -> [u8; TOY_LEN] { core::array::from_fn(|i| 0xF0 ^ (i as u8)) } @@ -82,14 +104,13 @@ fn pinned_rng(iv: [u8; TOY_LEN]) -> FixedSeedRNG { } fn pinned_encryptor(iv: [u8; TOY_LEN]) -> ToyCfb { - let (enc, got) = ToyCfb::::do_encrypt_init_rng(&toy_key(), &mut pinned_rng(iv)) - .expect("encrypt init"); + let (enc, got) = cfb_key().create_encryptor(&mut pinned_rng(iv)).expect("encrypt init"); assert_eq!(got, iv, "the pinned RNG should reproduce the IV"); enc } fn pinned_decryptor(iv: [u8; TOY_LEN]) -> ToyCfb { - ToyCfb::::do_decrypt_init(&toy_key(), &iv).expect("decrypt init") + cfb_key().create_decryptor(&iv) } /// A test message of `len` bytes with no repeating structure at the block size. @@ -105,8 +126,7 @@ const CHUNKINGS: [usize; 12] = [1, 3, 5, 7, 15, 16, 17, 31, 32, 33, 64, 100]; #[test] fn cfb_conforms_to_the_stream_cipher_framework() { - TestFrameworkStreamCipher::new() - .test::, ToyCfb>(); + TestFrameworkStreamCipher::new().test_key::(&cfb_key(), &mut os_rng()); } // ---- the spec equations ------------------------------------------------------------------- @@ -150,9 +170,8 @@ fn reference_cfb(perm: &Toy, iv: [u8; TOY_LEN], input: &[u8], encrypt: bool) -> /// keystream block itself. #[test] fn the_mode_matches_the_spec_equations() { - let key = toy_key(); let iv = pinned_iv(); - let perm = >::new(&key).unwrap(); + let perm = >::new(&toy_key()).unwrap(); for len in [5 * TOY_LEN, 5 * TOY_LEN + 9, TOY_LEN - 1, 1] { let plaintext = message(len); @@ -191,9 +210,10 @@ fn the_mode_matches_the_spec_equations() { ); // ...and CFB is not CBC: CBC computes `CIPH_K(P1 XOR IV)`, CFB computes `P1 XOR CIPH_K(IV)`. - let (mut cbc, _) = - Cbc::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)) - .unwrap(); + let (mut cbc, _) = ToyKey::>::from_key_material(&toy_key()) + .unwrap() + .create_encryptor(&mut pinned_rng(iv)) + .unwrap(); let mut cbc_c1: [u8; TOY_LEN] = plaintext[..TOY_LEN].try_into().unwrap(); cbc.do_encrypt_blocks_inplace(cbc_c1.as_chunks_mut().0).unwrap(); assert_ne!(&cbc_c1[..], &ct[..TOY_LEN], "CFB must not agree with CBC"); @@ -208,9 +228,8 @@ fn the_mode_matches_the_spec_equations() { /// `CIPH_K(C2)`), so both the "only segment" and "final segment" cases are covered. #[test] fn the_final_short_segment_is_xored_with_the_leading_keystream_bytes() { - let key = toy_key(); let iv = pinned_iv(); - let perm = >::new(&key).unwrap(); + let perm = >::new(&toy_key()).unwrap(); let mut o1 = iv; perm.encrypt_block(&mut o1); @@ -274,20 +293,18 @@ fn the_ciphertext_of_a_prefix_is_a_prefix_of_the_ciphertext() { /// encrypting anything. #[test] fn neither_direction_uses_the_inverse_cipher() { - let key = toy_key(); let iv = pinned_iv(); let plaintext = message(11 * TOY_LEN + 5); - let (mut e, _) = - ForwardOnlyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let (mut e, _) = forward_only_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); let ct = enc(&mut e, &plaintext); // One call: two fours, then a pair, then a single, then the short segment. - let mut d = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = forward_only_key().create_decryptor(&iv); assert_eq!(dec(&mut d, &ct), plaintext, "all paths, forward cipher only"); // Byte by byte: the byte path only. - let mut d = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = forward_only_key().create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "byte path, forward cipher only"); // The forward-only toy must agree with the real one, or the above proves nothing. @@ -387,27 +404,24 @@ fn call_chunking_does_not_change_the_result() { fn chunking_matches_a_single_call_over_several_batches() { fn check(name: &str) where - P: ElectronicCodeBook + ModeNames, + P: ElectronicCodeBook + ModeNames + Clone, { let key_bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(31).wrapping_add(7)); let key = KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) .expect("a valid key"); + let key = ToyKey::::from_perm(P::new(&key).expect("a valid key")); let iv: [u8; 16] = core::array::from_fn(|i| 0xC3 ^ (i as u8)); let plaintext: Vec = (0..171).map(|i| (i * 7 + i / 16) as u8).collect(); let encryptor = || { - let (enc, got) = Cfb::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::<16>::new(iv), - ) - .expect("encrypt init"); + let (enc, got) = + key.create_encryptor(&mut FixedSeedRNG::<16>::new(iv)).expect("encrypt init"); assert_eq!(got, iv, "{name}: the pinned RNG should reproduce the IV"); enc }; - let decryptor = - || Cfb::::do_decrypt_init(&key, &iv).expect("decrypt init"); + let decryptor = || key.create_decryptor(&iv); // The reference: the whole message in one call. let mut reference = plaintext.clone(); @@ -455,7 +469,6 @@ fn chunking_matches_a_single_call_over_several_batches() { /// came out right, the pair path would be dead code and every claim about it would be untested. #[test] fn the_pair_path_is_really_used() { - let key = toy_key(); let iv = pinned_iv(); let plaintext = message(2 * TOY_LEN); @@ -465,21 +478,20 @@ fn the_pair_path_is_really_used() { // The swapped-pair toy encrypts identically -- CFB encryption is serial and never pairs, so its // `encrypt_2blocks` override is not reached from the encryptor at all. - let (mut e, _) = - SwappedCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let (mut e, _) = swapped_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(enc(&mut e, &plaintext), ct, "CFB encryption must not use the pair path"); // ...but decrypting the pair together must now be wrong, because the pair path is used. - let mut d = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_key().create_decryptor(&iv); assert_ne!(dec(&mut d, &ct), plaintext, "decrypting a pair must go through encrypt_2blocks"); // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. - let mut d = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_key().create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, TOY_LEN), plaintext, "the single-block path must not pair"); // So does splitting the pair across a segment boundary: 5 bytes, then 27. The second call has // an 11-byte head, one whole block and no tail, so there is no pair to form. - let mut d = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_key().create_decryptor(&iv); let mut got = ct.clone(); d.do_decrypt_inplace(&mut got[..5]).unwrap(); d.do_decrypt_inplace(&mut got[5..]).unwrap(); @@ -495,7 +507,6 @@ fn the_pair_path_is_really_used() { /// decrypt correctly. Encryption is serial and never batches, so it is unaffected. #[test] fn the_four_block_path_is_really_used() { - let key = toy_key(); let iv = pinned_iv(); let plaintext = message(5 * TOY_LEN); @@ -504,17 +515,16 @@ fn the_four_block_path_is_really_used() { assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); // The rotated-four toy encrypts identically: CFB encryption is serial and never batches. - let (mut e, _) = - SwappedFourCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let (mut e, _) = swapped_four_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(enc(&mut e, &plaintext), ct, "CFB encryption must not use the four path"); // ...but five blocks together must now be wrong, because the first four go through // encrypt_4blocks. - let mut d = SwappedFourCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_four_key().create_decryptor(&iv); assert_ne!(dec(&mut d, &ct), plaintext, "five blocks must go through encrypt_4blocks"); // Pairs use the pair path only, so they are correct even for this toy... - let mut d = SwappedFourCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_four_key().create_decryptor(&iv); assert_eq!( dec_chunked(&mut d, &ct, 2 * TOY_LEN), plaintext, @@ -522,7 +532,7 @@ fn the_four_block_path_is_really_used() { ); // ...and so is one block at a time. - let mut d = SwappedFourCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut d = swapped_four_key().create_decryptor(&iv); assert_eq!( dec_chunked(&mut d, &ct, TOY_LEN), plaintext, @@ -534,7 +544,7 @@ fn the_four_block_path_is_really_used() { /// produces, for a message ending in a short segment and one that does not, in both directions. #[test] fn one_shots_agree_with_the_streaming_api() { - let key = toy_key(); + let key = cfb_key(); let iv = pinned_iv(); for len in [3 * TOY_LEN + 7, 4 * TOY_LEN] { @@ -542,18 +552,17 @@ fn one_shots_agree_with_the_streaming_api() { let streamed = enc(&mut pinned_encryptor(iv), &plaintext); let mut buf = plaintext.clone(); - let (_, iv_b) = - ToyCfb::::encrypt_rng_inplace(&key, &mut pinned_rng(iv), &mut buf).unwrap(); + let (_, iv_b) = key.encrypt_inplace(&mut pinned_rng(iv), &mut buf).unwrap(); assert_eq!(iv_b, iv); assert_eq!(buf, streamed, "len {len}: one-shot must equal streaming"); - ToyCfb::::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + key.decrypt_inplace(&iv, &mut buf).unwrap(); assert_eq!(buf, plaintext); // The OS-RNG variant round-trips too. let mut buf = plaintext.clone(); - let (_, iv_fresh) = ToyCfb::::encrypt_inplace(&key, &mut buf).unwrap(); + let (_, iv_fresh) = key.encrypt_inplace(&mut os_rng(), &mut buf).unwrap(); assert_ne!(buf, plaintext); - ToyCfb::::decrypt_inplace(&key, &iv_fresh, &mut buf).unwrap(); + key.decrypt_inplace(&iv_fresh, &mut buf).unwrap(); assert_eq!(buf, plaintext); } } @@ -683,10 +692,10 @@ fn an_iv_bit_error_damages_only_the_first_block_through_the_cipher() { /// plaintexts, not merely their equality (see the crate docs, "Key and IV reuse"). #[test] fn each_encryption_gets_a_fresh_iv() { - let key = toy_key(); + let key = cfb_key(); let mut seen = std::collections::BTreeSet::new(); for _ in 0..64 { - let (_, iv) = ToyCfb::::do_encrypt_init(&key).unwrap(); + let (_, iv) = key.create_encryptor(&mut os_rng()).unwrap(); assert!(seen.insert(iv), "IV repeated across encryptions: {iv:02x?}"); } } @@ -694,13 +703,13 @@ fn each_encryption_gets_a_fresh_iv() { /// Identical plaintext under the same key must give different ciphertext, because the IV differs. #[test] fn identical_plaintext_gives_different_ciphertext() { - let key = toy_key(); + let key = cfb_key(); let plaintext = [0x77u8; 2 * TOY_LEN]; let mut first = plaintext; - ToyCfb::::encrypt_inplace(&key, &mut first).unwrap(); + key.encrypt_inplace(&mut os_rng(), &mut first).unwrap(); let mut second = plaintext; - ToyCfb::::encrypt_inplace(&key, &mut second).unwrap(); + key.encrypt_inplace(&mut os_rng(), &mut second).unwrap(); assert_ne!(first, second); // ...and, within one message, two identical plaintext blocks must not give identical ciphertext @@ -714,12 +723,11 @@ fn identical_plaintext_gives_different_ciphertext() { // ---- key handling ------------------------------------------------------------------------ +/// The key's construction suite: the type, binding and strength policy of `from_key_material`. #[test] -fn a_key_of_the_wrong_type_is_rejected() { - let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); - let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); - assert!(ToyCfb::::do_encrypt_init(&seed).is_err()); - assert!(ToyCfb::::do_decrypt_init(&seed, &[0u8; TOY_LEN]).is_err()); +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::(Toy::CFB_ALG_NAME, SecurityStrength::_128bit); } // ---- every length, no padding ------------------------------------------------------------ @@ -729,11 +737,11 @@ fn a_key_of_the_wrong_type_is_rejected() { /// covers the empty message, a lone short segment, exact multiples and every partial final segment. #[test] fn every_length_round_trips_without_padding() { - let key = toy_key(); + let key = cfb_key(); for len in 0..=(3 * TOY_LEN + 1) { let plaintext = message(len); let mut data = plaintext.clone(); - let (n, iv) = ToyCfb::::encrypt_inplace(&key, &mut data).expect("encryption"); + let (n, iv) = key.encrypt_inplace(&mut os_rng(), &mut data).expect("encryption"); assert_eq!(n, len, "len {len}: encrypt must report the number of bytes written"); assert_eq!(data.len(), len, "len {len}: the ciphertext is as long as the plaintext"); // Only meaningful once the message is long enough that agreeing with the keystream by @@ -744,7 +752,7 @@ fn every_length_round_trips_without_padding() { if len >= 8 { assert_ne!(data, plaintext, "len {len}: the data must actually be encrypted"); } - ToyCfb::::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); + key.decrypt_inplace(&iv, &mut data).expect("decryption"); assert_eq!(data, plaintext, "len {len}: round trip"); } } diff --git a/crypto/cipher/tests/modes/cmac_tests.rs b/crypto/cipher/tests/modes/cmac_tests.rs index fd447200c..36921b3e9 100644 --- a/crypto/cipher/tests/modes/cmac_tests.rs +++ b/crypto/cipher/tests/modes/cmac_tests.rs @@ -4,16 +4,27 @@ //! `aes` crate, `crypto/aes/tests/cmac_*.rs`. mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; -use bouncycastle_cipher::modes::{CMAC_MIN_TAG_LEN, Cmac, CmacKeyState}; -use bouncycastle_core::errors::{KeyMaterialError, MACError, SuspendableError}; +use bouncycastle_cipher::modes::{CMAC_MIN_TAG_LEN, Cmac, CmacKeyState, ModeNames}; +use bouncycastle_core::errors::{MACError, SuspendableError}; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::{MAC, SuspendableKeyed}; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_core_test_framework::mac::TestFrameworkMAC; use common::{TOY_LEN, Toy, toy_key}; +use toy_key::{ToyCMAC, ToyKey}; type ToyCmac = Cmac; +type ToyCmacKey = ToyKey; + +fn cmac_key() -> ToyCmacKey { + ToyCmacKey::from_key_material(&mac_key()).expect("the toy MAC key") +} const STATE_LEN: usize = ToyCmac::SUSPENDED_STATE_LEN; @@ -29,15 +40,20 @@ fn message(len: usize) -> Vec { (0..len).map(|i| (i as u8).wrapping_mul(29).wrapping_add(3)).collect() } +/// The key object and the bare key state give the same tag, and the key passes the shared MAC +/// key suite against it. #[test] -fn the_key_state_and_the_material_constructor_agree() { +fn the_key_and_the_key_state_agree() { let state = key_state(); + let key = cmac_key(); for len in [0, 1, 15, 16, 17, 32, 33, 100] { let msg = message(len); - let from_material = ToyCmac::new(&mac_key()).unwrap().mac(&msg); + let from_key = key.mac(&msg); let from_state = ToyCmac::from_key_state(&state).mac(&msg); - assert_eq!(from_state, from_material, "len {len}"); - assert!(ToyCmac::from_key_state(&state).verify(&msg, &from_material)); + assert_eq!(from_state, from_key, "len {len}"); + assert!(ToyCmac::from_key_state(&state).verify(&msg, &from_key)); + assert!(key.verify(&msg, &from_state)); + TestFrameworkMAC::new().test_mac_key(&key, &msg, &from_state); } } @@ -79,38 +95,13 @@ fn a_complete_final_block_and_its_padded_twin_differ() { assert_ne!(ToyCmac::from_key_state(&state).mac(&[]), ToyCmac::from_key_state(&state).mac(&[0])); } +/// The key's construction suite: a `MACKey` of the permutation's length and strength, bound to +/// the CMAC name or to nothing; a block cipher key is not a CMAC key (SP 800-38B Sec 5.2, key +/// separation). #[test] -fn key_validation() { - // A block cipher key is not a CMAC key (SP 800-38B Sec 5.2, key separation). - assert!(matches!( - ToyCmac::new_allow_weak_key(&toy_key()), - Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) - )); - - // A full-strength MAC key of the wrong length. - let key_32 = KeyMaterial::<32>::from_bytes_as_type(&[0x55; 32], KeyType::MACKey).unwrap(); - for result in [ToyCmac::new(&key_32).err(), ToyCmac::new_allow_weak_key(&key_32).err()] { - assert_eq!(result, Some(MACError::KeyMaterialError(KeyMaterialError::InvalidLength))); - } - - // A key tagged below the permutation's strength, accepted only when asked. - let mut weak = mac_key(); - weak.set_security_strength(SecurityStrength::_112bit).unwrap(); - assert!(matches!( - ToyCmac::new(&weak), - Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) - )); - assert!(ToyCmac::new_allow_weak_key(&weak).is_ok()); - - // The all-zero key is `KeyType::Zeroized`: weak, but a permitted type. - let zero = - KeyMaterial::::from_bytes_as_type(&[0u8; TOY_LEN], KeyType::MACKey).unwrap(); - assert_eq!(zero.key_type(), KeyType::Zeroized); - assert!(matches!( - ToyCmac::new(&zero), - Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) - )); - assert!(ToyCmac::new_allow_weak_key(&zero).is_ok()); +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::(Toy::CMAC_ALG_NAME, SecurityStrength::_128bit); } #[test] @@ -153,9 +144,9 @@ fn suspend_resume() { let key = mac_key(); for mlen in [0, 16, 20, 64] { let msg = message(mlen); - let expected = ToyCmac::new(&key).unwrap().mac(&msg); + let expected = cmac_key().mac(&msg); for split in 0..=mlen { - let mut cmac = ToyCmac::new(&key).unwrap(); + let mut cmac = cmac_key().create_generator(); cmac.do_update(&msg[..split]); let state: [u8; STATE_LEN] = cmac.suspend(); let mut resumed = ToyCmac::from_suspended(state, &key).unwrap(); @@ -168,7 +159,7 @@ fn suspend_resume() { #[test] fn suspend_resume_rejects() { let key = mac_key(); - let mut cmac = ToyCmac::new(&key).unwrap(); + let mut cmac = cmac_key().create_generator(); cmac.do_update(b"some data"); let state: [u8; STATE_LEN] = cmac.suspend(); assert_eq!(STATE_LEN, 3 + 2 * TOY_LEN + 1); diff --git a/crypto/cipher/tests/modes/ctr_tests.rs b/crypto/cipher/tests/modes/ctr_tests.rs index ee37e51bd..18431107c 100644 --- a/crypto/cipher/tests/modes/ctr_tests.rs +++ b/crypto/cipher/tests/modes/ctr_tests.rs @@ -22,31 +22,60 @@ //! ACVP suite supplies the actual known-answer coverage. mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; -use bouncycastle_cipher::modes::{Ctr, ModeNames}; +use bouncycastle_cipher::modes::{CTR, Ctr, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCipher; use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; +use toy_key::{ToyKey, os_rng}; /// The default shape under test: a 12-byte nonce, so a 4-byte counter. const NONCE_LEN: usize = 12; type ToyCtr = Ctr; -type SwappedCtr = Ctr; -type ForwardOnlyCtr = Ctr; -type SwappedFourCtr = Ctr; /// A 15-byte nonce leaves a **1-byte** counter, so the whole counter space is 256 blocks -- 4 KiB /// of keystream. That makes the exhaustion behaviour reachable in a test. const SHORT_CTR_NONCE_LEN: usize = 15; type TinyCtr = Ctr; + +type ToyCtrKey = ToyKey>; + +fn ctr_key() -> ToyCtrKey { + ctr_key_n() +} + +/// The toy key under CTR at any permitted nonce length. +fn ctr_key_n() -> ToyKey> { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn tiny_key() -> ToyKey> { + ctr_key_n() +} + +fn swapped_key() -> ToyKey> { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn swapped_four_key() -> ToyKey> { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn forward_only_key() -> ToyKey> { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} /// Capacity of a 1-byte counter, in bytes. const TINY_CAPACITY: usize = 256 * TOY_LEN; @@ -83,14 +112,13 @@ fn pinned_rng(nonce: [u8; NONCE_LEN]) -> FixedSeedRNG { } fn pinned_encryptor(nonce: [u8; NONCE_LEN]) -> ToyCtr { - let (e, got) = - ToyCtr::::do_encrypt_init_rng(&toy_key(), &mut pinned_rng(nonce)).unwrap(); + let (e, got) = ctr_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); assert_eq!(got, nonce, "the pinned RNG should reproduce the nonce"); e } fn pinned_decryptor(nonce: [u8; NONCE_LEN]) -> ToyCtr { - ToyCtr::::do_decrypt_init(&toy_key(), &nonce).unwrap() + ctr_key().create_decryptor(&nonce) } fn message(len: usize) -> Vec { @@ -103,8 +131,7 @@ const CHUNKINGS: [usize; 12] = [1, 3, 5, 7, 15, 16, 17, 31, 32, 33, 64, 100]; #[test] fn ctr_conforms_to_the_stream_cipher_framework() { - TestFrameworkStreamCipher::new() - .test::, ToyCtr>(); + TestFrameworkStreamCipher::new().test_key::(&ctr_key(), &mut os_rng()); } // ---- the spec equations ------------------------------------------------------------------- @@ -140,9 +167,8 @@ fn reference_ctr(perm: &Toy, nonce: [u8; NONCE_LEN], input: &[u8]) -> Vec { /// encrypting zeros reveals the keystream itself. #[test] fn the_mode_matches_the_spec_equations() { - let key = toy_key(); let nonce = pinned_nonce(); - let perm = >::new(&key).unwrap(); + let perm = >::new(&toy_key()).unwrap(); for len in [1, TOY_LEN - 1, TOY_LEN, TOY_LEN + 1, 5 * TOY_LEN, 5 * TOY_LEN + 9] { let plaintext = message(len); @@ -215,15 +241,11 @@ fn check_counter_blocks(blocks: usize) { TOY_LEN - N } - let key = toy_key(); - let perm = >::new(&key).unwrap(); + let perm = >::new(&toy_key()).unwrap(); let nonce: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(13).wrapping_add(5)); - let (mut e, got) = Ctr::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(nonce), - ) - .unwrap(); + let (mut e, got) = + ctr_key_n::().create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); assert_eq!(got, nonce); let mut keystream = vec![0u8; blocks * TOY_LEN]; e.do_encrypt_inplace(&mut keystream).expect("the run must fit in the counter space"); @@ -316,15 +338,12 @@ fn the_f5_counter_blocks_have_the_shape_this_type_assumes() { /// happens to be a single byte. #[test] fn the_counter_limit_is_enforced() { - let key = toy_key(); let nonce: [u8; SHORT_CTR_NONCE_LEN] = core::array::from_fn(|i| 0x5A ^ (i as u8)); let encryptor = || { - let (e, got) = TinyCtr::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(nonce), - ) - .unwrap(); + let (e, got) = tiny_key() + .create_encryptor(&mut FixedSeedRNG::::new(nonce)) + .unwrap(); assert_eq!(got, nonce); e }; @@ -387,15 +406,11 @@ fn the_counter_limit_is_enforced() { fn the_counter_limit_is_enforced_at_two_bytes_too() { const NONCE: usize = 14; const CAPACITY: usize = 65536 * TOY_LEN; - let key = toy_key(); let nonce: [u8; NONCE] = core::array::from_fn(|i| 0x3C ^ (i as u8)); let encryptor = || { - let (e, got) = Ctr::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(nonce), - ) - .unwrap(); + let (e, got) = + ctr_key_n::().create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); assert_eq!(got, nonce); e }; @@ -417,9 +432,8 @@ fn the_counter_limit_is_enforced_at_two_bytes_too() { /// rather than decrypted with repeated keystream. #[test] fn the_counter_limit_is_enforced_when_decrypting_too() { - let key = toy_key(); let nonce: [u8; SHORT_CTR_NONCE_LEN] = core::array::from_fn(|i| 0x5A ^ (i as u8)); - let mut d = TinyCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut d = tiny_key().create_decryptor(&nonce); let mut data = vec![0u8; TINY_CAPACITY + 1]; assert!( d.do_decrypt_inplace(&mut data).is_err(), @@ -434,25 +448,21 @@ fn the_counter_limit_is_enforced_when_decrypting_too() { /// direction may reach the inverse cipher. [`ForwardOnlyToy`] panics from every inverse entry point. #[test] fn neither_direction_uses_the_inverse_cipher() { - let key = toy_key(); let nonce = pinned_nonce(); let plaintext = message(11 * TOY_LEN + 5); - let (mut e, _) = ForwardOnlyCtr::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(nonce), - ) - .unwrap(); + let (mut e, _) = + forward_only_key().create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); let mut ct = plaintext.clone(); e.do_encrypt_inplace(&mut ct).unwrap(); - let mut d = ForwardOnlyCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut d = forward_only_key().create_decryptor(&nonce); let mut back = ct.clone(); d.do_decrypt_inplace(&mut back).unwrap(); assert_eq!(back, plaintext, "all paths, forward cipher only"); // Byte by byte, so the single-block path runs too. - let mut d = ForwardOnlyCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut d = forward_only_key().create_decryptor(&nonce); let mut back = ct.clone(); for piece in back.chunks_mut(1) { d.do_decrypt_inplace(piece).unwrap(); @@ -510,29 +520,24 @@ fn call_chunking_does_not_change_the_result() { fn chunking_matches_a_single_call_over_several_batches() { fn check(name: &str) where - P: ElectronicCodeBook + ModeNames, + P: ElectronicCodeBook + ModeNames + Clone, { let key_bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(31).wrapping_add(7)); let key = KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) .expect("a valid key"); + let key = ToyKey::>::from_perm(P::new(&key).expect("a valid key")); let nonce: [u8; 12] = core::array::from_fn(|i| 0xC3 ^ (i as u8)); let plaintext: Vec = (0..171).map(|i| (i * 7 + i / 16) as u8).collect(); let encryptor = || { - let (e, got) = Ctr::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::<12>::new(nonce), - ) - .expect("encrypt init"); + let (e, got) = + key.create_encryptor(&mut FixedSeedRNG::<12>::new(nonce)).expect("encrypt init"); assert_eq!(got, nonce, "{name}: the pinned RNG should reproduce the nonce"); e }; - let decryptor = || { - Ctr::::do_decrypt_init(&key, &nonce) - .expect("decrypt init") - }; + let decryptor = || key.create_decryptor(&nonce); let mut reference = plaintext.clone(); encryptor().do_encrypt_inplace(&mut reference).expect("one-call encryption"); @@ -572,7 +577,6 @@ fn chunking_matches_a_single_call_over_several_batches() { /// batches too, because counter blocks do not depend on cipher output (Sec 6.5). #[test] fn the_pair_path_is_really_used_in_both_directions() { - let key = toy_key(); let nonce = pinned_nonce(); let plaintext = message(2 * TOY_LEN); @@ -580,15 +584,13 @@ fn the_pair_path_is_really_used_in_both_directions() { assert_eq!(dec(&mut pinned_decryptor(nonce), &ct), plaintext); // Encryption: two blocks together must go through encrypt_2blocks, so the swapped toy differs. - let (mut e, _) = - SwappedCtr::::do_encrypt_init_rng(&key, &mut pinned_rng(nonce)).unwrap(); + let (mut e, _) = swapped_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); let mut swapped = plaintext.clone(); e.do_encrypt_inplace(&mut swapped).unwrap(); assert_ne!(swapped, ct, "CTR encryption must use the pair path"); // ...but one block at a time avoids it, and then it agrees with the correct toy. - let (mut e, _) = - SwappedCtr::::do_encrypt_init_rng(&key, &mut pinned_rng(nonce)).unwrap(); + let (mut e, _) = swapped_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); let mut single = plaintext.clone(); for piece in single.chunks_mut(TOY_LEN) { e.do_encrypt_inplace(piece).unwrap(); @@ -596,7 +598,7 @@ fn the_pair_path_is_really_used_in_both_directions() { assert_eq!(single, ct, "the single-block path must not pair"); // Decryption: the same, on the correct ciphertext. - let mut d = SwappedCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut d = swapped_key().create_decryptor(&nonce); let mut back = ct.clone(); d.do_decrypt_inplace(&mut back).unwrap(); assert_ne!(back, plaintext, "CTR decryption must use the pair path"); @@ -605,28 +607,25 @@ fn the_pair_path_is_really_used_in_both_directions() { /// The four-block path must be taken, in both directions, and only for full fours. #[test] fn the_four_block_path_is_really_used_in_both_directions() { - let key = toy_key(); let nonce = pinned_nonce(); let plaintext = message(5 * TOY_LEN); let ct = enc(&mut pinned_encryptor(nonce), &plaintext); - let (mut e, _) = - SwappedFourCtr::::do_encrypt_init_rng(&key, &mut pinned_rng(nonce)).unwrap(); + let (mut e, _) = swapped_four_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); let mut swapped = plaintext.clone(); e.do_encrypt_inplace(&mut swapped).unwrap(); assert_ne!(swapped, ct, "five blocks must go through encrypt_4blocks"); // Two blocks at a time uses pairs only, so the rotated-four toy is correct there. - let (mut e, _) = - SwappedFourCtr::::do_encrypt_init_rng(&key, &mut pinned_rng(nonce)).unwrap(); + let (mut e, _) = swapped_four_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); let mut pairs = plaintext.clone(); for piece in pairs.chunks_mut(2 * TOY_LEN) { e.do_encrypt_inplace(piece).unwrap(); } assert_eq!(pairs, ct, "pairs must not use the four path"); - let mut d = SwappedFourCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut d = swapped_four_key().create_decryptor(&nonce); let mut back = ct.clone(); d.do_decrypt_inplace(&mut back).unwrap(); assert_ne!(back, plaintext, "decryption must batch fours too"); @@ -638,23 +637,23 @@ fn the_four_block_path_is_really_used_in_both_directions() { /// security argument: a repeated nonce repeats the counter blocks and so the keystream. #[test] fn each_encryption_gets_a_fresh_nonce() { - let key = toy_key(); + let key = ctr_key(); let mut seen = std::collections::BTreeSet::new(); for _ in 0..64 { - let (_, nonce) = ToyCtr::::do_encrypt_init(&key).unwrap(); + let (_, nonce) = key.create_encryptor(&mut os_rng()).unwrap(); assert!(seen.insert(nonce), "nonce repeated across encryptions: {nonce:02x?}"); } } #[test] fn identical_plaintext_gives_different_ciphertext() { - let key = toy_key(); + let key = ctr_key(); let plaintext = [0x77u8; 2 * TOY_LEN]; let mut first = plaintext; - ToyCtr::::encrypt_inplace(&key, &mut first).unwrap(); + key.encrypt_inplace(&mut os_rng(), &mut first).unwrap(); let mut second = plaintext; - ToyCtr::::encrypt_inplace(&key, &mut second).unwrap(); + key.encrypt_inplace(&mut os_rng(), &mut second).unwrap(); assert_ne!(first, second); // ...and two identical plaintext blocks within one message differ, because the counter moves. @@ -663,12 +662,11 @@ fn identical_plaintext_gives_different_ciphertext() { // ---- key handling -------------------------------------------------------------------------- +/// The key's construction suite: the type, binding and strength policy of `from_key_material`. #[test] -fn a_key_of_the_wrong_type_is_rejected() { - let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); - let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); - assert!(ToyCtr::::do_encrypt_init(&seed).is_err()); - assert!(ToyCtr::::do_decrypt_init(&seed, &[0u8; NONCE_LEN]).is_err()); +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::(Toy::CTR_ALG_NAME, SecurityStrength::_128bit); } // ---- every length -------------------------------------------------------------------------- @@ -677,18 +675,17 @@ fn a_key_of_the_wrong_type_is_rejected() { /// plaintext. #[test] fn every_length_round_trips_without_padding() { - let key = toy_key(); + let key = ctr_key(); for len in 0..=(3 * TOY_LEN + 1) { let plaintext = message(len); let mut data = plaintext.clone(); - let (n, nonce) = - ToyCtr::::encrypt_inplace(&key, &mut data).expect("encryption"); + let (n, nonce) = key.encrypt_inplace(&mut os_rng(), &mut data).expect("encryption"); assert_eq!(n, len, "len {len}: encrypt must report the number of bytes written"); assert_eq!(data.len(), len, "len {len}: the ciphertext is as long as the plaintext"); if len >= 8 { assert_ne!(data, plaintext, "len {len}: the data must actually be encrypted"); } - ToyCtr::::decrypt_inplace(&key, &nonce, &mut data).expect("decryption"); + key.decrypt_inplace(&nonce, &mut data).expect("decryption"); assert_eq!(data, plaintext, "len {len}: round trip"); } } @@ -701,22 +698,17 @@ fn every_length_round_trips_without_padding() { #[test] fn every_permitted_nonce_length_works() { fn round_trip() { - let key = toy_key(); let nonce: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(11).wrapping_add(3)); let plaintext = (0..100u8).collect::>(); - let (mut e, got) = Ctr::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(nonce), - ) - .unwrap(); + let (mut e, got) = + ctr_key_n::().create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); assert_eq!(got, nonce); let mut ct = plaintext.clone(); e.do_encrypt_inplace(&mut ct).unwrap(); assert_ne!(ct, plaintext, "nonce length {N}: must actually encrypt"); - Ctr::::decrypt_inplace(&key, &nonce, &mut ct) - .unwrap(); + ctr_key_n::().decrypt_inplace(&nonce, &mut ct).unwrap(); assert_eq!(ct, plaintext, "nonce length {N}: round trip"); } diff --git a/crypto/cipher/tests/modes/ecb_tests.rs b/crypto/cipher/tests/modes/ecb_tests.rs index 89e100f21..01f8acd36 100644 --- a/crypto/cipher/tests/modes/ecb_tests.rs +++ b/crypto/cipher/tests/modes/ecb_tests.rs @@ -12,22 +12,41 @@ mod common; -use bouncycastle_cipher::modes::Cbc; -use bouncycastle_cipher::modes::hazmat::ecb::Ecb; -use bouncycastle_cipher::padding::{PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +#[path = "../common/toy_key.rs"] +mod toy_key; + +use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; +use bouncycastle_cipher::modes::{Blocks, CBC, Cbc, ModeNames}; +use bouncycastle_cipher::padding::PKCS7; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey, ElectronicCodeBook}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{ BlockCipherDecryptor, BlockCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use common::{SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; +use toy_key::{ToyKey, os_rng}; type ToyEcb = Ecb; -type SwappedEcb = Ecb; -type SwappedFourEcb = Ecb; +type ToyEcbKey = ToyKey>; + +fn ecb_key() -> ToyEcbKey { + ToyEcbKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn swapped_key() -> ToyKey> { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + +fn swapped_four_key() -> ToyKey> { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} /// The implementor hook `do_encrypt_blocks_inplace`, by value, for tests whose data is /// block-shaped. @@ -51,19 +70,18 @@ fn dec_blocks( } fn encryptor() -> ToyEcb { - ToyEcb::::do_encrypt_init(&toy_key()).unwrap().0 + ecb_key().create_encryptor() } fn decryptor() -> ToyEcb { - ToyEcb::::do_decrypt_init(&toy_key(), &[]).unwrap() + ecb_key().create_decryptor() } // ---- the mode against the shared framework ------------------------------------------------ #[test] fn ecb_conforms_to_the_block_cipher_framework() { - TestFrameworkBlockCipher::new() - .test::, ToyEcb>(); + TestFrameworkBlockCipher::new().test_ecb_key::(&ecb_key()); } // ---- the spec equations ------------------------------------------------------------------- @@ -96,14 +114,11 @@ fn reference_ecb(perm: &Toy, input: &[[u8; TOY_LEN]], encrypt: bool) -> Vec<[u8; /// first block, since CBC XORs the IV in before the cipher call. #[test] fn the_mode_matches_the_spec_equations() { - let key = toy_key(); - let perm = >::new(&key).unwrap(); + let perm = >::new(&toy_key()).unwrap(); let plaintext: [[u8; TOY_LEN]; 5] = core::array::from_fn(|i| core::array::from_fn(|j| (i * 31 + j * 7 + 1) as u8)); - let (mut enc, init) = ToyEcb::::do_encrypt_init(&key).unwrap(); - assert_eq!(init, [0u8; 0], "ECB has no init data"); - let ct = enc_blocks(&mut enc, &plaintext); + let ct = enc_blocks(&mut encryptor(), &plaintext); assert_eq!( ct.to_vec(), reference_ecb(&perm, &plaintext, true), @@ -128,11 +143,10 @@ fn the_mode_matches_the_spec_equations() { // ...and ECB is not CBC: CBC computes CIPH_K(P1 XOR IV), ECB computes CIPH_K(P1). let iv: [u8; TOY_LEN] = core::array::from_fn(|i| 0xF0 ^ (i as u8)); - let (mut cbc, _) = Cbc::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(iv), - ) - .unwrap(); + let (mut cbc, _) = ToyKey::>::from_key_material(&toy_key()) + .unwrap() + .create_encryptor(&mut FixedSeedRNG::::new(iv)) + .unwrap(); let mut first = plaintext[0]; cbc.do_encrypt_blocks_inplace(first.as_chunks_mut().0).unwrap(); assert_ne!(first, ct[0], "ECB must not agree with CBC"); @@ -146,7 +160,7 @@ fn the_mode_matches_the_spec_equations() { /// counter and stop being ECB. #[test] fn ecb_is_deterministic_and_leaks_equal_blocks() { - let key = toy_key(); + let key = ecb_key(); let block = [0x5Au8; TOY_LEN]; let plaintext = [block, [0x11; TOY_LEN], block, block]; @@ -158,50 +172,19 @@ fn ecb_is_deterministic_and_leaks_equal_blocks() { assert_eq!(ct_a[0], ct_a[3]); assert_ne!(ct_a[0], ct_a[1], "different plaintext blocks give different ciphertext blocks"); - // The one-shot sees the same thing: `encrypt` returns the empty init data and is repeatable. - // (The RNG-taking one-shot is not an alternative here -- it panics; see below.) + // The one-shot sees the same thing: the ECB key path takes no RNG and returns no init data, + // so it is repeatable. let flat: [u8; 4 * TOY_LEN] = plaintext.as_flattened().try_into().unwrap(); let mut once = flat; - let (n_a, init_a): (usize, [u8; 0]) = - ToyEcb::::encrypt_inplace(&key, &mut once).unwrap(); + let n_a = key.encrypt_inplace(&mut once).unwrap(); assert_eq!(n_a, once.len(), "encrypt must report the number of bytes written"); let mut twice = flat; - let (n_b, init_b) = ToyEcb::::encrypt_inplace(&key, &mut twice).unwrap(); + let n_b = key.encrypt_inplace(&mut twice).unwrap(); assert_eq!(n_b, twice.len(), "encrypt must report the number of bytes written"); - assert_eq!(init_a, init_b); assert_eq!(once, twice, "no init data and no randomness, so the one-shot is repeatable"); assert_eq!(once, *ct_a.as_flattened()); } -/// The RNG-taking constructor must panic rather than quietly ignore the RNG. ECB has no init data -/// to generate (SP 800-38A Table D.2 lists its IV column as "Not applicable"), so a caller reaching -/// for `do_encrypt_init_rng` has mistaken ECB for a randomized mode; -/// [`BlockCipherEncryptor::do_encrypt_init_rng`]'s contract requires an implementation with -/// `INIT_DATA_LEN == 0` to say so. It is a programmer error, not bad input, hence a panic and not a -/// [`SymmetricCipherError`]. -#[test] -#[should_panic(expected = "ECB has no initialization data")] -fn the_rng_constructor_panics() { - let key = toy_key(); - let mut rng = FixedSeedRNG::<0>::new([]); - let _ = ToyEcb::::do_encrypt_init_rng(&key, &mut rng); -} - -/// ...and so does the one-shot provided over it: `encrypt_rng_inplace` is `do_encrypt_init_rng` -/// followed by `do_encrypt_inplace`, so it panics in the same case and for the same reason. Pinned -/// separately because it is the call a user is most likely to reach for. -#[test] -#[should_panic(expected = "ECB has no initialization data")] -fn the_rng_one_shot_panics() { - let key = toy_key(); - let mut block = [0x42u8; TOY_LEN]; - let _ = ToyEcb::::encrypt_rng_inplace( - &key, - &mut FixedSeedRNG::<0>::new([]), - &mut block, - ); -} - // ---- batching: pairs and fours, in both directions ---------------------------------------- /// Sec 6.1: "multiple forward cipher functions and inverse cipher functions can be computed in @@ -210,30 +193,29 @@ fn the_rng_one_shot_panics() { /// handed over singly come out right. #[test] fn the_pair_path_is_used_in_both_directions() { - let key = toy_key(); let plaintext = [[0xA5u8; TOY_LEN], [0x5Au8; TOY_LEN]]; let ct = enc_blocks(&mut encryptor(), &plaintext); // Encryption: a pair goes through encrypt_2blocks, so the swapped toy returns them swapped. - let (mut enc, _) = SwappedEcb::::do_encrypt_init(&key).unwrap(); + let mut enc = swapped_key().create_encryptor(); let swapped_ct = enc_blocks(&mut enc, &plaintext); assert_eq!(swapped_ct, [ct[1], ct[0]], "encrypting a pair must go through encrypt_2blocks"); // ...and one block at a time avoids the pair path. - let (mut enc, _) = SwappedEcb::::do_encrypt_init(&key).unwrap(); + let mut enc = swapped_key().create_encryptor(); assert_eq!( [enc_blocks(&mut enc, &[plaintext[0]])[0], enc_blocks(&mut enc, &[plaintext[1]])[0]], ct ); // Decryption likewise. - let mut dec = SwappedEcb::::do_decrypt_init(&key, &[]).unwrap(); + let mut dec = swapped_key().create_decryptor(); assert_eq!( dec_blocks(&mut dec, &ct), [plaintext[1], plaintext[0]], "decrypting a pair must go through decrypt_2blocks" ); - let mut dec = SwappedEcb::::do_decrypt_init(&key, &[]).unwrap(); + let mut dec = swapped_key().create_decryptor(); assert_eq!([dec_blocks(&mut dec, &[ct[0]])[0], dec_blocks(&mut dec, &[ct[1]])[0]], plaintext); } @@ -243,25 +225,24 @@ fn the_pair_path_is_used_in_both_directions() { /// same blocks as two pairs or singly are right. #[test] fn the_four_block_path_is_used_in_both_directions() { - let key = toy_key(); let plaintext: [[u8; TOY_LEN]; 5] = core::array::from_fn(|i| [0x10 * i as u8 + 1; TOY_LEN]); let ct = enc_blocks(&mut encryptor(), &plaintext); assert_eq!(dec_blocks(&mut decryptor(), &ct), plaintext); - let (mut enc, _) = SwappedFourEcb::::do_encrypt_init(&key).unwrap(); + let mut enc = swapped_four_key().create_encryptor(); let rotated = enc_blocks(&mut enc, &plaintext); assert_ne!(rotated, ct, "five blocks must go through encrypt_4blocks"); assert_eq!(rotated[4], ct[4], "the fifth block goes through the single path and is right"); assert_eq!(&rotated[..4], &[ct[1], ct[2], ct[3], ct[0]], "four rotated"); - let (mut enc, _) = SwappedFourEcb::::do_encrypt_init(&key).unwrap(); + let mut enc = swapped_four_key().create_encryptor(); let a = enc_blocks(&mut enc, &[plaintext[0], plaintext[1]]); let b = enc_blocks(&mut enc, &[plaintext[2], plaintext[3]]); assert_eq!([a, b].as_flattened(), &ct[..4], "pairs use the pair path only"); - let mut dec = SwappedFourEcb::::do_decrypt_init(&key, &[]).unwrap(); + let mut dec = swapped_four_key().create_decryptor(); assert_ne!(dec_blocks(&mut dec, &ct), plaintext, "five blocks must go through decrypt_4blocks"); - let mut dec = SwappedFourEcb::::do_decrypt_init(&key, &[]).unwrap(); + let mut dec = swapped_four_key().create_decryptor(); for (c, p) in ct.iter().zip(plaintext.iter()) { assert_eq!(&dec_blocks(&mut dec, &[*c])[0], p, "the single-block path must not batch"); } @@ -298,16 +279,16 @@ fn call_grouping_does_not_change_the_result() { /// The one-shots must agree with the block-shaped hook. #[test] fn one_shots_agree_with_the_block_hook() { - let key = toy_key(); + let key = ecb_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 block_ct = enc_blocks(&mut encryptor(), &plaintext); let mut buf = flat_plaintext; - let (_, init) = ToyEcb::::encrypt_inplace(&key, &mut buf).unwrap(); + key.encrypt_inplace(&mut buf).unwrap(); assert_eq!(buf, *block_ct.as_flattened(), "one-shot must equal streaming"); - ToyEcb::::decrypt_inplace(&key, &init, &mut buf).unwrap(); + key.decrypt_inplace(&mut buf).unwrap(); assert_eq!(buf, flat_plaintext); assert_eq!(dec_blocks(&mut decryptor(), &block_ct), plaintext); @@ -348,33 +329,37 @@ fn a_ciphertext_bit_error_affects_only_its_own_block() { // ---- key handling ------------------------------------------------------------------------ +/// The key's construction suite: the type, binding and strength policy of `from_key_material`, +/// for both the block-aligned and the padded key. #[test] -fn a_key_of_the_wrong_type_is_rejected() { - let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); - let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); - assert!(ToyEcb::::do_encrypt_init(&seed).is_err()); - assert!(ToyEcb::::do_decrypt_init(&seed, &[]).is_err()); +fn the_keys_enforce_the_material_policy() { + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::(Toy::ECB_ALG_NAME, SecurityStrength::_128bit); + framework + .test::>>(Toy::ECB_ALG_NAME, SecurityStrength::_128bit); } // ---- composition with the padding layer -------------------------------------------------- /// ECB is block-aligned by contract, so arbitrary-length data goes through `bouncycastle_cipher::padding` -/// like the other modes; its `INIT_DATA_LEN` of 0 flows through the adapters as an empty array. +/// like the other modes: `ECB` is the symmetric key path over the adapters, with its +/// `INIT_DATA_LEN` of 0 flowing through as an empty array and the RNG unused. #[test] fn the_padding_layer_round_trips_every_length() { - type Enc = PaddedBlockCipherEncryptor, PKCS7, TOY_LEN, 0, TOY_LEN>; - type Dec = PaddedBlockCipherDecryptor, PKCS7, TOY_LEN, 0, TOY_LEN>; + type Key = ToyKey>; + type Enc = >::Encryptor; + type Dec = >::Decryptor; + let key = Key::from_key_material(&toy_key()).expect("the toy key"); for len in 0..=(3 * TOY_LEN + 1) { let plaintext: Vec = (0..len).map(|i| (i * 5 + 3) as u8).collect(); let mut ciphertext = vec![0u8; Enc::encrypt_out_len(len)]; - let (init, written) = - Enc::encrypt_out(&toy_key(), &plaintext, &mut ciphertext).expect("padded encryption"); + let (written, init) = + key.encrypt_out(&mut os_rng(), &plaintext, &mut ciphertext).expect("padded encryption"); assert_eq!(init, [0u8; 0]); assert_eq!(written, ciphertext.len(), "len {len}"); let mut recovered = vec![0u8; Dec::decrypt_out_len(written)]; - let n = Dec::decrypt_out(&toy_key(), &init, &ciphertext, &mut recovered) - .expect("padded decryption"); + let n = key.decrypt_out(&init, &ciphertext, &mut recovered).expect("padded decryption"); assert_eq!(&recovered[..n], &plaintext[..], "len {len}: round trip through PKCS7"); } } diff --git a/crypto/cipher/tests/modes/gcm_tests.rs b/crypto/cipher/tests/modes/gcm_tests.rs index fb7d6060d..7af30c953 100644 --- a/crypto/cipher/tests/modes/gcm_tests.rs +++ b/crypto/cipher/tests/modes/gcm_tests.rs @@ -7,17 +7,26 @@ //! `gcm_bc_java_tests.rs`. mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; -use bouncycastle_cipher::modes::{Gcm, ModeNames}; -use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_cipher::modes::ModeNames; use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use common::{ForwardOnlyToy, TOY_LEN, Toy, toy_key}; +use toy_key::{ToyGCM, ToyKey, os_rng}; -type ToyGcm = Gcm; +type ToyGcmKey = ToyKey>; + +fn gcm_key() -> ToyGcmKey { + ToyGcmKey::from_key_material(&toy_key()).expect("the toy key") +} /// Encrypts `message` under `aad` through the detached one-shot, with the nonce driven by `seed` /// so repeated calls are comparable. Returns the nonce, the ciphertext and the tag. @@ -27,14 +36,9 @@ fn toy_encrypt( seed: [u8; 12], ) -> ([u8; 12], Vec, [u8; TAG_LEN]) { let mut ct = vec![0u8; message.len()]; - let (nonce, _, tag) = ToyGcm::::encrypt_detached_rng_out( - &toy_key(), - &mut FixedSeedRNG::<12>::new(seed), - aad, - message, - &mut ct, - ) - .unwrap(); + let (_, nonce, tag) = gcm_key::() + .encrypt_detached_out(&mut FixedSeedRNG::<12>::new(seed), aad, message, &mut ct) + .unwrap(); (nonce, ct, tag) } @@ -42,8 +46,8 @@ fn toy_encrypt( /// after data has started is refused, while an empty one is always accepted as a no-op. #[test] fn aad_after_data_is_a_state_error_unless_empty() { - let key = toy_key(); - let (mut enc, _nonce) = ToyGcm::::do_encrypt_init(&key).unwrap(); + let key = gcm_key::<16>(); + let (mut enc, _nonce) = key.create_encryptor(&mut os_rng()).unwrap(); enc.do_update_aad(b"header").unwrap(); let mut out = [0u8; 8]; enc.do_encrypt_out(&[0x11u8; 8], &mut out).unwrap(); @@ -62,8 +66,8 @@ fn aad_after_data_is_a_state_error_unless_empty() { /// must be refused all the same, or it would be absorbed as if it came before the ciphertext. #[test] fn aad_after_held_back_data_is_still_a_state_error() { - let key = toy_key(); - let mut dec = ToyGcm::::do_decrypt_init(&key, &[0u8; 12]).unwrap(); + let key = gcm_key::<16>(); + let mut dec = key.create_decryptor(&[0u8; 12]); let mut nothing = [0u8; 0]; assert_eq!(dec.do_decrypt_out(&[0x22u8; 5], &mut nothing).unwrap(), 0, "all held back"); match dec.do_update_aad(b"too late") { @@ -76,7 +80,7 @@ fn aad_after_held_back_data_is_still_a_state_error() { /// must give the same ciphertext and tag as absorbing each in one call. #[test] fn chunking_is_independent_for_aad_and_data() { - let key = toy_key(); + let key = gcm_key::<16>(); let aad: [u8; 40] = core::array::from_fn(|i| i as u8); let message: [u8; 50] = core::array::from_fn(|i| (i as u8).wrapping_mul(3).wrapping_add(1)); let seed = [0x5Au8; 12]; @@ -84,11 +88,8 @@ fn chunking_is_independent_for_aad_and_data() { for aad_split in [0usize, 1, 17, 40] { for data_split in [0usize, 1, 23, 50] { - let (mut enc, got_nonce) = ToyGcm::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::<12>::new(seed), - ) - .unwrap(); + let (mut enc, got_nonce) = + key.create_encryptor(&mut FixedSeedRNG::<12>::new(seed)).unwrap(); assert_eq!(got_nonce, nonce); enc.do_update_aad(&aad[..aad_split]).unwrap(); enc.do_update_aad(&aad[aad_split..]).unwrap(); @@ -106,7 +107,6 @@ fn chunking_is_independent_for_aad_and_data() { /// for the same inputs -- Algorithm 4 step 6's `T = MSB_t(...)`. #[test] fn tag_length_variants_round_trip_and_nest() { - let key = toy_key(); let aad = b"associated"; let message = *b"a toy message, sixteen+"; let seed = [0x6Bu8; 12]; @@ -123,8 +123,7 @@ fn tag_length_variants_round_trip_and_nest() { $n ); let mut pt = [0u8; 23]; - ToyGcm::::decrypt_detached_out(&key, &nonce, aad, &ct, &tag, &mut pt) - .unwrap(); + gcm_key::<$n>().decrypt_detached_out(&nonce, aad, &ct, &tag, &mut pt).unwrap(); assert_eq!(pt, message); }}; } @@ -139,14 +138,13 @@ fn tag_length_variants_round_trip_and_nest() { /// bytes of ciphertext against it verifies. Sec 5.2: GMAC is GCM restricted to `P = ""`. #[test] fn an_aad_only_message_is_gmac() { - let key = toy_key(); + let key = gcm_key::<16>(); let aad = b"the whole message is AAD"; let (nonce, _, tag) = toy_encrypt::<16>(aad, &[], [0x7Cu8; 12]); - ToyGcm::::decrypt_detached_out(&key, &nonce, aad, &[], &tag, &mut []).unwrap(); + key.decrypt_detached_out(&nonce, aad, &[], &tag, &mut []).unwrap(); // Wrong AAD must fail verification. - match ToyGcm::::decrypt_detached_out(&key, &nonce, b"wrong", &[], &tag, &mut []) - { + match key.decrypt_detached_out(&nonce, b"wrong", &[], &tag, &mut []) { Err(SymmetricCipherError::AEADTagCheckFailed) => {} other => panic!("expected AEADTagCheckFailed, got {other:?}"), } @@ -156,16 +154,16 @@ fn an_aad_only_message_is_gmac() { /// shorter than `TAG_LEN` is `DecryptionFailed`. #[test] fn inline_decryptor_handles_short_and_tag_only_input() { - let key = toy_key(); + let key = gcm_key::<16>(); let (nonce, _, tag) = toy_encrypt::<16>(b"", &[], [0x8Du8; 12]); let mut plaintext = [0u8; 16]; - let n = ToyGcm::::decrypt_out(&key, &nonce, &tag, &mut plaintext).unwrap(); + let n = key.decrypt_with_aad_out(&nonce, &[], &tag, &mut plaintext).unwrap(); assert_eq!(n, 0, "a tag-only input releases no plaintext"); for short_len in 0..16 { let short = &tag[..short_len]; - match ToyGcm::::decrypt_out(&key, &nonce, short, &mut plaintext) { + match key.decrypt_with_aad_out(&nonce, &[], short, &mut plaintext) { Err(SymmetricCipherError::DecryptionFailed) => {} other => panic!("len {short_len}: expected DecryptionFailed, got {other:?}"), } @@ -176,14 +174,14 @@ fn inline_decryptor_handles_short_and_tag_only_input() { /// the tail hold-back boundary. #[test] fn update_out_len_is_exact_across_irregular_chunking() { - let key = toy_key(); + let key = gcm_key::<16>(); let message: [u8; 64] = core::array::from_fn(|i| i as u8); let (nonce, ct, tag) = toy_encrypt::<16>(b"aad", &message, [0x9Eu8; 12]); let mut full_ct = [0u8; 80]; full_ct[..64].copy_from_slice(&ct); full_ct[64..].copy_from_slice(&tag); - let mut dec = ToyGcm::::do_decrypt_init(&key, &nonce).unwrap(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut released = 0usize; for chunk in [1usize, 15, 16, 17, 31] { @@ -210,16 +208,14 @@ fn update_out_len_is_exact_across_irregular_chunking() { /// nature) has already released plaintext before the forgery is detected. Pinning the difference. #[test] fn one_shot_releases_nothing_on_forgery_but_streaming_does() { - let key = toy_key(); + let key = gcm_key::<16>(); let message = *b"do not trust me yet"; let (nonce, ct, mut tag) = toy_encrypt::<16>(b"aad", &message, [0xAFu8; 12]); tag[0] ^= 0xFF; // forge it - // One-shot: verify-then-decrypt, so a forged tag leaves nothing but zeros behind. + // One-shot: a forged tag leaves nothing but zeros behind. let mut one_shot_buf = [0xEEu8; 19]; - match ToyGcm::::decrypt_detached_out( - &key, &nonce, b"aad", &ct, &tag, &mut one_shot_buf, - ) { + match key.decrypt_detached_out(&nonce, b"aad", &ct, &tag, &mut one_shot_buf) { Err(SymmetricCipherError::AEADTagCheckFailed) => {} other => panic!("expected AEADTagCheckFailed, got {other:?}"), } @@ -227,7 +223,7 @@ fn one_shot_releases_nothing_on_forgery_but_streaming_does() { // Streaming: everything but the held-back last 16 bytes has already been released as // plaintext by the time the final call rejects the tag. - let mut dec = ToyGcm::::do_decrypt_init(&key, &nonce).unwrap(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut streaming_buf = [0u8; 19]; let released = dec.do_decrypt_out(&ct, &mut streaming_buf).unwrap(); @@ -270,27 +266,19 @@ fn streaming_detached_final_zeroizes_its_buffer_on_forgery() { fn neither_direction_uses_the_inverse_cipher() { fn round_trip

() -> ([u8; 48], [u8; 16]) where - P: bouncycastle_core::hazmat::ElectronicCodeBook + ModeNames, + P: bouncycastle_core::hazmat::ElectronicCodeBook + ModeNames + Clone, { - let key = toy_key(); + let key = ToyKey::>::from_key_material(&toy_key()).unwrap(); let aad = b"associated data of no particular length"; let message = b"a message that is not a whole number of blocks!!"; let mut ct = [0u8; 48]; - let (nonce, _, tag) = Gcm::::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::<12>::new([0x4Du8; 12]), - aad, - message, - &mut ct, - ) - .unwrap(); + let (_, nonce, tag) = key + .encrypt_detached_out(&mut FixedSeedRNG::<12>::new([0x4Du8; 12]), aad, message, &mut ct) + .unwrap(); assert_ne!(&ct[..], &message[..]); let mut pt = [0u8; 48]; - Gcm::::decrypt_detached_out( - &key, &nonce, aad, &ct, &tag, &mut pt, - ) - .unwrap(); + key.decrypt_detached_out(&nonce, aad, &ct, &tag, &mut pt).unwrap(); assert_eq!(&pt[..], &message[..]); (ct, tag) } @@ -299,40 +287,38 @@ fn neither_direction_uses_the_inverse_cipher() { assert_eq!(round_trip::(), round_trip::(), "the two toys must agree"); } -/// The whole [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] contract -- which runs the -/// symmetric-cipher suite first -- through the shared framework, over the toy at both ends of the -/// tag-length range. `FINAL_LEN` is `TAG_LEN`: GCM holds nothing back on encryption and exactly -/// the possible tag on decryption. -/// -/// [`AEADCipherEncryptor`]: bouncycastle_core::traits::AEADCipherEncryptor -/// [`AEADCipherDecryptor`]: bouncycastle_core::traits::AEADCipherDecryptor +/// The whole AEAD key contract -- which runs the symmetric-cipher suite first -- through the +/// shared framework, over the toy at both ends of the tag-length range. `FINAL_LEN` is +/// `TAG_LEN`: GCM holds nothing back on encryption and exactly the possible tag on decryption. #[test] -fn aead_trait_framework() { +fn aead_key_framework() { use bouncycastle_core_test_framework::aead::TestFrameworkAEADCipher; TestFrameworkAEADCipher::new() - .test_encryptor_decryptor::, ToyGcm>( - ); + .test_key::(&gcm_key::<16>(), &mut os_rng()); TestFrameworkAEADCipher::new() - .test_encryptor_decryptor::, ToyGcm>( - ); + .test_key::(&gcm_key::<12>(), &mut os_rng()); } -/// The trait one-shots check the tag before decrypting anything, as the inherent -/// `decrypt_detached` does, and on a forgery leave the caller's buffer zeroized -- the trait -/// contract -- rather than holding the ciphertext they staged there. +/// The key's construction suite: the type, binding and strength policy of `from_key_material`. #[test] -fn aead_trait_one_shots_release_nothing_on_forgery() { - type Enc = ToyGcm; - type Dec = ToyGcm; +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::(Toy::GCM_ALG_NAME, SecurityStrength::_128bit); +} - let key = toy_key(); +/// The key's one-shots leave the caller's buffer zeroized on a forgery -- the role contract -- +/// rather than holding the ciphertext they staged there. +#[test] +fn the_key_one_shots_release_nothing_on_forgery() { + let key = gcm_key::<16>(); let mut ct = [0u8; 32 + 16]; - let (nonce, n) = Enc::encrypt_with_aad_out(&key, b"aad", &[0x33u8; 32], &mut ct).unwrap(); + let (n, nonce) = + key.encrypt_with_aad_out(&mut os_rng(), b"aad", &[0x33u8; 32], &mut ct).unwrap(); ct[0] ^= 1; let mut out = [0xEEu8; 32]; assert!(matches!( - Dec::decrypt_with_aad_out(&key, &nonce, b"aad", &ct[..n], &mut out), + key.decrypt_with_aad_out(&nonce, b"aad", &ct[..n], &mut out), Err(SymmetricCipherError::AEADTagCheckFailed) )); assert_eq!(out, [0u8; 32], "decrypt_with_aad_out must zeroize on a failed tag check"); @@ -340,14 +326,7 @@ fn aead_trait_one_shots_release_nothing_on_forgery() { let tag: [u8; 16] = ct[32..48].try_into().unwrap(); let mut out = [0xEEu8; 32]; assert!(matches!( - >::decrypt_detached_out( - &key, - &nonce, - b"aad", - &ct[..32], - &tag, - &mut out - ), + key.decrypt_detached_out(&nonce, b"aad", &ct[..32], &tag, &mut out), Err(SymmetricCipherError::AEADTagCheckFailed) )); assert_eq!(out, [0u8; 32], "decrypt_detached_out must zeroize on a failed tag check"); diff --git a/crypto/cipher/tests/modes/gmac_tests.rs b/crypto/cipher/tests/modes/gmac_tests.rs index 76769c289..8e08c02c7 100644 --- a/crypto/cipher/tests/modes/gmac_tests.rs +++ b/crypto/cipher/tests/modes/gmac_tests.rs @@ -3,19 +3,27 @@ //! The known-answer tests (ACVP, Wycheproof) are in the `aes` crate, `crypto/aes/tests/gmac_*.rs`. mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; -use bouncycastle_cipher::modes::{GCM_NONCE_LEN, Gcm, GcmKeyState, Gmac}; +use bouncycastle_cipher::modes::{GCM_NONCE_LEN, GcmKeyState, Gmac, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::AEADCipherEncryptor; +use bouncycastle_core::symmetric_key::{AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use common::{TOY_LEN, Toy, toy_key}; +use toy_key::{ToyGCM, ToyGMAC, ToyKey}; type ToyGmac = Gmac; -type ToyGcm = Gcm; +type ToyGmacKey = ToyKey>; + +fn gmac_key() -> ToyGmacKey { + ToyGmacKey::from_key_material(&mac_key()).expect("the toy MAC key") +} const NONCE: [u8; GCM_NONCE_LEN] = [0x5A; GCM_NONCE_LEN]; @@ -42,16 +50,23 @@ fn every_route_gives_gcm_s_aad_only_tag() { let state = key_state(); for len in [0, 1, 15, 16, 17, 32, 33, 100] { let msg = message(len); - let (gcm_nonce, _, expected) = ToyGcm::::encrypt_detached_rng_out( - &toy_key(), - &mut rng(), - &msg, - &[], - &mut [], - ) - .unwrap(); + let (_, gcm_nonce, expected) = ToyKey::::from_key_material(&toy_key()) + .unwrap() + .encrypt_detached_out(&mut rng(), &msg, &[], &mut []) + .unwrap(); assert_eq!(gcm_nonce, NONCE); + let key = gmac_key::<16>(); + let (gen_, nonce) = key.create_generator(&mut rng()).unwrap(); + assert_eq!(nonce, NONCE, "len {len}"); + assert_eq!(gen_.mac(&msg), expected, "key.create_generator, len {len}"); + assert_eq!( + key.create_generator_nonce(&NONCE).mac(&msg), + expected, + "key.create_generator_nonce, len {len}" + ); + assert!(key.create_verifier(&NONCE).verify(&msg, &expected)); + let (gen_, nonce) = ToyGmac::::from_key_state(&state, &mut rng()).unwrap(); assert_eq!(nonce, NONCE, "len {len}"); assert_eq!(gen_.mac(&msg), expected, "from_key_state, len {len}"); @@ -159,3 +174,11 @@ fn key_validation() { let zero = KeyMaterial::::from_bytes_as_type(&[0; TOY_LEN], KeyType::MACKey).unwrap(); assert!(ToyGmac::::new(&zero, &NONCE).is_err()); } + +/// The key's construction suite: a `MACKey` of the permutation's length and strength, bound to +/// the GMAC name or to nothing. +#[test] +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::(Toy::GMAC_ALG_NAME, SecurityStrength::_128bit); +} diff --git a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs index 24eb59870..e1510136e 100644 --- a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs +++ b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs @@ -17,21 +17,30 @@ //! * and that the length predictions are exact, not upper bounds. mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; -use bouncycastle_cipher::modes::{Cfb, Cfb8, Ctr}; +use bouncycastle_cipher::modes::{CFB, CFB8, CTR, Ctr}; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{ StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkSymmetricCipher; use common::{TOY_LEN, Toy, toy_key}; +use toy_key::{ToyKey, os_rng}; -type ToyCfb

= Cfb; -type ToyCfb8 = Cfb8; type ToyCtr = Ctr; +/// The toy key under the stream mode `M`. +fn key_for>() -> ToyKey { + ToyKey::from_key_material(&toy_key()).expect("the toy key") +} + /// All three stream modes must satisfy the shared conformance suite for the symmetric-cipher /// traits -- the same suite the padded adapters run, with `required_alignment` left at 1 because a /// stream cipher accepts every length. @@ -44,12 +53,9 @@ type ToyCtr = Ctr; #[test] fn the_stream_modes_conform_to_the_symmetric_cipher_suite() { let framework = TestFrameworkSymmetricCipher::new(); - framework - .test_encryptor_decryptor::, ToyCfb>(); - framework - .test_encryptor_decryptor::, ToyCfb8>( - ); - framework.test_encryptor_decryptor::, ToyCtr>(); + framework.test_key::(&key_for::(), &mut os_rng()); + framework.test_key::(&key_for::(), &mut os_rng()); + framework.test_key::(&key_for::>(), &mut os_rng()); } /// The separate-output API must produce exactly what the in-place API produces, for the same key @@ -57,29 +63,23 @@ fn the_stream_modes_conform_to_the_symmetric_cipher_suite() { /// the check that the bridge adds nothing and loses nothing. #[test] fn the_two_apis_agree_byte_for_byte() { - fn check( - name: &str, - key: &KeyMaterial, - ) where - E: StreamCipherEncryptor - + SymmetricCipherEncryptor, - D: StreamCipherDecryptor - + SymmetricCipherDecryptor, + fn check(name: &str, key: &K) + where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { for len in [0usize, 1, 15, 16, 17, 63, 64, 171] { let plaintext: Vec = (0..len).map(|i| (i * 7 + 1) as u8).collect(); // The in-place API, which the mode implements directly. - let (mut enc, init) = E::do_encrypt_init(key).unwrap(); + let (mut enc, init) = key.create_encryptor(&mut os_rng()).unwrap(); let mut in_place = plaintext.clone(); enc.do_encrypt_inplace(&mut in_place).unwrap(); // The separate-output API, under the same init data. - let mut dec_as_sym = - >::do_decrypt_init( - key, &init, - ) - .unwrap(); + let mut dec_as_sym = key.create_decryptor(&init); let mut out = vec![0u8; plaintext.len()]; let n = dec_as_sym.do_decrypt_out(&in_place, &mut out).unwrap(); let (last, last_len) = dec_as_sym.do_decrypt_final().unwrap(); @@ -90,9 +90,9 @@ fn the_two_apis_agree_byte_for_byte() { } } - check::, ToyCfb, TOY_LEN, TOY_LEN>("Cfb", &toy_key()); - check::, ToyCfb8, TOY_LEN, TOY_LEN>("Cfb8", &toy_key()); - check::, ToyCtr, TOY_LEN, 12>("Ctr", &toy_key()); + check::<_, TOY_LEN, TOY_LEN>("Cfb", &key_for::()); + check::<_, TOY_LEN, TOY_LEN>("Cfb8", &key_for::()); + check::<_, TOY_LEN, 12>("Ctr", &key_for::>()); } /// The separate-output API must leave the caller's input untouched. That is the whole reason a @@ -100,15 +100,10 @@ fn the_two_apis_agree_byte_for_byte() { /// assuming. #[test] fn the_input_buffer_is_not_modified() { - let key = toy_key(); let plaintext: Vec = (0..100u8).collect(); let original = plaintext.clone(); - let (mut enc, _init) = - as SymmetricCipherEncryptor>::do_encrypt_init( - &key, - ) - .unwrap(); + let (mut enc, _init) = key_for::().create_encryptor(&mut os_rng()).unwrap(); let mut ciphertext = vec![0u8; plaintext.len()]; enc.do_encrypt_out(&plaintext, &mut ciphertext).unwrap(); @@ -119,7 +114,7 @@ fn the_input_buffer_is_not_modified() { /// The length predictions are exact for a stream cipher, not upper bounds: what goes in comes out. #[test] fn the_length_predictions_are_exact() { - let key = toy_key(); + let key = key_for::>(); for len in [0usize, 1, 15, 16, 17, 1000] { assert_eq!( as SymmetricCipherEncryptor>::encrypt_out_len(len), @@ -132,9 +127,7 @@ fn the_length_predictions_are_exact() { "decrypt_out_len is exact, not an upper bound" ); - let (enc, _) = - as SymmetricCipherEncryptor>::do_encrypt_init(&key) - .unwrap(); + let (enc, _) = key.create_encryptor(&mut os_rng()).unwrap(); assert_eq!(enc.do_encrypt_out_len(len), len, "update_out_len is the identity"); } } @@ -145,14 +138,10 @@ fn the_length_predictions_are_exact() { fn a_short_output_buffer_is_refused_without_consuming_anything() { use bouncycastle_core::errors::SymmetricCipherError; - let key = toy_key(); + let key = key_for::(); let plaintext: Vec = (0..32u8).collect(); - let (mut enc, init) = - as SymmetricCipherEncryptor>::do_encrypt_init( - &key, - ) - .unwrap(); + let (mut enc, init) = key.create_encryptor(&mut os_rng()).unwrap(); let mut too_small = vec![0u8; plaintext.len() - 1]; match enc.do_encrypt_out(&plaintext, &mut too_small) { @@ -167,11 +156,9 @@ fn a_short_output_buffer_is_refused_without_consuming_anything() { let mut big_enough = vec![0u8; plaintext.len()]; enc.do_encrypt_out(&plaintext, &mut big_enough).unwrap(); - let (mut fresh, _) = ToyCfb::::do_encrypt_init_rng( - &key, - &mut bouncycastle_core_test_framework::FixedSeedRNG::::new(init), - ) - .unwrap(); + let (mut fresh, _) = key + .create_encryptor(&mut bouncycastle_core_test_framework::FixedSeedRNG::::new(init)) + .unwrap(); let mut reference = plaintext.clone(); fresh.do_encrypt_inplace(&mut reference).unwrap(); assert_eq!(big_enough, reference, "the refused call must not have advanced the keystream"); @@ -186,19 +173,15 @@ fn a_short_output_buffer_is_refused_without_consuming_anything() { fn a_short_output_buffer_is_refused_when_decrypting_too() { use bouncycastle_core::errors::SymmetricCipherError; - let key = toy_key(); + let key = key_for::(); let plaintext: Vec = (0..32u8).collect(); // Encrypt normally, then try to decrypt into a buffer one byte too small. - let (mut enc, init) = ToyCfb::::do_encrypt_init(&key).unwrap(); + let (mut enc, init) = key.create_encryptor(&mut os_rng()).unwrap(); let mut ciphertext = plaintext.clone(); enc.do_encrypt_inplace(&mut ciphertext).unwrap(); - let mut dec = - as SymmetricCipherDecryptor>::do_decrypt_init( - &key, &init, - ) - .unwrap(); + let mut dec = key.create_decryptor(&init); let mut too_small = vec![0u8; ciphertext.len() - 1]; match dec.do_decrypt_out(&ciphertext, &mut too_small) { @@ -217,11 +200,7 @@ fn a_short_output_buffer_is_refused_when_decrypting_too() { // An oversized buffer is fine, the data lands in the leading bytes and the rest is zeroed: the // check is "too short", not "not exactly equal". let mut oversized = vec![0xAAu8; ciphertext.len() + 8]; - let mut dec = - as SymmetricCipherDecryptor>::do_decrypt_init( - &key, &init, - ) - .unwrap(); + let mut dec = key.create_decryptor(&init); let n = dec.do_decrypt_out(&ciphertext, &mut oversized).expect("an oversized buffer is fine"); assert_eq!(n, ciphertext.len()); assert_eq!(&oversized[..n], &plaintext[..], "the data lands in the leading bytes"); @@ -233,40 +212,22 @@ fn a_short_output_buffer_is_refused_when_decrypting_too() { /// modes: the shape a caller most often wants from this API. #[test] fn the_allocating_one_shots_round_trip() { - let key = toy_key(); let message = b"a message of no particular length at all"; // CFB128 - let (iv, ct) = as SymmetricCipherEncryptor>::encrypt( - &key, message, - ) - .unwrap(); + let key = key_for::(); + let (iv, ct) = key.encrypt(&mut os_rng(), message).unwrap(); assert_eq!(ct.len(), message.len(), "a stream cipher does not change the length"); - let back = as SymmetricCipherDecryptor>::decrypt( - &key, &iv, &ct, - ) - .unwrap(); - assert_eq!(back, message); + assert_eq!(key.decrypt(&iv, &ct).unwrap(), message); // CFB8 - let (iv, ct) = as SymmetricCipherEncryptor>::encrypt( - &key, message, - ) - .unwrap(); - let back = as SymmetricCipherDecryptor>::decrypt( - &key, &iv, &ct, - ) - .unwrap(); - assert_eq!(back, message); + let key = key_for::(); + let (iv, ct) = key.encrypt(&mut os_rng(), message).unwrap(); + assert_eq!(key.decrypt(&iv, &ct).unwrap(), message); // CTR - let (nonce, ct) = - as SymmetricCipherEncryptor>::encrypt(&key, message) - .unwrap(); + let key = key_for::>(); + let (nonce, ct) = key.encrypt(&mut os_rng(), message).unwrap(); assert_eq!(nonce.len(), 12, "CTR's init data is its 12-byte nonce"); - let back = as SymmetricCipherDecryptor>::decrypt( - &key, &nonce, &ct, - ) - .unwrap(); - assert_eq!(back, message); + assert_eq!(key.decrypt(&nonce, &ct).unwrap(), message); } diff --git a/crypto/cipher/tests/padding/padded_tests.rs b/crypto/cipher/tests/padding/padded_tests.rs index a0b2d2574..119526ca7 100644 --- a/crypto/cipher/tests/padding/padded_tests.rs +++ b/crypto/cipher/tests/padding/padded_tests.rs @@ -1,108 +1,56 @@ -//! Tests for PaddedBlockCipherEncryptor / PaddedBlockCipherDecryptor. -//! -//! No real block cipher exists in the this crate, so these tests drive the adapters with a toy -//! CBC-style cipher whose "block permutation" is XOR with the key. It is cryptographically worthless -//! but exercises every code path of the adapters: IV generation, chaining state across calls, and -//! the one-block lag on decryption. - -use bouncycastle_cipher::padding::{ - NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, -}; -use bouncycastle_core::errors::{KeyMaterialError, PaddingError, SymmetricCipherError}; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +//! Tests for PaddedBlockCipherEncryptor / PaddedBlockCipherDecryptor, through the key path: +//! `CBC` and `CBC` over the toy permutation in `common/toy_block_cipher.rs`. +//! They exercise every code path of the adapters: IV generation, chaining state across calls, +//! the one-block lag on decryption, and the alignment check `NoPadding` turns the adapters into. + +#[path = "../common/toy_block_cipher.rs"] +mod toy_block_cipher; +#[path = "../common/toy_key.rs"] +mod toy_key; + +use bouncycastle_cipher::modes::{Blocks, CBC, ModeNames}; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::errors::{PaddingError, SymmetricCipherError}; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{ - Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, + BlockCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkSymmetricCipher; -use bouncycastle_rng::hash_drbg80090a::{HashDRBG80090A, HashDRBG80090AParams_SHA256}; +use toy_block_cipher::{TOY_BLOCK_LEN, ToyBlockCipher}; +use toy_key::{ToyKey, os_rng}; -const B: usize = 8; +const B: usize = TOY_BLOCK_LEN; -/// c_j = p_j ^ c_{j-1} ^ key ; p_j = c_j ^ c_{j-1} ^ key -struct ToyCbc { - key: [u8; B], - chain: [u8; B], -} +type Key = ToyKey>; +type Enc = >::Encryptor; +type Dec = >::Decryptor; +/// The same adapters over `NoPadding`: an alignment check rather than a padding scheme. +type KeyNP = ToyKey>; +type EncNP = >::Encryptor; +type DecNP = >::Decryptor; -impl ToyCbc { - fn check_key(key: &KeyMaterial) -> Result<[u8; B], SymmetricCipherError> { - if key.key_type() != KeyType::SymmetricCipherKey { - return Err(KeyMaterialError::InvalidKeyType("expected SymmetricCipherKey"))?; - } - if key.security_strength() < Self::MAX_SECURITY_STRENGTH { - return Err(KeyMaterialError::GenericError("key too weak"))?; - } - let mut k = [0u8; B]; - k.copy_from_slice(key.ref_to_bytes()); - Ok(k) - } +fn material() -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&[0x5a; B], KeyType::SymmetricCipherKey).unwrap() } -impl Algorithm for ToyCbc { - const ALG_NAME: &'static str = "ToyCbc"; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +fn key_for>() -> ToyKey { + ToyKey::from_key_material(&material()).expect("the toy key") } -impl BlockCipherEncryptor for ToyCbc { - fn do_encrypt_init(key: &KeyMaterial) -> Result<(Self, [u8; B]), SymmetricCipherError> { - let mut rng = HashDRBG80090A::::new_from_os(); - Self::do_encrypt_init_rng(key, &mut rng) - } - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; B]), SymmetricCipherError> { - let key = Self::check_key(key)?; - let mut iv = [0u8; B]; - rng.next_bytes_out(&mut iv)?; - Ok((Self { key, chain: iv }, iv)) - } - fn do_encrypt_blocks_inplace( - &mut self, - blocks: &mut [[u8; B]], - ) -> Result { - 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 = *block; - } - Ok(blocks.len() * B) - } +fn key() -> Key { + key_for() } -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_inplace( - &mut self, - blocks: &mut [[u8; B]], - ) -> Result { - let len = blocks.len() * B; - 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 = ct; - } - Ok(len) - } -} - -type Enc = PaddedBlockCipherEncryptor; -type Dec = PaddedBlockCipherDecryptor; -/// The same adapters over `NoPadding`: an alignment check rather than a padding scheme. -type EncNP = PaddedBlockCipherEncryptor; -type DecNP = PaddedBlockCipherDecryptor; - -fn key() -> KeyMaterial { - KeyMaterial::::from_bytes_as_type(&[0x5a; B], KeyType::SymmetricCipherKey).unwrap() +fn key_np() -> KeyNP { + key_for() } fn msg(len: usize) -> Vec { @@ -111,14 +59,24 @@ fn msg(len: usize) -> Vec { #[test] fn toy_cipher_passes_core_test_framework() { - TestFrameworkBlockCipher::new().test::(); + TestFrameworkBlockCipher::new() + .test_key::(&key_for::>(), &mut os_rng()); } -/// The padded adapters are the first implementors of `SymmetricCipherEncryptor` / -/// `SymmetricCipherDecryptor`, so this is also what exercises those traits' provided one-shots. +/// The padded adapters are what the symmetric key path hands out for a block mode, so this is +/// also what exercises the role traits' provided one-shots over a mode with a final block. #[test] fn padded_adapters_pass_the_symmetric_cipher_framework() { - TestFrameworkSymmetricCipher::new().test_encryptor_decryptor::(); + TestFrameworkSymmetricCipher::new().test_key::(&key(), &mut os_rng()); +} + +/// The construction suite for both keys: the type, binding and strength policy of +/// `from_key_material`. +#[test] +fn the_keys_enforce_the_material_policy() { + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::(ToyBlockCipher::CBC_ALG_NAME, SecurityStrength::_128bit); + framework.test::(ToyBlockCipher::CBC_ALG_NAME, SecurityStrength::_128bit); } #[test] @@ -127,12 +85,12 @@ fn one_shot_roundtrip_all_lengths() { for len in 0..=3 * B + 1 { let pt = msg(len); let mut ct = vec![0u8; Enc::encrypt_out_len(len)]; - let (iv, n) = Enc::encrypt_out(&key, &pt, &mut ct).unwrap(); + let (n, iv) = key.encrypt_out(&mut os_rng(), &pt, &mut ct).unwrap(); assert_eq!(n, ct.len()); assert_eq!(n, (len / B + 1) * B, "always one extra padding block"); let mut out = vec![0u8; Dec::decrypt_out_len(n)]; - let m = Dec::decrypt_out(&key, &iv, &ct[..n], &mut out).unwrap(); + let m = key.decrypt_out(&iv, &ct[..n], &mut out).unwrap(); assert_eq!(&out[..m], &pt[..]); } } @@ -145,7 +103,7 @@ fn streaming_matches_one_shot_for_every_chunking() { for chunk in [1usize, 2, 3, 7, 8, 9, 15, 16, 17, len] { // encrypt in chunks - let (mut enc, iv) = Enc::do_encrypt_init(&key).unwrap(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let mut ct = Vec::new(); for piece in pt.chunks(chunk) { let expect = enc.do_encrypt_out_len(piece.len()); @@ -161,11 +119,11 @@ fn streaming_matches_one_shot_for_every_chunking() { // one-shot decrypt let mut out = vec![0u8; Dec::decrypt_out_len(ct.len())]; - let m = Dec::decrypt_out(&key, &iv, &ct, &mut out).unwrap(); + let m = key.decrypt_out(&iv, &ct, &mut out).unwrap(); assert_eq!(&out[..m], &pt[..], "chunk {chunk}"); // decrypt in the same chunks - let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut rec = Vec::new(); for piece in ct.chunks(chunk) { let expect = dec.do_decrypt_out_len(piece.len()); @@ -185,11 +143,11 @@ fn decryptor_lags_by_exactly_one_block() { let key = key(); let (iv, ct) = { let mut ct = vec![0u8; Enc::encrypt_out_len(2 * B)]; - let (iv, _) = Enc::encrypt_out(&key, &msg(2 * B), &mut ct).unwrap(); + let (_, iv) = key.encrypt_out(&mut os_rng(), &msg(2 * B), &mut ct).unwrap(); (iv, ct) }; assert_eq!(ct.len(), 3 * B); - let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut out = [0u8; 3 * B]; // first block: nothing can be released yet assert_eq!(dec.do_decrypt_out_len(B), 0); @@ -208,7 +166,7 @@ fn decryptor_lags_by_exactly_one_block() { #[test] fn final_out_variants() { let key = key(); - let (mut enc, iv) = Enc::do_encrypt_init(&key).unwrap(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let mut ct = [0u8; 2 * B]; let n = enc.do_encrypt_out(&msg(B + 2), &mut ct).unwrap(); assert_eq!(n, B); @@ -216,7 +174,7 @@ fn final_out_variants() { assert_eq!(enc.do_encrypt_final_out(&mut last).unwrap(), B); ct[B..].copy_from_slice(&last); - let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut out = [0u8; B]; assert_eq!(dec.do_decrypt_out(&ct, &mut out).unwrap(), B); let mut last_pt = [0u8; B]; @@ -232,14 +190,14 @@ fn tampered_final_block_is_rejected() { let key = key(); for len in [0, 1, B - 1, B, B + 5] { let mut ct = vec![0u8; Enc::encrypt_out_len(len)]; - let (iv, n) = Enc::encrypt_out(&key, &msg(len), &mut ct).unwrap(); + let (n, iv) = key.encrypt_out(&mut os_rng(), &msg(len), &mut ct).unwrap(); // flipping the low bit of the final byte corrupts the PKCS7 length byte ct[n - 1] ^= 0x01; // Pre-filled with a non-zero byte, so a buffer the decryptor never wrote is not mistaken // for one it zeroized. At `B + 5` the first block has been decrypted into `out` by the // time the padding fails, and `decrypt_out` must wipe it. let mut out = vec![0xEEu8; n]; - match Dec::decrypt_out(&key, &iv, &ct, &mut out) { + match key.decrypt_out(&iv, &ct, &mut out) { Err(SymmetricCipherError::PaddingError(PaddingError::InvalidPadding)) => {} other => panic!("len {len}: expected InvalidPadding, got {other:?}"), } @@ -255,20 +213,20 @@ fn malformed_ciphertext_lengths_are_rejected() { // empty assert!(matches!( - Dec::decrypt_out(&key, &iv, &[], &mut out), + key.decrypt_out(&iv, &[], &mut out), Err(SymmetricCipherError::DecryptionFailed) )); // not a multiple of the block length assert!(matches!( - Dec::decrypt_out(&key, &iv, &[0u8; B + 1], &mut out), + key.decrypt_out(&iv, &[0u8; B + 1], &mut out), Err(SymmetricCipherError::DataNotBlockAligned) )); // streaming: partial trailing block at final - let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); dec.do_decrypt_out(&[0u8; B + 3], &mut out).unwrap(); assert!(matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DataNotBlockAligned))); // streaming: nothing fed at all - let dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + let dec = key.create_decryptor(&iv); assert!(matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed))); } @@ -278,12 +236,12 @@ fn output_buffer_too_small_reports_required_length() { let pt = msg(2 * B + 1); let mut small = [0u8; 2 * B]; - match Enc::encrypt_out(&key, &pt, &mut small) { + match key.encrypt_out(&mut os_rng(), &pt, &mut small) { Err(SymmetricCipherError::OutputBufferTooSmall(need)) => assert_eq!(need, 3 * B), other => panic!("{other:?}"), } - let (mut enc, iv) = Enc::do_encrypt_init(&key).unwrap(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let mut tiny = [0u8; B - 1]; match enc.do_encrypt_out(&pt, &mut tiny) { Err(SymmetricCipherError::OutputBufferTooSmall(need)) => assert_eq!(need, 2 * B), @@ -293,7 +251,7 @@ fn output_buffer_too_small_reports_required_length() { let ct = [0u8; 3 * B]; let mut small = [0u8; 3 * B - 2]; - match Dec::decrypt_out(&key, &iv, &ct, &mut small) { + match key.decrypt_out(&iv, &ct, &mut small) { Err(SymmetricCipherError::OutputBufferTooSmall(need)) => { assert_eq!(need, 3 * B - 1) } @@ -301,19 +259,6 @@ fn output_buffer_too_small_reports_required_length() { } } -#[test] -fn wrong_key_type_is_rejected_by_adapters() { - let mac_key = KeyMaterial::::from_bytes_as_type(&[1u8; B], KeyType::MACKey).unwrap(); - assert!(matches!( - Enc::do_encrypt_init(&mac_key), - Err(SymmetricCipherError::KeyMaterialError(_)) - )); - assert!(matches!( - Dec::do_decrypt_init(&mac_key, &[0u8; B]), - Err(SymmetricCipherError::KeyMaterialError(_)) - )); -} - // ---- NoPadding through the adapters -------------------------------------------------------- /// With `NoPadding` the adapters enforce alignment: the framework is told that only multiples of @@ -323,14 +268,14 @@ fn wrong_key_type_is_rejected_by_adapters() { fn no_padding_adapters_pass_the_symmetric_cipher_framework() { let mut framework = TestFrameworkSymmetricCipher::new(); framework.required_alignment = B; - framework.test_encryptor_decryptor::(); + framework.test_key::(&key_np(), &mut os_rng()); } /// An aligned message passes through with its length unchanged -- no final block is added -- and the /// ciphertext is exactly what the bare mode produces: NoPadding is a check, not a transformation. #[test] fn no_padding_adds_nothing_to_aligned_data() { - let key = key(); + let key_np = key_np(); for blocks in 0..=4usize { let len = blocks * B; let pt = msg(len); @@ -338,23 +283,23 @@ fn no_padding_adds_nothing_to_aligned_data() { assert_eq!(DecNP::decrypt_out_len(len), len); let mut ct = vec![0u8; len]; - let (iv, n) = EncNP::encrypt_out(&key, &pt, &mut ct).unwrap(); + let (n, iv) = key_np.encrypt_out(&mut os_rng(), &pt, &mut ct).unwrap(); assert_eq!(n, len, "{blocks} blocks: output length equals input length"); // Byte for byte the bare cipher's output under the same IV. let mut bare = pt.clone(); let (mut enc, _) = - ToyCbc::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(iv)).unwrap(); + key_for::>().create_encryptor(&mut FixedSeedRNG::::new(iv)).unwrap(); let (blocks_mut, _) = bare.as_chunks_mut::(); enc.do_encrypt_blocks_inplace(blocks_mut).unwrap(); assert_eq!(ct, bare, "{blocks} blocks: the adapter must not alter the ciphertext"); let mut out = vec![0u8; len]; - let m = DecNP::decrypt_out(&key, &iv, &ct, &mut out).unwrap(); + let m = key_np.decrypt_out(&iv, &ct, &mut out).unwrap(); assert_eq!(&out[..m], &pt[..], "{blocks} blocks: round trip"); // Streaming: do_encrypt_final reports zero output bytes. - let (mut enc, _) = EncNP::do_encrypt_init(&key).unwrap(); + let (mut enc, _) = key_np.create_encryptor(&mut os_rng()).unwrap(); let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; assert_eq!(enc.do_encrypt_out(&pt, &mut buf).unwrap(), len); let (_, last_len) = enc.do_encrypt_final().unwrap(); @@ -366,19 +311,19 @@ fn no_padding_adds_nothing_to_aligned_data() { /// streaming `do_encrypt_final`, and nothing is written for the final block. #[test] fn no_padding_refuses_unaligned_data() { - let key = key(); + let key_np = key_np(); for len in [1usize, B - 1, B + 1, 2 * B + 3, 3 * B - 1] { let pt = msg(len); let mut ct = vec![0u8; len + B]; assert!( matches!( - EncNP::encrypt_out(&key, &pt, &mut ct), + key_np.encrypt_out(&mut os_rng(), &pt, &mut ct), Err(SymmetricCipherError::DataNotBlockAligned) ), "len {len}: one-shot must refuse an unaligned message" ); - let (mut enc, _) = EncNP::do_encrypt_init(&key).unwrap(); + let (mut enc, _) = key_np.create_encryptor(&mut os_rng()).unwrap(); let whole = len / B * B; let mut buf = vec![0u8; whole]; assert_eq!(enc.do_encrypt_out(&pt, &mut buf).unwrap(), whole, "whole blocks still stream"); @@ -393,18 +338,18 @@ fn no_padding_refuses_unaligned_data() { /// demand), and an unaligned ciphertext is still malformed. #[test] fn no_padding_decryptor_accepts_empty_and_rejects_unaligned() { - let key = key(); + let key_np = key_np(); let iv = [0x11u8; B]; let mut out = [0u8; 0]; - assert_eq!(DecNP::decrypt_out(&key, &iv, &[], &mut out).unwrap(), 0); - let dec = DecNP::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(key_np.decrypt_out(&iv, &[], &mut out).unwrap(), 0); + let dec = key_np.create_decryptor(&iv); assert_eq!(dec.do_decrypt_final().unwrap().1, 0); for len in [1usize, B - 1, B + 1, 2 * B + 5] { let mut out = vec![0u8; len]; assert!( matches!( - DecNP::decrypt_out(&key, &iv, &msg(len), &mut out), + key_np.decrypt_out(&iv, &msg(len), &mut out), Err(SymmetricCipherError::DataNotBlockAligned) ), "len {len}: an unaligned ciphertext is malformed" diff --git a/crypto/cipher/tests/suspend_tests.rs b/crypto/cipher/tests/suspend_tests.rs index 7397e0024..0279d7a39 100644 --- a/crypto/cipher/tests/suspend_tests.rs +++ b/crypto/cipher/tests/suspend_tests.rs @@ -8,11 +8,18 @@ //! The AES aliases get the same impls through these generic types, so this is where they are //! pinned; `bouncycastle-aes` only checks that each alias reaches them. -use bouncycastle_cipher::modes::hazmat::ecb::Ecb; -use bouncycastle_cipher::modes::{Cbc, CcmDecryptor, CcmEncryptor, Cfb, Cfb8, Ctr, Gcm}; +use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; +use bouncycastle_cipher::modes::{ + Blocks, CBC, CCMFrame, CFB, CFB8, CTR, Cbc, CcmDecryptor, CcmEncryptor, Cfb, Cfb8, Ctr, Gcm, +}; use bouncycastle_cipher::padding::{PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + AeadDecryptionKey, AeadEncryptionKey, BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, + SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, @@ -22,7 +29,10 @@ use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendabl #[path = "common/toy_block_cipher.rs"] mod toy_block_cipher; +#[path = "common/toy_key.rs"] +mod toy_key; use toy_block_cipher::ToyBlockCipher; +use toy_key::{ToyGCM, ToyKey, os_rng}; type ToyEcb = Ecb; type ToyCbc = Cbc; @@ -31,6 +41,7 @@ type ToyCfb8 = Cfb8; type ToyCtr = Ctr; type ToyGcm = Gcm; // A 64-byte AAD capacity and 37-byte frames, the message `ccm_both_directions` uses. +type ToyCcmFrame = CCMFrame<12, 16, 64, 37>; type ToyCcmEnc = CcmEncryptor; type ToyCcmDec = CcmDecryptor; type ToyPaddedEnc = PaddedBlockCipherEncryptor, PKCS7, 16, 16, 16>; @@ -40,6 +51,11 @@ fn key() -> KeyMaterial<16> { KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap() } +/// The toy key under the mode `M`; resume still takes the material, through `key()`. +fn key_for>() -> ToyKey { + ToyKey::from_key_material(&key()).expect("the toy key") +} + fn message(len: usize) -> Vec { (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(3)).collect() } @@ -60,7 +76,7 @@ where #[test] fn cbc_both_directions() { - let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key()).unwrap(); + let (mut enc, iv) = key_for::>().create_encryptor(&mut os_rng()).unwrap(); let mut first = [0x11u8; 16]; enc.do_encrypt_blocks_inplace(first.as_chunks_mut().0).unwrap(); let rest = round_trip::<{ ToyCbc::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { @@ -69,7 +85,7 @@ fn cbc_both_directions() { data.to_vec() }); - let mut dec = ToyCbc::::do_decrypt_init(&key(), &iv).unwrap(); + let mut dec = key_for::>().create_decryptor(&iv); dec.do_decrypt_blocks_inplace(first.as_chunks_mut().0).unwrap(); assert_eq!(first, [0x11u8; 16]); let plain = round_trip::<{ ToyCbc::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { @@ -82,14 +98,14 @@ fn cbc_both_directions() { #[test] fn ecb_both_directions() { - let (mut enc, _) = ToyEcb::::do_encrypt_init(&key()).unwrap(); + let mut enc = key_for::>().create_encryptor(); enc.do_encrypt_blocks_inplace(&mut [[0x11u8; 16]]).unwrap(); round_trip::<{ ToyEcb::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { let mut data = [0x22u8; 32]; e.do_encrypt_blocks_inplace(data.as_chunks_mut().0).unwrap(); data.to_vec() }); - let dec = ToyEcb::::do_decrypt_init(&key(), &[]).unwrap(); + let dec = key_for::>().create_decryptor(); round_trip::<{ ToyEcb::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { let mut data = [0x33u8; 32]; d.do_decrypt_blocks_inplace(data.as_chunks_mut().0).unwrap(); @@ -100,7 +116,7 @@ fn ecb_both_directions() { #[test] fn cfb_mid_segment_both_directions() { let msg = message(40); - let (mut enc, iv) = ToyCfb::::do_encrypt_init(&key()).unwrap(); + let (mut enc, iv) = key_for::().create_encryptor(&mut os_rng()).unwrap(); let mut head = msg[..7].to_vec(); enc.do_encrypt_inplace(&mut head).unwrap(); let tail = round_trip::<{ ToyCfb::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { @@ -109,7 +125,7 @@ fn cfb_mid_segment_both_directions() { data }); - let mut dec = ToyCfb::::do_decrypt_init(&key(), &iv).unwrap(); + let mut dec = key_for::().create_decryptor(&iv); dec.do_decrypt_inplace(&mut head).unwrap(); assert_eq!(head, msg[..7]); let plain = round_trip::<{ ToyCfb::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { @@ -123,7 +139,7 @@ fn cfb_mid_segment_both_directions() { #[test] fn cfb8_both_directions() { let msg = message(20); - let (mut enc, iv) = ToyCfb8::::do_encrypt_init(&key()).unwrap(); + let (mut enc, iv) = key_for::().create_encryptor(&mut os_rng()).unwrap(); let mut head = msg[..5].to_vec(); enc.do_encrypt_inplace(&mut head).unwrap(); let tail = round_trip::<{ ToyCfb8::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { @@ -131,7 +147,7 @@ fn cfb8_both_directions() { e.do_encrypt_inplace(&mut data).unwrap(); data }); - let mut dec = ToyCfb8::::do_decrypt_init(&key(), &iv).unwrap(); + let mut dec = key_for::().create_decryptor(&iv); dec.do_decrypt_inplace(&mut head).unwrap(); let plain = round_trip::<{ ToyCfb8::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { let mut data = tail.clone(); @@ -144,7 +160,7 @@ fn cfb8_both_directions() { #[test] fn ctr_mid_block_both_directions() { let msg = message(50); - let (mut enc, nonce) = ToyCtr::::do_encrypt_init(&key()).unwrap(); + let (mut enc, nonce) = key_for::>().create_encryptor(&mut os_rng()).unwrap(); let mut head = msg[..7].to_vec(); enc.do_encrypt_inplace(&mut head).unwrap(); let tail = round_trip::<{ ToyCtr::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { @@ -152,7 +168,7 @@ fn ctr_mid_block_both_directions() { e.do_encrypt_inplace(&mut data).unwrap(); data }); - let mut dec = ToyCtr::::do_decrypt_init(&key(), &nonce).unwrap(); + let mut dec = key_for::>().create_decryptor(&nonce); dec.do_decrypt_inplace(&mut head).unwrap(); let plain = round_trip::<{ ToyCtr::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { let mut data = tail.clone(); @@ -167,7 +183,7 @@ fn gcm_both_directions_with_aad() { let msg = message(45); let aad = b"authenticated header"; - let (mut enc, nonce) = ToyGcm::::do_encrypt_init(&key()).unwrap(); + let (mut enc, nonce) = key_for::>().create_encryptor(&mut os_rng()).unwrap(); enc.do_update_aad(aad).unwrap(); let mut head = [0u8; 5]; enc.do_encrypt_out(&msg[..5], &mut head).unwrap(); @@ -184,7 +200,7 @@ fn gcm_both_directions_with_aad() { // The decryptor holds the last 16 bytes back, so after 10 bytes nothing has been released, // but data has started and the AAD phase is closed: a state worth suspending. - let mut dec = ToyGcm::::do_decrypt_init(&key(), &nonce).unwrap(); + let mut dec = key_for::>().create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut nothing = [0u8; 0]; assert_eq!(dec.do_decrypt_out(&ciphertext[..10], &mut nothing).unwrap(), 0); @@ -206,7 +222,7 @@ fn ccm_both_directions() { // Suspended with half the AAD held back and no payload yet, and again mid-payload; both // must continue to the one answer. - let mut enc = ToyCcmEnc::do_encrypt_init_nonce(&key(), &nonce).unwrap(); + let mut enc = key_for::().create_encryptor_nonce(&nonce); enc.do_update_aad(&aad[..3]).unwrap(); let sealed = round_trip::<{ ToyCcmEnc::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { e.do_update_aad(&aad[3..]).unwrap(); @@ -216,7 +232,7 @@ fn ccm_both_directions() { out.extend_from_slice(&tag); out }); - let mut enc = ToyCcmEnc::do_encrypt_init_nonce(&key(), &nonce).unwrap(); + let mut enc = key_for::().create_encryptor_nonce(&nonce); enc.do_update_aad(aad).unwrap(); let mut head = vec![0u8; 9]; enc.do_encrypt_out(&msg[..9], &mut head).unwrap(); @@ -231,7 +247,7 @@ fn ccm_both_directions() { let (ct, tag) = sealed.split_at(37); let tag: [u8; 16] = tag.try_into().unwrap(); - let mut dec = ToyCcmDec::do_decrypt_init(&key(), &nonce).unwrap(); + let mut dec = key_for::().create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut opened_head = vec![0u8; 9]; dec.do_decrypt_out(&ct[..9], &mut opened_head).unwrap(); @@ -244,7 +260,7 @@ fn ccm_both_directions() { assert_eq!([&opened_head[..], &plain[..]].concat(), msg); // ...and with part of an inline tag already held back at the suspend point. - let mut dec = ToyCcmDec::do_decrypt_init(&key(), &nonce).unwrap(); + let mut dec = key_for::().create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut opened = vec![0u8; 37]; dec.do_decrypt_out(&sealed[..40], &mut opened).unwrap(); @@ -259,7 +275,7 @@ fn ccm_both_directions() { #[test] fn padded_cbc_both_directions() { let msg = message(45); - let (mut enc, iv) = ToyPaddedEnc::do_encrypt_init(&key()).unwrap(); + let (mut enc, iv) = key_for::>().create_encryptor(&mut os_rng()).unwrap(); let mut head = [0u8; 16]; // 20 bytes in: one block out, four buffered. assert_eq!(enc.do_encrypt_out(&msg[..20], &mut head).unwrap(), 16); @@ -276,7 +292,7 @@ fn padded_cbc_both_directions() { assert_eq!(ciphertext.len(), 48); // 20 bytes in: one block released, one held back, four buffered. - let mut dec = ToyPaddedDec::do_decrypt_init(&key(), &iv).unwrap(); + let mut dec = key_for::>().create_decryptor(&iv); let mut first = [0u8; 16]; assert_eq!(dec.do_decrypt_out(&ciphertext[..36], &mut first).unwrap(), 16); let plain = round_trip::<{ ToyPaddedDec::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { From 6216fe940b62b93f775979bdd4e5adbefbd5e5b4 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 23:23:23 +1100 Subject: [PATCH 06/54] cipher: doc examples onto the mode-trait route (prototype) The module examples built their workers through the trait constructors. They now build the mode's key state and call the mode traits, which is what a mode-bound key does, with GCM and CMAC on their from_key_state routes; the one-shots are the key's and are shown in the key crate. The CcmEncryptor examples and compile_fail checks move to CCMFrame. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/lib.rs | 15 ++- crypto/cipher/src/modes/cbc.rs | 60 ++++----- crypto/cipher/src/modes/ccm.rs | 117 ++++++++++-------- crypto/cipher/src/modes/cfb.rs | 20 +-- crypto/cipher/src/modes/cmac.rs | 15 ++- crypto/cipher/src/modes/ctr.rs | 16 ++- crypto/cipher/src/modes/gcm.rs | 108 ++++++++++------ crypto/cipher/src/modes/hazmat/ecb.rs | 25 ++-- .../core-test-framework/src/block_cipher.rs | 2 +- 9 files changed, 216 insertions(+), 162 deletions(-) diff --git a/crypto/cipher/src/lib.rs b/crypto/cipher/src/lib.rs index 3b17b89d3..375093ecc 100644 --- a/crypto/cipher/src/lib.rs +++ b/crypto/cipher/src/lib.rs @@ -20,24 +20,29 @@ //! protect it as the plaintext it governs, and never resume one state twice. //! //! ``` -//! use bouncycastle_cipher::modes::Cbc; +//! use bouncycastle_cipher::modes::{Blocks, CBC, Cbc}; //! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_core::hazmat::{BlockEncryptMode, ElectronicCodeBook, Mode}; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{BlockCipherEncryptor, SuspendableKeyed}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! # mod toy { include!("../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; //! //! type ToyCbc = Cbc; //! const STATE_LEN: usize = ToyCbc::SUSPENDED_STATE_LEN; //! +//! // The mode's key state, as a mode-bound key such as `AES128Key>` holds it. //! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let (mut enc, _iv) = ToyCbc::do_encrypt_init(&key).unwrap(); +//! let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); +//! let (mut enc, _iv) = CBC::::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()).unwrap(); //! let mut first = [[0x11u8; 16]]; //! enc.do_encrypt_blocks_inplace(&mut first).unwrap(); //! -//! // Suspending consumes the cipher. The key is not in the state and is re-supplied to resume. -//! let state: [u8; STATE_LEN] = enc.suspend(); -//! let mut enc = ToyCbc::from_suspended(state, &key).unwrap(); +//! // Suspending consumes the cipher. The key is not in the suspended state; resume borrows the +//! // key state again, as `create_encryptor` did. +//! let suspended: [u8; STATE_LEN] = enc.suspend(); +//! let mut enc = ToyCbc::from_suspended(suspended, &state).unwrap(); //! let mut second = [[0x22u8; 16]]; //! enc.do_encrypt_blocks_inplace(&mut second).unwrap(); //! ``` diff --git a/crypto/cipher/src/modes/cbc.rs b/crypto/cipher/src/modes/cbc.rs index 755223fda..eb45fe26f 100644 --- a/crypto/cipher/src/modes/cbc.rs +++ b/crypto/cipher/src/modes/cbc.rs @@ -19,30 +19,39 @@ //! The direction is part of the type: [`Cbc`](Cbc) implements //! [`BlockCipherEncryptor`] and nothing else, and [`Cbc`](Cbc) implements //! [`BlockCipherDecryptor`] and nothing else. -//! The IV is generated and returned; there is no API for supplying one. +//! The IV is drawn from the RNG and returned; there is no API for supplying one. +//! +//! The pair is reached through a mode-bound key -- [`CBC`] on a key type such as +//! `bouncycastle_aes::AES128Key>` for whole blocks, [`CBC`](CBC) for the padded +//! form -- whose role traits carry the one-shots. The key holds the mode's key state and calls the +//! mode traits shown here, which is the route over the toy permutation: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; +//! use bouncycastle_core::hazmat::{BlockDecryptMode, BlockEncryptMode, ElectronicCodeBook, Mode}; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; -//! use bouncycastle_cipher::modes::Cbc; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! type ToyCbc = Cbc; +//! use bouncycastle_cipher::modes::{Blocks, CBC}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! //! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); +//! let mut rng = HashDRBG_SHA512::new_from_os(); //! -//! // 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!"; +//! // Three whole blocks: the workers take `&mut [[u8; 16]]`, so a partial block cannot be +//! // expressed. +//! let plaintext: [[u8; 16]; 3] = +//! [*b"The quick brown ", *b"fox jumps over t", *b"he lazy dog. OK!"]; //! -//! // One shot, in place: encrypts under a freshly generated IV, which is returned. +//! // In place, under a freshly drawn IV, which is returned. //! let mut data = plaintext; -//! let (_, iv) = ToyCbc::::encrypt_inplace(&key, &mut data).expect("encryption"); +//! let (mut enc, iv) = CBC::::encrypt_init(&state, &mut rng).expect("iv draw"); +//! enc.do_encrypt_blocks_inplace(&mut data).expect("encryption"); //! assert_ne!(data, plaintext); //! -//! ToyCbc::::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); +//! CBC::::decrypt_init(&state, &iv).do_decrypt_blocks_inplace(&mut data).expect("decryption"); //! assert_eq!(data, plaintext); //! ``` //! @@ -52,49 +61,30 @@ //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; +//! use bouncycastle_core::hazmat::{BlockDecryptMode, BlockEncryptMode, ElectronicCodeBook, Mode}; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; -//! use bouncycastle_cipher::modes::Cbc; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! type ToyCbc = Cbc; +//! use bouncycastle_cipher::modes::{Blocks, CBC}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); //! //! let (mut encryptor, iv) = -//! ToyCbc::::do_encrypt_init(&key).expect("encrypt init"); +//! CBC::::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()).expect("iv draw"); //! let mut first = [[0xAAu8; 16]]; //! let mut rest = [[0xBBu8; 16]; 2]; //! encryptor.do_encrypt_blocks_inplace(&mut first).expect("block 1"); //! encryptor.do_encrypt_blocks_inplace(&mut rest).expect("blocks 2-3"); //! -//! let mut decryptor = ToyCbc::::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! let mut decryptor = CBC::::decrypt_init(&state, &iv); //! decryptor.do_decrypt_blocks_inplace(&mut first).unwrap(); //! decryptor.do_decrypt_blocks_inplace(&mut rest).unwrap(); //! assert_eq!(first, [[0xAAu8; 16]]); //! assert_eq!(rest, [[0xBBu8; 16]; 2]); //! ``` //! -//! A length that is not a whole number of blocks is refused, not truncated: -//! -//! ``` -//! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } -//! # use toy::ToyBlockCipher; -//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::errors::SymmetricCipherError; -//! use bouncycastle_core::traits::BlockCipherEncryptor; -//! use bouncycastle_cipher::modes::Cbc; -//! use bouncycastle_cipher::Encrypting; -//! -//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let mut data = [0u8; 20]; -//! assert_eq!( -//! Cbc::::encrypt_inplace(&key, &mut data), -//! Err(SymmetricCipherError::DataNotBlockAligned) -//! ); -//! ``` -//! //! # Suspending and resuming execution //! //! [`Cbc`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 26846d6c1..373d53410 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -12,49 +12,52 @@ //! //! # Usage Examples //! -//! CCM must know the payload length before it starts (Sec 3), and the generic traits' streaming -//! constructor is handed no lengths, so [`CcmEncryptor`] / [`CcmDecryptor`] fix the frame in the -//! type: `DATA_LEN` bytes of payload exactly, up to `AAD_LEN` of AAD. Their one-shots have the -//! whole message in hand and take any length. The nonce is drawn unless a `_nonce` form is used; -//! CCM needs it unique, not random (Sec 5.3), so a caller with a counter supplies its own. The -//! AAD is authenticated but not encrypted, and the tag is the extra output. +//! CCM must know the payload length before it starts (Sec 3). A key bound to [`CCM`] takes the +//! lengths per message through the AEAD *packet* role pair, and its in-place one-shots take any +//! length with no second buffer; a key bound to [`CCMFrame`] fixes them in the type, as the +//! [`CcmEncryptor`] / [`CcmDecryptor`] streams it hands out do: `DATA_LEN` bytes of payload +//! exactly, up to `AAD_LEN` of AAD. The nonce is drawn unless a `_nonce` form is used; CCM needs +//! it unique, not random (Sec 5.3), so a caller with a counter supplies its own. The AAD is +//! authenticated but not encrypted, and the tag is the extra output. +//! +//! The key holds the mode's key state and calls the mode traits, which is the route over the toy +//! permutation: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; //! use bouncycastle_core::errors::SymmetricCipherError; +//! use bouncycastle_core::hazmat::{AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook, Mode}; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; -//! use bouncycastle_cipher::modes::{CcmDecryptor, CcmEncryptor}; +//! use bouncycastle_cipher::modes::CCM; //! -//! // A 12-byte nonce, a 16-byte tag, 64 bytes of AAD capacity and 32-byte frames. -//! type ToyEnc = CcmEncryptor; -//! type ToyDec = CcmDecryptor; +//! // A 12-byte nonce and a 16-byte tag. +//! type Mode12 = CCM<12, 16>; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let state = >::key_state(ToyBlockCipher::new(&key).unwrap()); //! let nonce = [0x01u8; 12]; //! let header = b"authenticated, not encrypted"; //! let message = b"any length: CCM pads internally"; //! -//! // The one-shots, in the spec's own layout (Sec 6.1 step 8): `ciphertext || tag`. -//! let ct_and_tag = ToyEnc::encrypt_with_aad_nonce(&key, &nonce, header, message).expect("encryption"); -//! let recovered = ToyDec::decrypt_with_aad(&key, &nonce, header, &ct_and_tag).expect("decryption"); -//! assert_eq!(&recovered[..], message); +//! // In place, with the tag detached. +//! let mut buf = message.to_vec(); +//! let tag = Mode12::encrypt_detached_in_place_nonce(&state, &nonce, header, &mut buf).expect("encryption"); +//! let ciphertext = buf.clone(); +//! Mode12::decrypt_detached_in_place(&state, &nonce, header, &mut buf, &tag).expect("decryption"); +//! assert_eq!(&buf[..], message); //! //! // Tampering with the ciphertext, or changing the AAD, fails the tag check. -//! let mut tampered = ct_and_tag.clone(); +//! let mut tampered = ciphertext.clone(); //! tampered[0] ^= 1; -//! assert_eq!(ToyDec::decrypt_with_aad(&key, &nonce, header, &tampered).unwrap_err(), +//! assert_eq!(Mode12::decrypt_detached_in_place(&state, &nonce, header, &mut tampered, &tag).unwrap_err(), //! SymmetricCipherError::AEADTagCheckFailed); -//! assert_eq!(ToyDec::decrypt_with_aad(&key, &nonce, b"other header", &ct_and_tag).unwrap_err(), +//! let mut other = ciphertext.clone(); +//! assert_eq!(Mode12::decrypt_detached_in_place(&state, &nonce, b"other header", &mut other, &tag).unwrap_err(), //! SymmetricCipherError::AEADTagCheckFailed); //! ``` //! -//! A mode-bound key, `AES128Key>` and the like, reaches the same pair through -//! its role traits, and its in-place one-shots take any length with no second buffer; see -//! [`CCM`]. -//! //! # CCM is not a stream cipher //! //! It does not have an indefinite-length streaming mode. @@ -1083,20 +1086,22 @@ where /// ``` /// # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } /// # use toy::ToyBlockCipher; +/// use bouncycastle_core::hazmat::{AeadDecryptMode, AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; /// use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_cipher::modes::{CcmDecryptor, CcmEncryptor}; +/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_rng::HashDRBG_SHA512; /// -/// // Frames of exactly 40 payload bytes, with up to 16 bytes of header. -/// type Enc = CcmEncryptor; -/// type Dec = CcmDecryptor; +/// // Frames of exactly 40 payload bytes, with up to 16 bytes of header, as a key is bound to them. +/// type Frame = CCMFrame<12, 16, 16, 40>; /// /// let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) /// .expect("a 16-byte symmetric cipher key"); +/// let state = >::key_state(ToyBlockCipher::new(&key).unwrap()); /// let header = b"frame 7"; /// let frame = [0x5Au8; 40]; /// -/// let (mut enc, nonce) = Enc::do_encrypt_init(&key).expect("init"); +/// let (mut enc, nonce) = Frame::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()).expect("nonce draw"); /// enc.do_update_aad(header).expect("within AAD_LEN"); /// let mut ct = [0u8; 40]; /// // Every byte comes straight out; the chunking is the caller's business. @@ -1107,7 +1112,7 @@ where /// assert_eq!(written, 40); /// let (_, _, tag) = enc.do_encrypt_final_detached().expect("exactly DATA_LEN was supplied"); /// -/// let mut dec = Dec::do_decrypt_init(&key, &nonce).expect("init"); +/// let mut dec = Frame::decrypt_init(&state, &nonce); /// dec.do_update_aad(header).expect("within AAD_LEN"); /// let mut pt = [0u8; 40]; /// dec.do_decrypt_out(&ct, &mut pt).expect("released, but not yet authenticated"); @@ -1115,7 +1120,7 @@ where /// assert_eq!(pt, frame); /// /// // 39 bytes is not a frame: the final refuses rather than authenticate a length `B0` did not commit to. -/// let (mut short, _) = Enc::do_encrypt_init(&key).expect("init"); +/// let mut short = Frame::encrypt_init_nonce(&state, &nonce); /// short.do_encrypt_out(&frame[..39], &mut ct).expect("within DATA_LEN"); /// assert!(short.do_encrypt_final().is_err()); /// ``` @@ -1124,26 +1129,30 @@ where /// /// The forms that generate a nonce need `NONCE_LEN` of at least 12, to keep the random-collision /// bound useful; the check runs only when one of them is instantiated. The forms that take the -/// caller's nonce -- [`do_encrypt_init_nonce`](AEADCipherEncryptor::do_encrypt_init_nonce) and -/// the `_nonce` one-shots -- and [`CcmDecryptor`] accept every A.1 nonce length from 7 through -/// 13, as the inherent `CcmPacket` API does, with uniqueness the caller's to guarantee. So a short -/// nonce compiles when it is supplied: +/// caller's nonce -- the `_nonce` constructor and one-shots of a key bound to [`CCMFrame`] -- and +/// [`CcmDecryptor`] accept every A.1 nonce length from 7 through 13, as the inherent `CcmPacket` +/// API does, with uniqueness the caller's to guarantee. So a short nonce compiles when it is +/// supplied: /// /// ``` /// # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } /// # use toy::ToyBlockCipher; +/// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::AEADCipherEncryptor; -/// use bouncycastle_cipher::modes::CcmEncryptor; +/// use bouncycastle_core::traits::{AEADCipherEncryptor, SymmetricCipherEncryptor}; +/// use bouncycastle_cipher::modes::CCMFrame; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // n = 8 is permitted by A.1; the caller's counter supplies it. /// let counter = 7u64.to_be_bytes(); +/// let mut enc = CCMFrame::<8, 16, 64, 256>::encrypt_init_nonce(&state, &counter); +/// enc.do_update_aad(b"hdr").unwrap(); /// let mut sealed = [0u8; 256 + 16]; -/// let n = CcmEncryptor::::encrypt_with_aad_nonce_out( -/// &key, &counter, b"hdr", &[0x5A; 256], &mut sealed, -/// ).unwrap(); -/// assert_eq!(n, 256 + 16); +/// let n = enc.do_encrypt_out(&[0x5A; 256], &mut sealed).unwrap(); +/// let (tag, tag_len) = enc.do_encrypt_final().unwrap(); +/// sealed[n..].copy_from_slice(&tag[..tag_len]); +/// assert_eq!(n + tag_len, 256 + 16); /// ``` /// /// ...and not when it would be drawn: @@ -1151,13 +1160,15 @@ where /// ```compile_fail /// # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } /// # use toy::ToyBlockCipher; +/// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_cipher::modes::CcmEncryptor; +/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_rng::HashDRBG_SHA512; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // n = 8 is permitted by A.1, but too short for a random draw. -/// let _ = CcmEncryptor::::do_encrypt_init(&key); +/// let _ = CCMFrame::<8, 16, 64, 256>::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()); /// ``` /// /// Nor does a `DATA_LEN` that `B0` could not carry under this `NONCE_LEN` (A.1's `p < 2^8q`): @@ -1165,13 +1176,15 @@ where /// ```compile_fail /// # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } /// # use toy::ToyBlockCipher; +/// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_cipher::modes::CcmEncryptor; +/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_rng::HashDRBG_SHA512; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // n = 13 leaves q = 2, so the payload is at most 65535 bytes. -/// let _ = CcmEncryptor::::do_encrypt_init(&key); +/// let _ = CCMFrame::<13, 16, 64, 65536>::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()); /// ``` /// /// # Memory @@ -1185,14 +1198,15 @@ where /// ```compile_fail /// # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } /// # use toy::ToyBlockCipher; +/// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::AEADCipherEncryptor; -/// use bouncycastle_cipher::modes::CcmEncryptor; +/// use bouncycastle_cipher::modes::CCMFrame; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) /// .unwrap(); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // n = 6 is not in {7, ..., 13}: it would make q = 9, which A.1 does not allow. -/// let _ = CcmEncryptor::::do_encrypt_init_nonce(&key, &[0u8; 6]); +/// let _ = CCMFrame::<6, 16, 64, 256>::encrypt_init_nonce(&state, &[0u8; 6]); /// ``` /// /// Nor does an odd tag length: @@ -1200,14 +1214,15 @@ where /// ```compile_fail /// # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } /// # use toy::ToyBlockCipher; +/// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::AEADCipherEncryptor; -/// use bouncycastle_cipher::modes::CcmEncryptor; +/// use bouncycastle_cipher::modes::CCMFrame; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) /// .unwrap(); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // t = 15 is not in {4, 6, 8, 10, 12, 14, 16}. -/// let _ = CcmEncryptor::::do_encrypt_init_nonce(&key, &[0u8; 12]); +/// let _ = CCMFrame::<12, 15, 64, 256>::encrypt_init_nonce(&state, &[0u8; 12]); /// ``` #[derive(Clone)] pub struct CcmEncryptor< diff --git a/crypto/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs index 0d0dba473..a37d38724 100644 --- a/crypto/cipher/src/modes/cfb.rs +++ b/crypto/cipher/src/modes/cfb.rs @@ -54,30 +54,36 @@ //! They take a `&mut [u8]` of any length; there is no padding layer and the ciphertext is exactly //! as long as the plaintext: //! +//! The pair is reached through a mode-bound key, [`CFB`] or [`CFB8`](crate::modes::CFB8) on a key type such as +//! `bouncycastle_aes::AES128Key`, whose role traits carry the one-shots. The key holds the +//! mode's key state and calls the mode traits shown here, which is the route over the toy +//! permutation: +//! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; +//! use bouncycastle_core::hazmat::{ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode}; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; -//! use bouncycastle_cipher::modes::{Cfb, Cfb8}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! type ToyCfb = Cfb; -//! type ToyCfb8 = Cfb8; +//! use bouncycastle_cipher::modes::CFB; +//! use bouncycastle_rng::HashDRBG_SHA512; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let state = >::key_state(ToyBlockCipher::new(&key).unwrap()); +//! let mut rng = HashDRBG_SHA512::new_from_os(); //! //! // Start with the plaintext. //! let plaintext = b"the quick brown fox!!"; //! let mut data = *plaintext; //! -//! let (bytes_written, iv) = ToyCfb::::encrypt_inplace(&key, &mut data).expect("encryption"); +//! let (mut enc, iv) = CFB::encrypt_init(&state, &mut rng).expect("iv draw"); +//! let bytes_written = enc.do_encrypt_inplace(&mut data).expect("encryption"); //! assert_eq!(bytes_written, plaintext.len()); //! //! // `data` now contains the ciphertext //! -//! ToyCfb::::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); +//! CFB::decrypt_init(&state, &iv).do_decrypt_inplace(&mut data).expect("decryption"); //! assert_eq!(data, *b"the quick brown fox!!"); //! ``` //! diff --git a/crypto/cipher/src/modes/cmac.rs b/crypto/cipher/src/modes/cmac.rs index 7efa291a1..4e609d900 100644 --- a/crypto/cipher/src/modes/cmac.rs +++ b/crypto/cipher/src/modes/cmac.rs @@ -12,24 +12,27 @@ //! //! # Usage Examples //! -//! One-shot, and verifying, over `ToyBlockCipher` (see the [module docs](super) for why): +//! One-shot, and verifying, from a key state -- what a mode-bound key holds -- over +//! `ToyBlockCipher` (see the [module docs](super) for why): //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_cipher::modes::Cmac; +//! use bouncycastle_cipher::modes::{Cmac, CmacKeyState}; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::traits::MAC; //! //! type ToyCmac = Cmac; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::MACKey).unwrap(); -//! let tag = ToyCmac::new(&key).unwrap().mac(b"a message"); +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let state = CmacKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); +//! let tag = ToyCmac::from_key_state(&state).mac(b"a message"); //! assert_eq!(tag.len(), 16); -//! assert!(ToyCmac::new(&key).unwrap().verify(b"a message", &tag)); +//! assert!(ToyCmac::from_key_state(&state).verify(b"a message", &tag)); //! ``` //! -//! Streaming from a key state, with a tag truncated to 8 bytes: +//! Streaming, with a tag truncated to 8 bytes: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } diff --git a/crypto/cipher/src/modes/ctr.rs b/crypto/cipher/src/modes/ctr.rs index 9453ddd80..48894896e 100644 --- a/crypto/cipher/src/modes/ctr.rs +++ b/crypto/cipher/src/modes/ctr.rs @@ -197,19 +197,23 @@ where /// 13, 14 or 15 bytes. Both bounds are inline `const` assertions in the constructors, so a nonce /// length outside that range is a **compile** error at the call site rather than a runtime `Err`. /// -/// The permitted lengths all work: +/// The permitted lengths all work, here through the mode type [`CTR`] a key is bound to: /// /// ``` /// # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } /// # use toy::ToyBlockCipher; +/// use bouncycastle_core::hazmat::{ElectronicCodeBook, Mode, SymmetricEncryptMode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_cipher::modes::Ctr; -/// use bouncycastle_cipher::Encrypting; +/// use bouncycastle_cipher::modes::CTR; +/// use bouncycastle_rng::HashDRBG_SHA512; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -/// let _ = Ctr::::do_encrypt_init(&key).unwrap(); // 4-byte counter -/// let _ = Ctr::::do_encrypt_init(&key).unwrap(); // 1-byte counter +/// let perm = ToyBlockCipher::new(&key).unwrap(); +/// let mut rng = HashDRBG_SHA512::new_from_os(); +/// // 4-byte counter +/// let _ = CTR::<12>::encrypt_init(& as Mode>::key_state(perm.clone()), &mut rng).unwrap(); +/// // 1-byte counter +/// let _ = CTR::<15>::encrypt_init(& as Mode>::key_state(perm), &mut rng).unwrap(); /// ``` pub type Ctr = StreamCipher< diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index 4de0605c1..71454d30a 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -5,9 +5,8 @@ //! //! [`Gcm`] fixes the nonce at 96 bits (12 bytes, [`GCM_NONCE_LEN`]): SP 800-38D Sec 5.2.1.1 //! recommends that implementations "restrict support to the length of 96 bits", and the other IV -//! lengths are not implemented. The nonce is never taken from the caller: instead -//! [`SymmetricCipherEncryptor::do_encrypt_init`] and [`SymmetricCipherEncryptor::do_encrypt_init_rng`] -//! draw it from the default OS RNG or the provided RNG, respectively. +//! lengths are not implemented. The nonce is drawn from the RNG the key is given, or supplied +//! through the `_nonce` forms, which put its uniqueness on the caller. //! //! The tag length is a const generic `TAG_LEN`, checked at compile time to lie in `12..=16` bytes //! (96, 104, 112, 120 or 128 bits -- Sec 5.2.1.2's five recommended values). The 32- and 64-bit tags @@ -15,100 +14,131 @@ //! //! # Usage Examples //! -//! [`Gcm`] is used through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], with -//! `FINAL_LEN = TAG_LEN`, and through the [`SymmetricCipherEncryptor`] / -//! [`SymmetricCipherDecryptor`] traits they extend: +//! [`Gcm`] is the worker a mode-bound key hands out -- [`GCM`] on a key type such as +//! `bouncycastle_aes::AES128Key`, whose AEAD role traits carry the one-shots -- and it +//! implements [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] with `FINAL_LEN = TAG_LEN`, and the +//! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] traits they extend. The key holds a +//! [`GcmKeyState`] and starts each message from it, which is the route over the toy permutation. //! -//! Used through the SymmetricCipher traits, there is no option to include additional associated data (aad), -//! and the tag is inlined into the ciphertext as `ciphertext || tag`. +//! Through the SymmetricCipher methods there is no additional associated data (aad), and the tag +//! is inlined into the ciphertext as `ciphertext || tag`: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; +//! use bouncycastle_core::errors::SymmetricCipherError; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_core::errors::SymmetricCipherError; -//! use bouncycastle_cipher::modes::Gcm; +//! use bouncycastle_cipher::modes::{Gcm, GcmKeyState}; //! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! //! type ToyGcm = Gcm; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let aad = b"header, sent in the clear"; +//! let state = GcmKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); //! let plaintext: [u8; 16] = *b"attack at dawn!!"; //! -//! let (nonce, ciphertext) = ToyGcm::::encrypt(&key, &plaintext).expect("encrypt"); +//! let (mut enc, nonce) = ToyGcm::::from_key_state(&state, &mut HashDRBG_SHA512::new_from_os()) +//! .expect("nonce draw"); +//! let mut ciphertext = vec![0u8; 16 + 16]; +//! let n = enc.do_encrypt_out(&plaintext, &mut ciphertext).expect("encrypt"); +//! let (tag, tag_len) = enc.do_encrypt_final().expect("the tag"); +//! ciphertext[n..].copy_from_slice(&tag[..tag_len]); //! -//! let mut recovered = ToyGcm::::decrypt(&key, &nonce, &ciphertext).expect("decrypt"); -//! assert_eq!(recovered, plaintext); +//! let open = |ct: &[u8]| -> Result, SymmetricCipherError> { +//! let mut dec = ToyGcm::::from_key_state(&state, &nonce); +//! let mut pt = vec![0u8; ct.len()]; +//! let n = dec.do_decrypt_out(ct, &mut pt)?; +//! let (last, last_len) = dec.do_decrypt_final()?; +//! pt.truncate(n); +//! pt.extend_from_slice(&last[..last_len]); +//! Ok(pt) +//! }; +//! assert_eq!(open(&ciphertext).expect("decrypt"), plaintext); //! //! // A tampered ciphertext will be caught by the tag //! let mut tampered_ct = ciphertext.clone(); //! tampered_ct[1] ^= 0xFF; -//! match ToyGcm::::decrypt(&key, &nonce, &tampered_ct).unwrap_err() { -//! SymmetricCipherError::AEADTagCheckFailed => { /* good */ } -//! _ => { panic!() } -//! } +//! assert_eq!(open(&tampered_ct).unwrap_err(), SymmetricCipherError::AEADTagCheckFailed); //! ``` //! -//! The AEADCipher traits provide the AEAD-specific functionality, including accepting the aad, and -//! the `_detached()` methods handle the tag separately, instead of inlined into the ciphertext. +//! The AEADCipher methods provide the AEAD-specific functionality, including accepting the aad, +//! and the `_detached` finals handle the tag separately, instead of inlined into the ciphertext. +//! The aad must be supplied before any plaintext or ciphertext; attempting to call +//! `do_update_aad()` after a `do_encrypt()` will result in a [`SymmetricCipherError::StateError`]. //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; -//! use bouncycastle_cipher::modes::Gcm; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{ +//! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +//! }; +//! use bouncycastle_cipher::modes::{Gcm, GcmKeyState}; //! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! //! type ToyGcm = Gcm; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let aad = b"header, sent in the clear"; -//! let plaintext = *b"attack at dawn!!"; +//! let state = GcmKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); +//! let aad = b"some associated data"; +//! let message = b"a message that streams in over more than one call"; //! -//! let mut ciphertext = [0u8; 16]; -//! let (nonce, _bytes_written, tag) = -//! ToyGcm::::encrypt_detached_out(&key, aad, &plaintext, &mut ciphertext).unwrap(); +//! let (mut enc, nonce) = +//! ToyGcm::::from_key_state(&state, &mut HashDRBG_SHA512::new_from_os()).unwrap(); +//! enc.do_update_aad(aad).unwrap(); +//! let mut ct = vec![0u8; message.len()]; +//! enc.do_encrypt_out(message, &mut ct).unwrap(); +//! let (_, tag) = enc.do_encrypt_final_detached_out(&mut [0u8; 16]).unwrap(); //! -//! let mut recovered = [0u8; 16]; -//! ToyGcm::::decrypt_detached_out(&key, &nonce, aad, &ciphertext, &tag, &mut recovered) -//! .unwrap(); -//! assert_eq!(recovered, plaintext); +//! let mut dec = ToyGcm::::from_key_state(&state, &nonce); +//! dec.do_update_aad(aad).unwrap(); +//! let mut pt = vec![0u8; ct.len()]; +//! let written = dec.do_decrypt_out(&ct, &mut pt).unwrap(); +//! let mut last = [0u8; 16]; +//! let last_len = dec.do_decrypt_final_detached_out(&tag, &mut last).unwrap(); +//! pt.truncate(written); +//! pt.extend_from_slice(&last[..last_len]); +//! assert_eq!(pt, message); //! ``` //! -//! There is also a streaming mode. -//! Note that the aad must be supplied before any plaintext or ciphertext; attempting to call -//! `do_update_aad()` after a `do_encrypt()` will result in a [`SymmetricCipherError::StateError`]. +//! The same with an inline tag: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{ //! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, //! }; -//! use bouncycastle_cipher::modes::Gcm; +//! use bouncycastle_cipher::modes::{Gcm, GcmKeyState}; //! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! //! type ToyGcm = Gcm; //! //! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let state = GcmKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); //! let aad = b"some associated data"; //! let message = b"a message that streams in over more than one call"; //! -//! let (mut enc, nonce) = ToyGcm::::do_encrypt_init(&key).unwrap(); +//! let (mut enc, nonce) = +//! ToyGcm::::from_key_state(&state, &mut HashDRBG_SHA512::new_from_os()).unwrap(); //! enc.do_update_aad(aad).unwrap(); //! let mut ct = vec![0u8; message.len()]; //! enc.do_encrypt_out(message, &mut ct).unwrap(); //! let (tag_block, tag_len) = enc.do_encrypt_final().unwrap(); //! ct.extend_from_slice(&tag_block[..tag_len]); //! -//! let mut dec = ToyGcm::::do_decrypt_init(&key, &nonce).unwrap(); +//! let mut dec = ToyGcm::::from_key_state(&state, &nonce); //! dec.do_update_aad(aad).unwrap(); //! let mut pt = vec![0u8; ct.len()]; //! let written = dec.do_decrypt_out(&ct, &mut pt).unwrap(); diff --git a/crypto/cipher/src/modes/hazmat/ecb.rs b/crypto/cipher/src/modes/hazmat/ecb.rs index f2b1c03b3..a2875253e 100644 --- a/crypto/cipher/src/modes/hazmat/ecb.rs +++ b/crypto/cipher/src/modes/hazmat/ecb.rs @@ -10,29 +10,30 @@ //! //! # Usage Examples //! -//! ECB has the same shape with no IV: `encrypt` returns an empty array and `decrypt` takes one. -//! The codebook property that makes it unsuitable for data is visible in the ciphertext: +//! ECB has the same shape with no IV and no RNG: a key bound to [`ECB`] (the `hazmat` +//! role pair) hands out its workers infallibly, and [`ECB`](ECB) puts the padded form on the +//! symmetric pair with an empty init array. The codebook property that makes it unsuitable for +//! data is visible in the ciphertext: //! //! ``` //! # mod toy { include!("../../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; +//! use bouncycastle_core::hazmat::{EcbDecryptMode, EcbEncryptMode, ElectronicCodeBook, Mode}; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; -//! use bouncycastle_cipher::modes::hazmat::ecb::Ecb; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! type ToyEcb = Ecb; +//! use bouncycastle_cipher::modes::hazmat::ecb::ECB; +//! use bouncycastle_cipher::modes::Blocks; //! //! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let mut data = [0x5Au8; 32]; // two equal blocks +//! let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); +//! let mut data = [[0x5Au8; 16]; 2]; // two equal blocks //! -//! let (bytes_written, no_iv): (usize, [u8; 0]) = ToyEcb::::encrypt_inplace(&key, &mut data).expect("encryption"); -//! assert_eq!(no_iv.len(), 0, "ECB mode returns the IV as an empty array"); -//! assert_eq!(data[..16], data[16..], "equal plaintext blocks give equal ciphertext blocks"); +//! ECB::::encrypt_init(&state).do_encrypt_blocks_inplace(&mut data).expect("encryption"); +//! assert_eq!(data[0], data[1], "equal plaintext blocks give equal ciphertext blocks"); //! -//! ToyEcb::::decrypt_inplace(&key, &[], &mut data).expect("decryption"); -//! assert_eq!(data, [0x5Au8; 32]); +//! ECB::::decrypt_init(&state).do_decrypt_blocks_inplace(&mut data).expect("decryption"); +//! assert_eq!(data, [[0x5Au8; 16]; 2]); //! ``` //! //! # A mode with no state diff --git a/crypto/core-test-framework/src/block_cipher.rs b/crypto/core-test-framework/src/block_cipher.rs index b1e3b4765..7eb83ac4f 100644 --- a/crypto/core-test-framework/src/block_cipher.rs +++ b/crypto/core-test-framework/src/block_cipher.rs @@ -5,7 +5,7 @@ use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey}; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey}; use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, RNG}; From 3f4a7c6742439fd8d282a5ebce32fc442bd43c5e Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 23:34:32 +1100 Subject: [PATCH 07/54] ascon: AsconAEAD128Key (prototype) The 128-bit key validated once, on FromKeyMaterial, with the AEAD role pair handing out Ascon_AEAD128 / per message; the cipher's constructor splits into the key check and the per-message init so both routes share it. The workers get the temporary worker-trait forwarding the role traits are bounded on. Tests, the bench and the memory harness move onto the key, the key gets the construction suite, and the crate docs show the key. The stack-usage rows for the init and one-shot entry points are withdrawn until they are re-measured on the key path. Assisted-by: Claude:claude-mythos-5-1 --- crypto/ascon/benches/ascon_benches.rs | 20 +- crypto/ascon/src/ascon_aead128.rs | 216 ++++++++++++++++-- crypto/ascon/src/lib.rs | 96 ++++---- crypto/ascon/tests/aead128_tests.rs | 162 ++++++------- crypto/ascon/tests/ascon_bc-test-data.rs | 25 +- crypto/ascon/tests/ascon_wycheproof.rs | 24 +- .../src/bench_ascon_mem_usage.rs | 88 ++++--- 7 files changed, 406 insertions(+), 225 deletions(-) diff --git a/crypto/ascon/benches/ascon_benches.rs b/crypto/ascon/benches/ascon_benches.rs index 07aad22cf..6eefa6b2c 100644 --- a/crypto/ascon/benches/ascon_benches.rs +++ b/crypto/ascon/benches/ascon_benches.rs @@ -2,13 +2,13 @@ use bouncycastle_rng as rng; use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; -use bouncycastle_ascon::Ascon_AEAD128; +use bouncycastle_ascon::AsconAEAD128Key; use bouncycastle_ascon::AsconCXof128; use bouncycastle_ascon::AsconHash256; use bouncycastle_ascon::AsconXof128; -use bouncycastle_cipher::Encrypting; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{AEADCipherEncryptor, Hash, RNG, XOF}; +use bouncycastle_core::symmetric_key::{AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_core::traits::{Hash, RNG, XOF}; const DATA_LEN: usize = 16 * 1024; @@ -19,8 +19,9 @@ fn random_data(len: usize) -> Vec { } fn bench_aead128_encrypt(c: &mut Criterion) { - let key = + let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); + let key = AsconAEAD128Key::from_key_material(&material).unwrap(); let nonce = [0x24u8; 16]; let data = random_data(DATA_LEN); let mut out = vec![0u8; DATA_LEN + 16]; @@ -28,16 +29,9 @@ fn bench_aead128_encrypt(c: &mut Criterion) { let mut group = c.benchmark_group("ascon::Ascon_AEAD128"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); - group.bench_function(format!("{DATA_LEN} bytes -- ::encrypt()"), |b| { + group.bench_function(format!("{DATA_LEN} bytes -- encrypt_with_aad_nonce_out"), |b| { b.iter(|| { - Ascon_AEAD128::::encrypt_with_aad_nonce_out( - &key, - &nonce, - &[], - black_box(&data), - &mut out, - ) - .unwrap(); + key.encrypt_with_aad_nonce_out(&nonce, &[], black_box(&data), &mut out).unwrap(); black_box(&out); }) }); diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 2e45c78ef..50998b1ea 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -22,6 +22,7 @@ use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SuspendableError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, @@ -81,35 +82,43 @@ impl Phase { /// so the wrong direction is a compile error. One instance performs one operation under one /// (key, nonce) pair. /// -/// Both directions implement [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] and, through them, -/// [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] -- which is the AEAD with no -/// associated data and the tag inline: +/// Both directions are made by [`AsconAEAD128Key`], and implement [`AEADCipherEncryptor`] / +/// [`AEADCipherDecryptor`] and, through them, [`SymmetricCipherEncryptor`] / +/// [`SymmetricCipherDecryptor`] -- which is the AEAD with no associated data and the tag inline: /// /// ``` -/// use bouncycastle_ascon::Ascon_AEAD128; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_ascon::{Ascon_AEAD128, AsconAEAD128Key}; /// use bouncycastle_cipher::{Decrypting, Encrypting}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; /// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +/// use bouncycastle_rng::HashDRBG_SHA512; /// /// type Enc = Ascon_AEAD128; /// type Dec = Ascon_AEAD128; /// -/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) /// .expect("a 16-byte symmetric cipher key"); +/// let key = AsconAEAD128Key::from_key_material(&material).expect("a valid key"); /// /// let message = b"hello"; +/// let (mut enc, nonce) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); /// let mut ciphertext = [0u8; 5 + 16]; // Enc::encrypt_out_len(5): ciphertext || tag -/// let (nonce, written) = Enc::encrypt_out(&key, message, &mut ciphertext).expect("encryption"); -/// assert_eq!(written, 21); +/// let written = enc.do_encrypt_out(message, &mut ciphertext).expect("encryption"); +/// let (tag, tag_len) = enc.do_encrypt_final().expect("the tag"); +/// ciphertext[written..].copy_from_slice(&tag[..tag_len]); /// +/// let mut dec = key.create_decryptor(&nonce); /// let mut plaintext = [0u8; 5]; // Dec::decrypt_out_len(21) -/// let n = Dec::decrypt_out(&key, &nonce, &ciphertext, &mut plaintext).expect("decryption"); +/// let n = dec.do_decrypt_out(&ciphertext, &mut plaintext).expect("decryption"); +/// dec.do_decrypt_final().expect("the tag verifies"); /// assert_eq!(&plaintext[..n], message); /// ``` /// /// The encrypting side implements [`AEADCipherEncryptor`] and [`SymmetricCipherEncryptor`]; the -/// nonce is drawn by `do_encrypt_init` and returned, or supplied to `do_encrypt_init_nonce` by a -/// caller who takes on its uniqueness. `FINAL_LEN` is `TAG_LEN`: nothing is held back, so the +/// nonce is drawn by the key's `create_encryptor` and returned, or supplied to +/// `create_encryptor_nonce` by a caller who takes on its uniqueness. `FINAL_LEN` is `TAG_LEN`: +/// nothing is held back, so the /// inline [`SymmetricCipherEncryptor::do_encrypt_final`] writes only the tag, and the detached /// [`AEADCipherEncryptor::do_encrypt_final_detached_out`] only zeroes its buffer. /// @@ -121,12 +130,13 @@ impl Phase { /// so they need no [`Secret`] wrapper; the encrypting side carries the same fields unused. /// /// ```compile_fail -/// # use bouncycastle_ascon::Ascon_AEAD128; -/// # use bouncycastle_cipher::Decrypting; +/// # use bouncycastle_ascon::AsconAEAD128Key; /// # use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// # use bouncycastle_core::symmetric_key::{AeadDecryptionKey, FromKeyMaterial}; /// # use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// # let key = KeyMaterial::<16>::from_bytes_as_type(&[7; 16], KeyType::SymmetricCipherKey).unwrap(); -/// let mut dec = Ascon_AEAD128::::do_decrypt_init(&key, &[0u8; 16]).unwrap(); +/// # let material = KeyMaterial::<16>::from_bytes_as_type(&[7; 16], KeyType::SymmetricCipherKey).unwrap(); +/// # let key = AsconAEAD128Key::from_key_material(&material).unwrap(); +/// let mut dec = key.create_decryptor(&[0u8; 16]); /// dec.do_encrypt_out(&[0u8; 4], &mut [0u8; 4]); // no such method on the decrypting type /// ``` #[allow(non_camel_case_types)] @@ -207,9 +217,13 @@ impl Ascon_AEAD128 { key: &KeyMaterial, nonce: &[u8; NONCE_LEN], ) -> Result { - let key_words = Self::checked_key(key)?; + Ok(Self::from_key_words(&Self::checked_key(key)?, nonce)) + } + + /// Step 1 from already-validated key words: what [`AsconAEAD128Key`] calls per message. + fn from_key_words(key_words: &[u64; 2], nonce: &[u8; NONCE_LEN]) -> Self { let mut key_secret: Secret<[u64; 2]> = Secret::new(); - *key_secret = key_words; + *key_secret = *key_words; let mut state: Secret = Secret::new(); // Initialization (SP 800-232 §4.1.1 step 1 / Eq. 15-17): S = IV||K||N, then Ascon-p[12], @@ -223,7 +237,7 @@ impl Ascon_AEAD128 { state[3] ^= key_words[0]; state[4] ^= key_words[1]; - Ok(Ascon_AEAD128 { + Ascon_AEAD128 { key: key_secret, state, pos: 0, @@ -231,7 +245,7 @@ impl Ascon_AEAD128 { held: [0u8; TAG_LEN], held_len: 0, _dir: PhantomData, - }) + } } /// Read the value of state byte `pos` (0 = LSB of word 0, ..., 15 = MSB of word 1). @@ -645,6 +659,170 @@ impl Ascon_AEAD128 { } } +/// An Ascon-AEAD128 key: the 128-bit key validated once, from which the encryptors and +/// decryptors for each message are made. The key words live in a [`Secret`], zeroized on drop; +/// there is no `Debug` and no way to read them back. +/// +/// ``` +/// use bouncycastle_ascon::AsconAEAD128Key; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let mut rng = HashDRBG_SHA512::new_from_os(); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// let key = AsconAEAD128Key::from_key_material(&material).expect("a valid Ascon-AEAD128 key"); +/// +/// // The nonce is drawn from `rng` and returned with the ciphertext; the tag is detached. +/// let (nonce, ciphertext, tag) = key.encrypt_detached(&mut rng, b"header", b"attack at dawn").unwrap(); +/// let plaintext = key.decrypt_detached(&nonce, b"header", &ciphertext, &tag).unwrap(); +/// assert_eq!(plaintext, b"attack at dawn"); +/// ``` +#[allow(non_camel_case_types)] +pub struct AsconAEAD128Key { + key: Secret<[u64; 2]>, +} + +impl Clone for AsconAEAD128Key { + fn clone(&self) -> Self { + Self { key: self.key.clone() } + } +} + +impl FromKeyMaterial for AsconAEAD128Key { + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not a `SymmetricCipherKey`, or is + /// bound to another algorithm (`KeyMaterialTrait::set_algorithm`). + /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. + fn from_key_material(key: &KeyMaterial) -> Result { + let mut key_secret: Secret<[u64; 2]> = Secret::new(); + *key_secret = Ascon_AEAD128::::checked_key(key)?; + Ok(Self { key: key_secret }) + } +} + +impl AeadEncryptionKey for AsconAEAD128Key { + type Encryptor = Ascon_AEAD128; + + /// # Errors + /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. + fn create_encryptor( + &self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + let mut nonce = [0u8; NONCE_LEN]; + rng.next_bytes_out(&mut nonce)?; + Ok((Ascon_AEAD128::from_key_words(&self.key, &nonce), nonce)) + } + + /// Under a nonce the caller supplies, who takes on the obligation that it never repeat + /// under this key (SP 800-232 R3). + fn create_encryptor_nonce(&self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor { + Ascon_AEAD128::from_key_words(&self.key, nonce) + } +} + +impl AeadDecryptionKey for AsconAEAD128Key { + type Decryptor = Ascon_AEAD128; + + fn create_decryptor(&self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor { + Ascon_AEAD128::from_key_words(&self.key, nonce) + } +} + +// The worker traits the key's role traits are bounded on, forwarded to the cipher traits by +// path; they fold into those traits once the trait constructors go. +impl bouncycastle_core::symmetric_key::AeadEncryptWorker + for Ascon_AEAD128 +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + AEADCipherEncryptor::do_update_aad(self, aad) + } + + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + SymmetricCipherEncryptor::do_encrypt_out_len(self, input_len) + } + + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + SymmetricCipherEncryptor::do_encrypt_out(self, plaintext, ciphertext) + } + + fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + SymmetricCipherEncryptor::do_encrypt_final(self) + } + + fn do_encrypt_final_detached_out( + self, + ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + AEADCipherEncryptor::do_encrypt_final_detached_out(self, ciphertext) + } + + fn encrypt_out_len(plaintext_len: usize) -> usize { + >::encrypt_out_len( + plaintext_len, + ) + } + + fn encrypt_detached_out_len(plaintext_len: usize) -> usize { + >::encrypt_detached_out_len( + plaintext_len, + ) + } +} + +impl bouncycastle_core::symmetric_key::AeadDecryptWorker + for Ascon_AEAD128 +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + AEADCipherDecryptor::do_update_aad(self, aad) + } + + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + SymmetricCipherDecryptor::do_decrypt_out_len(self, input_len) + } + + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + SymmetricCipherDecryptor::do_decrypt_out(self, ciphertext, plaintext) + } + + fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + SymmetricCipherDecryptor::do_decrypt_final(self) + } + + fn do_decrypt_final_detached_out( + self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + AEADCipherDecryptor::do_decrypt_final_detached_out(self, tag, plaintext) + } + + fn decrypt_out_len(ciphertext_len: usize) -> usize { + >::decrypt_out_len( + ciphertext_len, + ) + } + + fn decrypt_detached_out_len(ciphertext_len: usize) -> usize { + >::decrypt_detached_out_len( + ciphertext_len, + ) + } +} + impl Debug for Ascon_AEAD128 { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "Ascon-AEAD128 (key/state masked)") diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs index e8fbfa5c5..014c044e7 100644 --- a/crypto/ascon/src/lib.rs +++ b/crypto/ascon/src/lib.rs @@ -17,7 +17,7 @@ //! (August 2025), which can be found here: //! //! - [`Ascon-AEAD128`](Ascon_AEAD128) — authenticated encryption (128-bit key/nonce/tag, 128-bit -//! single-key security). +//! single-key security), used through [`AsconAEAD128Key`]. //! - [`Ascon-Hash256`](AsconHash256) — hash function (256-bit digest, 128-bit security). //! - [`Ascon-XOF128`](AsconXof128) — extendable-output function. //! - [`Ascon-CXOF128`](AsconCXof128) — customized extendable-output function. @@ -43,50 +43,53 @@ //! assert_eq!(&out[..], &digest[..]); //! ``` //! -//! Authenticated encryption (one-shot, under a nonce the caller manages): +//! Authenticated encryption (one-shot, under a nonce the caller manages), through the key: //! ``` -//! use bouncycastle_ascon::Ascon_AEAD128; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_ascon::AsconAEAD128Key; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; //! -//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = AsconAEAD128Key::from_key_material(&material).unwrap(); //! let nonce = [1u8; 16]; // MUST be unique per encryption under a given key //! let ad = b"associated data"; //! let plaintext = b"secret message"; //! //! let mut ct = vec![0u8; plaintext.len() + 16]; // ciphertext || 16-byte tag -//! let n = Ascon_AEAD128::::encrypt_with_aad_nonce_out(&key, &nonce, ad, plaintext, &mut ct).unwrap(); +//! let n = key.encrypt_with_aad_nonce_out(&nonce, ad, plaintext, &mut ct).unwrap(); //! ct.truncate(n); //! //! let mut pt = vec![0u8; ct.len() - 16]; -//! let m = Ascon_AEAD128::::decrypt_with_aad_out(&key, &nonce, ad, &ct, &mut pt).unwrap(); +//! let m = key.decrypt_with_aad_out(&nonce, ad, &ct, &mut pt).unwrap(); //! pt.truncate(m); //! assert_eq!(&pt, plaintext); //! ``` //! -//! Authenticated encryption (streaming, detached tag). The decryptor holds back the last 16 -//! bytes it has seen, in case they are an inline tag, so `do_decrypt_final_detached_out` is -//! where they come out: +//! Authenticated encryption (streaming, detached tag), with the nonce drawn from the caller's +//! RNG. The decryptor holds back the last 16 bytes it has seen, in case they are an inline tag, +//! so `do_decrypt_final_detached_out` is where they come out: //! ``` -//! use bouncycastle_ascon::Ascon_AEAD128; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_ascon::AsconAEAD128Key; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; //! use bouncycastle_core::traits::{ //! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, //! }; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = AsconAEAD128Key::from_key_material(&material).unwrap(); //! //! let plaintext = b"secret message!!"; -//! let (mut enc, nonce) = Ascon_AEAD128::::do_encrypt_init(&key).unwrap(); +//! let (mut enc, nonce) = key.create_encryptor(&mut rng).unwrap(); //! enc.do_update_aad(b"associated data").unwrap(); //! let mut ciphertext = [0u8; 16]; //! enc.do_encrypt_out(plaintext, &mut ciphertext).unwrap(); //! let mut final_buf = [0u8; 16]; //! let (_, tag) = enc.do_encrypt_final_detached_out(&mut final_buf).unwrap(); //! -//! let mut dec = Ascon_AEAD128::::do_decrypt_init(&key, &nonce).unwrap(); +//! let mut dec = key.create_decryptor(&nonce); //! dec.do_update_aad(b"associated data").unwrap(); //! let mut recovered = [0u8; 16]; //! let n = dec.do_decrypt_out(&ciphertext, &mut recovered).unwrap(); // 0: all 16 held back @@ -95,34 +98,27 @@ //! assert_eq!(&recovered, plaintext); //! ``` //! -//! For the inline `ciphertext || tag` layout that most wire formats and files use, the pair is -//! also a [`bouncycastle_core::traits::SymmetricCipherEncryptor`] / -//! [`bouncycastle_core::traits::SymmetricCipherDecryptor`], which covers the no-AAD case -- -//! streaming, or through its `encrypt_out` / `decrypt_out` one-shots -- and -//! [`bouncycastle_core::traits::AEADCipherEncryptor::encrypt_with_aad_out`] / -//! [`bouncycastle_core::traits::AEADCipherDecryptor::decrypt_with_aad_out`] are the one-shots with AAD: +//! For the inline `ciphertext || tag` layout that most wire formats and files use, the key's +//! `encrypt_with_aad_out` / `decrypt_with_aad_out` are the one-shots, with an empty AAD when +//! there is none, and the workers are also a +//! [`bouncycastle_core::traits::SymmetricCipherEncryptor`] / +//! [`bouncycastle_core::traits::SymmetricCipherDecryptor`] pair for streaming it: //! ``` -//! use bouncycastle_ascon::Ascon_AEAD128; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_ascon::AsconAEAD128Key; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::{ -//! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, -//! }; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = AsconAEAD128Key::from_key_material(&material).unwrap(); //! let plaintext = b"secret message!!"; //! -//! // No AAD: just a symmetric cipher. -//! let mut inline = [0u8; 32]; // Ascon_AEAD128::::encrypt_out_len(16) -//! let (nonce, len) = Ascon_AEAD128::::encrypt_out(&key, plaintext, &mut inline).unwrap(); +//! let mut inline = [0u8; 32]; // plaintext.len() + 16 +//! let (len, nonce) = key.encrypt_with_aad_out(&mut rng, b"aad", plaintext, &mut inline).unwrap(); //! assert_eq!(len, plaintext.len() + 16); // ciphertext || tag //! let mut recovered = [0u8; 16]; -//! let n = Ascon_AEAD128::::decrypt_out(&key, &nonce, &inline[..len], &mut recovered).unwrap(); -//! assert_eq!(&recovered[..n], plaintext); -//! -//! // With AAD. -//! let (nonce, len) = Ascon_AEAD128::::encrypt_with_aad_out(&key, b"aad", plaintext, &mut inline).unwrap(); -//! let n = Ascon_AEAD128::::decrypt_with_aad_out(&key, &nonce, b"aad", &inline[..len], &mut recovered).unwrap(); +//! let n = key.decrypt_with_aad_out(&nonce, b"aad", &inline[..len], &mut recovered).unwrap(); //! assert_eq!(&recovered[..n], plaintext); //! ``` //! @@ -144,6 +140,7 @@ //! //! | Type | In-memory size (bytes) | Suspended state size (bytes) | //! |------|-------------------------|-------------------------------| +//! | [`AsconAEAD128Key`] | 16 | | //! | [`Ascon_AEAD128`] | 96 | [`SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN`] (46) | //! | [`Ascon_AEAD128`] | 96 | [`SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN`] (63) | //! | [`AsconHash256`] | 64 | [`SUSPENDED_ASCON_HASH256_STATE_LEN`] (53) | @@ -160,15 +157,18 @@ //! //! | Ascon-AEAD128 entry point | Encrypting | Decrypting | //! |---|---|---| -//! | `do_encrypt_init_rng` / `do_decrypt_init` | 536 B | 504 B | +//! | `AsconAEAD128Key::from_key_material` (the key check, once per key) | - | - | +//! | `create_encryptor` / `create_decryptor` | - | - | //! | `do_update_aad` | 104 B | 104 B | //! | `do_encrypt_out` / `do_decrypt_out` | 176 B | 208 B | //! | `do_encrypt_final` / `do_decrypt_final` (tag inline) | 176 B | 208 B | //! | `do_encrypt_final_detached_out` / `do_decrypt_final_detached_out` | 176 B | 272 B | -//! | `encrypt_with_aad_rng_out` / `decrypt_with_aad_out` (one-shot) | 648 B | 888 B | +//! | `encrypt_with_aad_out` / `decrypt_with_aad_out` (one-shot) | - | - | //! -//! The encrypting figures do not include the caller's RNG; `do_encrypt_init` and the one-shots -//! without `_rng` draw the nonce from the library's default DRBG, whose stack is not counted here. +//! The key path's rows have not been measured yet: the key check moved out of the per-message +//! init into `from_key_material`, and the one-shots are now the role traits' provided bodies, +//! so the old figures for those entry points no longer apply. `bench_ascon_mem_usage` has the +//! harness. The encrypting figures do not include the caller's RNG. //! //! | Sponge entry point | Ascon-Hash256 | Ascon-XOF128 | Ascon-CXOF128 | //! |---|---|---|---| @@ -191,11 +191,10 @@ //! caller that needs a partial-byte final block should reach for SHA-3, which supports one. //! - **Decryption tag check failure:** a ciphertext decryption whose finalization returns //! `Err(SymmetricCipherError::AEADTagCheckFailed)` must be treated as tampered, and the entire -//! plaintext rejected. The one-shot APIs -//! ([`bouncycastle_core::traits::AEADCipherDecryptor::decrypt_detached_out`], -//! [`bouncycastle_core::traits::AEADCipherDecryptor::decrypt_with_aad_out`] and -//! [`bouncycastle_core::traits::SymmetricCipherDecryptor::decrypt_out`]) zeroize their output -//! buffer before returning that error. The streaming API (`do_decrypt_out` followed by +//! plaintext rejected. The key's one-shots +//! ([`bouncycastle_core::symmetric_key::AeadDecryptionKey::decrypt_detached_out`] and +//! [`bouncycastle_core::symmetric_key::AeadDecryptionKey::decrypt_with_aad_out`]) zeroize +//! their output buffer before returning that error. The streaming API (`do_decrypt_out` followed by //! [`bouncycastle_core::traits::AEADCipherDecryptor::do_decrypt_final_detached_out`] or //! [`bouncycastle_core::traits::SymmetricCipherDecryptor::do_decrypt_final`]) does not: plaintext //! bytes are necessarily written to the caller's buffer *before* the tag can be checked, so an @@ -217,8 +216,9 @@ mod ascon_hash256; mod ascon_xof128; pub use ascon_aead128::{ - Ascon_AEAD128, KEY_LEN, NONCE_LEN, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, - SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, TAG_LEN, + Ascon_AEAD128, AsconAEAD128Key, KEY_LEN, NONCE_LEN, + SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, + TAG_LEN, }; pub use ascon_cxof128::{AsconCXof128, AsconCXof128Squeezer, SUSPENDED_ASCON_CXOF128_STATE_LEN}; pub use ascon_hash256::{AsconHash256, SUSPENDED_ASCON_HASH256_STATE_LEN}; diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs index 1fbb42d1e..6da33208f 100644 --- a/crypto/ascon/tests/aead128_tests.rs +++ b/crypto/ascon/tests/aead128_tests.rs @@ -9,17 +9,20 @@ //! the detached-tag (`*_detached`) and the inline `ciphertext || tag` layouts -- the latter also //! through the `SymmetricCipherEncryptor`/`SymmetricCipherDecryptor` traits they extend. -use bouncycastle_ascon::Ascon_AEAD128; +use bouncycastle_ascon::{ASCON_AEAD128_NAME, Ascon_AEAD128, AsconAEAD128Key}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::aead::TestFrameworkAEADCipher; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_hex as hex; +use bouncycastle_rng::HashDRBG_SHA512; // All embedded vectors use this fixed key/nonce (the NIST LWC KAT convention). const KEY: [u8; 16] = [ @@ -82,11 +85,14 @@ fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { km } +fn aead_key(key: &[u8; 16]) -> AsconAEAD128Key { + AsconAEAD128Key::from_key_material(&key_material(key)).expect("a valid key") +} + fn enc_oneshot(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8]) -> Vec { - let km = key_material(key); + let km = aead_key(key); let mut out = vec![0u8; Ascon_AEAD128::::encrypt_out_len(pt.len())]; - let n = Ascon_AEAD128::::encrypt_with_aad_nonce_out(&km, nonce, ad, pt, &mut out) - .unwrap(); + let n = km.encrypt_with_aad_nonce_out(nonce, ad, pt, &mut out).unwrap(); out.truncate(n); out } @@ -97,9 +103,9 @@ fn dec_oneshot( ad: &[u8], ct: &[u8], ) -> Result, SymmetricCipherError> { - let km = key_material(key); + let km = aead_key(key); let mut out = vec![0u8; Ascon_AEAD128::::decrypt_out_len(ct.len())]; - let n = Ascon_AEAD128::::decrypt_with_aad_out(&km, nonce, ad, ct, &mut out)?; + let n = km.decrypt_with_aad_out(nonce, ad, ct, &mut out)?; out.truncate(n); Ok(out) } @@ -107,8 +113,8 @@ fn dec_oneshot( /// Streams `pt` through an encryptor in `chunk`-byte pieces; inline layout, so the tag is the /// final's output. fn enc_chunked(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8], chunk: usize) -> Vec { - let km = key_material(key); - let mut enc = Ascon_AEAD128::::do_encrypt_init_nonce(&km, nonce).unwrap(); + let km = aead_key(key); + let mut enc = km.create_encryptor_nonce(nonce); enc.do_update_aad(ad).unwrap(); let mut out = Vec::with_capacity(pt.len() + 16); for piece in pt.chunks(chunk.max(1)) { @@ -130,8 +136,8 @@ fn dec_chunked( ct: &[u8], chunk: usize, ) -> Result, SymmetricCipherError> { - let km = key_material(key); - let mut dec = Ascon_AEAD128::::do_decrypt_init(&km, nonce)?; + let km = aead_key(key); + let mut dec = km.create_decryptor(nonce); dec.do_update_aad(ad)?; let mut out = Vec::with_capacity(ct.len()); for piece in ct.chunks(chunk.max(1)) { @@ -220,10 +226,10 @@ fn aead_chunked_aad_matches_one_shot() { let pt = pattern(30); let ad = pattern(40); let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); - let km = key_material(&KEY); + let km = aead_key(&KEY); for &chunk in CHUNK_SIZES.iter() { - let mut e = Ascon_AEAD128::::do_encrypt_init_nonce(&km, &NONCE).unwrap(); + let mut e = km.create_encryptor_nonce(&NONCE); for piece in ad.chunks(chunk) { e.do_update_aad(piece).unwrap(); } @@ -240,14 +246,14 @@ fn aead_chunked_aad_matches_one_shot() { /// each buffer and holds the newest 16 bytes back, so the inline final finds the tag. #[test] fn inplace_streaming_matches_one_shot() { - let km = key_material(&KEY); + let km = aead_key(&KEY); for &pt_len in PT_SIZES.iter() { let pt = pattern(pt_len); let ad = pattern(20); let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); for &chunk in CHUNK_SIZES.iter() { - let mut e = Ascon_AEAD128::::do_encrypt_init_nonce(&km, &NONCE).unwrap(); + let mut e = km.create_encryptor_nonce(&NONCE); e.do_update_aad(&ad).unwrap(); let mut buf = pt.clone(); for piece in buf.chunks_mut(chunk) { @@ -257,7 +263,7 @@ fn inplace_streaming_matches_one_shot() { buf.extend_from_slice(&last[..n]); assert_eq!(buf, ct_ref, "in-place encrypt mismatch (pt_len={pt_len}, chunk={chunk})"); - let mut d = Ascon_AEAD128::::do_decrypt_init(&km, &NONCE).unwrap(); + let mut d = km.create_decryptor(&NONCE); d.do_update_aad(&ad).unwrap(); let mut buf = ct_ref.clone(); let mut back = Vec::with_capacity(pt_len); @@ -270,7 +276,7 @@ fn inplace_streaming_matches_one_shot() { assert_eq!(back, pt, "in-place decrypt mismatch (pt_len={pt_len}, chunk={chunk})"); // The two decrypt forms interleave, since they share the hold-back. - let mut d = Ascon_AEAD128::::do_decrypt_init(&km, &NONCE).unwrap(); + let mut d = km.create_decryptor(&NONCE); d.do_update_aad(&ad).unwrap(); let mut back = Vec::with_capacity(pt_len); let mut buf = ct_ref.clone(); @@ -297,7 +303,7 @@ fn inplace_streaming_matches_one_shot() { #[test] fn aead_streaming_chunk_sweep() { - let km = key_material(&KEY); + let km = aead_key(&KEY); for pt_len in 0..=40 { let pt = pattern(pt_len); for ad_len in [0, 1, 15, 16, 17, 33] { @@ -309,8 +315,7 @@ fn aead_streaming_chunk_sweep() { for &chunk in [1, 2, 7, 15, 16, 17, 31, 32, 1024].iter() { // Detached layout: the body streams out chunk by chunk, the tag comes from the // final. - let mut e = - Ascon_AEAD128::::do_encrypt_init_nonce(&km, &NONCE).unwrap(); + let mut e = km.create_encryptor_nonce(&NONCE); e.do_update_aad(&ad).unwrap(); let mut out = Vec::with_capacity(pt_len); for piece in pt.chunks(chunk) { @@ -323,7 +328,7 @@ fn aead_streaming_chunk_sweep() { assert_eq!(out, ct_ref_body, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); assert_eq!(tag, tag_ref, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); - let mut d = Ascon_AEAD128::::do_decrypt_init(&km, &NONCE).unwrap(); + let mut d = km.create_decryptor(&NONCE); d.do_update_aad(&ad).unwrap(); let mut back = Vec::with_capacity(pt_len); for piece in ct_ref_body.chunks(chunk) { @@ -342,9 +347,9 @@ fn aead_streaming_chunk_sweep() { #[test] fn detached_final_rejects_wrong_tag() { - let km = key_material(&KEY); + let km = aead_key(&KEY); let pt = pattern(20); - let mut d = Ascon_AEAD128::::do_decrypt_init(&km, &NONCE).unwrap(); + let mut d = km.create_decryptor(&NONCE); let mut buf = vec![0u8; pt.len()]; d.do_decrypt_out(&pt, &mut buf).unwrap(); let wrong_tag = [0xFFu8; 16]; @@ -409,9 +414,9 @@ fn aead_tamper_leaves_no_plaintext_in_output_buffer() { let mut tampered = ct.clone(); tampered[0] ^= 0x01; - let km = key_material(&KEY); + let km = aead_key(&KEY); let mut out = vec![0xAAu8; pt.len()]; - let n = Ascon_AEAD128::::decrypt_with_aad_out(&km, &NONCE, ad, &tampered, &mut out); + let n = km.decrypt_with_aad_out(&NONCE, ad, &tampered, &mut out); assert!(matches!(n, Err(SymmetricCipherError::AEADTagCheckFailed))); assert!(out.iter().all(|&b| b == 0), "output buffer must be zeroized on tag failure"); } @@ -419,9 +424,9 @@ fn aead_tamper_leaves_no_plaintext_in_output_buffer() { #[test] fn aead_short_ciphertext_is_error() { let short = [0u8; 8]; // shorter than the 16-byte tag - let km = key_material(&KEY); + let km = aead_key(&KEY); let mut out = [0xEEu8; 16]; - match Ascon_AEAD128::::decrypt_with_aad_out(&km, &NONCE, &[], &short, &mut out) { + match km.decrypt_with_aad_out(&NONCE, &[], &short, &mut out) { Err(SymmetricCipherError::DecryptionFailed) => {} other => panic!("expected DecryptionFailed for short ciphertext, got {other:?}"), } @@ -448,11 +453,11 @@ fn aead_is_deterministic_and_nonce_sensitive() { #[test] fn aead_debug_display_are_masked() { - let km = key_material(&KEY); - let e = Ascon_AEAD128::::do_encrypt_init_nonce(&km, &NONCE).unwrap(); + let km = aead_key(&KEY); + let e = km.create_encryptor_nonce(&NONCE); assert!(format!("{e:?}").contains("masked")); assert!(format!("{e}").contains("masked")); - let d = Ascon_AEAD128::::do_decrypt_init(&km, &NONCE).unwrap(); + let d = km.create_decryptor(&NONCE); assert!(format!("{d:?}").contains("masked")); assert!(format!("{d}").contains("masked")); } @@ -461,17 +466,21 @@ fn aead_debug_display_are_masked() { /* Trait conformance (shared core-test-framework) */ /* -------------------------------------------------------------------------- */ -/// Exercises [`AEADCipherEncryptor`]/[`AEADCipherDecryptor`] on the streaming pair -/// [`Ascon_AEAD128`]/[`Ascon_AEAD128`]: `update_out_len` -/// correctness, chunking-independence of both AAD and data, the AAD-after-data `StateError`, and -/// tamper detection, all against the generic conformance suite rather than hand-written here. -/// -/// [`AEADCipherEncryptor`]: bouncycastle_core::traits::AEADCipherEncryptor -/// [`AEADCipherDecryptor`]: bouncycastle_core::traits::AEADCipherDecryptor +/// Exercises the AEAD role pair on [`AsconAEAD128Key`] and the streaming pair it hands out, +/// [`Ascon_AEAD128`]/[`Ascon_AEAD128`]: `update_out_len` correctness, +/// chunking-independence of both AAD and data, the AAD-after-data `StateError`, and tamper +/// detection, all against the generic conformance suite rather than hand-written here. #[test] -fn aead128_encryptor_decryptor_trait_framework() { +fn aead128_key_framework() { TestFrameworkAEADCipher::new() - .test_encryptor_decryptor::<16, 16, 16, 16, Ascon_AEAD128, Ascon_AEAD128>(); + .test_key::<16, 16, 16, 16, _>(&aead_key(&KEY), &mut HashDRBG_SHA512::new_from_os()); +} + +/// The key's construction suite: the type, binding and strength policy of `from_key_material`. +#[test] +fn aead128_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::<16, AsconAEAD128Key>(ASCON_AEAD128_NAME, SecurityStrength::_128bit); } /// The two tag layouts must agree byte for byte: `direct_ciphertext || direct_tag`, produced by @@ -483,18 +492,15 @@ fn aead128_encryptor_decryptor_trait_framework() { fn aead128_tagged_and_direct_layouts_agree() { use bouncycastle_core_test_framework::FixedSeedRNG; - let km = key_material(&KEY); + let km = aead_key(&KEY); let aad = b"tagged-layout-aad"; for pt_len in [0usize, 1, 15, 16, 17, 40] { let pt = pattern(pt_len); let pinned = [0x11u8; 16]; // detached tag, streamed - let (mut direct_enc, direct_nonce) = Ascon_AEAD128::::do_encrypt_init_rng( - &km, - &mut FixedSeedRNG::<16>::new(pinned), - ) - .unwrap(); + let (mut direct_enc, direct_nonce) = + km.create_encryptor(&mut FixedSeedRNG::<16>::new(pinned)).unwrap(); direct_enc.do_update_aad(aad).unwrap(); let mut direct_ct = vec![0u8; pt.len()]; direct_enc.do_encrypt_out(&pt, &mut direct_ct).unwrap(); @@ -505,11 +511,8 @@ fn aead128_tagged_and_direct_layouts_agree() { direct_inline.extend_from_slice(&direct_tag); // inline tag, streamed - let (mut tagged_enc, tagged_nonce) = Ascon_AEAD128::::do_encrypt_init_rng( - &km, - &mut FixedSeedRNG::<16>::new(pinned), - ) - .unwrap(); + let (mut tagged_enc, tagged_nonce) = + km.create_encryptor(&mut FixedSeedRNG::<16>::new(pinned)).unwrap(); tagged_enc.do_update_aad(aad).unwrap(); let mut tagged_out = vec![0u8; Ascon_AEAD128::::encrypt_out_len(pt.len())]; let written = tagged_enc.do_encrypt_out(&pt, &mut tagged_out).unwrap(); @@ -524,26 +527,19 @@ fn aead128_tagged_and_direct_layouts_agree() { // inline tag, one-shot: its own generated nonce, so what must match is the round trip // and the length, not the bytes. let mut one_shot = vec![0u8; Ascon_AEAD128::::encrypt_out_len(pt.len())]; - let (one_nonce, one_len) = - Ascon_AEAD128::::encrypt_with_aad_out(&km, aad, &pt, &mut one_shot) - .unwrap(); + let (one_len, one_nonce) = km + .encrypt_with_aad_out(&mut HashDRBG_SHA512::new_from_os(), aad, &pt, &mut one_shot) + .unwrap(); assert_eq!(one_len, tagged_out.len(), "pt_len {pt_len}: one-shot writes the same length"); let mut one_back = vec![0u8; Ascon_AEAD128::::decrypt_out_len(one_len)]; - let one_n = Ascon_AEAD128::::decrypt_with_aad_out( - &km, - &one_nonce, - aad, - &one_shot[..one_len], - &mut one_back, - ) - .unwrap(); + let one_n = + km.decrypt_with_aad_out(&one_nonce, aad, &one_shot[..one_len], &mut one_back).unwrap(); assert_eq!(&one_back[..one_n], &pt[..], "pt_len {pt_len}: one-shot round trip"); // ...and all of it decrypts back, each through its own view. The decryptor holds the // last 16 bytes back either way; detached, `do_decrypt_final_detached_out` releases // them. - let mut direct_dec = - Ascon_AEAD128::::do_decrypt_init(&km, &direct_nonce).unwrap(); + let mut direct_dec = km.create_decryptor(&direct_nonce); direct_dec.do_update_aad(aad).unwrap(); let mut direct_pt = vec![0u8; direct_ct.len()]; let got = direct_dec.do_decrypt_out(&direct_ct, &mut direct_pt).unwrap(); @@ -554,8 +550,7 @@ fn aead128_tagged_and_direct_layouts_agree() { direct_pt[got..].copy_from_slice(&last[..last_len]); assert_eq!(direct_pt, pt, "pt_len {pt_len}: direct decrypt round trip"); - let mut tagged_dec = - Ascon_AEAD128::::do_decrypt_init(&km, &tagged_nonce).unwrap(); + let mut tagged_dec = km.create_decryptor(&tagged_nonce); tagged_dec.do_update_aad(aad).unwrap(); let mut tagged_pt = vec![0u8; tagged_out.len()]; let got = tagged_dec.do_decrypt_out(&tagged_out, &mut tagged_pt).unwrap(); @@ -565,25 +560,19 @@ fn aead128_tagged_and_direct_layouts_agree() { assert_eq!(&tagged_pt[..got], &pt[..], "pt_len {pt_len}: tagged decrypt round trip"); let mut one_pt = vec![0u8; Ascon_AEAD128::::decrypt_out_len(tagged_out.len())]; - let n = Ascon_AEAD128::::decrypt_with_aad_out( - &km, &tagged_nonce, aad, &tagged_out, &mut one_pt, - ) - .unwrap(); + let n = km.decrypt_with_aad_out(&tagged_nonce, aad, &tagged_out, &mut one_pt).unwrap(); assert_eq!(&one_pt[..n], &pt[..], "pt_len {pt_len}: streamed ciphertext, one-shot decrypt"); } } -/// With no associated data, the pair used purely as a [`SymmetricCipherEncryptor`] / -/// [`SymmetricCipherDecryptor`] -- nonce driven to the KAT's by a fixed RNG -- reproduces the -/// embedded NIST LWC vectors' `ciphertext || tag`, and decrypts them back. -/// -/// [`SymmetricCipherEncryptor`]: bouncycastle_core::traits::SymmetricCipherEncryptor -/// [`SymmetricCipherDecryptor`]: bouncycastle_core::traits::SymmetricCipherDecryptor +/// With no associated data, the key's inline one-shots with an empty AAD -- nonce driven to the +/// KAT's by a fixed RNG -- reproduce the embedded NIST LWC vectors' `ciphertext || tag`, and +/// decrypt them back. #[test] -fn aead128_symmetric_cipher_view_matches_kat() { +fn aead128_no_aad_one_shots_match_kat() { use bouncycastle_core_test_framework::FixedSeedRNG; - let km = key_material(&KEY); + let km = aead_key(&KEY); // The NIST LWC AEAD KAT convention uses Key == Nonce == 000102…0F (i.e. KEY for both). let kat_nonce = KEY; let mut tested = 0; @@ -593,18 +582,14 @@ fn aead128_symmetric_cipher_view_matches_kat() { assert!(dh(ad_hex).is_empty()); let mut ct = vec![0u8; Ascon_AEAD128::::encrypt_out_len(pt.len())]; - let (nonce, n) = Ascon_AEAD128::::encrypt_rng_out( - &km, - &mut FixedSeedRNG::<16>::new(kat_nonce), - &pt, - &mut ct, - ) - .unwrap(); + let (n, nonce) = km + .encrypt_with_aad_out(&mut FixedSeedRNG::<16>::new(kat_nonce), &[], &pt, &mut ct) + .unwrap(); assert_eq!(nonce, kat_nonce); - assert_eq!(&ct[..n], &expected[..], "pt {pt_hex}: SymmetricCipherEncryptor view vs KAT"); + assert_eq!(&ct[..n], &expected[..], "pt {pt_hex}: inline one-shot vs KAT"); - let recovered = Ascon_AEAD128::::decrypt(&km, &kat_nonce, &expected).unwrap(); - assert_eq!(recovered, pt, "pt {pt_hex}: SymmetricCipherDecryptor view vs KAT"); + let recovered = km.decrypt_with_aad(&kat_nonce, &[], &expected).unwrap(); + assert_eq!(recovered, pt, "pt {pt_hex}: inline one-shot decrypt vs KAT"); tested += 1; } assert!(tested >= 2, "the embedded KATs must include no-AD vectors"); @@ -622,11 +607,12 @@ fn aead128_suspendable_keyed_state() { let pt = pattern(40); let ad = b"suspend-ad"; let ct_ref = enc_oneshot(&KEY, &NONCE, ad, &pt); + let key = aead_key(&KEY); let km = key_material(&KEY); // Encrypt part of the plaintext, suspend, resume with the re-supplied key, finish, and confirm // the output matches a one-shot encryption. The key is never part of the serialized state. - let mut e = Ascon_AEAD128::::do_encrypt_init_nonce(&km, &NONCE).unwrap(); + let mut e = key.create_encryptor_nonce(&NONCE); e.do_update_aad(ad).unwrap(); let mut out = vec![0u8; pt.len() + 16]; e.do_encrypt_out(&pt[..18], &mut out[..18]).unwrap(); @@ -677,7 +663,7 @@ fn aead128_suspendable_keyed_state() { // The decryptor: part of the ciphertext in (all but 16 bytes released), suspend, resume, // the rest in, and the inline final checks the tag. - let mut d = Ascon_AEAD128::::do_decrypt_init(&km, &NONCE).unwrap(); + let mut d = key.create_decryptor(&NONCE); d.do_update_aad(ad).unwrap(); let mut back = vec![0u8; ct_ref.len()]; let n1 = d.do_decrypt_out(&ct_ref[..30], &mut back).unwrap(); diff --git a/crypto/ascon/tests/ascon_bc-test-data.rs b/crypto/ascon/tests/ascon_bc-test-data.rs index 8499386cf..19309f8d8 100644 --- a/crypto/ascon/tests/ascon_bc-test-data.rs +++ b/crypto/ascon/tests/ascon_bc-test-data.rs @@ -8,14 +8,15 @@ //! These full sweeps (1025–1089 cases each) complement the small embedded vector sets in the //! per-primitive test files. -use bouncycastle_ascon::Ascon_AEAD128; use bouncycastle_ascon::AsconCXof128; use bouncycastle_ascon::AsconHash256; use bouncycastle_ascon::AsconXof128; +use bouncycastle_ascon::{Ascon_AEAD128, AsconAEAD128Key}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, Hash, SymmetricCipherDecryptor, SymmetricCipherEncryptor, XOF, @@ -100,6 +101,10 @@ fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { km } +fn aead_key(key: &[u8; 16]) -> AsconAEAD128Key { + AsconAEAD128Key::from_key_material(&key_material(key)).expect("a valid key") +} + #[test] fn ascon_aead128_kat() { let Some(contents) = bc_test_data(TEST_DATA_DIR, "asconaead128/LWC_AEAD_KAT_128_128.txt") @@ -111,7 +116,7 @@ fn ascon_aead128_kat() { assert!(!cases.is_empty(), "no AEAD cases parsed"); for case in &cases { - let key = key_material(&to_16(&decode_hex(field(case, &["Key", "K"])), "key")); + let key = aead_key(&to_16(&decode_hex(field(case, &["Key", "K"])), "key")); let nonce = to_16(&decode_hex(field(case, &["Nonce", "N"])), "nonce"); let ad = decode_hex(field(case, &["AD", "A"])); let pt = decode_hex(field(case, &["PT", "P"])); @@ -119,27 +124,23 @@ fn ascon_aead128_kat() { // One-shot encrypt. let mut ct = vec![0u8; Ascon_AEAD128::::encrypt_out_len(pt.len())]; - let n = Ascon_AEAD128::::encrypt_with_aad_nonce_out( - &key, &nonce, &ad, &pt, &mut ct, - ) - .unwrap(); + let n = key.encrypt_with_aad_nonce_out(&nonce, &ad, &pt, &mut ct).unwrap(); ct.truncate(n); assert_eq!(ct, expected_ct, "encrypt mismatch (Count {})", field(case, &["Count"])); // One-shot decrypt round-trip. let mut pt_out = vec![0u8; Ascon_AEAD128::::decrypt_out_len(expected_ct.len())]; - let m = Ascon_AEAD128::::decrypt_with_aad_out( - &key, &nonce, &ad, &expected_ct, &mut pt_out, - ) - .expect("decrypt should authenticate"); + let m = key + .decrypt_with_aad_out(&nonce, &ad, &expected_ct, &mut pt_out) + .expect("decrypt should authenticate"); pt_out.truncate(m); assert_eq!(pt_out, pt, "decrypt mismatch (Count {})", field(case, &["Count"])); // Byte-at-a-time streaming encrypt/decrypt. - let mut enc = Ascon_AEAD128::::do_encrypt_init_nonce(&key, &nonce).unwrap(); + let mut enc = key.create_encryptor_nonce(&nonce); enc.do_update_aad(&ad).unwrap(); let mut stream_ct = Vec::with_capacity(expected_ct.len()); @@ -159,7 +160,7 @@ fn ascon_aead128_kat() { field(case, &["Count"]) ); - let mut dec = Ascon_AEAD128::::do_decrypt_init(&key, &nonce).unwrap(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(&ad).unwrap(); let mut stream_pt = Vec::with_capacity(pt.len()); diff --git a/crypto/ascon/tests/ascon_wycheproof.rs b/crypto/ascon/tests/ascon_wycheproof.rs index cdd82cd6e..798a001f3 100644 --- a/crypto/ascon/tests/ascon_wycheproof.rs +++ b/crypto/ascon/tests/ascon_wycheproof.rs @@ -9,15 +9,14 @@ //! A `valid` case must encrypt to exactly `ct || tag` and decrypt back to `msg`. An `invalid` case //! must be rejected with `AEADTagCheckFailed`, leaving the output buffer zeroized. -use bouncycastle_ascon::{Ascon_AEAD128, KEY_LEN, NONCE_LEN, TAG_LEN}; +use bouncycastle_ascon::{Ascon_AEAD128, AsconAEAD128Key, KEY_LEN, NONCE_LEN, TAG_LEN}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{ - AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, -}; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; /// Wraps the vector's key bytes as a cipher key, promoting them if `KeyMaterial`'s entropy @@ -33,6 +32,10 @@ fn cipher_key(bytes: &[u8]) -> KeyMaterial { key } +fn aead_key(bytes: &[u8]) -> AsconAEAD128Key { + AsconAEAD128Key::from_key_material(&cipher_key(bytes)).expect("a wycheproof key") +} + #[test] fn wycheproof_ascon_aead128() { let Some(doc) = wycheproof_json("ascon_sp800_232_aead128_test.json") else { return }; @@ -48,7 +51,7 @@ fn wycheproof_ascon_aead128() { for test in group.get("tests").and_then(Value::as_array).expect("tests") { let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); - let key = cipher_key(&hex_field(test, "key", tc_id)); + let key = aead_key(&hex_field(test, "key", tc_id)); let nonce: [u8; NONCE_LEN] = hex_field(test, "iv", tc_id).try_into().expect("a 16-byte nonce"); let aad = hex_field(test, "aad", tc_id); @@ -57,18 +60,15 @@ fn wycheproof_ascon_aead128() { let mut pt = vec![0xEEu8; Ascon_AEAD128::::decrypt_out_len(ct_and_tag.len())]; - let decrypted = Ascon_AEAD128::::decrypt_with_aad_out( - &key, &nonce, &aad, &ct_and_tag, &mut pt, - ); + let decrypted = key.decrypt_with_aad_out(&nonce, &aad, &ct_and_tag, &mut pt); match test.get("result").and_then(Value::as_str).expect("result") { "valid" => { let mut out = vec![0u8; Ascon_AEAD128::::encrypt_out_len(msg.len())]; - let n = Ascon_AEAD128::::encrypt_with_aad_nonce_out( - &key, &nonce, &aad, &msg, &mut out, - ) - .unwrap_or_else(|e| panic!("tcId {tc_id}: encrypt failed: {e:?}")); + let n = key + .encrypt_with_aad_nonce_out(&nonce, &aad, &msg, &mut out) + .unwrap_or_else(|e| panic!("tcId {tc_id}: encrypt failed: {e:?}")); assert_eq!(&out[..n], &ct_and_tag[..], "tcId {tc_id}: ct || tag"); let n = decrypted.unwrap_or_else(|e| panic!("tcId {tc_id}: decrypt: {e:?}")); diff --git a/mem_usage_benches/src/bench_ascon_mem_usage.rs b/mem_usage_benches/src/bench_ascon_mem_usage.rs index bb13906b0..a32da9ab0 100644 --- a/mem_usage_benches/src/bench_ascon_mem_usage.rs +++ b/mem_usage_benches/src/bench_ascon_mem_usage.rs @@ -56,7 +56,7 @@ use core::hint::black_box; -use bouncycastle::ascon::Ascon_AEAD128; +use bouncycastle::ascon::{Ascon_AEAD128, AsconAEAD128Key}; use bouncycastle::ascon::{AsconCXof128, AsconCXof128Squeezer, SUSPENDED_ASCON_CXOF128_STATE_LEN}; use bouncycastle::ascon::{AsconHash256, SUSPENDED_ASCON_HASH256_STATE_LEN}; use bouncycastle::ascon::{AsconXof128, AsconXof128Squeezer, SUSPENDED_ASCON_XOF128_STATE_LEN}; @@ -68,6 +68,7 @@ use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::core::errors::{HashError, RNGError, SymmetricCipherError}; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::core::security_strength::SecurityStrength; +use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle::core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, Hash, RNG, SymmetricCipherDecryptor, SymmetricCipherEncryptor, XOF, XOFSqueezer, @@ -93,8 +94,7 @@ const NONCE: [u8; NONCE_LEN] = [ /// Stands in for the caller's RNG and hands back [`NONCE`], so that the encrypting init runs /// Ascon's initialization and nothing else: a DRBG's generate would be the deepest part of that -/// chain, and the default `do_encrypt_init`'s OS entropy call is outside what the frame remarks -/// can see. Not a random number generator; it exists only for this harness. +/// chain, and a DRBG's own stack is outside what the frame remarks can see. Not a random number generator; it exists only for this harness. struct FixedNonce; impl RNG for FixedNonce { @@ -181,6 +181,12 @@ fn key() -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&KEY, KeyType::SymmetricCipherKey).unwrap() } +/// The key object the workers are made from, likewise in its own frame. +#[inline(never)] +fn aead_key() -> AsconAEAD128Key { + AsconAEAD128Key::from_key_material(&key()).unwrap() +} + /// A message filled at run time, so that its contents are not a constant the compiler could fold /// the whole computation over. #[inline(never)] @@ -194,7 +200,7 @@ fn message(fill: u8) -> [u8; N] { #[inline(never)] fn encryptor() -> Enc { - Enc::do_encrypt_init_rng(&key(), &mut FixedNonce).unwrap().0 + aead_key().create_encryptor(&mut FixedNonce).unwrap().0 } /// An encryptor that has taken the AAD and the message, so the final pads a partial block. @@ -209,21 +215,21 @@ fn encryptor_after_data() -> Enc { #[inline(never)] fn decryptor() -> Dec { - Dec::do_decrypt_init(&key(), &NONCE).unwrap() + aead_key().create_decryptor(&NONCE) } /// `MSG_LEN` bytes sealed under [`NONCE`] with the AAD, as ciphertext || tag. #[inline(never)] fn sealed() -> [u8; SEALED_LEN] { let mut out = [0u8; SEALED_LEN]; - Enc::encrypt_with_aad_rng_out( - &key(), - &mut FixedNonce, - &message::(0x5a), - &message::(0xa5), - &mut out, - ) - .unwrap(); + aead_key() + .encrypt_with_aad_out( + &mut FixedNonce, + &message::(0x5a), + &message::(0xa5), + &mut out, + ) + .unwrap(); out } @@ -276,16 +282,31 @@ fn cxof128_squeezer() -> AsconCXof128Squeezer { // ---- Ascon-AEAD128, encrypting ------------------------------------------------------------ -/// Initialization: the key check, then `IV || K || N` through `Ascon-p[12]`. +/// The key: the key check, in its own frame. #[inline(never)] -fn bench_aead_encrypt_init() { - eprintln!("Ascon_AEAD128::do_encrypt_init_rng"); +fn bench_aead_from_key_material() { + eprintln!("AsconAEAD128Key::from_key_material"); let k = key(); measure(|| { let op = black_box( - >::do_encrypt_init_rng + >::from_key_material + as fn(&KeyMaterial) -> Result, + ); + let key = op(&k).unwrap(); + black_box(&key); + }); +} + +/// Initialization: the nonce draw, then `IV || K || N` through `Ascon-p[12]`. +#[inline(never)] +fn bench_aead_encrypt_init() { + eprintln!("AsconAEAD128Key::create_encryptor"); + let k = aead_key(); + measure(|| { + let op = black_box( + >::create_encryptor as fn( - &KeyMaterial, + &AsconAEAD128Key, &mut dyn RNG, ) -> Result<(Enc, [u8; NONCE_LEN]), CipherErr>, ); @@ -361,23 +382,23 @@ fn bench_aead_encrypt_final_detached() { /// The one-shot with AAD and the tag inline: init, AAD, data and final in one call. #[inline(never)] fn bench_aead_encrypt_oneshot() { - eprintln!("Ascon_AEAD128::encrypt_with_aad_rng_out, {MSG_LEN} B"); - let k = key(); + eprintln!("AsconAEAD128Key::encrypt_with_aad_out, {MSG_LEN} B"); + let k = aead_key(); let aad = black_box(message::(0x5a)); let pt = black_box(message::(0xa5)); measure(move || { let mut ct = [0u8; SEALED_LEN]; let op = black_box( - >::encrypt_with_aad_rng_out + >::encrypt_with_aad_out as fn( - &KeyMaterial, + &AsconAEAD128Key, &mut dyn RNG, &[u8], &[u8], &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize), CipherErr>, + ) -> Result<(usize, [u8; NONCE_LEN]), CipherErr>, ); - let (_, n) = op(&k, &mut FixedNonce, &aad, &pt, &mut ct).unwrap(); + let (n, _) = op(&k, &mut FixedNonce, &aad, &pt, &mut ct).unwrap(); print!("{:x?}", &ct[..n]); }); } @@ -386,14 +407,14 @@ fn bench_aead_encrypt_oneshot() { #[inline(never)] fn bench_aead_decrypt_init() { - eprintln!("Ascon_AEAD128::do_decrypt_init"); - let k = key(); + eprintln!("AsconAEAD128Key::create_decryptor"); + let k = aead_key(); measure(|| { let op = black_box( - >::do_decrypt_init - as fn(&KeyMaterial, &[u8; NONCE_LEN]) -> Result, + >::create_decryptor + as fn(&AsconAEAD128Key, &[u8; NONCE_LEN]) -> Dec, ); - let dec = op(&k, &NONCE).unwrap(); + let dec = op(&k, &NONCE); black_box(&dec); }); } @@ -468,16 +489,16 @@ fn bench_aead_decrypt_final_detached() { /// The one-shot with AAD and the tag inline. #[inline(never)] fn bench_aead_decrypt_oneshot() { - eprintln!("Ascon_AEAD128::decrypt_with_aad_out, {SEALED_LEN} B"); - let k = key(); + eprintln!("AsconAEAD128Key::decrypt_with_aad_out, {SEALED_LEN} B"); + let k = aead_key(); let aad = black_box(message::(0x5a)); let ct = black_box(sealed()); measure(move || { let mut pt = [0u8; MSG_LEN]; let op = black_box( - >::decrypt_with_aad_out + >::decrypt_with_aad_out as fn( - &KeyMaterial, + &AsconAEAD128Key, &[u8; NONCE_LEN], &[u8], &[u8], @@ -644,6 +665,7 @@ fn bench_cxof128_oneshot() { fn main() { print_struct_sizes() // bench_do_nothing() + // bench_aead_from_key_material() // bench_aead_encrypt_init() // bench_aead_encrypt_update_aad() // bench_aead_encrypt_out() From 6ea891e44e83b48ba8d35020695c91ea639cf0de Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 23:39:42 +1100 Subject: [PATCH 08/54] sha3: KMACKey (prototype) KMACKeyInternal, as KMAC128Key / KMAC256Key: the cSHAKE state after the key binding of SP 800-185 Sec 4.3 step 1, with the customization and output length that make it one KMAC function, on FromAnyKeyMaterial (nominal length, no customization) and with_params / with_params_allow_weak for the rest, handing out KMACInternal workers through the MAC role pair. KMACInternal::new_with_params becomes the crate's own, written over the key; MAC::new stays until the deletion. The tests move onto the key and gain the construction suite; the CLI's mac commands build KMACKey and HmacKey objects in place of the trait constructors. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/mac_cmd.rs | 37 ++- crypto/sha3/src/kmac.rs | 245 +++++++++++++----- crypto/sha3/src/lib.rs | 2 +- crypto/sha3/tests/kmac_bc-test-data.rs | 15 +- crypto/sha3/tests/kmac_tests.rs | 117 +++++---- crypto/sha3/tests/kmac_wycheproof.rs | 14 +- crypto/sha3/tests/sp800_185_suspend_tests.rs | 15 +- mem_usage_benches/src/bench_sha3_mem_usage.rs | 4 +- 8 files changed, 307 insertions(+), 142 deletions(-) diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index 562bc9b55..67864ec39 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -4,11 +4,13 @@ use std::{fs, io}; use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; +use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; use bouncycastle::core::traits::MAC; use bouncycastle::hex; -use bouncycastle::sha2::hmac::{HMAC_SHA256, HMAC_SHA512, HMAC_SHA512_224, HMAC_SHA512_256}; -use bouncycastle::sha3::kmac::{KMAC128, KMAC256}; -use bouncycastle::sm3::hmac::HMAC_SM3; +use bouncycastle::hmac::HmacKey; +use bouncycastle::sha2::{SHA256, SHA512, SHA512_224, SHA512_256}; +use bouncycastle::sha3::kmac::{KMAC128Key, KMAC256Key}; +use bouncycastle::sm3::SM3; #[allow(non_camel_case_types)] pub(crate) enum HMACVariant { @@ -51,23 +53,30 @@ pub(crate) fn mac_cmd( // instantiate the MAC object and call do_mac() match hmac_variant { HMACVariant::SHA256 => { - let mac = HMAC_SHA256::new_allow_weak_key(&key).unwrap(); + let mac = + HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); do_mac(mac, verify_val, output_hex); } HMACVariant::SHA512 => { - let mac = HMAC_SHA512::new_allow_weak_key(&key).unwrap(); + let mac = + HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); do_mac(mac, verify_val, output_hex); } HMACVariant::SHA512_224 => { - let mac = HMAC_SHA512_224::new_allow_weak_key(&key).unwrap(); + let mac = HmacKey::::from_key_material_allow_weak(&key) + .unwrap() + .create_generator(); do_mac(mac, verify_val, output_hex); } HMACVariant::SHA512_256 => { - let mac = HMAC_SHA512_256::new_allow_weak_key(&key).unwrap(); + let mac = HmacKey::::from_key_material_allow_weak(&key) + .unwrap() + .create_generator(); do_mac(mac, verify_val, output_hex); } HMACVariant::SM3 => { - let mac = HMAC_SM3::new_allow_weak_key(&key).unwrap(); + let mac = + HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); do_mac(mac, verify_val, output_hex); } } @@ -86,16 +95,20 @@ pub(crate) fn kmac_cmd( ) { let key = load_mac_key(key, key_file); let s = customization.as_deref().unwrap_or("").as_bytes(); - // new_allow_weak_key, as the HMAC commands do: a CLI is used for test vectors and scripting, - // where a short or all-zero key is a legitimate thing to want. + // The allow-weak form, as the HMAC commands use: a CLI is used for test vectors and + // scripting, where a short or all-zero key is a legitimate thing to want. match bit_len { 128 => do_mac( - KMAC128::new_with_params(&key, s, length, true).expect("a valid MAC key"), + KMAC128Key::with_params_allow_weak(&key, s, length) + .expect("a valid MAC key") + .create_generator(), verify_val, output_hex, ), 256 => do_mac( - KMAC256::new_with_params(&key, s, length, true).expect("a valid MAC key"), + KMAC256Key::with_params_allow_weak(&key, s, length) + .expect("a valid MAC key") + .create_generator(), verify_val, output_hex, ), diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index a3ea3928c..57d8eec28 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -1,23 +1,28 @@ //! KMAC, the Keccak Message Authentication Code of NIST SP 800-185 Sec 4. //! //! # KMAC -//! KMAC is a [`MAC`]. [`MAC::new`] takes a key tagged [`KeyType::MACKey`] and produces the nominal -//! output length; [`KMACInternal::new_with_params`] chooses `S` and the output length, which is -//! bound into the function rather than a truncation of it (see [`KMAC128`]): +//! KMAC is a [`MAC`], reached through a [`KMAC128Key`] / [`KMAC256Key`]. The key takes material +//! tagged [`KeyType::MACKey`]; `from_key_material` gives the nominal output length and +//! [`KMACKeyInternal::with_params`] chooses `S` and the output length, which is bound into the +//! function rather than a truncation of it (see [`KMAC128`]): //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; //! use bouncycastle_core::traits::MAC; -//! use bouncycastle_sha3::kmac::KMAC128; +//! use bouncycastle_sha3::kmac::KMAC128Key; //! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x42u8; 32], KeyType::MACKey).unwrap(); +//! let material = KeyMaterial256::from_bytes_as_type(&[0x42u8; 32], KeyType::MACKey).unwrap(); +//! let key = KMAC128Key::from_key_material(&material).unwrap(); //! -//! let tag: Vec = KMAC128::new(&key).unwrap().mac(b"Hello, world!"); -//! assert!(KMAC128::new(&key).unwrap().verify(b"Hello, world!", &tag)); +//! let tag: Vec = key.mac(b"Hello, world!"); +//! assert!(key.verify(b"Hello, world!", &tag)); //! -//! // 16-byte tags, under a customization string. -//! let kmac = KMAC128::new_with_params(&key, b"My Tagged Application", 16, false).unwrap(); -//! let short_tag: Vec = kmac.mac(b"Hello, world!"); -//! assert_eq!(short_tag.len(), 16); +//! // 16-byte tags, under a customization string: a different function, so a different key. +//! let short = KMAC128Key::with_params(&material, b"My Tagged Application", 16).unwrap(); +//! let mut kmac = short.create_generator(); +//! kmac.do_update(b"Hello, "); +//! kmac.do_update(b"world!"); +//! assert_eq!(kmac.do_final().len(), 16); //! ``` //! //! # KMACXOF @@ -32,8 +37,9 @@ //! Example of `KMACXOF128`: //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::{Hash, MAC, XOF, XOFSqueezer}; -//! use bouncycastle_sha3::kmac::{KMAC128, KMACXOF128}; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +//! use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +//! use bouncycastle_sha3::kmac::{KMAC128Key, KMACXOF128}; //! //! let key = KeyMaterial256::from_bytes_as_type(&[0x42u8; 32], KeyType::MACKey).unwrap(); //! @@ -45,16 +51,18 @@ //! //! // A final read of 32 bytes is KMAC128 at its nominal length, not a prefix of the stream above. //! let bound: Vec = KMACXOF128::new(&key, b"", false).unwrap().xof(b"Hello, world!", 32); -//! assert_eq!(bound, KMAC128::new(&key).unwrap().mac(b"Hello, world!")); +//! assert_eq!(bound, KMAC128Key::from_key_material(&key).unwrap().mac(b"Hello, world!")); //! assert_ne!(bound[..16], first[..]); //! ``` use crate::cshake::{CSHAKE_COMPONENT_LEN, CSHAKEInternal, CSHAKESqueezer, right_encode}; use crate::{SHAKE128Params, SHAKE256Params, SHAKEParams}; use bouncycastle_core::errors::{HashError, KeyMaterialError, MACError, SuspendableError}; -use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{Algorithm, Hash, MAC, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, RNG, Suspendable, XOF, XOFSqueezer}; use bouncycastle_utils::ct; use bouncycastle_utils::suspendable_state::{ Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, @@ -72,9 +80,10 @@ pub const KMACXOF256_NAME: &str = "KMACXOF256"; /// KMAC128: the Keccak MAC of NIST SP 800-185 Sec 4, at a 128-bit security strength. /// -/// [`bouncycastle_core::traits::MAC::new`] gives the common case -- no customization, 32-byte -/// output. [`KMACInternal::new_with_params`] chooses the customization string and output length, -/// [`KMACXOF128`] is the separate arbitrary-length function of Sec 4.3.1. +/// The worker a [`KMAC128Key`] hands out: `from_key_material` gives the common case -- no +/// customization, 32-byte output -- and [`KMACKeyInternal::with_params`] chooses the +/// customization string and output length. [`KMACXOF128`] is the separate arbitrary-length +/// function of Sec 4.3.1. pub type KMAC128 = KMACInternal; /// KMAC256: the Keccak MAC of NIST SP 800-185 Sec 4, at a 256-bit security strength. /// @@ -100,6 +109,144 @@ pub const SUSPENDED_KMACXOF_STATE_LEN: usize = LIB_VERSION_LEN + CSHAKE_COMPONEN /// it is what separates KMAC from any other cSHAKE-derived function. const KMAC_FUNCTION_NAME: &[u8] = b"KMAC"; +/// Internal struct for the KMAC key. Use [`KMAC128Key`] or [`KMAC256Key`]. +/// +/// The key is the cSHAKE state after the key binding of Sec 4.3 step 1, `bytepad(encode_string(K), +/// rate)`, plus the customization and the output length chosen for it, so every worker starts +/// from that state and absorbs only its message. A different `S` or `L` is a different KMAC +/// function (Sec 1), and so a different key object over the same material. +#[derive(Clone)] +pub struct KMACKeyInternal { + cshake: CSHAKEInternal, + output_len: usize, + strength: SecurityStrength, +} + +/// A KMAC128 key: see [`KMACKeyInternal`]. +pub type KMAC128Key = KMACKeyInternal; +/// A KMAC256 key: see [`KMACKeyInternal`]. +pub type KMAC256Key = KMACKeyInternal; + +impl KMACKeyInternal { + /// The nominal output length: 32 bytes for KMAC128 and 64 for KMAC256. + const NOMINAL_OUTPUT_LEN: usize = (PARAMS::SIZE as usize) / 4; + + /// A key for KMAC under a customization string and an output length of the caller's + /// choosing. `output_len` is `L` in bytes and is bound into the computation, so it must be + /// the length the verifier will use; `customization` may be empty. + /// + /// Sec 8.4.1 requires the key to be at least as long as the security strength for approved + /// use; that is enforced through the key's [`SecurityStrength`] tag, exactly as HMAC does, + /// and [`Self::with_params_allow_weak`] is the escape hatch. + /// + /// # Errors + /// [`MACError::KeyMaterialError`] if the key is not tagged as a MAC key, or is tagged below + /// this KMAC's security strength. + pub fn with_params( + key: &dyn KeyMaterialTrait, + customization: &[u8], + output_len: usize, + ) -> Result { + Self::with_params_checked(key, customization, output_len, false) + } + + /// As [`with_params`](Self::with_params) without the strength check. + /// + /// # Errors + /// [`MACError::KeyMaterialError`] if the key is not tagged as a MAC key. + pub fn with_params_allow_weak( + key: &dyn KeyMaterialTrait, + customization: &[u8], + output_len: usize, + ) -> Result { + Self::with_params_checked(key, customization, output_len, true) + } + + fn with_params_checked( + key: &dyn KeyMaterialTrait, + customization: &[u8], + output_len: usize, + allow_weak_key: bool, + ) -> Result { + // Same stance as HMAC: an all-zero key is Zeroized rather than MACKey, and is allowed + // through so callers are not forced to re-tag it. + if !(key.key_type() == KeyType::Zeroized || key.key_type() == KeyType::MACKey) { + return Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType( + "Key type must be a MAC key.", + ))); + } + let strength = SecurityStrength::from_bits(PARAMS::SIZE as usize); + if !allow_weak_key && key.security_strength() < strength { + Err(KeyMaterialError::SecurityStrength( + "KMAC: provided key has a lower security strength than the instantiated KMAC", + ))? + } + + let mut cshake = CSHAKEInternal::::new(KMAC_FUNCTION_NAME, customization); + // Sec 4.3 step 1: bytepad(encode_string(K), rate), absorbed rather than materialised. + crate::cshake::absorb_bytepad_strings(&mut cshake, &[key.ref_to_bytes()]); + + Ok(Self { cshake, output_len, strength }) + } +} + +impl FromAnyKeyMaterial for KMACKeyInternal { + /// No customization string and the nominal output length, 32 bytes for KMAC128 and 64 for + /// KMAC256; the errors are [`with_params`](Self::with_params)'s. + fn from_key_material(key: &dyn KeyMaterialTrait) -> Result { + Self::with_params(key, &[], Self::NOMINAL_OUTPUT_LEN) + } + + /// As [`from_key_material`](Self::from_key_material) without the strength check. + fn from_key_material_allow_weak(key: &dyn KeyMaterialTrait) -> Result { + Self::with_params_allow_weak(key, &[], Self::NOMINAL_OUTPUT_LEN) + } + + /// Draws a key of the security strength's length (Sec 8.4.1: 16 bytes for KMAC128, 32 for + /// KMAC256) from `rng`, tagged [`KeyType::MACKey`] at the RNG's strength, and builds the key + /// from it with no customization and the nominal output length. + /// + /// # Errors + /// [`MACError::KeyMaterialError`] if the RNG fails, or with + /// [`KeyMaterialError::SecurityStrength`] if its strength is below the KMAC's. + fn generate(rng: &mut dyn RNG) -> Result { + let key_len = (PARAMS::SIZE as usize) / 8; + let mut key = KeyMaterial::<32>::new(); + do_hazardous_operations(&mut key, |key| { + rng.next_bytes_out(&mut key.ref_to_bytes_mut()?[..key_len]) + .map_err(|_| KeyMaterialError::GenericError("RNG failed."))?; + key.set_key_len(key_len)?; + key.set_key_type(KeyType::MACKey)?; + // The RNG's strength, capped at what the length carries, as KeyMaterial::from_rng + // does: a 16-byte KMAC128 key from a 256-bit DRBG is a 128-bit key. + let by_len = SecurityStrength::from_bits(key_len * 8); + let by_rng = rng.security_strength(); + key.set_security_strength(if by_rng < by_len { by_rng } else { by_len }) + })?; + Self::from_key_material(&key) + } +} + +impl MacGenerationKey for KMACKeyInternal { + type Generator = KMACInternal; + + fn create_generator(&self) -> Self::Generator { + KMACInternal { + cshake: self.cshake.clone(), + output_len: self.output_len, + strength: self.strength, + } + } +} + +impl MacVerificationKey for KMACKeyInternal { + type Verifier = KMACInternal; + + fn create_verifier(&self) -> Self::Verifier { + self.create_generator() + } +} + /// Internal struct for KMAC. Use [`KMAC128`] or [`KMAC256`]. /// /// KMAC is cSHAKE with the function name `"KMAC"`, the key bound to the front of the message and @@ -114,8 +261,8 @@ const KMAC_FUNCTION_NAME: &[u8] = b"KMAC"; /// /// The output length is *absorbed*, so KMAC at one length is unrelated to KMAC at another -- /// Sec 1 puts it as "any change in the requested output length completely changes the function". -/// That is why [`Self::new_with_params`] takes the length up front and [`MAC::do_final`] produces -/// exactly that many bytes. +/// That is why [`KMACKeyInternal::with_params`] takes the length up front and [`MAC::do_final`] +/// produces exactly that many bytes. /// /// [`KMACXOFInternal`] is the separate function of Sec 4.3.1, KMACXOF, which binds /// `right_encode(0)` instead and produces as much output as asked for. Its bytes are *not* a @@ -133,44 +280,18 @@ impl Algorithm for KMACInternal { } impl KMACInternal { - /// A new KMAC with a customization string and an output length of the caller's choosing. - /// - /// `output_len` is `L` in bytes and is bound into the computation, so it must be the length the - /// verifier will use. `customization` may be empty. [`MAC::new`] is this with no customization - /// and the nominal output length. - /// - /// Sec 8.4.1 requires the key to be at least as long as the security strength for approved use; - /// that is enforced through the key's [`SecurityStrength`] tag, exactly as `HMAC` does, and - /// [`MAC::new_allow_weak_key`] is the escape hatch. - /// - /// # Errors - /// [`MACError::KeyMaterialError`] if the key is not tagged as a MAC key, or -- unless - /// `allow_weak_key` -- if it is tagged below this KMAC's security strength. - pub fn new_with_params( - key: &impl KeyMaterialTrait, + /// A worker for one message under a key built for this message alone: what + /// [`KMACKeyInternal::with_params`] does once, done per message. + pub(crate) fn new_with_params( + key: &dyn KeyMaterialTrait, customization: &[u8], output_len: usize, allow_weak_key: bool, ) -> Result { - // Same stance as HMAC: an all-zero key is Zeroized rather than MACKey, and is allowed - // through so callers are not forced to re-tag it. - if !(key.key_type() == KeyType::Zeroized || key.key_type() == KeyType::MACKey) { - return Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType( - "Key type must be a MAC key.", - ))); - } - let strength = SecurityStrength::from_bits(PARAMS::SIZE as usize); - if !allow_weak_key && key.security_strength() < strength { - Err(KeyMaterialError::SecurityStrength( - "KMAC::new(): provided key has a lower security strength than the instantiated KMAC", - ))? - } - - let mut cshake = CSHAKEInternal::::new(KMAC_FUNCTION_NAME, customization); - // Sec 4.3 step 1: bytepad(encode_string(K), rate), absorbed rather than materialised. - crate::cshake::absorb_bytepad_strings(&mut cshake, &[key.ref_to_bytes()]); - - Ok(Self { cshake, output_len, strength }) + Ok(KMACKeyInternal::::with_params_checked( + key, customization, output_len, allow_weak_key, + )? + .create_generator()) } /// Absorbs `right_encode(value)`, the length binding of Sec 4.3 step 1. @@ -198,7 +319,7 @@ impl SuspendableComponent for KMACInternal { let mut r = Cursor::new(rest); let output_len = bounded_usize(r.u64(), usize::MAX)?; debug_assert!(r.is_done()); - // The strength is fixed by the parameter set (see `new_with_params`), not stored. + // The strength is fixed by the parameter set (see `with_params`), not stored. Ok(Self { cshake, output_len, @@ -209,7 +330,7 @@ impl SuspendableComponent for KMACInternal { // `Suspendable` rather than `SuspendableKeyed`, keyed though KMAC is. `SuspendableKeyed` lets a // state omit the key because the key is wanted again at resume -- HMAC needs it for the outer -// `K xor opad` step. KMAC's key goes into the sponge in `new_with_params` and is never touched +// `K xor opad` step. KMAC's key goes into the sponge in `with_params` and is never touched // again, so a re-supplied key could neither rebuild anything nor be checked. /// The suspended state is not key-free: Keccak-f is a permutation, so anyone holding the state /// and the data absorbed so far can invert it back to the key. Store it as securely as the key. @@ -225,15 +346,13 @@ impl Suspendable for KMACInternal impl MAC for KMACInternal { /// A KMAC with no customization string, producing the nominal output length -- 32 bytes for - /// KMAC128 and 64 for KMAC256. Use [`Self::new_with_params`] to choose either. + /// KMAC128 and 64 for KMAC256. [`KMACKeyInternal::with_params`] chooses either. fn new(key: &impl KeyMaterialTrait) -> Result { - let len = (PARAMS::SIZE as usize) / 4; - Self::new_with_params(key, &[], len, false) + Ok(KMACKeyInternal::::from_key_material(key)?.create_generator()) } fn new_allow_weak_key(key: &impl KeyMaterialTrait) -> Result { - let len = (PARAMS::SIZE as usize) / 4; - Self::new_with_params(key, &[], len, true) + Ok(KMACKeyInternal::::from_key_material_allow_weak(key)?.create_generator()) } fn output_len(&self) -> usize { @@ -337,7 +456,7 @@ impl Algorithm for KMACXOFInternal { impl KMACXOFInternal { /// A new KMACXOF under `key`, optionally customized by `customization`. /// - /// The key requirements are [`KMACInternal::new_with_params`]'s: tagged as a MAC key, and at + /// The key requirements are [`KMACKeyInternal::with_params`]'s: tagged as a MAC key, and at /// least the security strength unless `allow_weak_key`. /// /// # Errors diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 6d96a0346..86de5bec8 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -224,7 +224,7 @@ //! * The sponge state and queue are held in [`bouncycastle_utils::secret::Secret`] and zeroized on //! drop. //! * KMAC's security rests on its key and output lengths (SP 800-185 Sec 8.4): the key check is -//! [`kmac::KMACInternal::new_with_params`]'s, with `allow_weak_key` as the bypass, and an output +//! [`kmac::KMACKeyInternal::with_params`]'s, with `with_params_allow_weak` as the bypass, and an output //! shorter than 8 bytes is the caller's to justify (Sec 8.4.2: never below 4, and below 8 //! only after a risk analysis). //! * cSHAKE has SHAKE's prefix property; the fixed-length KMAC, TupleHash and ParallelHash do diff --git a/crypto/sha3/tests/kmac_bc-test-data.rs b/crypto/sha3/tests/kmac_bc-test-data.rs index efeb55ed3..ae4f029f9 100644 --- a/crypto/sha3/tests/kmac_bc-test-data.rs +++ b/crypto/sha3/tests/kmac_bc-test-data.rs @@ -6,11 +6,12 @@ use bouncycastle_core::errors::MACError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::MacGenerationKey; use bouncycastle_core::traits::{Hash, MAC, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_hex as hex; -use bouncycastle_sha3::kmac::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; +use bouncycastle_sha3::kmac::{KMAC128Key, KMAC256Key, KMACXOF128, KMACXOF256}; const TEST_DATA_DIR: &str = "crypto/sp800-185"; @@ -82,10 +83,10 @@ fn nist_sp800_185_kmac_sample_values() { let key = key_material(&v.key); let got = match v.strength { - 128 => KMAC128::new_with_params(&key, v.s.as_bytes(), want, false) + 128 => KMAC128Key::with_params(&key, v.s.as_bytes(), want) .expect("a valid key") .mac(&v.msg), - 256 => KMAC256::new_with_params(&key, v.s.as_bytes(), want, false) + 256 => KMAC256Key::with_params(&key, v.s.as_bytes(), want) .expect("a valid key") .mac(&v.msg), other => panic!("COUNT {i}: unexpected strength {other}"), @@ -173,7 +174,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { match f.strength { 128 => check_do_final_binds_length( || KMACXOF128::new(&key, s, false).expect("a valid key"), - |n| KMAC128::new_with_params(&key, s, n, false).expect("a valid key").mac(&f.msg), + |n| KMAC128Key::with_params(&key, s, n).expect("a valid key").mac(&f.msg), &f.msg, &f.output, &x.output, @@ -181,7 +182,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { ), 256 => check_do_final_binds_length( || KMACXOF256::new(&key, s, false).expect("a valid key"), - |n| KMAC256::new_with_params(&key, s, n, false).expect("a valid key").mac(&f.msg), + |n| KMAC256Key::with_params(&key, s, n).expect("a valid key").mac(&f.msg), &f.msg, &f.output, &x.output, @@ -335,13 +336,13 @@ fn mac_out_and_do_final_out_agree_with_the_sample_values() { let ctx = format!("COUNT {i}: KMAC{} S={:?}", v.strength, v.s); match v.strength { 128 => check_out_variants( - || KMAC128::new_with_params(&key, s, n, false).unwrap(), + || KMAC128Key::with_params(&key, s, n).unwrap().create_generator(), &v.msg, &v.output, &ctx, ), 256 => check_out_variants( - || KMAC256::new_with_params(&key, s, n, false).unwrap(), + || KMAC256Key::with_params(&key, s, n).unwrap().create_generator(), &v.msg, &v.output, &ctx, diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs index 71c6f8189..281e8b6a6 100644 --- a/crypto/sha3/tests/kmac_tests.rs +++ b/crypto/sha3/tests/kmac_tests.rs @@ -2,8 +2,10 @@ use bouncycastle_core::errors::{KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF, XOFSqueezer}; -use bouncycastle_sha3::kmac::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromAnyKeyMaterial; +use bouncycastle_sha3::kmac::{KMAC128, KMAC128Key, KMAC256, KMAC256Key, KMACXOF128, KMACXOF256}; /// A 32-byte MAC key carries a 256-bit strength, so it satisfies both KMAC128 and KMAC256 without /// the weak-key escape hatch. @@ -17,8 +19,8 @@ fn key_material(bytes: &[u8]) -> KeyMaterial<32> { #[test] fn output_length_changes_the_function() { let key = key_material(&[0x42u8; 32]); - let short = KMAC128::new_with_params(&key, b"", 16, false).unwrap().mac(b"abc"); - let long = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"); + let short = KMAC128Key::with_params(&key, b"", 16).unwrap().mac(b"abc"); + let long = KMAC128Key::with_params(&key, b"", 32).unwrap().mac(b"abc"); assert_eq!(short.len(), 16); assert_eq!(long.len(), 32); @@ -29,9 +31,8 @@ fn output_length_changes_the_function() { #[test] fn customization_separates_the_functions() { let key = key_material(&[0x42u8; 32]); - let plain = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"); - let custom = - KMAC128::new_with_params(&key, b"My Tagged Application", 32, false).unwrap().mac(b"abc"); + let plain = KMAC128Key::with_params(&key, b"", 32).unwrap().mac(b"abc"); + let custom = KMAC128Key::with_params(&key, b"My Tagged Application", 32).unwrap().mac(b"abc"); assert_ne!(plain, custom, "a customization string must change the output"); } @@ -41,29 +42,21 @@ fn streaming_and_verification() { let key = key_material(&[0x11u8; 32]); let msg: Vec = (0..=255u8).collect(); - let one = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(&msg); + let key = KMAC128Key::with_params(&key, b"", 32).unwrap(); + let one = key.mac(&msg); - let mut k = KMAC128::new_with_params(&key, b"", 32, false).unwrap(); + let mut k = key.create_generator(); for chunk in msg.chunks(13) { k.do_update(chunk); } assert_eq!(k.do_final(), one, "chunked input must equal the one-shot"); - assert!( - KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &one), - "the correct tag must verify" - ); + assert!(key.verify(&msg, &one), "the correct tag must verify"); let mut wrong = one.clone(); wrong[0] ^= 1; - assert!( - !KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &wrong), - "a corrupted tag must not verify" - ); - assert!( - !KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &one[..16]), - "a truncated tag must not verify" - ); + assert!(!key.verify(&msg, &wrong), "a corrupted tag must not verify"); + assert!(!key.verify(&msg, &one[..16]), "a truncated tag must not verify"); } /// Sec 8.4.1 wants the key at least as long as the security strength; the tag on the key material @@ -76,25 +69,55 @@ fn weak_keys_are_refused_unless_allowed() { weak.security_strength() < bouncycastle_core::security_strength::SecurityStrength::_256bit ); - assert!(KMAC256::new(&weak).is_err(), "a 128-bit key must not instantiate KMAC256"); - assert!(KMAC256::new_allow_weak_key(&weak).is_ok(), "... unless explicitly allowed"); - assert!(KMAC128::new(&weak).is_ok(), "but it is enough for KMAC128"); + assert!( + KMAC256Key::from_key_material(&weak).is_err(), + "a 128-bit key must not make a KMAC256 key" + ); + assert!( + KMAC256Key::from_key_material_allow_weak(&weak).is_ok(), + "... unless explicitly allowed" + ); + assert!(KMAC256Key::with_params(&weak, b"S", 16).is_err()); + assert!(KMAC256Key::with_params_allow_weak(&weak, b"S", 16).is_ok()); + assert!(KMAC128Key::from_key_material(&weak).is_ok(), "but it is enough for KMAC128"); } -/// The default constructor: no customization, nominal output length. +/// The construction suite for both keys: a weak key accepted only through the `_allow_weak` +/// form, a no-strength key refused at any strength, generate at the key's strength. #[test] -fn default_constructor_uses_the_nominal_length() { +fn the_keys_enforce_the_material_policy() { let key = key_material(&[0x42u8; 32]); - assert_eq!(KMAC128::new(&key).unwrap().output_len(), 32); - assert_eq!(KMAC256::new(&key).unwrap().output_len(), 64); + let framework = TestFrameworkFromAnyKeyMaterial::new(); + framework.test::(&key); + framework.test::(&key); +} + +/// `from_key_material`: no customization, nominal output length. +#[test] +fn from_key_material_uses_the_nominal_length() { + let key = key_material(&[0x42u8; 32]); + assert_eq!(KMAC128Key::from_key_material(&key).unwrap().create_generator().output_len(), 32); + assert_eq!(KMAC256Key::from_key_material(&key).unwrap().create_generator().output_len(), 64); // ... and agrees with spelling the same thing out in full. assert_eq!( - KMAC128::new(&key).unwrap().mac(b"abc"), - KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"), + KMAC128Key::from_key_material(&key).unwrap().mac(b"abc"), + KMAC128Key::with_params(&key, b"", 32).unwrap().mac(b"abc"), ); } +/// `generate` draws a key of the strength's length and tags it a MAC key, so the key it builds +/// is a full-strength one at the nominal length. +#[test] +fn generate_draws_a_key_at_the_nominal_length() { + let mut rng = bouncycastle_rng::HashDRBG_SHA512::new_from_os(); + let key = KMAC256Key::generate(&mut rng).unwrap(); + assert_eq!(key.create_generator().output_len(), 64); + let tag = key.mac(b"abc"); + assert!(key.verify(b"abc", &tag)); + assert_ne!(KMAC256Key::generate(&mut rng).unwrap().mac(b"abc"), tag, "a fresh key each time"); +} + #[test] fn algorithm_names() { assert_eq!(KMAC128::ALG_NAME, "KMAC128"); @@ -125,7 +148,7 @@ fn kmacxof_output_is_one_stream() { assert_ne!(&long[..32], &via_hash[..], "the Hash view binds L, so it leaves the stream"); assert_eq!( via_hash, - KMAC128::new(&key).unwrap().mac(b"abc"), + KMAC128Key::from_key_material(&key).unwrap().mac(b"abc"), "... and lands on fixed-length KMAC128 at the nominal length" ); } @@ -154,19 +177,19 @@ fn kmacxof_algorithm_names() { assert_eq!(KMACXOF256::ALG_NAME, "KMACXOF256"); } -/// `new_allow_weak_key` is `new` without the strength check: same customization, same nominal -/// length, same tag. +/// `from_key_material_allow_weak` is `from_key_material` without the strength check: same +/// customization, same nominal length, same tag. #[test] -fn new_allow_weak_key_uses_the_nominal_length() { +fn from_key_material_allow_weak_uses_the_nominal_length() { let key = key_material(&[0x42u8; 32]); - let k = KMAC128::new_allow_weak_key(&key).unwrap(); - assert_eq!(k.output_len(), 32); - assert_eq!(k.mac(b"abc"), KMAC128::new(&key).unwrap().mac(b"abc")); + let k = KMAC128Key::from_key_material_allow_weak(&key).unwrap(); + assert_eq!(k.create_generator().output_len(), 32); + assert_eq!(k.mac(b"abc"), KMAC128Key::from_key_material(&key).unwrap().mac(b"abc")); - let k = KMAC256::new_allow_weak_key(&key).unwrap(); - assert_eq!(k.output_len(), 64); - assert_eq!(k.mac(b"abc"), KMAC256::new(&key).unwrap().mac(b"abc")); + let k = KMAC256Key::from_key_material_allow_weak(&key).unwrap(); + assert_eq!(k.create_generator().output_len(), 64); + assert_eq!(k.mac(b"abc"), KMAC256Key::from_key_material(&key).unwrap().mac(b"abc")); } /// The same stance as HMAC: a key tagged `MACKey` or `Zeroized` is accepted, anything else is @@ -177,11 +200,11 @@ fn key_type_is_checked() { let cipher_key = KeyMaterial::<32>::from_bytes_as_type(&[0x42u8; 32], KeyType::SymmetricCipherKey).unwrap(); assert!(matches!( - KMAC128::new(&cipher_key), + KMAC128Key::from_key_material(&cipher_key), Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) )); assert!(matches!( - KMAC128::new_with_params(&cipher_key, b"", 32, true), + KMAC128Key::with_params_allow_weak(&cipher_key, b"", 32), Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) )); assert!(matches!( @@ -191,9 +214,15 @@ fn key_type_is_checked() { let zero = KeyMaterial::<32>::new(); assert_eq!(zero.key_type(), KeyType::Zeroized); - assert!(KMAC128::new(&zero).is_err(), "a zeroized key has no security strength"); - assert!(KMAC128::new_with_params(&zero, b"", 32, true).is_ok(), "... but is the right type"); - assert!(KMAC128::new_allow_weak_key(&zero).is_ok()); + assert!( + KMAC128Key::from_key_material(&zero).is_err(), + "a zeroized key has no security strength" + ); + assert!( + KMAC128Key::with_params_allow_weak(&zero, b"", 32).is_ok(), + "... but is the right type" + ); + assert!(KMAC128Key::from_key_material_allow_weak(&zero).is_ok()); assert!(KMACXOF128::new(&zero, b"", true).is_ok()); } diff --git a/crypto/sha3/tests/kmac_wycheproof.rs b/crypto/sha3/tests/kmac_wycheproof.rs index 5e01ef607..311139f2c 100644 --- a/crypto/sha3/tests/kmac_wycheproof.rs +++ b/crypto/sha3/tests/kmac_wycheproof.rs @@ -11,18 +11,18 @@ //! KMAC of that size and goes through `verify`, as well as through `mac` for the valid ones. use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::MAC; +use bouncycastle_core::symmetric_key::{MacGenerationKey, MacVerificationKey}; use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; -use bouncycastle_sha3::kmac::{KMAC128, KMAC256}; +use bouncycastle_sha3::kmac::{KMAC128Key, KMAC256Key}; /// The longest key in either file is 129 bytes. const MAX_KEY_LEN: usize = 160; -/// Runs every case in one KMAC file; `new_kmac(key, output_len)` builds the KMAC under test. -fn run( +/// Runs every case in one KMAC file; `new_kmac(key, output_len)` builds the KMAC key under test. +fn run( filename: &str, algorithm: &str, - new_kmac: impl Fn(&KeyMaterial, usize) -> M, + new_kmac: impl Fn(&KeyMaterial, usize) -> K, ) { let Some(doc) = wycheproof_json(filename) else { return }; @@ -67,13 +67,13 @@ fn run( #[test] fn wycheproof_kmac128() { run("kmac128_no_customization_test.json", "KMAC128", |key, output_len| { - KMAC128::new_with_params(key, b"", output_len, false).expect("a KMAC128 instance") + KMAC128Key::with_params(key, b"", output_len).expect("a KMAC128 key") }); } #[test] fn wycheproof_kmac256() { run("kmac256_no_customization_test.json", "KMAC256", |key, output_len| { - KMAC256::new_with_params(key, b"", output_len, false).expect("a KMAC256 instance") + KMAC256Key::with_params(key, b"", output_len).expect("a KMAC256 key") }); } diff --git a/crypto/sha3/tests/sp800_185_suspend_tests.rs b/crypto/sha3/tests/sp800_185_suspend_tests.rs index a017904fd..d925b7529 100644 --- a/crypto/sha3/tests/sp800_185_suspend_tests.rs +++ b/crypto/sha3/tests/sp800_185_suspend_tests.rs @@ -7,6 +7,7 @@ use bouncycastle_core::errors::SuspendableError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; use bouncycastle_core::traits::{Hash, MAC, Suspendable, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; use bouncycastle_sha3::kmac::*; @@ -92,10 +93,10 @@ fn kmac_round_trips() { resumed.do_update(PART2); assert!(resumed.do_verify_final(&expected)); } - mac_round_trip(|| KMAC128::new(&key()).unwrap()); - mac_round_trip(|| KMAC256::new(&key()).unwrap()); + mac_round_trip(|| KMAC128Key::from_key_material(&key()).unwrap().create_generator()); + mac_round_trip(|| KMAC256Key::from_key_material(&key()).unwrap().create_generator()); mac_round_trip(|| { - KMAC128::new_with_params(&key(), b"My Tagged Application", 16, false).unwrap() + KMAC128Key::with_params(&key(), b"My Tagged Application", 16).unwrap().create_generator() }); } @@ -156,7 +157,7 @@ fn each_type_rejects_the_others() { rejected::<_, KMACXOF128>(squeezer.clone().suspend(), "an unbound squeezer in KMACXOF128"); rejected::<_, CSHAKE128>(squeezer.suspend(), "an unbound squeezer in cSHAKE128"); - let mut k = KMAC128::new(&key()).unwrap(); + let mut k = KMAC128Key::from_key_material(&key()).unwrap().create_generator(); k.do_update(PART1); rejected::<_, TupleHash128>(k.clone().suspend(), "a KMAC128 state in TupleHash128"); rejected::<_, KMAC256>(k.suspend(), "a KMAC128 state in KMAC256"); @@ -191,7 +192,7 @@ fn corrupt_fields_are_rejected() { bad[SQUEEZING_FLAG] = 1; rejected::<_, CSHAKE128>(bad, "a squeezing sponge in an absorbing cSHAKE"); - let mut k = KMAC128::new(&key()).unwrap(); + let mut k = KMAC128Key::from_key_material(&key()).unwrap().create_generator(); k.do_update(PART1); let mut bad = k.suspend(); bad[CUSTOMIZED] = 0; @@ -241,7 +242,7 @@ fn state_tags_are_distinct() { tag(SHAKE128::new()), tag(SHAKE256::new()), tag(CSHAKE128::new(b"", b"S")), - tag(KMAC128::new(&key()).unwrap()), + tag(KMAC128Key::from_key_material(&key()).unwrap().create_generator()), tag(KMACXOF128::new(&key(), b"", false).unwrap()), tag(TupleHash128::new(b"", 32)), tag(TupleHashXOF128::new(b"")), @@ -249,7 +250,7 @@ fn state_tags_are_distinct() { tag(ParallelHashXOF128::new(8, b"")), tag(TupleHashXOF128::new(b"").into_squeezer()), tag(CSHAKE256::new(b"", b"S")), - tag(KMAC256::new(&key()).unwrap()), + tag(KMAC256Key::from_key_material(&key()).unwrap().create_generator()), tag(KMACXOF256::new(&key(), b"", false).unwrap()), tag(TupleHash256::new(b"", 32)), tag(TupleHashXOF256::new(b"")), diff --git a/mem_usage_benches/src/bench_sha3_mem_usage.rs b/mem_usage_benches/src/bench_sha3_mem_usage.rs index 006c0b637..5c2a493d2 100644 --- a/mem_usage_benches/src/bench_sha3_mem_usage.rs +++ b/mem_usage_benches/src/bench_sha3_mem_usage.rs @@ -26,7 +26,7 @@ #![allow(unused_imports)] use bouncycastle::core::traits::{Hash, Suspendable, XOF, XOFSqueezer}; -use bouncycastle::sha3::kmac::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; +use bouncycastle::sha3::kmac::{KMAC128, KMAC128Key, KMAC256, KMAC256Key, KMACXOF128, KMACXOF256}; use bouncycastle::sha3::parallelhash::{ ParallelHash128, ParallelHash256, ParallelHashXOF128, ParallelHashXOF256, }; @@ -56,6 +56,8 @@ fn print_struct_sizes() { println!("\nSP 800-185"); println!("size_of: {}", size_of::()); println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); println!("size_of: {}", size_of::()); println!("size_of: {}", size_of::()); println!("size_of: {}", size_of::()); From a21a58614ebbb44c16c3bdfa83cc1b91bc25e99e Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 23:39:42 +1100 Subject: [PATCH 09/54] hmac: HmacKey::generate caps the strength at the key length As KeyMaterial::from_rng now does: a 28-byte HMAC-SHA224 key drawn from a 256-bit DRBG was tagged 256-bit, which set_security_strength refuses for that length. Assisted-by: Claude:claude-mythos-5-1 --- crypto/hmac/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index 21e8ff668..ec7d6105d 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -306,7 +306,11 @@ impl FromAnyKeyMaterial for HmacKey { .map_err(|_| KeyMaterialError::GenericError("RNG failed."))?; key.set_key_len(output_len)?; key.set_key_type(KeyType::MACKey)?; - key.set_security_strength(rng.security_strength()) + // The RNG's strength, capped at what the length carries, as KeyMaterial::from_rng + // does: a 28-byte HMAC-SHA224 key from a 256-bit DRBG is a 192-bit key. + let by_len = SecurityStrength::from_bits(output_len * 8); + let by_rng = rng.security_strength(); + key.set_security_strength(if by_rng < by_len { by_rng } else { by_len }) })?; Self::from_key_material(&key) } From 3722b36ca896e38db5bb619633b3c938a2263389 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 19 Oct 2026 23:40:06 +1100 Subject: [PATCH 10/54] factory: MACFactory builds the MAC key objects (prototype) MACFactory::new makes an HmacKey or KMACKey from the material and hands out its generator, in place of MAC::new on the worker; the trait's own new and new_allow_weak_key arms go with the deletion. Assisted-by: Claude:claude-mythos-5-1 --- crypto/factory/Cargo.toml | 1 + crypto/factory/src/mac_factory.rs | 58 ++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/crypto/factory/Cargo.toml b/crypto/factory/Cargo.toml index c97657960..7350cb154 100644 --- a/crypto/factory/Cargo.toml +++ b/crypto/factory/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] bouncycastle-ascon.workspace = true bouncycastle-core.workspace = true +bouncycastle-hmac.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true bouncycastle-sm3.workspace = true diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index f040a8306..fe0509c2a 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -74,7 +74,9 @@ use crate::{DEFAULT, DEFAULT_128_BIT, DEFAULT_256_BIT, FactoryError}; use bouncycastle_core::errors::MACError; use bouncycastle_core::key_material::KeyMaterialTrait; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; use bouncycastle_core::traits::MAC; +use bouncycastle_hmac::HmacKey; use bouncycastle_sha2 as sha2; use bouncycastle_sha2::hmac::{ HMAC_SHA224_NAME, HMAC_SHA256_NAME, HMAC_SHA384_NAME, HMAC_SHA512_224_NAME, @@ -84,7 +86,9 @@ use bouncycastle_sha3 as sha3; use bouncycastle_sha3::hmac::{ HMAC_SHA3_224_NAME, HMAC_SHA3_256_NAME, HMAC_SHA3_384_NAME, HMAC_SHA3_512_NAME, }; -use bouncycastle_sha3::kmac::{KMAC128, KMAC128_NAME, KMAC256, KMAC256_NAME}; +use bouncycastle_sha3::kmac::{ + KMAC128, KMAC128_NAME, KMAC128Key, KMAC256, KMAC256_NAME, KMAC256Key, +}; use bouncycastle_sm3 as sm3; use bouncycastle_sm3::hmac::HMAC_SM3_NAME; @@ -153,23 +157,45 @@ impl MACFactory { DEFAULT => Self::default(key), DEFAULT_128_BIT => Self::default_128_bit(key), DEFAULT_256_BIT => Self::default_256_bit(key), - KMAC128_NAME => Ok(Self::KMAC128(KMAC128::new(key)?)), - KMAC256_NAME => Ok(Self::KMAC256(KMAC256::new(key)?)), - HMAC_SHA224_NAME => Ok(Self::HMAC_SHA224(sha2::hmac::HMAC_SHA224::new(key)?)), - HMAC_SHA256_NAME => Ok(Self::HMAC_SHA256(sha2::hmac::HMAC_SHA256::new(key)?)), - HMAC_SHA384_NAME => Ok(Self::HMAC_SHA384(sha2::hmac::HMAC_SHA384::new(key)?)), - HMAC_SHA512_NAME => Ok(Self::HMAC_SHA512(sha2::hmac::HMAC_SHA512::new(key)?)), - HMAC_SHA512_224_NAME => { - Ok(Self::HMAC_SHA512_224(sha2::hmac::HMAC_SHA512_224::new(key)?)) + KMAC128_NAME => { + Ok(Self::KMAC128(KMAC128Key::from_key_material(key)?.create_generator())) + } + KMAC256_NAME => { + Ok(Self::KMAC256(KMAC256Key::from_key_material(key)?.create_generator())) } - HMAC_SHA512_256_NAME => { - Ok(Self::HMAC_SHA512_256(sha2::hmac::HMAC_SHA512_256::new(key)?)) + HMAC_SHA224_NAME => Ok(Self::HMAC_SHA224( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA256_NAME => Ok(Self::HMAC_SHA256( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA384_NAME => Ok(Self::HMAC_SHA384( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA512_NAME => Ok(Self::HMAC_SHA512( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA512_224_NAME => Ok(Self::HMAC_SHA512_224( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA512_256_NAME => Ok(Self::HMAC_SHA512_256( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA3_224_NAME => Ok(Self::HMAC_SHA3_224( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA3_256_NAME => Ok(Self::HMAC_SHA3_256( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA3_384_NAME => Ok(Self::HMAC_SHA3_384( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SHA3_512_NAME => Ok(Self::HMAC_SHA3_512( + HmacKey::::from_key_material(key)?.create_generator(), + )), + HMAC_SM3_NAME => { + Ok(Self::HMAC_SM3(HmacKey::::from_key_material(key)?.create_generator())) } - HMAC_SHA3_224_NAME => Ok(Self::HMAC_SHA3_224(sha3::hmac::HMAC_SHA3_224::new(key)?)), - HMAC_SHA3_256_NAME => Ok(Self::HMAC_SHA3_256(sha3::hmac::HMAC_SHA3_256::new(key)?)), - HMAC_SHA3_384_NAME => Ok(Self::HMAC_SHA3_384(sha3::hmac::HMAC_SHA3_384::new(key)?)), - HMAC_SHA3_512_NAME => Ok(Self::HMAC_SHA3_512(sha3::hmac::HMAC_SHA3_512::new(key)?)), - HMAC_SM3_NAME => Ok(Self::HMAC_SM3(sm3::hmac::HMAC_SM3::new(key)?)), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known MAC", alg_name From 1e192d1782317c7d004b6ce504e9628ffad9d191 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 00:39:00 +1100 Subject: [PATCH 11/54] aes: the symmetric, block and ECB roles on AESKey, and everything onto the key path (prototype) AESKey forwards the symmetric, block and hazmat ECB role pairs to M's mode traits, so AES128Key>, >, , , , >, > and > are keys. The tests, benches, the CCM memory harness and the module docs move off the trait constructors and static one-shots onto the keys: the known-answer suites pin their IVs through create_encryptor and a FixedSeedRNG as before, the generic helpers are generic over the key type, the construction suite replaces the per-test wrong-key checks, and mode_key_tests.rs runs the framework over every new mode key. Dropped with no key path equivalent: the CMAC new_allow_weak_key acceptances and the wrong-length KeyMaterial512 refusals (a cipher key's length is in its type); the fixed-frame CCM key's one-shots take the frame length, so the "any length" cases move to the CCM packet key. The modes benches draw IVs from a FixedSeedRNG outside the timed closure, where the trait constructors drew from an OS DRBG inside it. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/benches/aes_gcm_packet_benches.rs | 87 ++- crypto/aes/benches/aes_modes_benches.rs | 387 +++++++------ crypto/aes/src/cbc.rs | 115 ++-- crypto/aes/src/ccm.rs | 114 ++-- crypto/aes/src/cfb.rs | 65 +-- crypto/aes/src/cfb8.rs | 83 ++- crypto/aes/src/cmac.rs | 27 +- crypto/aes/src/ctr.rs | 74 ++- crypto/aes/src/gcm.rs | 96 ++-- crypto/aes/src/hazmat/ecb.rs | 142 +++-- crypto/aes/src/key.rs | 197 +++++-- crypto/aes/src/lib.rs | 6 +- crypto/aes/tests/cbc_bc-test-data.rs | 40 +- crypto/aes/tests/cbc_wycheproof.rs | 72 ++- crypto/aes/tests/cfb8_bc-test-data.rs | 47 +- crypto/aes/tests/cfb_bc-test-data.rs | 47 +- crypto/aes/tests/cmac_bc-test-data.rs | 19 +- crypto/aes/tests/cmac_key_tests.rs | 31 +- crypto/aes/tests/cmac_tests.rs | 115 ++-- crypto/aes/tests/cmac_wycheproof.rs | 37 +- crypto/aes/tests/ctr_bc-test-data.rs | 47 +- crypto/aes/tests/ctr_bc_java_tests.rs | 29 +- crypto/aes/tests/ctr_vector_tests.rs | 42 +- crypto/aes/tests/gcm_tests.rs | 74 +-- crypto/aes/tests/mode_key_tests.rs | 180 ++++++ crypto/aes/tests/padded_alias_tests.rs | 129 +++-- crypto/aes/tests/sp800_38a_cbc_tests.rs | 71 +-- crypto/aes/tests/sp800_38a_cfb8_tests.rs | 59 +- crypto/aes/tests/sp800_38a_cfb_tests.rs | 69 +-- crypto/aes/tests/sp800_38c_tests.rs | 534 ++++++++---------- crypto/aes/tests/suspend_tests.rs | 105 ++-- .../core-test-framework/src/block_cipher.rs | 2 +- mem_usage_benches/src/bench_ccm_mem_usage.rs | 45 +- 33 files changed, 1691 insertions(+), 1496 deletions(-) create mode 100644 crypto/aes/tests/mode_key_tests.rs diff --git a/crypto/aes/benches/aes_gcm_packet_benches.rs b/crypto/aes/benches/aes_gcm_packet_benches.rs index 6d4aa1dab..41f7ef5e3 100644 --- a/crypto/aes/benches/aes_gcm_packet_benches.rs +++ b/crypto/aes/benches/aes_gcm_packet_benches.rs @@ -1,15 +1,15 @@ -//! AES-256-GCM over 16 KiB packets: the per-packet cost of starting from raw key material, -//! which validates and expands the key for every packet, against starting from an -//! `AES256Key`, which did that once (and derived GCM's hash subkey once) and hands the -//! stored state to each packet's encryptor. +//! AES-256-GCM over 16 KiB packets: the per-packet cost of building the `AES256Key` from raw +//! key material for every packet, which validates and expands the key and derives GCM's hash +//! subkey each time, against building it once and handing the stored state to each packet's +//! encryptor. //! -//! Both arms stream one packet through the same `Gcm` type -- init, one `do_encrypt_out` / -//! `do_decrypt_out` over the whole packet, final -- so the only difference between them is the -//! key expansion, validation and `H`. The nonce comes from a fixed test RNG so that the RNG is not -//! part of the comparison, except in the `OS DRBG per packet` arms, which instantiate a fresh -//! `HashDRBG_SHA512` from the OS for every packet: the cost of the default-RNG constructors the -//! key path does not offer. The `init` group isolates the construction cost with no data at all, -//! and includes the one-off cost of building the key object. +//! Both arms stream one packet through the same `Gcm` type -- `create_encryptor` / +//! `create_decryptor`, one `do_encrypt_out` / `do_decrypt_out` over the whole packet, final -- so +//! the only difference between them is where `from_key_material` is paid. The nonce comes from a +//! fixed test RNG so that the RNG is not part of the comparison, except in the `OS DRBG per +//! packet` arms, which instantiate a fresh `HashDRBG_SHA512` from the OS for every packet: what a +//! per-packet default RNG would add. The `init` group isolates the construction cost with no data +//! at all. //! //! The cipher works in place, so each measurement runs on a fresh output buffer made in //! criterion's untimed setup (`iter_batched_ref`); the copy is not part of the timing. @@ -77,11 +77,14 @@ fn bench_encrypt_packets(c: &mut Criterion) { let mut group = c.benchmark_group("gcm::AES_256 16KiB packet encrypt"); group.throughput(Throughput::Bytes(PACKET_LEN as u64)); - group.bench_function("KeyMaterial: do_encrypt_init_rng (expand per packet)", |b| { + group.bench_function("AES256Key: from_key_material per packet (expand per packet)", |b| { b.iter_batched_ref( || [0u8; PACKET_LEN], |out| { - let (enc, nonce) = Enc::do_encrypt_init_rng(black_box(&km), &mut rng).unwrap(); + let (enc, nonce) = Key::from_key_material(black_box(&km)) + .unwrap() + .create_encryptor(&mut rng) + .unwrap(); black_box((nonce, encrypt_packet(enc, black_box(&packet), out))) }, BatchSize::LargeInput, @@ -98,12 +101,16 @@ fn bench_encrypt_packets(c: &mut Criterion) { ) }); group.bench_function( - "KeyMaterial: do_encrypt_init (OS DRBG per packet, expand per packet)", + "AES256Key: from_key_material per packet (OS DRBG per packet, expand per packet)", |b| { b.iter_batched_ref( || [0u8; PACKET_LEN], |out| { - let (enc, nonce) = Enc::do_encrypt_init(black_box(&km)).unwrap(); + let mut os_rng = HashDRBG_SHA512::new_from_os(); + let (enc, nonce) = Key::from_key_material(black_box(&km)) + .unwrap() + .create_encryptor(&mut os_rng) + .unwrap(); black_box((nonce, encrypt_packet(enc, black_box(&packet), out))) }, BatchSize::LargeInput, @@ -132,17 +139,17 @@ fn bench_decrypt_packets(c: &mut Criterion) { // One valid packet to decrypt, made outside the timing. let mut ct = [0u8; PACKET_LEN]; - let (enc, nonce) = Enc::do_encrypt_init_rng(&km, &mut rng()).unwrap(); + let (enc, nonce) = key.create_encryptor(&mut rng()).unwrap(); let tag = encrypt_packet(enc, &packet, &mut ct); let mut group = c.benchmark_group("gcm::AES_256 16KiB packet decrypt"); group.throughput(Throughput::Bytes(PACKET_LEN as u64)); - group.bench_function("KeyMaterial: do_decrypt_init (expand per packet)", |b| { + group.bench_function("AES256Key: from_key_material per packet (expand per packet)", |b| { b.iter_batched_ref( || [0u8; PACKET_LEN], |out| { - let dec = Dec::do_decrypt_init(black_box(&km), &nonce).unwrap(); + let dec = Key::from_key_material(black_box(&km)).unwrap().create_decryptor(&nonce); black_box(decrypt_packet(dec, black_box(&ct), &tag, out)) }, BatchSize::LargeInput, @@ -174,21 +181,47 @@ fn bench_init(c: &mut Criterion) { group.bench_function("AES256Key::from_key_material (once per key)", |b| { b.iter(|| black_box(Key::from_key_material(black_box(&km)).unwrap())) }); - group.bench_function("KeyMaterial: do_encrypt_init_rng", |b| { - b.iter(|| black_box(Enc::do_encrypt_init_rng(black_box(&km), &mut rng).unwrap())) - }); + group.bench_function( + "AES256Key: from_key_material + create_encryptor (per packet)", + |b| { + b.iter(|| { + black_box( + Key::from_key_material(black_box(&km)) + .unwrap() + .create_encryptor(&mut rng) + .unwrap(), + ) + }) + }, + ); group.bench_function("AES256Key: create_encryptor", |b| { b.iter(|| black_box(black_box(&key).create_encryptor(&mut rng).unwrap())) }); - group.bench_function("KeyMaterial: do_decrypt_init", |b| { - b.iter(|| black_box(Dec::do_decrypt_init(black_box(&km), &nonce).unwrap())) - }); + group.bench_function( + "AES256Key: from_key_material + create_decryptor (per packet)", + |b| { + b.iter(|| { + black_box(Key::from_key_material(black_box(&km)).unwrap().create_decryptor(&nonce)) + }) + }, + ); group.bench_function("AES256Key: create_decryptor", |b| { b.iter(|| black_box(black_box(&key).create_decryptor(&nonce))) }); - group.bench_function("KeyMaterial: do_encrypt_init (OS DRBG per call)", |b| { - b.iter(|| black_box(Enc::do_encrypt_init(black_box(&km)).unwrap())) - }); + group.bench_function( + "AES256Key: from_key_material + create_encryptor (OS DRBG per call)", + |b| { + b.iter(|| { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + black_box( + Key::from_key_material(black_box(&km)) + .unwrap() + .create_encryptor(&mut os_rng) + .unwrap(), + ) + }) + }, + ); group.bench_function("AES256Key: create_encryptor (OS DRBG per call)", |b| { b.iter(|| { let mut os_rng = HashDRBG_SHA512::new_from_os(); diff --git a/crypto/aes/benches/aes_modes_benches.rs b/crypto/aes/benches/aes_modes_benches.rs index 9714ad81b..099ef1d6f 100644 --- a/crypto/aes/benches/aes_modes_benches.rs +++ b/crypto/aes/benches/aes_modes_benches.rs @@ -23,21 +23,21 @@ //! The cipher works in place, so each measurement runs on a fresh copy of the data made in //! criterion's untimed setup (`iter_batched`); the copy is not part of the timing. -use bouncycastle_aes::hazmat::{AES128Internal, AES256Internal}; -use bouncycastle_aes::{ - AES_CMAC_128, AES_CMAC_256, AES_GMAC_128, AES_GMAC_256, AES128Key, AES256Key, -}; -use bouncycastle_cipher::modes::hazmat::ecb::Ecb; +use bouncycastle_aes::hazmat::AES128Internal; +use bouncycastle_aes::{AES128Key, AES256Key}; +use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{ - CCM, CMAC, Cbc, CcmEncryptor, Cfb, Cfb8, Ctr, GCM_NONCE_LEN, GMAC, Gcm, + Blocks, CBC, CCM, CCM_Packet, CFB, CFB8, CMAC, CTR, Cfb, Cfb8, Ctr, GCM, GCM_NONCE_LEN, GMAC, }; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::{AeadPacketEncryptMode, ElectronicCodeBook}; +use bouncycastle_core::hazmat::{AeadDeclaredEncryptMode, EcbEncryptionKey, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey}; +use bouncycastle_core::symmetric_key::{ + AeadEncryptionKey, BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, MacGenerationKey, + SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{ - AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, MAC, StreamCipherDecryptor, - StreamCipherEncryptor, SymmetricCipherEncryptor, + BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; @@ -48,9 +48,8 @@ const BLOCK_LEN: usize = 16; const NUM_BLOCKS: usize = 1024; const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN; -type Aes128Cbc = Cbc; -type Aes128Cfb = Cfb; -type Aes128Cfb8 = Cfb8; +type Aes128Cfb<'k, Dir> = Cfb<'k, AES128Internal, Dir, BLOCK_LEN>; +type Aes128Cfb8<'k, Dir> = Cfb8<'k, AES128Internal, Dir, BLOCK_LEN>; /// CCM at the parameters the ACVP vectors and most protocols use: a 12-byte nonce and a full /// 16-byte tag. The mode's in-place one-shots over the keyed permutation are the direct path; @@ -59,32 +58,31 @@ const CCM_NONCE_LEN: usize = 12; const CCM_TAG_LEN: usize = 16; type Aes128CcmMode = CCM; -/// The trait adapter takes its frame size at compile time. Its one-shots are not bound by it, -/// but using the same 4 KiB message keeps this comparison representative of the public alias a +/// The frame key takes its frame size at compile time. Its one-shots are not bound by it, but +/// using the same 4 KiB message keeps this comparison representative of the public alias a /// packet protocol would choose. const CCM_BUFFER_LEN: usize = 4096; const CCM_AAD_LEN: usize = 64; -type Aes128CcmEncryptor = CcmEncryptor< - AES128Internal, - 16, - BLOCK_LEN, - CCM_NONCE_LEN, - CCM_TAG_LEN, - CCM_AAD_LEN, - CCM_BUFFER_LEN, ->; +type Aes128CcmFrameKey = + AES128Key>; /// GCM with the full 16-byte tag, as the `AES_GCM_*` aliases fix it. const GCM_TAG_LEN: usize = 16; -type Aes128Gcm = Gcm; -type Aes256Gcm = Gcm; -type Aes128Ctr = Ctr; -type Aes128Ecb = Ecb; +type Aes128GcmKey = AES128Key>; +type Aes256GcmKey = AES256Key>; +type Aes128Ctr<'k, Dir> = Ctr<'k, AES128Internal, Dir, BLOCK_LEN, 12>; +type Aes128Ecb<'k, Dir> = Ecb<'k, AES128Internal, Dir, BLOCK_LEN>; fn key() -> KeyMaterial { let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() } +/// IVs and nonces for the throughput measurements come from a cheap deterministic RNG created +/// outside the timed loop, so they measure the mode and not OS entropy or DRBG construction. +fn iv_rng() -> FixedSeedRNG { + FixedSeedRNG::::new([0x24u8; BLOCK_LEN]) +} + /// [`key`]'s bytes as a `KeyType::MACKey`, which is what CMAC takes. fn mac_key() -> KeyMaterial { let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); @@ -98,22 +96,20 @@ fn data() -> Vec<[u8; BLOCK_LEN]> { } fn bench_aes128cbc(c: &mut Criterion) { - let k = key::<16>(); + let key = AES128Key::>::from_key_material(&key::<16>()).unwrap(); + let mut rng = iv_rng(); let blocks = data(); let mut group = c.benchmark_group("modes::cbc::AES_128"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); - // Serial: one block at a time is all it can do, whatever the call length. group.bench_function("16KiB encrypt -- N=8", |b| { b.iter_batched( || blocks.clone(), |mut scratch| { - let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); + let (mut enc, _) = key.create_encryptor(&mut rng).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_blocks_inplace(arr.as_chunks_mut().0).unwrap(); + enc.do_encrypt_blocks_inplace(chunk).unwrap(); } black_box(&scratch); }, @@ -121,22 +117,20 @@ fn bench_aes128cbc(c: &mut Criterion) { ) }); - let (mut enc, iv) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); + // ---- decryption: parallel, uses decrypt_2blocks for every pair ---- + let (mut enc, iv) = key.create_encryptor(&mut rng).unwrap(); let mut ciphertext = blocks.clone(); for chunk in ciphertext.chunks_exact_mut(8) { enc.do_encrypt_blocks_inplace(chunk).unwrap(); } - // Parallel: N=8 is two fours, so every block goes through decrypt_4blocks. group.bench_function("16KiB decrypt -- N=8 (all fours)", |b| { b.iter_batched( || ciphertext.clone(), |mut scratch| { - let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); 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_blocks_inplace(arr.as_chunks_mut().0).unwrap(); + dec.do_decrypt_blocks_inplace(chunk).unwrap(); } black_box(&scratch); }, @@ -144,86 +138,80 @@ fn bench_aes128cbc(c: &mut Criterion) { ) }); + // The controlled comparison: identical N, identical cipher, bit-sliced pair vs single-block loops. + // This pair of numbers -- and only this pair -- measures what `decrypt_2blocks` buys. group.finish(); } -/// Runs the 16 KiB through a stream-cipher encryptor in `call_len`-byte calls. Used by the CFB, -/// CFB8 and CTR groups: it is generic over the trait, not over the mode. -fn cfb_encrypt_in_calls< - E: StreamCipherEncryptor, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, ->( - k: &KeyMaterial, +/// Runs the 16 KiB through a freshly made stream-cipher encryptor in `call_len`-byte calls. Used +/// by the CFB, CFB8 and CTR groups: it is generic over the trait, not over the mode, and the +/// caller's key (or mode constructor) supplies the worker. +fn stream_encrypt_in_calls, const INIT_DATA_LEN: usize>( + mut enc: E, scratch: &mut [u8], call_len: usize, ) { - let (mut enc, _) = E::do_encrypt_init(k).unwrap(); for piece in scratch.chunks_mut(call_len) { enc.do_encrypt_inplace(piece).unwrap(); } } -/// Runs the 16 KiB through a stream-cipher decryptor in `call_len`-byte calls. Shared as above. -fn cfb_decrypt_in_calls< - D: StreamCipherDecryptor, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, ->( - k: &KeyMaterial, - iv: &[u8; INIT_DATA_LEN], +/// Runs the 16 KiB through a freshly made stream-cipher decryptor in `call_len`-byte calls. +/// Shared as above. +fn stream_decrypt_in_calls, const INIT_DATA_LEN: usize>( + mut dec: D, scratch: &mut [u8], call_len: usize, ) { - let mut dec = D::do_decrypt_init(k, iv).unwrap(); for piece in scratch.chunks_mut(call_len) { dec.do_decrypt_inplace(piece).unwrap(); } } fn bench_cfb_aes128(c: &mut Criterion) { - let k = key::<16>(); - let flat: Vec = data().as_flattened().to_vec(); + let key = AES128Key::::from_key_material(&key::<16>()).unwrap(); + let mut rng = iv_rng(); + let blocks = data(); + let flat: Vec = blocks.as_flattened().to_vec(); let mut group = c.benchmark_group("modes::cfb::AES_128"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); - // Serial: Oj+1 = CIPH_K(Cj), and Cj is the previous block's output. - group.bench_function("16KiB encrypt -- N=8", |b| { - b.iter_batched( - || flat.clone(), - |mut scratch| { - cfb_encrypt_in_calls::, 16, BLOCK_LEN>( - &k, - &mut scratch, - 8 * BLOCK_LEN, - ); - black_box(&scratch); - }, - BatchSize::LargeInput, - ) - }); + // ---- encryption: serial. Oj+1 = CIPH_K(Cj), and Cj is the previous call's output ---- + for (name, call_len) in [("16KiB encrypt -- N=8", 8 * BLOCK_LEN)] { + group.bench_function(name, |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + let (enc, _): (Aes128Cfb, _) = + key.create_encryptor(&mut rng).unwrap(); + stream_encrypt_in_calls(enc, &mut scratch, call_len); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } - let (mut enc, iv) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + // ---- decryption: parallel, and uses `encrypt_4blocks` / `encrypt_2blocks` -- the FORWARD + // batch methods ---- + let (mut enc, iv) = key.create_encryptor(&mut rng).unwrap(); let mut ciphertext = flat.clone(); enc.do_encrypt_inplace(&mut ciphertext).unwrap(); - // Parallel, through the FORWARD four-block path. - group.bench_function("16KiB decrypt -- N=8 (all fours)", |b| { - b.iter_batched( - || ciphertext.clone(), - |mut scratch| { - cfb_decrypt_in_calls::, 16, BLOCK_LEN>( - &k, - &iv, - &mut scratch, - 8 * BLOCK_LEN, - ); - black_box(&scratch); - }, - BatchSize::LargeInput, - ) - }); + for (name, call_len) in [("16KiB decrypt -- N=8 (all fours)", 8 * BLOCK_LEN)] { + group.bench_function(name, |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let dec: Aes128Cfb = key.create_decryptor(&iv); + stream_decrypt_in_calls(dec, &mut scratch, call_len); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } group.finish(); } @@ -234,41 +222,46 @@ fn bench_cfb_aes128(c: &mut Criterion) { /// Encryption is strictly serial. Decryption builds its input blocks in series and then runs them /// through `encrypt_4blocks`, so it should still beat encryption by the four path's margin. fn bench_cfb8_aes128(c: &mut Criterion) { - let k = key::<16>(); + let key = AES128Key::::from_key_material(&key::<16>()).unwrap(); + let mut rng = iv_rng(); let flat: Vec = data().as_flattened().to_vec(); let mut group = c.benchmark_group("modes::cfb8::AES_128"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); + // Serial by construction: I_{j+1} needs Cj, which this call just produced. group.bench_function("16KiB encrypt -- whole message in one call", |b| { b.iter_batched( || flat.clone(), |mut scratch| { - cfb_encrypt_in_calls::, 16, BLOCK_LEN>( - &k, &mut scratch, DATA_LEN, - ); + let (enc, _): (Aes128Cfb8, _) = key.create_encryptor(&mut rng).unwrap(); + stream_encrypt_in_calls(enc, &mut scratch, DATA_LEN); black_box(&scratch); }, BatchSize::LargeInput, ) }); - let (mut enc, iv) = Aes128Cfb8::::do_encrypt_init(&k).unwrap(); + let (mut enc, iv) = key.create_encryptor(&mut rng).unwrap(); let mut ciphertext = flat.clone(); enc.do_encrypt_inplace(&mut ciphertext).unwrap(); - group.bench_function("16KiB decrypt -- whole message in one call (batched)", |b| { - b.iter_batched( - || ciphertext.clone(), - |mut scratch| { - cfb_decrypt_in_calls::, 16, BLOCK_LEN>( - &k, &iv, &mut scratch, DATA_LEN, - ); - black_box(&scratch); - }, - BatchSize::LargeInput, - ) - }); + for (name, call_len) in [ + // One call: fours, then pairs, then the tail. This is the batched path. + ("16KiB decrypt -- whole message in one call (batched)", DATA_LEN), + ] { + group.bench_function(name, |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let dec: Aes128Cfb8 = key.create_decryptor(&iv); + stream_decrypt_in_calls(dec, &mut scratch, call_len); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } group.finish(); } @@ -277,26 +270,27 @@ fn bench_cfb8_aes128(c: &mut Criterion) { /// keystream function, so one measurement covers it. It should match CFB decryption, not CFB /// encryption. fn bench_ctr_aes128(c: &mut Criterion) { - let k = key::<16>(); + let key = AES128Key::::from_key_material(&key::<16>()).unwrap(); + let mut rng = FixedSeedRNG::<12>::new([0x24u8; 12]); let flat: Vec = data().as_flattened().to_vec(); let mut group = c.benchmark_group("modes::ctr::AES_128"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); - group.bench_function("16KiB encrypt -- N=8 (two fours per call)", |b| { - b.iter_batched( - || flat.clone(), - |mut scratch| { - cfb_encrypt_in_calls::, 16, 12>( - &k, - &mut scratch, - 8 * BLOCK_LEN, - ); - black_box(&scratch); - }, - BatchSize::LargeInput, - ) - }); + for (name, call_len) in [("16KiB encrypt -- N=8 (two fours per call)", 8 * BLOCK_LEN)] { + group.bench_function(name, |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + let (enc, _): (Aes128Ctr, _) = + key.create_encryptor(&mut rng).unwrap(); + stream_encrypt_in_calls(enc, &mut scratch, call_len); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } group.finish(); } @@ -304,7 +298,7 @@ fn bench_ctr_aes128(c: &mut Criterion) { /// ECB is a thin wrapper over the permutation's batch methods, so this should match `aes_benches`' /// `encrypt_4blocks` figure; it is here to catch a regression in the wrapper. fn bench_ecb_aes128(c: &mut Criterion) { - let k = key::<16>(); + let key = AES128Key::>::from_key_material(&key::<16>()).unwrap(); let blocks = data(); let mut group = c.benchmark_group("modes::ecb::AES_128"); @@ -314,11 +308,9 @@ fn bench_ecb_aes128(c: &mut Criterion) { b.iter_batched( || blocks.clone(), |mut scratch| { - let (mut enc, _) = Aes128Ecb::::do_encrypt_init(&k).unwrap(); + let mut enc: Aes128Ecb = key.create_encryptor(); 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_blocks_inplace(arr.as_chunks_mut().0).unwrap(); + enc.do_encrypt_blocks_inplace(chunk).unwrap(); } black_box(&scratch); }, @@ -326,19 +318,49 @@ fn bench_ecb_aes128(c: &mut Criterion) { ) }); + // The controlled comparison: identical N, identical cipher, bit-sliced pair vs single-block loops. group.finish(); } -/// `do_encrypt_init` is a key expansion plus an IV draw from the OS-backed DRBG, and for short -/// messages it dominates. The key expansion alone is `aes_benches`' `key expansion` group, so the -/// difference is the DRBG. Every mode does the same work here, so one is measured. +/// What a message pays before its first byte, split as the key path splits it: `from_key_material` +/// is the key expansion, paid once per key; `create_encryptor` is a clone of the stored schedule +/// plus an IV draw, and `create_decryptor` the clone alone, paid per message. The IV comes from +/// the cheap deterministic RNG, so the draw's cost is the copy and not a DRBG. Worth its own +/// measurement, because for short messages construction dominates. fn bench_init(c: &mut Criterion) { let k128 = key::<16>(); + let k256 = key::<32>(); + let iv = [0u8; BLOCK_LEN]; + let mut rng = iv_rng(); + let cbc128 = AES128Key::>::from_key_material(&k128).unwrap(); + let cfb128 = AES128Key::::from_key_material(&k128).unwrap(); let mut group = c.benchmark_group("modes::init"); - group.bench_function("AES_128 do_encrypt_init (key schedule + IV)", |b| { - b.iter(|| black_box(Aes128Cbc::::do_encrypt_init(black_box(&k128)).unwrap().1)) + group.bench_function("AES_128 from_key_material, CBC (key schedule)", |b| { + b.iter(|| black_box(AES128Key::>::from_key_material(black_box(&k128)).unwrap())) + }); + group.bench_function("AES_256 from_key_material, CBC (key schedule)", |b| { + b.iter(|| black_box(AES256Key::>::from_key_material(black_box(&k256)).unwrap())) + }); + group.bench_function("AES_128 create_encryptor, CBC (schedule clone + IV draw)", |b| { + b.iter(|| black_box(black_box(&cbc128).create_encryptor(&mut rng).unwrap().1)) + }); + group.bench_function("AES_128 create_decryptor, CBC (schedule clone)", |b| { + b.iter(|| black_box(black_box(&cbc128).create_decryptor(&iv))) + }); + + // CFB does exactly the same work here -- one key expansion per key, one clone plus an IV + // draw per encryptor -- so these should match the CBC numbers. A divergence would mean one + // mode is doing something extra at construction time. + group.bench_function("AES_128 from_key_material, CFB (key schedule)", |b| { + b.iter(|| black_box(AES128Key::::from_key_material(black_box(&k128)).unwrap())) + }); + group.bench_function("AES_128 create_encryptor, CFB (schedule clone + IV draw)", |b| { + b.iter(|| black_box(black_box(&cfb128).create_encryptor(&mut rng).unwrap().1)) + }); + group.bench_function("AES_128 create_decryptor, CFB (schedule clone)", |b| { + b.iter(|| black_box(black_box(&cfb128).create_decryptor(&iv))) }); group.finish(); @@ -383,7 +405,7 @@ fn bench_ccm_aes128(c: &mut Criterion) { }); // AAD only: CCM as a pure authentication mode, which Sec 5.3's footnote calls out as the - // empty-payload degenerate case. + // empty-payload degenerate case. One cipher call per block, so this is the CTR-comparable half. group.bench_function("authenticate 16KiB AAD, empty payload", |b| { b.iter(|| { let mut out: [u8; 0] = []; @@ -402,14 +424,16 @@ fn bench_ccm_aes128(c: &mut Criterion) { group.finish(); } -/// The [`AEADCipherEncryptor`] one-shot against the mode's in-place one-shot on the same message. +/// The frame key's [`AeadEncryptionKey`] one-shot against the mode's in-place one-shot on the +/// same message. /// -/// Both end in the same packet implementation; the trait form copies the message into the -/// caller's output buffer and draws the nonce. A cheap deterministic RNG, created once outside -/// the timed loop, isolates that draw from OS entropy and DRBG construction. +/// Both end in the same `Ccm` implementation; the key form copies the message into the caller's +/// output buffer and draws the nonce. A cheap deterministic RNG, created once outside the timed +/// loop, isolates that draw from OS entropy and DRBG construction. fn bench_ccm_one_shot_pair(c: &mut Criterion) { - let key = key::<16>(); - let perm = AES128Internal::new(&key).unwrap(); + let material = key::<16>(); + let key = Aes128CcmFrameKey::from_key_material(&material).unwrap(); + let perm = AES128Internal::new(&material).unwrap(); let data = [0xA5u8; CCM_BUFFER_LEN]; let no_aad: [u8; 0] = []; let nonce = [0x24u8; CCM_NONCE_LEN]; @@ -418,19 +442,14 @@ fn bench_ccm_one_shot_pair(c: &mut Criterion) { let mut group = c.benchmark_group("modes::ccm::one_shot"); group.throughput(Throughput::Bytes(CCM_BUFFER_LEN as u64)); - group.bench_function("AEADCipherEncryptor::encrypt_detached_rng_out 4KiB", |b| { + group.bench_function("AeadEncryptionKey::encrypt_detached_out 4KiB", |b| { b.iter_batched_ref( || [0u8; CCM_BUFFER_LEN], |out| { black_box( - Aes128CcmEncryptor::encrypt_detached_rng_out( - black_box(&key), - &mut rng, - &no_aad, - black_box(&data), - out, - ) - .unwrap(), + black_box(&key) + .encrypt_detached_out(&mut rng, &no_aad, black_box(&data), out) + .unwrap(), ) }, BatchSize::LargeInput, @@ -466,7 +485,7 @@ fn bench_ccm_one_shot_pair(c: &mut Criterion) { /// Decryption is the same work in the other order, and AAD alongside a payload costs the sum of /// the two measurements here, so neither is measured separately. fn bench_gcm_aes128(c: &mut Criterion) { - let key = key::<16>(); + let key = Aes128GcmKey::from_key_material(&key::<16>()).unwrap(); let data = [0xA5u8; DATA_LEN]; let no_aad: [u8; 0] = []; let mut rng = FixedSeedRNG::::new([0x24u8; GCM_NONCE_LEN]); @@ -479,14 +498,9 @@ fn bench_gcm_aes128(c: &mut Criterion) { || [0u8; DATA_LEN], |out| { black_box( - Aes128Gcm::::encrypt_detached_rng_out( - black_box(&key), - &mut rng, - &no_aad, - black_box(&data), - out, - ) - .unwrap(), + black_box(&key) + .encrypt_detached_out(&mut rng, &no_aad, black_box(&data), out) + .unwrap(), ) }, BatchSize::LargeInput, @@ -499,14 +513,9 @@ fn bench_gcm_aes128(c: &mut Criterion) { b.iter(|| { let mut out: [u8; 0] = []; black_box( - Aes128Gcm::::encrypt_detached_rng_out( - black_box(&key), - &mut rng, - black_box(&data), - &no_aad, - &mut out, - ) - .unwrap(), + black_box(&key) + .encrypt_detached_out(&mut rng, black_box(&data), &no_aad, &mut out) + .unwrap(), ) }) }); @@ -517,7 +526,7 @@ fn bench_gcm_aes128(c: &mut Criterion) { /// AES-256 GCM: the one mode-level key-length point. The other modes scale with the round count /// as `aes_benches` shows for the permutation itself. fn bench_gcm_aes256(c: &mut Criterion) { - let key = key::<32>(); + let key = Aes256GcmKey::from_key_material(&key::<32>()).unwrap(); let data = [0xA5u8; DATA_LEN]; let no_aad: [u8; 0] = []; let mut rng = FixedSeedRNG::::new([0x24u8; GCM_NONCE_LEN]); @@ -530,14 +539,9 @@ fn bench_gcm_aes256(c: &mut Criterion) { || [0u8; DATA_LEN], |out| { black_box( - Aes256Gcm::::encrypt_detached_rng_out( - black_box(&key), - &mut rng, - &no_aad, - black_box(&data), - out, - ) - .unwrap(), + black_box(&key) + .encrypt_detached_out(&mut rng, &no_aad, black_box(&data), out) + .unwrap(), ) }, BatchSize::LargeInput, @@ -548,24 +552,29 @@ fn bench_gcm_aes256(c: &mut Criterion) { } /// CMAC is serial like CBC encryption (SP 800-38B Sec 6.2 step 6 chains every block), so it -/// should come out close to `modes::cbc::AES_128` encryption. The key-object route skips the -/// per-message subkey derivation, one block encryption, which is invisible at 16 KiB. +/// should come out close to `modes::cbc::AES_128` encryption. Building the key once skips the +/// per-tag key expansion and subkey derivation, one block encryption, which is invisible at 16 +/// KiB; the `from_key_material per tag` arms pay it every time, for the comparison. fn bench_cmac(c: &mut Criterion) { let data: Vec = data().concat(); let mut group = c.benchmark_group("modes::cmac"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); let key128 = mac_key::<16>(); - group.bench_function("AES_128 MAC::new 16KiB", |b| { - b.iter(|| black_box(AES_CMAC_128::new(&key128).unwrap().mac(black_box(&data)))) + group.bench_function("AES_128 from_key_material per tag 16KiB", |b| { + b.iter(|| { + black_box(AES128Key::::from_key_material(&key128).unwrap().mac(black_box(&data))) + }) }); let key_object_128 = AES128Key::::from_key_material(&key128).unwrap(); group.bench_function("AES_128 key object 16KiB", |b| { b.iter(|| black_box(key_object_128.mac(black_box(&data)))) }); let key256 = mac_key::<32>(); - group.bench_function("AES_256 MAC::new 16KiB", |b| { - b.iter(|| black_box(AES_CMAC_256::new(&key256).unwrap().mac(black_box(&data)))) + group.bench_function("AES_256 from_key_material per tag 16KiB", |b| { + b.iter(|| { + black_box(AES256Key::::from_key_material(&key256).unwrap().mac(black_box(&data))) + }) }); let key_object_256 = AES256Key::::from_key_material(&key256).unwrap(); group.bench_function("AES_256 key object 16KiB", |b| { @@ -576,7 +585,7 @@ fn bench_cmac(c: &mut Criterion) { } /// GMAC is GHASH over the message plus two cipher calls, so it is several times faster than CMAC -/// and the same as GCM's AAD-only measurement; the key object saves the schedule and `H` per tag. +/// and the same as GCM's AAD-only measurement. fn bench_gmac(c: &mut Criterion) { let data: Vec = data().concat(); let mut group = c.benchmark_group("modes::gmac"); @@ -584,12 +593,6 @@ fn bench_gmac(c: &mut Criterion) { let mut rng = FixedSeedRNG::::new([0x24u8; GCM_NONCE_LEN]); let key128 = mac_key::<16>(); - group.bench_function("AES_128 Gmac::new 16KiB", |b| { - b.iter(|| { - let (gen_, _) = AES_GMAC_128::::new(&key128, &mut rng).unwrap(); - black_box(gen_.mac(black_box(&data))) - }) - }); let key_object_128 = AES128Key::::from_key_material(&key128).unwrap(); group.bench_function("AES_128 key object 16KiB", |b| { b.iter(|| { @@ -598,12 +601,6 @@ fn bench_gmac(c: &mut Criterion) { }) }); let key256 = mac_key::<32>(); - group.bench_function("AES_256 Gmac::new 16KiB", |b| { - b.iter(|| { - let (gen_, _) = AES_GMAC_256::::new(&key256, &mut rng).unwrap(); - black_box(gen_.mac(black_box(&data))) - }) - }); let key_object_256 = AES256Key::::from_key_material(&key256).unwrap(); group.bench_function("AES_256 key object 16KiB", |b| { b.iter(|| { diff --git a/crypto/aes/src/cbc.rs b/crypto/aes/src/cbc.rs index f853644e4..bf490210e 100644 --- a/crypto/aes/src/cbc.rs +++ b/crypto/aes/src/cbc.rs @@ -2,93 +2,83 @@ //! //! See [`bouncycastle_cipher::modes::cbc`] for details on the CipherBlockChaining construction. //! -//! The aliases here are padded block ciphers that accept input of any size; `NoPadding` accepts +//! The aliases here name the padded workers, which accept input of any size; `NoPadding` accepts //! only whole blocks but goes through the same adapter. The unpadded mode underneath them, which //! implements the block-cipher traits directly, is [`Cbc`] and is not re-exported from this crate. +//! The workers come from a key: [`AES128Key>`](crate::AES128Key) (or `CBC`) +//! hands them out through the [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] pair, and +//! `AES128Key>` hands out [`Cbc`] itself through the block pair. //! //! # Usage Examples //! //! ## One-shot API //! -//! Basic usage can be obtained via the [`SymmetricCipherEncryptor`] and [`SymmetricCipherDecryptor`] API: -//! //! ``` -//! use bouncycastle_aes::AES_CBC_256; -//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_aes::AES256Key; +//! use bouncycastle_cipher::modes::CBC; //! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! // Define ourselves convenience types. -//! type AESEnc = AES_CBC_256; -//! type AESDec = AES_CBC_256; -//! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); +//! let key = AES256Key::>::from_key_material(&material).expect("a valid AES-256 key"); //! -//! // An arbitrary plaintext to encrypt //! // Any length: PKCS#7 pads it out to whole blocks, so 50 bytes is as good as 48. //! let plaintext = [0x5Au8; 50]; //! -//! // The IV is generated for you and returned; there is no API for supplying one. -//! let (iv, ciphertext) = AESEnc::encrypt(&key, &plaintext).expect("encryption"); +//! // The IV is drawn from `rng` and returned; there is no API for supplying one. +//! let (iv, ciphertext) = key.encrypt(&mut rng, &plaintext).expect("encryption"); //! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); //! -//! let recovered = AESDec::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); //! assert_eq!(recovered, plaintext); //! ``` //! //! ## Streaming API //! -//! For data that arrives in pieces, the following APIs can be used: +//! For data that arrives in pieces, the workers' streaming methods can be used: //! //! ``` -//! use bouncycastle_aes::{AES_CBC_128, AES_BLOCK_LEN}; +//! use bouncycastle_aes::{AES128Key, AES_BLOCK_LEN}; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::PKCS7; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; //! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! use bouncycastle_cipher::padding::PKCS7; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_CBC_128; -//! type AESDec = AES_CBC_128; -//! +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::>::from_key_material(&material).expect("a valid AES-128 key"); //! -//! // An arbitrary plaintext to encrypt //! let plaintext = [0x5Au8; 50]; //! -//! // The streaming (chunked) API allows for data to be handed to the cipher as it arrives, in chunks -//! // of any length, but it will only be processed once a full block has been received. -//! // Here, we will use 7-byte chunks -//! let (mut encryptor, iv) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! // Data can be handed to the encryptor as it arrives, in pieces of any length, but a block is +//! // only written out once it is complete. Here, 7-byte pieces. +//! let (mut encryptor, iv) = key.create_encryptor(&mut rng).expect("IV draw"); //! //! let mut ciphertext = Vec::new(); -//! //! for piece in plaintext.chunks(7) { //! let mut out = [0u8; AES_BLOCK_LEN]; //! let bytes_written = encryptor.do_encrypt_out(piece, &mut out).expect("encryption"); -//! //! // If that doesn't complete a block, then nothing is written. -//! if bytes_written != 0 { -//! ciphertext.extend_from_slice(&out[..bytes_written]); -//! } +//! ciphertext.extend_from_slice(&out[..bytes_written]); //! } //! let (last_block, last_len) = encryptor.do_encrypt_final().expect("padding the final block"); //! ciphertext.extend_from_slice(&last_block[..last_len]); //! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); //! -//! // Decrypt the ciphertext in 19-byte chunks. -//! let mut decryptor = AESDec::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! // Decrypt the ciphertext in 19-byte pieces; the decryptor is infallible to set up. +//! let mut decryptor = key.create_decryptor(&iv); //! let mut recovered = Vec::new(); //! for piece in ciphertext.chunks(19) { //! let mut out = [0u8; AES_BLOCK_LEN]; //! let bytes_written = decryptor.do_decrypt_out(piece, &mut out).expect("decryption"); -//! if bytes_written != 0 { -//! recovered.extend_from_slice(&out[..bytes_written]); -//! } +//! recovered.extend_from_slice(&out[..bytes_written]); //! } //! let (last_block, last_len) = decryptor.do_decrypt_final().expect("a valid final block"); //! recovered.extend_from_slice(&last_block[..last_len]); @@ -101,41 +91,44 @@ //! error at `do_encrypt_final` rather than something silently padded: //! //! ``` -//! use bouncycastle_aes::AES_CBC_128; -//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::SymmetricCipherEncryptor; -//! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::CBC; //! use bouncycastle_cipher::padding::NoPadding; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! // Define ourselves a convenience type for the encryption direction with no padding. -//! type Enc = AES_CBC_128; -//! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = AES128Key::>::from_key_material(&material).unwrap(); //! //! // A whole block is fine, and comes out the same length. //! let mut out = [0u8; 16]; -//! let (_iv, written) = Enc::encrypt_out(&key, &[0u8; 16], &mut out).expect("aligned"); +//! let (written, _iv) = key.encrypt_out(&mut rng, &[0u8; 16], &mut out).expect("aligned"); //! assert_eq!(written, 16); //! //! // Five bytes is not, and is refused rather than padded. -//! let mut out = [0u8; 16]; -//! assert!(Enc::encrypt_out(&key, b"hello", &mut out).is_err()); +//! assert!(key.encrypt_out(&mut rng, b"hello", &mut out).is_err()); //! ``` //! -//! The padding scheme is part of the type, so the two schemes are different types and cannot be -//! interchanged. A value built with one will not satisfy a binding annotated with the other. +//! The padding scheme is part of the key type, so the two schemes' workers are different types +//! and cannot be interchanged. A value built with one will not satisfy a binding annotated with +//! the other. //! //! ```compile_fail -//! use bouncycastle_aes::AES_CBC_128; -//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::SymmetricCipherEncryptor; +//! use bouncycastle_aes::{AES_CBC_128, AES128Key}; //! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_cipher::modes::CBC; //! use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = AES128Key::>::from_key_material(&material).unwrap(); //! -//! // Built as NoPadding, annotated as PKCS7: mismatched types. -//! let (enc, _iv) = AES_CBC_128::::do_encrypt_init(&key).unwrap(); +//! // Built from a NoPadding key, annotated as PKCS7: mismatched types. +//! let (enc, _iv) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); //! let _mismatched: AES_CBC_128 = enc; //! ``` //! @@ -156,7 +149,7 @@ use bouncycastle_cipher::modes::cbc; #[allow(unused_imports)] use bouncycastle_cipher::padding::{NoPadding, PKCS7}; #[allow(unused_imports)] -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; // end of imports needed for docs /// AES-128 in CBC mode with a padding scheme. diff --git a/crypto/aes/src/ccm.rs b/crypto/aes/src/ccm.rs index 6c3b038bd..4e920f01e 100644 --- a/crypto/aes/src/ccm.rs +++ b/crypto/aes/src/ccm.rs @@ -6,18 +6,20 @@ //! and decryption either returns the plaintext or fails the tag check. `Dir` is [`Encrypting`] or //! [`Decrypting`]; the wrong direction is a compile error, not a runtime check. //! -//! CCM must know the payload length before it starts (Sec 3), and the generic traits' streaming -//! constructor is handed no lengths, so [`AES_CCM_128`] and friends fix the payload length in the -//! type as the const parameter `DATA_LEN` -- the fixed frame of a packet protocol -- hold back -//! nothing but up to `AAD_LEN` bytes of AAD, and refuse any other amount of payload. Their -//! one-shots, with the whole message in hand, take any payload and AAD length. The nonce is -//! generated unless the caller supplies one through the trait's `_nonce` forms, which CCM -//! permits because it requires the nonce to be unique but not random (Sec 5.3), so a caller with -//! a counter can do better than a draw from a DRBG. -//! -//! A mode-bound key, [`AES128Key`]`>` and friends, is the other way onto the mode: -//! its constructors take the AAD and payload lengths, so its workers hold nothing back, and its -//! in-place one-shots take any length with no second buffer. +//! CCM must know the payload length before it starts (Sec 3), and the generic AEAD pair's +//! `create_encryptor` is handed no lengths, so [`AES_CCM_128`] and friends fix the payload length +//! in the type as the const parameter `DATA_LEN` -- the fixed frame of a packet protocol -- hold +//! back nothing but up to `AAD_LEN` bytes of AAD, and refuse any other amount of payload. They +//! come from a key, [`AES128Key`]`>` and friends, +//! through the [`AeadEncryptionKey`] / [`AeadDecryptionKey`] pair, whose one-shots take the +//! frame length too. The nonce is drawn from the caller's RNG unless supplied through the +//! `_nonce` forms, which CCM permits because it requires the nonce to be unique but not random +//! (Sec 5.3), so a caller with a counter can do better than a draw from a DRBG. +//! +//! A message of any length goes through the other key, [`AES128Key`]`>` +//! and friends: its [`AeadPacketEncryptionKey`] / [`AeadPacketDecryptionKey`] constructors take +//! the AAD and payload lengths, so its workers hold nothing back, and its in-place one-shots +//! take any length with no second buffer. //! //! # The nonce and tag length are parametrizable //! @@ -32,7 +34,7 @@ //! data as authentic". //! //! Both are still checked at compile time against A.1's permitted sets, so a wrong value is a -//! compile error rather than a runtime `Err`. A generated nonce needs `NONCE_LEN` of at least 12; +//! compile error rather than a runtime `Err`. A drawn nonce needs `NONCE_LEN` of at least 12; //! a supplied one any A.1 length. //! //! [`CCM_NONCE_LEN`] and [`CCM_TAG_LEN`] are the default pair -- a 12-byte nonce and a @@ -41,56 +43,55 @@ //! //! ```text //! // 12-byte nonce, 16-byte tag, up to 64 bytes of AAD, 2 KiB frames -//! AES_CCM_128 +//! AES128Key> //! ``` //! //! Though some alternative choices do exist, for example: //! ```text //! // IEEE 802.11 CCMP's 13 byte nonce and 8 byte tag -//! AES_CCM_128 +//! AES128Key> //! ``` //! //! # Usage Examples //! -//! ## Generic AEAD API +//! ## Fixed frames, through the generic AEAD pair //! -//! For code written against [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], [`AES_CCM_128`] -//! generates the nonce and returns it. The streaming methods take exactly `DATA_LEN` bytes of -//! payload; the one-shots take any length: +//! For code written against [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], the `CCMFrame` +//! key draws the nonce and returns it. Its streaming methods and one-shots take exactly +//! `DATA_LEN` bytes of payload: //! //! ``` -//! use bouncycastle_aes::AES_CCM_128; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::CCMFrame; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; //! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); //! // Up to 64 bytes of AAD, and frames of exactly 2 KiB -- comfortably above an 802.11 frame, //! // the packet size CCM was designed for. -//! type AESEnc = AES_CCM_128; -//! type AESDec = AES_CCM_128; -//! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) -//! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::>::from_key_material(&material) +//! .expect("a valid AES-128 key"); //! //! let frame = [0x5Au8; 2048]; //! -//! // The one-shots: a frame, or any other length, since they have the whole message in hand. -//! let (nonce, ciphertext, tag) = AESEnc::encrypt_detached(&key, b"header", &frame).expect("encryption"); -//! let plaintext = AESDec::decrypt_detached(&key, &nonce, b"header", &ciphertext, &tag).expect("decryption"); +//! // The one-shots: a frame. +//! let (nonce, ciphertext, tag) = key.encrypt_detached(&mut rng, b"header", &frame).expect("encryption"); +//! let plaintext = key.decrypt_detached(&nonce, b"header", &ciphertext, &tag).expect("decryption"); //! assert_eq!(&plaintext[..], &frame[..]); -//! let (nonce, ciphertext, tag) = AESEnc::encrypt_detached(&key, b"header", b"message").expect("encryption"); -//! let plaintext = AESDec::decrypt_detached(&key, &nonce, b"header", &ciphertext, &tag).expect("decryption"); -//! assert_eq!(&plaintext[..], b"message"); //! //! // The streaming methods: exactly one frame, released as it is processed. -//! let (mut enc, nonce) = AESEnc::do_encrypt_init(&key).expect("init"); +//! let (mut enc, nonce) = key.create_encryptor(&mut rng).expect("nonce draw"); //! enc.do_update_aad(b"header").expect("aad"); //! let mut sealed = vec![0u8; 2048]; //! let n = enc.do_encrypt_out(&frame, &mut sealed).expect("the whole frame comes out"); //! assert_eq!(n, 2048); //! let (_, _, tag) = enc.do_encrypt_final_detached().expect("the tag"); //! -//! let mut dec = AESDec::do_decrypt_init(&key, &nonce).expect("init"); +//! let mut dec = key.create_decryptor(&nonce); //! dec.do_update_aad(b"header").expect("aad"); //! let mut opened = vec![0u8; 2048]; //! dec.do_decrypt_out(&sealed, &mut opened).expect("released, but not yet authenticated"); @@ -98,45 +99,44 @@ //! assert_eq!(opened, frame); //! ``` //! -//! ## A supplied nonce, in the spec's own layout +//! ## Any length, a supplied nonce, in the spec's own layout //! -//! The `_nonce` one-shots take the caller's nonce, and the inline forms produce and consume the -//! spec's own ciphertext layout, `ciphertext || tag` (Sec 6.1 step 8): +//! The `CCM` key's `_nonce` one-shots take the caller's nonce and any length, and the inline +//! forms produce and consume the spec's own ciphertext layout, `ciphertext || tag` (Sec 6.1 +//! step 8): //! //! ``` -//! use bouncycastle_aes::{AES_CCM_256, CCM_NONCE_LEN, CCM_TAG_LEN}; +//! use bouncycastle_aes::{AES256Key, CCM_NONCE_LEN, CCM_TAG_LEN}; +//! use bouncycastle_cipher::modes::CCM; //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // The frame parameters are unused by the one-shots. -//! type AESEnc = AES_CCM_256; -//! type AESDec = AES_CCM_256; +//! use bouncycastle_core::symmetric_key::{AeadPacketDecryptionKey, AeadPacketEncryptionKey, FromKeyMaterial}; //! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); +//! let key = AES256Key::>::from_key_material(&material) +//! .expect("a valid AES-256 key"); //! -//! // Supplied, not generated. It is the caller's responsibility that it never repeat under this key. +//! // Supplied, not drawn. It is the caller's responsibility that it never repeat under this key. //! let nonce = [0x01u8; CCM_NONCE_LEN]; //! //! // The associated data is authenticated but not encrypted; the message is both. //! let aad = b"header, sent in the clear"; //! let message = b"a message of no particular length"; //! -//! let sealed = AESEnc::encrypt_with_aad_nonce(&key, &nonce, aad, message).expect("encryption"); +//! let sealed = key.encrypt_with_aad_nonce(&nonce, aad, message).expect("encryption"); //! assert_eq!(sealed.len(), message.len() + CCM_TAG_LEN, "the ciphertext plus the tag"); //! -//! let opened = AESDec::decrypt_with_aad(&key, &nonce, aad, &sealed).expect("decryption"); +//! let opened = key.decrypt_with_aad(&nonce, aad, &sealed).expect("decryption"); //! assert_eq!(&opened[..], message); //! //! // Tampering with either the ciphertext or the associated data fails the tag check. //! let mut tampered = sealed.clone(); //! tampered[0] ^= 1; -//! assert!(AESDec::decrypt_with_aad(&key, &nonce, aad, &tampered).is_err()); -//! assert!(AESDec::decrypt_with_aad(&key, &nonce, b"other header", &sealed).is_err()); +//! assert!(key.decrypt_with_aad(&nonce, aad, &tampered).is_err()); +//! assert!(key.decrypt_with_aad(&nonce, b"other header", &sealed).is_err()); //! ``` //! -//! ## In place, through a key +//! ## In place //! //! A message already in a buffer of its own is sealed and opened there, with no second buffer: //! @@ -198,6 +198,10 @@ use crate::AES128Key; #[allow(unused_imports)] use bouncycastle_cipher::{Decrypting, Encrypting}; #[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{ + AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, +}; +#[allow(unused_imports)] use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; // end of imports needed for docs @@ -214,10 +218,10 @@ pub const CCM_TAG_LEN: usize = 16; /// frames of exactly `DATA_LEN` payload bytes. /// /// This is the fixed-frame pair, for code written against the generic AEAD traits: the streaming -/// methods accept exactly `DATA_LEN` bytes of payload and up to `AAD_LEN` of AAD, the one-shots -/// any amount of either. The nonce is generated unless the caller supplies one through -/// [`AEADCipherEncryptor::do_encrypt_init_nonce`] or the `_nonce` one-shots; a generated nonce -/// needs `NONCE_LEN` of at least 12, a supplied one any A.1 length. `NONCE_LEN` must be 7..=13 +/// methods accept exactly `DATA_LEN` bytes of payload and up to `AAD_LEN` of AAD. It comes from +/// an [`AES128Key`]`>`, whose `create_encryptor` +/// draws the nonce and whose `create_encryptor_nonce` and `_nonce` one-shots take the caller's; +/// a drawn nonce needs `NONCE_LEN` of at least 12, a supplied one any A.1 length. `NONCE_LEN` must be 7..=13 /// and `TAG_LEN` one of 4, 6, 8, 10, 12, 14, 16 (A.1); anything else is a compile error. Use /// [`CCM_NONCE_LEN`] and [`CCM_TAG_LEN`] if you have no reason to choose. See [`CcmEncryptor`] /// for the rules. diff --git a/crypto/aes/src/cfb.rs b/crypto/aes/src/cfb.rs index c72606290..6fc70ba58 100644 --- a/crypto/aes/src/cfb.rs +++ b/crypto/aes/src/cfb.rs @@ -3,9 +3,11 @@ //! See [`bouncycastle_cipher::modes::cfb`] for details on the CipherFeedback construction. //! //! The aliases here are stream ciphers: the data is a `&mut [u8]` of any length, encrypted or -//! decrypted in place, and the ciphertext is exactly as long as the plaintext. The IV is generated -//! by encryption and returned; there is no API for supplying one. `Dir` is [`Encrypting`] or -//! [`Decrypting`]; the wrong direction is a compile error, not a runtime check. +//! decrypted in place, and the ciphertext is exactly as long as the plaintext. They come from a +//! key, [`AES128Key`](crate::AES128Key) and friends, through the [`SymmetricEncryptionKey`] +//! / [`SymmetricDecryptionKey`] pair. The IV is drawn from the caller's RNG by encryption and +//! returned; there is no API for supplying one. `Dir` is [`Encrypting`] or [`Decrypting`]; the +//! wrong direction is a compile error, not a runtime check. //! //! The segment size is the full block, so the constructions used here are equivalent to **CFB128**. //! SP 800-38A's `s = 8` variant is a different, non-interoperable mode with its own aliases, @@ -15,68 +17,59 @@ //! //! ## One-shot API //! -//! Basic usage can be obtained via the [`StreamCipherEncryptor`] and [`StreamCipherDecryptor`] API: -//! //! ``` -//! use bouncycastle_aes::AES_CFB_256; +//! use bouncycastle_aes::AES256Key; +//! use bouncycastle_cipher::modes::CFB; //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_CFB_256; -//! type AESDec = AES_CFB_256; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); +//! let key = AES256Key::::from_key_material(&material).expect("a valid AES-256 key"); //! -//! // An arbitrary plaintext to encrypt. //! // Any length: a stream cipher does not need a whole number of blocks. //! let plaintext = [0x5Au8; 47]; //! -//! // Encryption works in place. The IV is generated for you and returned; there is no API for -//! // supplying one. +//! // Encryption works in place. The IV is drawn from `rng` and returned. //! let mut data = plaintext; -//! let (_, iv) = AESEnc::encrypt_inplace(&key, &mut data).expect("encryption"); +//! let (_, iv) = key.encrypt_inplace(&mut rng, &mut data).expect("encryption"); //! -//! AESDec::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); +//! key.decrypt_inplace(&iv, &mut data).expect("decryption"); //! assert_eq!(data, plaintext); //! ``` //! //! ## Streaming API //! -//! For data that arrives in pieces, the following APIs can be used. A stream cipher processes -//! every byte it is given, so nothing is held back between calls and the pieces can be of any -//! length: +//! For data that arrives in pieces, the workers' streaming methods can be used. A stream cipher +//! processes every byte it is given, so nothing is held back between calls and the pieces can be +//! of any length: //! //! ``` -//! use bouncycastle_aes::AES_CFB_128; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::CFB; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::{ -//! StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, -//! SymmetricCipherEncryptor, -//! }; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_CFB_128; -//! type AESDec = AES_CFB_128; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::::from_key_material(&material).expect("a valid AES-128 key"); //! -//! // An arbitrary plaintext to encrypt //! let plaintext = [0x5Au8; 50]; //! //! // Encrypt in 7-byte pieces, each in place. -//! let (mut encryptor, iv) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! let (mut encryptor, iv) = key.create_encryptor(&mut rng).expect("IV draw"); //! let mut ciphertext = plaintext; //! for piece in ciphertext.chunks_mut(7) { //! encryptor.do_encrypt_inplace(piece).expect("encryption"); //! } //! //! // Decrypt in 19-byte pieces: the boundaries need not match the encryptor's. -//! let mut decryptor = AESDec::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! let mut decryptor = key.create_decryptor(&iv); //! let mut recovered = ciphertext; //! for piece in recovered.chunks_mut(19) { //! decryptor.do_decrypt_inplace(piece).expect("decryption"); @@ -96,7 +89,7 @@ use bouncycastle_cipher::modes::Cfb; #[allow(unused_imports)] use bouncycastle_cipher::{Decrypting, Encrypting}; #[allow(unused_imports)] -use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; // end of imports needed for docs /// AES-128 in CFB128 mode. diff --git a/crypto/aes/src/cfb8.rs b/crypto/aes/src/cfb8.rs index c4ec07579..e46870de0 100644 --- a/crypto/aes/src/cfb8.rs +++ b/crypto/aes/src/cfb8.rs @@ -2,10 +2,12 @@ //! //! See [`bouncycastle_cipher::modes::cfb8`] for details on the CipherFeedback construction with an 8-bit segment. //! -//! The aliases here are stream ciphers: the data is a `&mut [u8]` of any -//! length, encrypted or decrypted in-place since the ciphertext is exactly as long as the plaintext. -//! The IV is generated by encryption and returned; there is no API for supplying one. `Dir` is -//! [`Encrypting`] or [`Decrypting`]; the wrong direction is a compile error, not a runtime check. +//! The aliases here are stream ciphers: the data is a `&mut [u8]` of any length, encrypted or +//! decrypted in-place since the ciphertext is exactly as long as the plaintext. They come from a +//! key, [`AES128Key`](crate::AES128Key) and friends, through the [`SymmetricEncryptionKey`] +//! / [`SymmetricDecryptionKey`] pair. The IV is drawn from the caller's RNG by encryption and +//! returned; there is no API for supplying one. `Dir` is [`Encrypting`] or [`Decrypting`]; the +//! wrong direction is a compile error, not a runtime check. //! //! CFB8 is a **different, non-interoperable mode** from CFB, not a variant of it: their //! ciphertexts differ from the second byte, and it costs a full AES call per byte, sixteen times @@ -16,68 +18,59 @@ //! //! ## One-shot API //! -//! Basic usage can be obtained via the [`StreamCipherEncryptor`] and [`StreamCipherDecryptor`] API: -//! //! ``` -//! use bouncycastle_aes::AES_CFB8_256; +//! use bouncycastle_aes::AES256Key; +//! use bouncycastle_cipher::modes::CFB8; //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_CFB8_256; -//! type AESDec = AES_CFB8_256; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); +//! let key = AES256Key::::from_key_material(&material).expect("a valid AES-256 key"); //! -//! // An arbitrary plaintext to encrypt. //! // 5 bytes: CFB8's segment is one byte, so any length at all is fine. //! let plaintext = *b"hello"; //! -//! // Encryption works in place. The IV is generated for you and returned; there is no API for -//! // supplying one. +//! // Encryption works in place. The IV is drawn from `rng` and returned. //! let mut data = plaintext; -//! let (_, iv) = AESEnc::encrypt_inplace(&key, &mut data).expect("encryption"); +//! let (_, iv) = key.encrypt_inplace(&mut rng, &mut data).expect("encryption"); //! -//! AESDec::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); +//! key.decrypt_inplace(&iv, &mut data).expect("decryption"); //! assert_eq!(data, plaintext); //! ``` //! //! ## Streaming API //! -//! For data that arrives in pieces, the following APIs can be used. A stream cipher processes -//! every byte it is given, so nothing is held back between calls and the pieces can be of any -//! length: +//! For data that arrives in pieces, the workers' streaming methods can be used. A stream cipher +//! processes every byte it is given, so nothing is held back between calls and the pieces can be +//! of any length: //! //! ``` -//! use bouncycastle_aes::AES_CFB8_128; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::CFB8; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::{ -//! StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, -//! SymmetricCipherEncryptor, -//! }; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_CFB8_128; -//! type AESDec = AES_CFB8_128; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::::from_key_material(&material).expect("a valid AES-128 key"); //! -//! // An arbitrary plaintext to encrypt //! let plaintext = [0x5Au8; 50]; //! //! // Encrypt in 3-byte pieces, each in place. -//! let (mut encryptor, iv) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! let (mut encryptor, iv) = key.create_encryptor(&mut rng).expect("IV draw"); //! let mut ciphertext = plaintext; //! for piece in ciphertext.chunks_mut(3) { //! encryptor.do_encrypt_inplace(piece).expect("encryption"); //! } //! //! // Decrypt in 20-byte pieces: the boundaries need not match the encryptor's. -//! let mut decryptor = AESDec::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! let mut decryptor = key.create_decryptor(&iv); //! let mut recovered = ciphertext; //! for piece in recovered.chunks_mut(20) { //! decryptor.do_decrypt_inplace(piece).expect("decryption"); @@ -91,19 +84,23 @@ //! a message encrypted with one does not decrypt with the other. //! //! ``` -//! use bouncycastle_aes::{AES_CFB8_128, AES_CFB_128}; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::{CFB, CFB8}; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let cfb8_key = AES128Key::::from_key_material(&material).unwrap(); +//! let cfb_key = AES128Key::::from_key_material(&material).unwrap(); //! let plaintext = *b"hello"; //! //! let mut data = plaintext; -//! let (_, iv) = AES_CFB8_128::::encrypt_inplace(&key, &mut data).unwrap(); +//! let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut data).unwrap(); //! //! // Decrypting CFB8 output as CFB128 does not recover the plaintext. -//! AES_CFB_128::::decrypt_inplace(&key, &iv, &mut data).unwrap(); +//! cfb_key.decrypt_inplace(&iv, &mut data).unwrap(); //! assert_ne!(data, plaintext); //! ``` //! @@ -119,7 +116,7 @@ use bouncycastle_cipher::modes::Cfb8; #[allow(unused_imports)] use bouncycastle_cipher::{Decrypting, Encrypting}; #[allow(unused_imports)] -use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; // end of imports needed for docs /// AES-128 in CFB8 mode. diff --git a/crypto/aes/src/cmac.rs b/crypto/aes/src/cmac.rs index 62f4493cf..bd782cff8 100644 --- a/crypto/aes/src/cmac.rs +++ b/crypto/aes/src/cmac.rs @@ -2,27 +2,13 @@ //! [`AES128Key`] and friends MAC keys. //! //! See [`bouncycastle_cipher::modes::cmac`] for the construction, the key requirements, -//! truncation and security considerations. [`AES_CMAC_128`] and friends pin the permutation and -//! its key and block lengths; the [`MAC`] trait on them derives the subkeys per message from the -//! key material. A mode-bound key, [`AES128Key`], derives them once and hands out the same -//! worker through [`MacGenerationKey`] and [`MacVerificationKey`]. +//! truncation and security considerations. [`AES_CMAC_128`] and friends name the worker, which +//! implements [`MAC`]; it comes from a key, [`AES128Key`], which takes +//! [`KeyType::MACKey`] material, derives the subkeys once and hands out the worker through +//! [`MacGenerationKey`] and [`MacVerificationKey`]. //! //! # Usage Examples //! -//! One message from the material: -//! -//! ``` -//! use bouncycastle_aes::AES_CMAC_128; -//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::MAC; -//! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::MACKey).unwrap(); -//! let tag = AES_CMAC_128::new(&key).unwrap().mac(b"a message"); -//! assert!(AES_CMAC_128::new(&key).unwrap().verify(b"a message", &tag)); -//! ``` -//! -//! Many messages from a key object, which takes the same [`KeyType::MACKey`] material: -//! //! ``` //! use bouncycastle_aes::AES256Key; //! use bouncycastle_cipher::modes::CMAC; @@ -33,12 +19,14 @@ //! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::MACKey).unwrap(); //! let key = AES256Key::::from_key_material(&material).expect("a valid AES-256 MAC key"); //! +//! // A message that arrives in pieces, through the worker. //! let mut generator = key.create_generator(); //! generator.do_update(b"a message that arrives "); //! generator.do_update(b"in pieces"); //! let mut tag = [0u8; 16]; //! assert_eq!(generator.do_final_out(&mut tag).unwrap(), 16); //! +//! // One-shots on the key. //! assert!(key.verify(b"a message that arrives in pieces", &tag)); //! assert_eq!(key.mac(b"a message that arrives in pieces"), tag); //! ``` @@ -47,8 +35,7 @@ //! //! A key is the expanded schedule plus the two 16-byte subkeys: 208, 240 and 272 bytes for //! AES-128, -192 and -256. A worker is a clone of that plus the running `C_i`, the pending block -//! and its length: 248, 280 and 312 bytes. The `MAC` route builds the same worker per message, -//! deriving the subkeys each time. +//! and its length: 248, 280 and 312 bytes. //! //! [`AES128Key`]: crate::AES128Key //! [`MAC`]: bouncycastle_core::traits::MAC diff --git a/crypto/aes/src/ctr.rs b/crypto/aes/src/ctr.rs index c2456c47e..6cba5c5be 100644 --- a/crypto/aes/src/ctr.rs +++ b/crypto/aes/src/ctr.rs @@ -3,85 +3,79 @@ //! See [`bouncycastle_cipher::modes::ctr`] for details on the Counter construction. //! //! The aliases here are stream ciphers: the data is a `&mut [u8]` of any length, encrypted or -//! decrypted in place since ciphertext is exactly as long as the plaintext. The nonce is generated -//! by encryption and returned; there is no API for supplying one. `Dir` is [`Encrypting`] or [`Decrypting`]; -//! the wrong direction is a compile error, not a runtime check. +//! decrypted in place since ciphertext is exactly as long as the plaintext. They come from a key, +//! [`AES128Key`](crate::AES128Key) and friends, through the [`SymmetricEncryptionKey`] / +//! [`SymmetricDecryptionKey`] pair. The nonce is drawn from the caller's RNG by encryption and +//! returned; there is no API for supplying one. `Dir` is [`Encrypting`] or [`Decrypting`]; the +//! wrong direction is a compile error, not a runtime check. //! //! # Nonce and counter length //! //! **The nonce length is 12 bytes, the counter is 4 bytes.** //! -//! These aliases fix a **12-byte nonce** ([`CTR_NONCE_LEN`]), leaving the remainder of each block to -//! be a 4-byte counter. That allows 2^32 blocks -- 64 GiB -- in one message, and past it the -//! mode errors rather than repeating the keystream. A shorter message limit in exchange for more nonce -//! bits is available by using `Ctr` directly with a 13, 14 or 15-byte nonce. +//! These aliases, and the key's mode `CTR` with its defaults, fix a **12-byte nonce** +//! ([`CTR_NONCE_LEN`]), leaving the remainder of each block to be a 4-byte counter. That allows +//! 2^32 blocks -- 64 GiB -- in one message, and past it the mode errors rather than repeating +//! the keystream. A shorter message limit in exchange for more nonce bits is available by naming +//! `CTR<13>`, `CTR<14>` or `CTR<15>` as the key's mode. //! //! # Usage Examples //! //! ## One-shot API //! -//! Basic usage can be obtained via the [`StreamCipherEncryptor`] and [`StreamCipherDecryptor`] API: -//! //! ``` -//! use bouncycastle_aes::AES_CTR_256; +//! use bouncycastle_aes::AES256Key; +//! use bouncycastle_cipher::modes::CTR; //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_CTR_256; -//! type AESDec = AES_CTR_256; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); +//! let key = AES256Key::::from_key_material(&material).expect("a valid AES-256 key"); //! -//! // An arbitrary plaintext to encrypt. //! // Any length: a stream cipher does not need a whole number of blocks. //! let plaintext = [0x5Au8; 47]; //! -//! // Encryption works in place. The nonce is generated for you and returned; there is no API for -//! // supplying one. +//! // Encryption works in place. The nonce is drawn from `rng` and returned. //! let mut data = plaintext; -//! let (_, nonce) = AESEnc::encrypt_inplace(&key, &mut data).expect("encryption"); +//! let (_, nonce) = key.encrypt_inplace(&mut rng, &mut data).expect("encryption"); //! -//! AESDec::decrypt_inplace(&key, &nonce, &mut data).expect("decryption"); +//! key.decrypt_inplace(&nonce, &mut data).expect("decryption"); //! assert_eq!(data, plaintext); //! ``` //! //! ## Streaming API //! -//! For data that arrives in pieces, the following APIs can be used. A stream cipher processes -//! every byte it is given, so nothing is held back between calls and the pieces can be of any -//! length: +//! For data that arrives in pieces, the workers' streaming methods can be used. A stream cipher +//! processes every byte it is given, so nothing is held back between calls and the pieces can be +//! of any length: //! //! ``` -//! use bouncycastle_aes::AES_CTR_128; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::CTR; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::{ -//! StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, -//! SymmetricCipherEncryptor, -//! }; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_CTR_128; -//! type AESDec = AES_CTR_128; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::::from_key_material(&material).expect("a valid AES-128 key"); //! -//! // An arbitrary plaintext to encrypt //! let plaintext = [0x5Au8; 50]; //! //! // Encrypt in 7-byte pieces, each in place. -//! let (mut encryptor, nonce) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! let (mut encryptor, nonce) = key.create_encryptor(&mut rng).expect("nonce draw"); //! let mut ciphertext = plaintext; //! for piece in ciphertext.chunks_mut(7) { //! encryptor.do_encrypt_inplace(piece).expect("encryption"); //! } //! //! // Decrypt in 19-byte pieces: the boundaries need not match the encryptor's. -//! let mut decryptor = AESDec::do_decrypt_init(&key, &nonce).expect("decrypt init"); +//! let mut decryptor = key.create_decryptor(&nonce); //! let mut recovered = ciphertext; //! for piece in recovered.chunks_mut(19) { //! decryptor.do_decrypt_inplace(piece).expect("decryption"); @@ -101,7 +95,7 @@ use bouncycastle_cipher::modes::Ctr; #[allow(unused_imports)] use bouncycastle_cipher::{Decrypting, Encrypting}; #[allow(unused_imports)] -use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; // end of imports needed for docs /// The nonce length these aliases use, leaving a 4-byte counter. diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs index c81378318..c5fbc691e 100644 --- a/crypto/aes/src/gcm.rs +++ b/crypto/aes/src/gcm.rs @@ -6,108 +6,104 @@ //! and decryption either returns the plaintext or fails the tag check. Both directions implement //! [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], which keep the tag detached, and //! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], which carry it inline as -//! `ciphertext || tag`. The nonce is generated by encryption and returned; there is no API for -//! supplying one. `Dir` is [`Encrypting`] or [`Decrypting`]; the wrong direction is a compile -//! error, not a runtime check. +//! `ciphertext || tag`. They come from a key, [`AES128Key`](crate::AES128Key) and friends, +//! through the [`AeadEncryptionKey`] / [`AeadDecryptionKey`] pair, which draws the nonce from +//! the caller's RNG and returns it, or takes the caller's through its `_nonce` forms. `Dir` is +//! [`Encrypting`] or [`Decrypting`]; the wrong direction is a compile error, not a runtime check. //! //! The tag is fixed at 128 bits, the maximum SP 800-38D Sec 5.2.1.2 allows. For a shorter tag -//! (96, 104, 112 or 120 bits), name [`Gcm`] directly with the desired `TAG_LEN`. The nonce is -//! always [`GCM_NONCE_LEN`] (12 bytes / 96 bits): `Gcm` has no nonce-length parameter at all, -//! unlike `Ctr`'s aliases, because SP 800-38D's `len(IV) != 96` branch (deriving `J0` from a GHASH -//! of the IV) is not implemented. +//! (96, 104, 112 or 120 bits), name the key's mode as `GCM<12>` and so on, or [`Gcm`] directly +//! with the desired `TAG_LEN`. The nonce is always [`GCM_NONCE_LEN`] (12 bytes / 96 bits): `Gcm` +//! has no nonce-length parameter at all, unlike `Ctr`'s aliases, because SP 800-38D's +//! `len(IV) != 96` branch (deriving `J0` from a GHASH of the IV) is not implemented. //! //! # Usage Examples //! //! ## One-shot API //! -//! Basic usage can be obtained via the [`AEADCipherEncryptor`] and [`AEADCipherDecryptor`] API, -//! which returns the tag separately from the ciphertext: +//! The detached one-shots return the tag separately from the ciphertext: //! //! ``` -//! use bouncycastle_aes::AES_GCM_256; +//! use bouncycastle_aes::AES256Key; +//! use bouncycastle_cipher::modes::GCM; //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! // Define ourselves convenience types. -//! type AESEnc = AES_GCM_256; -//! type AESDec = AES_GCM_256; -//! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); +//! let key = AES256Key::::from_key_material(&material).expect("a valid AES-256 key"); //! //! // The associated data is authenticated but not encrypted; the message is both. //! let aad = b"header, sent in the clear"; //! let message = b"a message of no particular length"; //! -//! // The nonce is generated for you and returned; there is no API for supplying one. -//! let (nonce, ciphertext, tag) = AESEnc::encrypt_detached(&key, aad, message).expect("encryption"); +//! // The nonce is drawn from `rng` and returned. +//! let (nonce, ciphertext, tag) = key.encrypt_detached(&mut rng, aad, message).expect("encryption"); //! assert_eq!(ciphertext.len(), message.len(), "GCM never expands the payload"); //! -//! let recovered = AESDec::decrypt_detached(&key, &nonce, aad, &ciphertext, &tag).expect("decryption"); +//! let recovered = key.decrypt_detached(&nonce, aad, &ciphertext, &tag).expect("decryption"); //! assert_eq!(recovered, message); //! //! // Tampering with the ciphertext, the tag or the associated data fails the tag check. //! let mut tampered = ciphertext.clone(); //! tampered[0] ^= 1; -//! assert!(AESDec::decrypt_detached(&key, &nonce, aad, &tampered, &tag).is_err()); -//! assert!(AESDec::decrypt_detached(&key, &nonce, b"other header", &ciphertext, &tag).is_err()); +//! assert!(key.decrypt_detached(&nonce, aad, &tampered, &tag).is_err()); +//! assert!(key.decrypt_detached(&nonce, b"other header", &ciphertext, &tag).is_err()); //! ``` //! //! ## Inline `ciphertext || tag` //! -//! Through the [`SymmetricCipherEncryptor`] and [`SymmetricCipherDecryptor`] API the tag is -//! appended to the ciphertext, so the output is 16 bytes longer than the input: +//! Through the `_with_aad` one-shots the tag is appended to the ciphertext, so the output is 16 +//! bytes longer than the input; an empty AAD is the no-AAD case: //! //! ``` -//! use bouncycastle_aes::AES_GCM_128; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::GCM; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_GCM_128; -//! type AESDec = AES_GCM_128; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::::from_key_material(&material).expect("a valid AES-128 key"); //! let message = b"a message of no particular length at all"; //! -//! let mut ciphertext = vec![0u8; AESEnc::encrypt_out_len(message.len())]; -//! let (nonce, written) = AESEnc::encrypt_out(&key, message, &mut ciphertext).expect("encryption"); -//! assert_eq!(written, message.len() + 16, "the ciphertext plus the tag"); +//! let (nonce, ciphertext) = key.encrypt_with_aad(&mut rng, &[], message).expect("encryption"); +//! assert_eq!(ciphertext.len(), message.len() + 16, "the ciphertext plus the tag"); //! -//! let mut plaintext = vec![0u8; AESDec::decrypt_out_len(ciphertext.len())]; -//! let n = AESDec::decrypt_out(&key, &nonce, &ciphertext, &mut plaintext).expect("decryption"); -//! assert_eq!(&plaintext[..n], message); +//! let plaintext = key.decrypt_with_aad(&nonce, &[], &ciphertext).expect("decryption"); +//! assert_eq!(plaintext, message); //! ``` //! //! ## Streaming API //! -//! For data that arrives in pieces, the following APIs can be used. All associated data must be -//! given via `do_update_aad` before the first piece of data, and the tag comes from +//! For data that arrives in pieces, the workers' streaming methods can be used. All associated +//! data must be given via `do_update_aad` before the first piece of data, and the tag comes from //! `do_encrypt_final`: //! //! ``` -//! use bouncycastle_aes::AES_GCM_128; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::GCM; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; //! use bouncycastle_core::traits::{ //! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, //! }; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! // Define ourselves convenience types. -//! type AESEnc = AES_GCM_128; -//! type AESDec = AES_GCM_128; -//! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::::from_key_material(&material).expect("a valid AES-128 key"); //! let aad = b"header"; //! let plaintext = [0x5Au8; 50]; //! //! // Encrypt in 7-byte pieces. A piece is encrypted as soon as it is given, so each call writes //! // exactly as many bytes as it was handed. -//! let (mut encryptor, nonce) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! let (mut encryptor, nonce) = key.create_encryptor(&mut rng).expect("nonce draw"); //! encryptor.do_update_aad(aad).expect("aad"); //! let mut ciphertext = Vec::new(); //! for piece in plaintext.chunks(7) { @@ -123,7 +119,7 @@ //! // Decrypt in 19-byte pieces. The decryptor holds back the last 16 bytes it has seen, since //! // those may be the tag, so a call can write fewer bytes than it was handed; `do_decrypt_final` //! // checks the tag and releases whatever is still held back. -//! let mut decryptor = AESDec::do_decrypt_init(&key, &nonce).expect("decrypt init"); +//! let mut decryptor = key.create_decryptor(&nonce); //! decryptor.do_update_aad(aad).expect("aad"); //! let mut recovered = Vec::new(); //! for piece in ciphertext.chunks(19) { @@ -150,6 +146,8 @@ use bouncycastle_core::traits::RNG; // Imports needed for docs #[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; +#[allow(unused_imports)] use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; diff --git a/crypto/aes/src/hazmat/ecb.rs b/crypto/aes/src/hazmat/ecb.rs index c2b44d912..b14f0b621 100644 --- a/crypto/aes/src/hazmat/ecb.rs +++ b/crypto/aes/src/hazmat/ecb.rs @@ -5,100 +5,90 @@ //! //! See [`bouncycastle_cipher::modes::hazmat::ecb::Ecb`] for details on the ElectronicCodebook construction. //! -//! The aliases here are padded block ciphers that accept input of any size; `NoPadding` accepts +//! The aliases here name the padded workers, which accept input of any size; `NoPadding` accepts //! only whole blocks but goes through the same adapter. The unpadded mode underneath them, which //! implements the block-cipher traits directly, is [`Ecb`] and is not re-exported from this crate. +//! The workers come from a key: [`AES128Key>`](crate::AES128Key) (or `ECB`) +//! hands them out through the [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] pair, and +//! `AES128Key>` hands out [`Ecb`] itself through the [`EcbEncryptionKey`] / +//! [`EcbDecryptionKey`] pair. //! -//! ECB has no IV, so its `INIT_DATA_LEN` is 0: encryption returns an empty array, decryption takes -//! one, and the ciphertext is exactly the padded plaintext with nothing prepended. The RNG-taking -//! constructors, `do_encrypt_init_rng` and `encrypt_rng_out`, panic, as -//! [`SymmetricCipherEncryptor::do_encrypt_init_rng`] requires of a cipher with no init data to -//! generate; use the plain `do_encrypt_init` / `encrypt_out`. +//! ECB has no IV, so its `INIT_DATA_LEN` is 0: the padded pair's encryption returns an empty +//! array, its decryption takes one, and the ciphertext is exactly the padded plaintext with +//! nothing prepended. The RNG that pair's `create_encryptor` and one-shots take is never drawn +//! from, and the `ECB` pair takes none at all. //! //! # Usage Examples //! //! ## One-shot API //! -//! Basic usage can be obtained via the [`SymmetricCipherEncryptor`] and [`SymmetricCipherDecryptor`] API: -//! //! ``` -//! use bouncycastle_aes::hazmat::AES_ECB_256; -//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_aes::AES256Key; +//! use bouncycastle_cipher::modes::hazmat::ecb::ECB; //! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! // Define ourselves convenience types. -//! type AESEnc = AES_ECB_256; -//! type AESDec = AES_ECB_256; -//! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); +//! let key = AES256Key::>::from_key_material(&material).expect("a valid AES-256 key"); //! -//! // An arbitrary plaintext to encrypt //! // Any length: PKCS#7 pads it out to whole blocks, so 50 bytes is as good as 48. //! let plaintext = [0x5Au8; 50]; //! //! // ECB has no IV, so the init data that comes back is empty. -//! let (no_iv, ciphertext) = AESEnc::encrypt(&key, &plaintext).expect("encryption"); +//! let (no_iv, ciphertext) = key.encrypt(&mut rng, &plaintext).expect("encryption"); //! assert_eq!(no_iv, [0u8; 0]); //! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); //! -//! let recovered = AESDec::decrypt(&key, &no_iv, &ciphertext).expect("decryption"); +//! let recovered = key.decrypt(&no_iv, &ciphertext).expect("decryption"); //! assert_eq!(recovered, plaintext); //! ``` //! //! ## Streaming API //! -//! For data that arrives in pieces, the following APIs can be used: +//! For data that arrives in pieces, the workers' streaming methods can be used: //! //! ``` -//! use bouncycastle_aes::AES_BLOCK_LEN; -//! use bouncycastle_aes::hazmat::AES_ECB_128; +//! use bouncycastle_aes::{AES128Key, AES_BLOCK_LEN}; +//! use bouncycastle_cipher::modes::hazmat::ecb::ECB; +//! use bouncycastle_cipher::padding::PKCS7; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; //! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! use bouncycastle_cipher::padding::PKCS7; -//! -//! // Define ourselves convenience types. -//! type AESEnc = AES_ECB_128; -//! type AESDec = AES_ECB_128; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::>::from_key_material(&material).expect("a valid AES-128 key"); //! -//! // An arbitrary plaintext to encrypt //! let plaintext = [0x5Au8; 50]; //! -//! // The streaming (chunked) API allows for data to be handed to the cipher as it arrives, in chunks -//! // of any length, but it will only be processed once a full block has been received. -//! // Here, we will use 7-byte chunks -//! let (mut encryptor, no_iv) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! // Data can be handed to the encryptor as it arrives, in pieces of any length, but a block is +//! // only written out once it is complete. Here, 7-byte pieces. +//! let (mut encryptor, no_iv) = key.create_encryptor(&mut rng).expect("nothing is drawn"); //! //! let mut ciphertext = Vec::new(); -//! //! for piece in plaintext.chunks(7) { //! let mut out = [0u8; AES_BLOCK_LEN]; //! let bytes_written = encryptor.do_encrypt_out(piece, &mut out).expect("encryption"); -//! //! // If that doesn't complete a block, then nothing is written. -//! if bytes_written != 0 { -//! ciphertext.extend_from_slice(&out[..bytes_written]); -//! } +//! ciphertext.extend_from_slice(&out[..bytes_written]); //! } //! let (last_block, last_len) = encryptor.do_encrypt_final().expect("padding the final block"); //! ciphertext.extend_from_slice(&last_block[..last_len]); //! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); //! -//! // Decrypt the ciphertext in 19-byte chunks. -//! let mut decryptor = AESDec::do_decrypt_init(&key, &no_iv).expect("decrypt init"); +//! // Decrypt the ciphertext in 19-byte pieces; the decryptor is infallible to set up. +//! let mut decryptor = key.create_decryptor(&no_iv); //! let mut recovered = Vec::new(); //! for piece in ciphertext.chunks(19) { //! let mut out = [0u8; AES_BLOCK_LEN]; //! let bytes_written = decryptor.do_decrypt_out(piece, &mut out).expect("decryption"); -//! if bytes_written != 0 { -//! recovered.extend_from_slice(&out[..bytes_written]); -//! } +//! recovered.extend_from_slice(&out[..bytes_written]); //! } //! let (last_block, last_len) = decryptor.do_decrypt_final().expect("a valid final block"); //! recovered.extend_from_slice(&last_block[..last_len]); @@ -111,41 +101,45 @@ //! error rather than something silently padded: //! //! ``` -//! use bouncycastle_aes::hazmat::AES_ECB_128; -//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::SymmetricCipherEncryptor; -//! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::hazmat::ecb::ECB; //! use bouncycastle_cipher::padding::NoPadding; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! // Define ourselves a convenience type for the encryption direction with no padding. -//! type Enc = AES_ECB_128; -//! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = AES128Key::>::from_key_material(&material).unwrap(); //! //! // A whole block is fine, and comes out the same length. //! let mut out = [0u8; 16]; -//! let (_no_iv, written) = Enc::encrypt_out(&key, &[0u8; 16], &mut out).expect("aligned"); +//! let (written, _no_iv) = key.encrypt_out(&mut rng, &[0u8; 16], &mut out).expect("aligned"); //! assert_eq!(written, 16); //! //! // Five bytes is not, and is refused rather than padded. -//! let mut out = [0u8; 16]; -//! assert!(Enc::encrypt_out(&key, b"hello", &mut out).is_err()); +//! assert!(key.encrypt_out(&mut rng, b"hello", &mut out).is_err()); //! ``` //! -//! The padding scheme is part of the type, so the two schemes are different types and cannot be -//! interchanged. A value built with one will not satisfy a binding annotated with the other. +//! The padding scheme is part of the key type, so the two schemes' workers are different types +//! and cannot be interchanged. A value built with one will not satisfy a binding annotated with +//! the other. //! //! ```compile_fail +//! use bouncycastle_aes::AES128Key; //! use bouncycastle_aes::hazmat::AES_ECB_128; -//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::SymmetricCipherEncryptor; //! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_cipher::modes::hazmat::ecb::ECB; //! use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = AES128Key::>::from_key_material(&material).unwrap(); //! -//! // Built as NoPadding, annotated as PKCS7: mismatched types. -//! let (enc, _no_iv) = AES_ECB_128::::do_encrypt_init(&key).unwrap(); +//! // Built from a NoPadding key, annotated as PKCS7: mismatched types. +//! let (enc, _no_iv) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); //! let _mismatched: AES_ECB_128 = enc; //! ``` //! @@ -157,19 +151,21 @@ //! that in the least. It makes ECB accept any length; it does not make it safe. //! //! ``` -//! use bouncycastle_aes::hazmat::AES_ECB_128; +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::Blocks; +//! use bouncycastle_cipher::modes::hazmat::ecb::ECB; +//! use bouncycastle_core::hazmat::EcbEncryptionKey; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::SymmetricCipherEncryptor; -//! use bouncycastle_cipher::Encrypting; -//! use bouncycastle_cipher::padding::NoPadding; +//! use bouncycastle_core::symmetric_key::FromKeyMaterial; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let key = AES128Key::>::from_key_material(&material).unwrap(); //! //! // Two identical blocks in... -//! let (_, ciphertext) = -//! AES_ECB_128::::encrypt(&key, &[0x5Au8; 32]).expect("encryption"); +//! let mut blocks = [0x5Au8; 32]; +//! key.encrypt_inplace(&mut blocks).expect("encryption"); //! // ...two identical blocks out. Nothing here chains, so nothing hides the repetition. -//! assert_eq!(ciphertext[..16], ciphertext[16..]); +//! assert_eq!(blocks[..16], blocks[16..]); //! ``` use crate::AES_BLOCK_LEN; @@ -188,7 +184,9 @@ use bouncycastle_core::key_material::KeyMaterial; #[allow(unused_imports)] use bouncycastle_cipher::padding::{NoPadding, PKCS7}; #[allow(unused_imports)] -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey}; +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; // end of imports needed for docs /// AES-128 in ECB mode with a padding scheme. diff --git a/crypto/aes/src/key.rs b/crypto/aes/src/key.rs index 81a0fe1ba..04c84043d 100644 --- a/crypto/aes/src/key.rs +++ b/crypto/aes/src/key.rs @@ -5,12 +5,14 @@ //! is bound to its AES variant and its mode by the Rust type rather than by the length of a byte //! buffer and the call site. Validation and key expansion happen once, in the constructor, and //! every message under the key starts from the stored schedule (and, for GCM, the stored hash -//! subkey, or for CMAC the stored subkeys). Using one key under two modes is two +//! subkey, or for CMAC the stored subkeys), which the workers borrow rather than copy. Using one key under two modes is two //! `from_key_material` calls on two key types; the accidental version does not compile. A MAC //! mode's key takes `KeyType::MACKey` material, a cipher mode's `KeyType::SymmetricCipherKey`. -//! Which workers a key hands out is decided by its mode: the AEAD and MAC role traits for the -//! modes that fit them, and for `GMAC`, whose tag is nonce-bound, an inherent pair (see -//! [`crate::gmac`]). +//! Which workers a key hands out is decided by its mode: the symmetric pair for the stream modes +//! and the padded block modes (`CFB`, `CFB8`, `CTR`, `CBC`, `ECB`), the block pair +//! for `CBC`, the `hazmat` ECB pair for `ECB`, the AEAD pairs for `GCM`, `CCM` +//! and `CCM_Packet`, the MAC pair for `CMAC`, and for `GMAC`, whose tag is nonce-bound, an inherent +//! pair (see [`crate::gmac`]). //! //! # Usage Examples //! @@ -71,13 +73,16 @@ use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal, AESInternal} use crate::{AES128Params, AES192Params, AES256Params, AESParams}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ - AeadDecryptMode, AeadEncryptMode, AeadPacketDecryptMode, AeadPacketEncryptMode, MacMode, Mode, + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, + BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbDecryptionKey, EcbEncryptMode, + EcbEncryptionKey, MacMode, Mode, ModeKey, SymmetricDecryptMode, SymmetricEncryptMode, do_hazardous_operations, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::symmetric_key::{ - AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, - FromKeyMaterial, MacGenerationKey, MacVerificationKey, + AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, AeadDecryptionKey, AeadEncryptionKey, + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, MacGenerationKey, MacVerificationKey, + SymmetricDecryptionKey, SymmetricEncryptionKey, }; use bouncycastle_core::traits::RNG; @@ -192,6 +197,12 @@ impl> FromKeyMaterial<32> for AES256Key { } } +impl>> ModeKey, M> for AESKey { + fn state(&self) -> &M::KeyState { + &self.state + } +} + // The role impls forward to whichever mode traits `M` implements over this permutation, so which // roles a key type carries is decided by its mode. They are per crate rather than a blanket in // `core` because the key state leaves the key only through here. @@ -208,16 +219,19 @@ where P: AESParams, M: AeadEncryptMode, KEY_LEN, NONCE_LEN, TAG_LEN, FINAL_LEN>, { - type Encryptor = M::Encryptor; + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { M::encrypt_init(&self.state, rng) } - fn create_encryptor_nonce(&self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor { + fn create_encryptor_nonce<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor<'k> { M::encrypt_init_nonce(&self.state, nonce) } @@ -254,9 +268,12 @@ where P: AESParams, M: AeadDecryptMode, KEY_LEN, NONCE_LEN, TAG_LEN, FINAL_LEN>, { - type Decryptor = M::Decryptor; + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; - fn create_decryptor(&self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor { + fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { M::decrypt_init(&self.state, nonce) } @@ -278,28 +295,31 @@ impl< const NONCE_LEN: usize, const TAG_LEN: usize, const FINAL_LEN: usize, -> AeadPacketEncryptionKey for AESKey +> AeadDeclaredEncryptionKey for AESKey where P: AESParams, - M: AeadPacketEncryptMode, KEY_LEN, NONCE_LEN, TAG_LEN, FINAL_LEN>, + M: AeadDeclaredEncryptMode, KEY_LEN, NONCE_LEN, TAG_LEN, FINAL_LEN>, { - type Encryptor = M::Encryptor; + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, aad_len: usize, payload_len: usize, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { M::encrypt_init(&self.state, rng, aad_len, payload_len) } - fn create_encryptor_nonce( - &self, + fn create_encryptor_nonce<'k>( + &'k self, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result { + ) -> Result, SymmetricCipherError> { M::encrypt_init_nonce(&self.state, nonce, aad_len, payload_len) } @@ -330,19 +350,22 @@ impl< const NONCE_LEN: usize, const TAG_LEN: usize, const FINAL_LEN: usize, -> AeadPacketDecryptionKey for AESKey +> AeadDeclaredDecryptionKey for AESKey where P: AESParams, - M: AeadPacketDecryptMode, KEY_LEN, NONCE_LEN, TAG_LEN, FINAL_LEN>, + M: AeadDeclaredDecryptMode, KEY_LEN, NONCE_LEN, TAG_LEN, FINAL_LEN>, { - type Decryptor = M::Decryptor; + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; - fn create_decryptor( - &self, + fn create_decryptor<'k>( + &'k self, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result { + ) -> Result, SymmetricCipherError> { M::decrypt_init(&self.state, nonce, aad_len, payload_len) } @@ -357,18 +380,126 @@ where } } +impl + SymmetricEncryptionKey for AESKey +where + P: AESParams, + M: SymmetricEncryptMode, KEY_LEN, INIT_DATA_LEN, FINAL_LEN>, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + SymmetricDecryptionKey for AESKey +where + P: AESParams, + M: SymmetricDecryptMode, KEY_LEN, INIT_DATA_LEN, FINAL_LEN>, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, init_data) + } +} + +impl + BlockEncryptionKey for AESKey +where + P: AESParams, + M: BlockEncryptMode, KEY_LEN, INIT_DATA_LEN, BLOCK_LEN>, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + BlockDecryptionKey for AESKey +where + P: AESParams, + M: BlockDecryptMode, KEY_LEN, INIT_DATA_LEN, BLOCK_LEN>, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, init_data) + } +} + +impl EcbEncryptionKey + for AESKey +where + P: AESParams, + M: EcbEncryptMode, KEY_LEN, BLOCK_LEN>, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>(&'k self) -> Self::Encryptor<'k> { + M::encrypt_init(&self.state) + } +} + +impl EcbDecryptionKey + for AESKey +where + P: AESParams, + M: EcbDecryptMode, KEY_LEN, BLOCK_LEN>, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state) + } +} + impl>> MacGenerationKey for AESKey { - type Generator = M::Worker; + type Generator<'k> + = M::Worker<'k> + where + Self: 'k; - fn create_generator(&self) -> Self::Generator { + fn create_generator<'k>(&'k self) -> Self::Generator<'k> { M::mac_init(&self.state) } } impl>> MacVerificationKey for AESKey { - type Verifier = M::Worker; + type Verifier<'k> + = M::Worker<'k> + where + Self: 'k; - fn create_verifier(&self) -> Self::Verifier { + fn create_verifier<'k>(&'k self) -> Self::Verifier<'k> { M::mac_init(&self.state) } } diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs index 5f240af11..20a95af36 100644 --- a/crypto/aes/src/lib.rs +++ b/crypto/aes/src/lib.rs @@ -23,9 +23,9 @@ //! * [AES_GCM](crate::gcm) //! * [AES_GMAC](crate::gmac), a nonce-based MAC rather than a cipher //! -//! A key that will encrypt many messages is better held as a mode-bound key object such as -//! [`AES256Key`](AES256Key), which validates and expands once and hands out the mode's -//! encryptors and decryptors; see [`key`]. +//! Each mode is used through a mode-bound key object such as [`AES256Key`](AES256Key), +//! which validates and expands the material once and hands out the mode's encryptors and +//! decryptors; see [`key`]. //! //! AES in ECB mode, [`AES_ECB_128`](hazmat::AES_ECB_128) and friends, is under [`hazmat`] because //! it is a building block for other modes, not itself a confidentiality mode for data. diff --git a/crypto/aes/tests/cbc_bc-test-data.rs b/crypto/aes/tests/cbc_bc-test-data.rs index c3bf389bd..ab98a03c2 100644 --- a/crypto/aes/tests/cbc_bc-test-data.rs +++ b/crypto/aes/tests/cbc_bc-test-data.rs @@ -30,11 +30,9 @@ //! than in SP 800-38A, and implementing it from anything else would be guesswork. The test reports //! how many it skipped so the gap stays visible. -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Cbc; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::{Blocks, CBC}; +use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; @@ -59,12 +57,12 @@ enum Grouping { Pairs, } -/// Runs one CBC case in one direction, for a given permutation, under the given grouping. +/// Runs one CBC case in one direction, for a given key type, under the given grouping. /// -/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's -/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- -/// so a change that ignored the RNG could not pass silently. -fn run_case( +/// Encryption is driven through the key's `create_encryptor` with a `FixedSeedRNG` emitting the +/// vector's IV, and the returned init data is checked against that IV before any ciphertext is +/// compared -- so a change that ignored the RNG could not pass silently. +fn run_case( key_bytes: &[u8], iv: [u8; BLOCK_LEN], input: &[[u8; BLOCK_LEN]], @@ -72,17 +70,16 @@ fn run_case( grouping: Grouping, ) -> Vec<[u8; BLOCK_LEN]> where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + + BlockEncryptionKey + + BlockDecryptionKey, { - let key = cipher_key::(key_bytes); + let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut out: Vec<[u8; BLOCK_LEN]> = Vec::with_capacity(input.len()); if encrypt { - let (mut enc, got_iv) = Cbc::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(iv), - ) - .expect("encrypt init"); + let (mut enc, got_iv) = + key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("encrypt init"); assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); match grouping { @@ -108,8 +105,7 @@ where } } } else { - let mut dec = - Cbc::::do_decrypt_init(&key, &iv).expect("dec init"); + let mut dec = key.create_decryptor(&iv); match grouping { Grouping::Single => { @@ -147,9 +143,9 @@ fn run_case_for_key_len( grouping: Grouping, ) -> Vec<[u8; BLOCK_LEN]> { match key_bytes.len() { - 16 => run_case::(key_bytes, iv, input, encrypt, grouping), - 24 => run_case::(key_bytes, iv, input, encrypt, grouping), - 32 => run_case::(key_bytes, iv, input, encrypt, grouping), + 16 => run_case::>, 16>(key_bytes, iv, input, encrypt, grouping), + 24 => run_case::>, 24>(key_bytes, iv, input, encrypt, grouping), + 32 => run_case::>, 32>(key_bytes, iv, input, encrypt, grouping), other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), } } diff --git a/crypto/aes/tests/cbc_wycheproof.rs b/crypto/aes/tests/cbc_wycheproof.rs index 9ca9a2c27..054536074 100644 --- a/crypto/aes/tests/cbc_wycheproof.rs +++ b/crypto/aes/tests/cbc_wycheproof.rs @@ -9,10 +9,11 @@ //! //! The file's "PKCS #5" is the padding of RFC 5652 Sec 6.3 (`k - (lth mod k)` octets of value //! `k - (lth mod k)`), which the cipher crate provides as [`PKCS7`]; the two names differ only in -//! that PKCS #5 was written for 8-byte blocks. So these vectors drive the padded aliases -//! `AES_CBC_128<_, PKCS7>` and friends, i.e. CBC through the `PaddedBlockCipherEncryptor` / -//! `PaddedBlockCipherDecryptor` adapters, where `cbc_bc-test-data.rs` and `sp800_38a_cbc_tests.rs` -//! drive the unpadded [`Cbc`](bouncycastle_cipher::modes::Cbc) underneath them. +//! that PKCS #5 was written for 8-byte blocks. So these vectors drive `AES128Key>` and +//! friends, whose workers are the padded aliases `AES_CBC_128<'_, _, PKCS7>`, i.e. CBC through the +//! `PaddedBlockCipherEncryptor` / `PaddedBlockCipherDecryptor` adapters, where +//! `cbc_bc-test-data.rs` and `sp800_38a_cbc_tests.rs` drive the unpadded +//! [`Cbc`](bouncycastle_cipher::modes::Cbc) underneath them through `CBC`. //! //! # Why this set is worth having alongside the ACVP one //! @@ -26,13 +27,16 @@ //! The IV is supplied through a `FixedSeedRNG`, as in `cbc_bc-test-data.rs`, and the returned IV is //! asserted to be the vector's. -use bouncycastle_aes::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CBC; use bouncycastle_cipher::padding::PKCS7; -use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{PaddingError, SymmetricCipherError}; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; @@ -69,15 +73,15 @@ enum Expected { NoFinalBlock, } -/// Runs one case through the padded `AES_CBC_*<_, PKCS7>` pair at one key length. +/// Runs one case through the `AES*Key>` key at one key length. /// /// For a valid case, `msg` must encrypt to exactly `expected_ct` under the vector's IV, and /// `expected_ct` must decrypt back to `msg`. For an invalid case only the decrypt direction is /// checked -- re-encrypting `msg` with correct padding has no reason to reproduce a deliberately /// mis-padded `ct` -- and it must fail with the variant the case's flags predict, leaving the -/// output buffer zeroized: CBC writes plaintext before it reaches the padding, and -/// `SymmetricCipherDecryptor::decrypt_out` must wipe it when the final block does not unpad. -fn run_case( +/// output buffer zeroized: CBC writes plaintext before it reaches the padding, and the key's +/// `decrypt_out` must wipe it when the final block does not unpad. +fn run_case( tc_id: u64, key_bytes: &[u8], iv: [u8; BLOCK_LEN], @@ -85,16 +89,22 @@ fn run_case( expected_ct: &[u8], expected: Expected, ) where - E: SymmetricCipherEncryptor, - D: SymmetricCipherDecryptor, + K: FromKeyMaterial + + SymmetricEncryptionKey + + SymmetricDecryptionKey, { - let key = cipher_key::(key_bytes); + let key = K::from_key_material(&cipher_key::(key_bytes)) + .unwrap_or_else(|e| panic!("tcId {tc_id}: the key should be accepted: {e:?}")); if expected == Expected::Valid { - let mut ct = vec![0u8; E::encrypt_out_len(msg.len())]; - let (got_iv, written) = - E::encrypt_rng_out(&key, &mut FixedSeedRNG::::new(iv), msg, &mut ct) - .unwrap_or_else(|e| panic!("tcId {tc_id}: valid case failed to encrypt: {e:?}")); + let ct_len = + as SymmetricCipherEncryptor>::encrypt_out_len( + msg.len(), + ); + let mut ct = vec![0u8; ct_len]; + let (written, got_iv) = key + .encrypt_out(&mut FixedSeedRNG::::new(iv), msg, &mut ct) + .unwrap_or_else(|e| panic!("tcId {tc_id}: valid case failed to encrypt: {e:?}")); assert_eq!(got_iv, iv, "tcId {tc_id}: the seeded RNG must reproduce the vector's IV"); ct.truncate(written); assert_eq!(ct, expected_ct, "tcId {tc_id}: ciphertext mismatch"); @@ -102,8 +112,12 @@ fn run_case( // Pre-filled with a non-zero byte, so a buffer the decryptor never wrote is not mistaken for // one it zeroized. - let mut plaintext = vec![0xEEu8; D::decrypt_out_len(expected_ct.len())]; - let outcome = D::decrypt_out(&key, &iv, expected_ct, &mut plaintext); + let pt_len = + as SymmetricCipherDecryptor>::decrypt_out_len( + expected_ct.len(), + ); + let mut plaintext = vec![0xEEu8; pt_len]; + let outcome = key.decrypt_out(&iv, expected_ct, &mut plaintext); if outcome.is_err() { assert!( plaintext.iter().all(|&b| b == 0), @@ -129,7 +143,7 @@ fn run_case( } } -/// Dispatches on the key length to the matching `AES_CBC_*` alias pair. +/// Dispatches on the key length to the matching `AES*Key>`. fn dispatch( tc_id: u64, key_bytes: &[u8], @@ -139,15 +153,15 @@ fn dispatch( expected: Expected, ) { match key_bytes.len() { - 16 => run_case::, AES_CBC_128, 16>( - tc_id, key_bytes, iv, msg, expected_ct, expected, - ), - 24 => run_case::, AES_CBC_192, 24>( - tc_id, key_bytes, iv, msg, expected_ct, expected, - ), - 32 => run_case::, AES_CBC_256, 32>( - tc_id, key_bytes, iv, msg, expected_ct, expected, - ), + 16 => { + run_case::>, 16>(tc_id, key_bytes, iv, msg, expected_ct, expected) + } + 24 => { + run_case::>, 24>(tc_id, key_bytes, iv, msg, expected_ct, expected) + } + 32 => { + run_case::>, 32>(tc_id, key_bytes, iv, msg, expected_ct, expected) + } other => panic!("tcId {tc_id}: AES keys are 16, 24 or 32 bytes, got {other}"), } } diff --git a/crypto/aes/tests/cfb8_bc-test-data.rs b/crypto/aes/tests/cfb8_bc-test-data.rs index 3b2ece09f..b54536c96 100644 --- a/crypto/aes/tests/cfb8_bc-test-data.rs +++ b/crypto/aes/tests/cfb8_bc-test-data.rs @@ -33,15 +33,12 @@ //! than in SP 800-38A, and implementing it from anything else would be guesswork. The test reports //! how many it skipped so the gap stays visible. -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Cfb8; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CFB8; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; use std::collections::BTreeMap; @@ -81,12 +78,12 @@ impl Grouping { } } -/// Runs one CFB8 case in one direction, for a given permutation, under the given grouping. +/// Runs one CFB8 case in one direction, for a given key type, under the given grouping. /// -/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's -/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- -/// so a change that ignored the RNG could not pass silently. -fn run_case( +/// Encryption is driven through the key's `create_encryptor` with a `FixedSeedRNG` emitting the +/// vector's IV, and the returned init data is checked against that IV before any ciphertext is +/// compared -- so a change that ignored the RNG could not pass silently. +fn run_case( key_bytes: &[u8], iv: [u8; BLOCK_LEN], input: &[u8], @@ -94,25 +91,25 @@ fn run_case( grouping: Grouping, ) -> Vec where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + + SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { - let key = cipher_key::(key_bytes); + let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); let chunk = grouping.chunk_len(data.len()); if encrypt { - let (mut enc, got_iv) = Cfb8::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(iv), - ) - .expect("encrypt init"); + let (mut enc, got_iv) = + key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("encrypt init"); assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); for piece in data.chunks_mut(chunk) { enc.do_encrypt_inplace(piece).unwrap(); } } else { - let mut dec = Cfb8::::do_decrypt_init(&key, &iv) - .expect("dec init"); + let mut dec = key.create_decryptor(&iv); for piece in data.chunks_mut(chunk) { dec.do_decrypt_inplace(piece).unwrap(); } @@ -130,9 +127,9 @@ fn run_case_for_key_len( grouping: Grouping, ) -> Vec { match key_bytes.len() { - 16 => run_case::(key_bytes, iv, input, encrypt, grouping), - 24 => run_case::(key_bytes, iv, input, encrypt, grouping), - 32 => run_case::(key_bytes, iv, input, encrypt, grouping), + 16 => run_case::, 16>(key_bytes, iv, input, encrypt, grouping), + 24 => run_case::, 24>(key_bytes, iv, input, encrypt, grouping), + 32 => run_case::, 32>(key_bytes, iv, input, encrypt, grouping), other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), } } diff --git a/crypto/aes/tests/cfb_bc-test-data.rs b/crypto/aes/tests/cfb_bc-test-data.rs index 52621ec3e..35be56106 100644 --- a/crypto/aes/tests/cfb_bc-test-data.rs +++ b/crypto/aes/tests/cfb_bc-test-data.rs @@ -37,15 +37,12 @@ //! than in SP 800-38A, and implementing it from anything else would be guesswork. The test reports //! how many it skipped so the gap stays visible. -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Cfb; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CFB; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; use std::collections::BTreeMap; @@ -86,12 +83,12 @@ impl Grouping { } } -/// Runs one CFB128 case in one direction, for a given permutation, under the given grouping. +/// Runs one CFB128 case in one direction, for a given key type, under the given grouping. /// -/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's -/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- -/// so a change that ignored the RNG could not pass silently. -fn run_case( +/// Encryption is driven through the key's `create_encryptor` with a `FixedSeedRNG` emitting the +/// vector's IV, and the returned init data is checked against that IV before any ciphertext is +/// compared -- so a change that ignored the RNG could not pass silently. +fn run_case( key_bytes: &[u8], iv: [u8; BLOCK_LEN], input: &[u8], @@ -99,25 +96,25 @@ fn run_case( grouping: Grouping, ) -> Vec where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + + SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { - let key = cipher_key::(key_bytes); + let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); let chunk = grouping.chunk_len(data.len()); if encrypt { - let (mut enc, got_iv) = Cfb::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(iv), - ) - .expect("encrypt init"); + let (mut enc, got_iv) = + key.create_encryptor(&mut FixedSeedRNG::::new(iv)).expect("encrypt init"); assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); for piece in data.chunks_mut(chunk) { enc.do_encrypt_inplace(piece).unwrap(); } } else { - let mut dec = - Cfb::::do_decrypt_init(&key, &iv).expect("dec init"); + let mut dec = key.create_decryptor(&iv); for piece in data.chunks_mut(chunk) { dec.do_decrypt_inplace(piece).unwrap(); } @@ -135,9 +132,9 @@ fn run_case_for_key_len( grouping: Grouping, ) -> Vec { match key_bytes.len() { - 16 => run_case::(key_bytes, iv, input, encrypt, grouping), - 24 => run_case::(key_bytes, iv, input, encrypt, grouping), - 32 => run_case::(key_bytes, iv, input, encrypt, grouping), + 16 => run_case::, 16>(key_bytes, iv, input, encrypt, grouping), + 24 => run_case::, 24>(key_bytes, iv, input, encrypt, grouping), + 32 => run_case::, 32>(key_bytes, iv, input, encrypt, grouping), other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), } } diff --git a/crypto/aes/tests/cmac_bc-test-data.rs b/crypto/aes/tests/cmac_bc-test-data.rs index d716e2074..df2a90df9 100644 --- a/crypto/aes/tests/cmac_bc-test-data.rs +++ b/crypto/aes/tests/cmac_bc-test-data.rs @@ -7,8 +7,9 @@ //! The set includes 8- and 56-bit tags, below the `CMAC_MIN_TAG_LEN` that `mac_out` will truncate //! to, so every case is computed as a full tag and compared on its first `macLen` bits. -use bouncycastle_aes::{AES_CMAC_128, AES_CMAC_192, AES_CMAC_256}; -use bouncycastle_core::traits::MAC; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CMAC; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; use std::collections::BTreeMap; @@ -23,19 +24,19 @@ const RESPONSE_FILE: &str = "CMAC-AES.4014544.rsp.json"; /// The full 16-byte AES-CMAC tag of `msg` under `key`, dispatched on the key length. fn aes_cmac(key: &[u8], msg: &[u8]) -> Vec { match key.len() { - 16 => AES_CMAC_128::new(&mac_key::<16>(key)).unwrap().mac(msg), - 24 => AES_CMAC_192::new(&mac_key::<24>(key)).unwrap().mac(msg), - 32 => AES_CMAC_256::new(&mac_key::<32>(key)).unwrap().mac(msg), + 16 => AES128Key::::from_key_material(&mac_key::<16>(key)).unwrap().mac(msg), + 24 => AES192Key::::from_key_material(&mac_key::<24>(key)).unwrap().mac(msg), + 32 => AES256Key::::from_key_material(&mac_key::<32>(key)).unwrap().mac(msg), other => panic!("unexpected AES key length {other}"), } } -/// `MAC::verify` of a full-length tag, dispatched on the key length. +/// The key's `verify` of a full-length tag, dispatched on the key length. fn aes_cmac_verify(key: &[u8], msg: &[u8], tag: &[u8]) -> bool { match key.len() { - 16 => AES_CMAC_128::new(&mac_key::<16>(key)).unwrap().verify(msg, tag), - 24 => AES_CMAC_192::new(&mac_key::<24>(key)).unwrap().verify(msg, tag), - 32 => AES_CMAC_256::new(&mac_key::<32>(key)).unwrap().verify(msg, tag), + 16 => AES128Key::::from_key_material(&mac_key::<16>(key)).unwrap().verify(msg, tag), + 24 => AES192Key::::from_key_material(&mac_key::<24>(key)).unwrap().verify(msg, tag), + 32 => AES256Key::::from_key_material(&mac_key::<32>(key)).unwrap().verify(msg, tag), other => panic!("unexpected AES key length {other}"), } } diff --git a/crypto/aes/tests/cmac_key_tests.rs b/crypto/aes/tests/cmac_key_tests.rs index ca26bb181..ef90143ac 100644 --- a/crypto/aes/tests/cmac_key_tests.rs +++ b/crypto/aes/tests/cmac_key_tests.rs @@ -1,9 +1,11 @@ //! `AES*Key`: the key object holding the subkeys, and the workers it hands out producing -//! exactly what `MAC::new` produces from the same material. +//! exactly what `Cmac::from_key_state` produces over a `CmacKeyState` built from the same bytes. +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_aes::{AES_CMAC_128, AES_CMAC_192, AES_CMAC_256, AES128Key, AES192Key, AES256Key}; use bouncycastle_cipher::modes::{CMAC, CmacKeyState}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; @@ -15,6 +17,13 @@ fn mac_key(fill: u8) -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&[fill; N], KeyType::MACKey).expect("a MAC key") } +/// The same bytes as [`mac_key`] tagged as the permutation itself takes them, for the bare key +/// state the key object is compared against. +fn cipher_key(fill: u8) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&[fill; N], KeyType::SymmetricCipherKey) + .expect("a cipher key") +} + const MSG: &[u8] = b"The quick brown fox jumps over the lazy dog"; /// NIST CSRC AES_CMAC.pdf Example #3 (Mlen = 20), AES-128: the key object reproduces a published @@ -32,23 +41,23 @@ fn the_key_object_gives_the_csrc_answer() { } #[test] -fn the_key_and_the_worker_constructor_agree_at_every_key_length() { +fn the_key_and_the_bare_key_state_agree_at_every_key_length() { + let state_16 = CmacKeyState::new::<16>(AES128Internal::new(&cipher_key::<16>(0x0b)).unwrap()); + let state_24 = CmacKeyState::new::<24>(AES192Internal::new(&cipher_key::<24>(0x0c)).unwrap()); + let state_32 = CmacKeyState::new::<32>(AES256Internal::new(&cipher_key::<32>(0x0d)).unwrap()); for len in [0usize, 1, 15, 16, 17, 43] { let msg = &MSG[..len]; - let k16 = mac_key::<16>(0x0b); assert_eq!( - AES128Key::::from_key_material(&k16).unwrap().mac(msg), - AES_CMAC_128::new(&k16).unwrap().mac(msg) + AES128Key::::from_key_material(&mac_key::<16>(0x0b)).unwrap().mac(msg), + AES_CMAC_128::from_key_state(&state_16).mac(msg) ); - let k24 = mac_key::<24>(0x0c); assert_eq!( - AES192Key::::from_key_material(&k24).unwrap().mac(msg), - AES_CMAC_192::new(&k24).unwrap().mac(msg) + AES192Key::::from_key_material(&mac_key::<24>(0x0c)).unwrap().mac(msg), + AES_CMAC_192::from_key_state(&state_24).mac(msg) ); - let k32 = mac_key::<32>(0x0d); assert_eq!( - AES256Key::::from_key_material(&k32).unwrap().mac(msg), - AES_CMAC_256::new(&k32).unwrap().mac(msg) + AES256Key::::from_key_material(&mac_key::<32>(0x0d)).unwrap().mac(msg), + AES_CMAC_256::from_key_state(&state_32).mac(msg) ); } } diff --git a/crypto/aes/tests/cmac_tests.rs b/crypto/aes/tests/cmac_tests.rs index 31f0de6c8..10c739e86 100644 --- a/crypto/aes/tests/cmac_tests.rs +++ b/crypto/aes/tests/cmac_tests.rs @@ -1,15 +1,17 @@ -//! `AES_CMAC_*` against the NIST CSRC AES-CMAC examples through the MAC trait test framework, -//! plus the streaming split points, key validation and suspend/resume over the real cipher. The -//! structural tests are in `bouncycastle-cipher` over a toy; the large vector sets are in -//! `cmac_bc-test-data.rs` and `cmac_wycheproof.rs`. +//! `AES*Key` against the NIST CSRC AES-CMAC examples through the MAC trait test framework, +//! plus the streaming split points, the key's construction policy and suspend/resume over the +//! real cipher. The structural tests are in `bouncycastle-cipher` over a toy; the large vector +//! sets are in `cmac_bc-test-data.rs` and `cmac_wycheproof.rs`. -use bouncycastle_aes::{AES_CMAC_128, AES_CMAC_192, AES_CMAC_256}; -use bouncycastle_cipher::modes::CMAC_MIN_TAG_LEN; -use bouncycastle_core::errors::{KeyMaterialError, MACError}; +use bouncycastle_aes::hazmat::{AES128Internal, AES256Internal}; +use bouncycastle_aes::{AES_CMAC_128, AES_CMAC_256, AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::{CMAC, CMAC_MIN_TAG_LEN, ModeNames}; use bouncycastle_core::hazmat::do_hazardous_operations; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial128, KeyMaterialTrait, KeyType}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::{Algorithm, MAC, SuspendableKeyed}; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_core_test_framework::mac::TestFrameworkMAC; use bouncycastle_hex as hex; @@ -62,26 +64,17 @@ const TAGS_256: [&str; 4] = [ fn csrc_examples() { let msg = hex::decode(MSG).unwrap(); let tf = TestFrameworkMAC::new(); + let key_128 = AES128Key::::from_key_material(&mac_key::<16>(KEY_128)).unwrap(); for (mlen, tag) in MLENS.iter().zip(TAGS_128) { - tf.test_mac::( - &mac_key::<16>(KEY_128), - &msg[..*mlen], - &hex::decode(tag).unwrap(), - ); + tf.test_mac_key(&key_128, &msg[..*mlen], &hex::decode(tag).unwrap()); } + let key_192 = AES192Key::::from_key_material(&mac_key::<24>(KEY_192)).unwrap(); for (mlen, tag) in MLENS.iter().zip(TAGS_192) { - tf.test_mac::( - &mac_key::<24>(KEY_192), - &msg[..*mlen], - &hex::decode(tag).unwrap(), - ); + tf.test_mac_key(&key_192, &msg[..*mlen], &hex::decode(tag).unwrap()); } + let key_256 = AES256Key::::from_key_material(&mac_key::<32>(KEY_256)).unwrap(); for (mlen, tag) in MLENS.iter().zip(TAGS_256) { - tf.test_mac::( - &mac_key::<32>(KEY_256), - &msg[..*mlen], - &hex::decode(tag).unwrap(), - ); + tf.test_mac_key(&key_256, &msg[..*mlen], &hex::decode(tag).unwrap()); } } @@ -89,90 +82,57 @@ fn csrc_examples() { /// held-back final block must come out the same wherever the call boundaries fall. #[test] fn streaming_split_points() { - let key = mac_key::<16>(KEY_128); + let key = AES128Key::::from_key_material(&mac_key::<16>(KEY_128)).unwrap(); let msg = hex::decode(MSG).unwrap(); for mlen in [20, 32, 33, 64] { - let expected = AES_CMAC_128::new(&key).unwrap().mac(&msg[..mlen]); + let expected = key.mac(&msg[..mlen]); for split in 0..=mlen { - let mut cmac = AES_CMAC_128::new(&key).unwrap(); + let mut cmac = key.create_generator(); cmac.do_update(&msg[..split]); cmac.do_update(&msg[split..mlen]); assert_eq!(cmac.do_final(), expected, "mlen {mlen}, split at {split}"); } - let mut cmac = AES_CMAC_128::new(&key).unwrap(); + let mut cmac = key.create_generator(); for b in &msg[..mlen] { cmac.do_update(core::slice::from_ref(b)); } assert_eq!(cmac.do_final(), expected, "mlen {mlen}, byte at a time"); } assert_eq!( - AES_CMAC_128::new(&key).unwrap().mac(&msg), + key.mac(&msg), hex::decode(TAGS_128[3]).unwrap(), "the 64-byte one-shot is the CSRC example" ); } +/// The key's construction suite at both ends of the key-length range: a `MACKey` of the right +/// length and strength, bound to the CMAC name or to nothing; a block cipher key is not a CMAC +/// key (SP 800-38B Sec 5.2, key separation). Wrong lengths are a compile-time matter on this path. #[test] -fn key_validation() { - // A block cipher key is not a CMAC key (SP 800-38B Sec 5.2, key separation). - let cipher_key = KeyMaterial128::from_bytes_as_type( - &hex::decode(KEY_128).unwrap(), - KeyType::SymmetricCipherKey, - ) - .unwrap(); - assert!(matches!( - AES_CMAC_128::new_allow_weak_key(&cipher_key), - Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) - )); - - // A full-strength MAC key of the wrong length. - let key_32 = mac_key::<32>(KEY_256); - for result in - [AES_CMAC_128::new(&key_32).err(), AES_CMAC_128::new_allow_weak_key(&key_32).err()] - { - assert_eq!(result, Some(MACError::KeyMaterialError(KeyMaterialError::InvalidLength))); - } - let key_16 = mac_key::<16>(KEY_128); - assert_eq!( - AES_CMAC_256::new_allow_weak_key(&key_16).err(), - Some(MACError::KeyMaterialError(KeyMaterialError::InvalidLength)) - ); - - // A 32-byte key tagged at 128 bits is too weak for AES-256, but accepted when asked. - let mut weak = mac_key::<32>(KEY_256); - weak.set_security_strength(SecurityStrength::_128bit).unwrap(); - assert!(matches!( - AES_CMAC_256::new(&weak), - Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) - )); - assert!(AES_CMAC_256::new_allow_weak_key(&weak).is_ok()); - - // The all-zero key is `KeyType::Zeroized`: weak, but a permitted type. - let zero = KeyMaterial128::from_bytes_as_type(&[0u8; 16], KeyType::MACKey).unwrap(); - assert_eq!(zero.key_type(), KeyType::Zeroized); - assert!(matches!( - AES_CMAC_128::new(&zero), - Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) - )); - assert!(AES_CMAC_128::new_allow_weak_key(&zero).is_ok()); +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::<16, AES128Key>(AES128Internal::CMAC_ALG_NAME, SecurityStrength::_128bit); + TestFrameworkFromKeyMaterial::new() + .test::<32, AES256Key>(AES256Internal::CMAC_ALG_NAME, SecurityStrength::_256bit); } #[test] fn truncation_to_the_minimum_tag() { - let key = mac_key::<16>(KEY_128); + let key = AES128Key::::from_key_material(&mac_key::<16>(KEY_128)).unwrap(); let msg = hex::decode(MSG).unwrap(); let full = hex::decode(TAGS_128[2]).unwrap(); let mut out = [0xAAu8; CMAC_MIN_TAG_LEN]; - assert_eq!(AES_CMAC_128::new(&key).unwrap().mac_out(&msg[..20], &mut out).unwrap(), 8); + assert_eq!(key.mac_out(&msg[..20], &mut out).unwrap(), 8); assert_eq!(out[..], full[..8]); - assert!(!AES_CMAC_128::new(&key).unwrap().verify(&msg[..20], &full[..8])); + assert!(!key.verify(&msg[..20], &full[..8])); } #[test] fn algorithm_params() { assert_eq!(AES_CMAC_128::ALG_NAME, "AES_CMAC_128"); assert_eq!(AES_CMAC_256::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); - let cmac = AES_CMAC_192::new(&mac_key::<24>(KEY_192)).unwrap(); + let cmac = + AES192Key::::from_key_material(&mac_key::<24>(KEY_192)).unwrap().create_generator(); assert_eq!(cmac.output_len(), 16); assert_eq!(cmac.max_security_strength(), SecurityStrength::_192bit); assert_eq!(STATE_LEN, 36); @@ -182,15 +142,16 @@ fn algorithm_params() { /// one-shot tag. #[test] fn suspend_resume() { - let key = mac_key::<16>(KEY_128); + let material = mac_key::<16>(KEY_128); + let key = AES128Key::::from_key_material(&material).unwrap(); let msg = hex::decode(MSG).unwrap(); for (mlen, tag) in MLENS.iter().zip(TAGS_128) { let expected = hex::decode(tag).unwrap(); for split in 0..=*mlen { - let mut cmac = AES_CMAC_128::new(&key).unwrap(); + let mut cmac = key.create_generator(); cmac.do_update(&msg[..split]); let state: [u8; STATE_LEN] = cmac.suspend(); - let mut resumed = AES_CMAC_128::from_suspended(state, &key).unwrap(); + let mut resumed = AES_CMAC_128::from_suspended(state, &material).unwrap(); resumed.do_update(&msg[split..*mlen]); assert_eq!(resumed.do_final(), expected, "mlen {mlen}, suspended at {split}"); } diff --git a/crypto/aes/tests/cmac_wycheproof.rs b/crypto/aes/tests/cmac_wycheproof.rs index d3e438132..c5206d907 100644 --- a/crypto/aes/tests/cmac_wycheproof.rs +++ b/crypto/aes/tests/cmac_wycheproof.rs @@ -4,15 +4,11 @@ //! this repository, i.e. at `../wycheproof` relative to the root of this git project. If it is //! missing, the test prints a warning and passes without checking anything. //! -//! Every valid tag is also checked through the key object, `AES*Key`, so the two routes are -//! held to the same answers. +//! Every tag is checked through the worker an `AES*Key` hands out and through the key's own +//! one-shots, so the two routes are held to the same answers. -use bouncycastle_aes::{AES_CMAC_128, AES_CMAC_192, AES_CMAC_256, AES128Key, AES192Key, AES256Key}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; use bouncycastle_cipher::modes::{CMAC, CMAC_MIN_TAG_LEN}; -use bouncycastle_core::errors::{KeyMaterialError, MACError}; -use bouncycastle_core::hazmat::do_hazardous_operations; -use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; -use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::MAC; use bouncycastle_core_test_framework::test_data_loaders::{hex_field, wycheproof_json}; @@ -28,18 +24,14 @@ where ::Generator: MAC, { let material = mac_key::(key); - let full = ::Generator::new(&material).unwrap().mac(msg); + let key = K::from_key_material(&material).unwrap_or_else(|e| panic!("tcId {tc_id}: {e:?}")); + let full = key.create_generator().mac(msg); assert_eq!(full == tag, valid, "tcId {tc_id}"); - assert_eq!( - ::Generator::new(&material).unwrap().verify(msg, tag), - valid, - "tcId {tc_id}" - ); + assert_eq!(key.create_generator().verify(msg, tag), valid, "tcId {tc_id}"); - let key = K::from_key_material(&material).unwrap_or_else(|e| panic!("tcId {tc_id}: {e:?}")); - assert_eq!(key.verify(msg, tag), valid, "tcId {tc_id}: via the key object"); + assert_eq!(key.verify(msg, tag), valid, "tcId {tc_id}: via the key's one-shot"); if valid { - assert_eq!(key.mac(msg), tag, "tcId {tc_id}: via the key object"); + assert_eq!(key.mac(msg), tag, "tcId {tc_id}: via the key's one-shot"); let mut out = [0u8; CMAC_MIN_TAG_LEN]; key.mac_out(msg, &mut out).unwrap(); assert_eq!(out[..], tag[..CMAC_MIN_TAG_LEN], "tcId {tc_id}: truncated tag"); @@ -69,18 +61,9 @@ fn wycheproof_aes_cmac() { 24 => run_case::<24, AES192Key>(tc_id, &key, &msg, &tag, expect_valid), 32 => run_case::<32, AES256Key>(tc_id, &key, &msg, &tag, expect_valid), _ => { - // "InvalidKeySize": no AES-CMAC accepts it, even without the strength check. + // "InvalidKeySize": no `AES*Key` takes it, since the key length is in + // the `KeyMaterial<16 | 24 | 32>` type; nothing to run at runtime. assert!(!expect_valid, "tcId {tc_id}"); - let mut k = KeyMaterial512::new(); - do_hazardous_operations(&mut k, |k| { - k.set_bytes_as_type(&key, KeyType::MACKey)?; - k.set_security_strength(SecurityStrength::None) - }) - .unwrap(); - let bad_len = Some(MACError::KeyMaterialError(KeyMaterialError::InvalidLength)); - assert_eq!(AES_CMAC_128::new_allow_weak_key(&k).err(), bad_len); - assert_eq!(AES_CMAC_192::new_allow_weak_key(&k).err(), bad_len); - assert_eq!(AES_CMAC_256::new_allow_weak_key(&k).err(), bad_len); bad_key += 1; continue; } diff --git a/crypto/aes/tests/ctr_bc-test-data.rs b/crypto/aes/tests/ctr_bc-test-data.rs index a0cb85326..be26dd0d5 100644 --- a/crypto/aes/tests/ctr_bc-test-data.rs +++ b/crypto/aes/tests/ctr_bc-test-data.rs @@ -37,15 +37,12 @@ //! `resultsArray` produced by a chained update rule defined in the ACVP AES specification rather //! than in SP 800-38A, and implementing it from anything else would be guesswork. -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Ctr; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CTR; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; use std::collections::BTreeMap; @@ -86,12 +83,12 @@ impl Grouping { } } -/// Runs one CTR case in one direction, for a given permutation, under the given grouping. +/// Runs one CTR case in one direction, for a given key type, under the given grouping. /// -/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's -/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- -/// so a change that ignored the RNG could not pass silently. -fn run_case( +/// Encryption is driven through the key's `create_encryptor` with a `FixedSeedRNG` emitting the +/// vector's nonce, and the returned init data is checked against that nonce before any ciphertext +/// is compared -- so a change that ignored the RNG could not pass silently. +fn run_case( key_bytes: &[u8], nonce: [u8; NONCE_LEN], input: &[u8], @@ -99,27 +96,25 @@ fn run_case( grouping: Grouping, ) -> Vec where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + + SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { - let key = cipher_key::(key_bytes); + let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); let chunk = grouping.chunk_len(data.len()); if encrypt { let (mut enc, got_iv) = - Ctr::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(nonce), - ) - .expect("encrypt init"); + key.create_encryptor(&mut FixedSeedRNG::::new(nonce)).expect("encrypt init"); assert_eq!(got_iv, nonce, "the pinned RNG should reproduce the vector's nonce"); for piece in data.chunks_mut(chunk) { enc.do_encrypt_inplace(piece).unwrap(); } } else { - let mut dec = - Ctr::::do_decrypt_init(&key, &nonce) - .expect("dec init"); + let mut dec = key.create_decryptor(&nonce); for piece in data.chunks_mut(chunk) { dec.do_decrypt_inplace(piece).unwrap(); } @@ -137,9 +132,9 @@ fn run_case_for_key_len( grouping: Grouping, ) -> Vec { match key_bytes.len() { - 16 => run_case::(key_bytes, nonce, input, encrypt, grouping), - 24 => run_case::(key_bytes, nonce, input, encrypt, grouping), - 32 => run_case::(key_bytes, nonce, input, encrypt, grouping), + 16 => run_case::>, 16>(key_bytes, nonce, input, encrypt, grouping), + 24 => run_case::>, 24>(key_bytes, nonce, input, encrypt, grouping), + 32 => run_case::>, 32>(key_bytes, nonce, input, encrypt, grouping), other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), } } diff --git a/crypto/aes/tests/ctr_bc_java_tests.rs b/crypto/aes/tests/ctr_bc_java_tests.rs index 2f680da89..43705a1fb 100644 --- a/crypto/aes/tests/ctr_bc_java_tests.rs +++ b/crypto/aes/tests/ctr_bc_java_tests.rs @@ -36,32 +36,33 @@ //! three key lengths -- and it is exact. Those cases are covered there and by the ACVP suite, so //! what is pinned here is specifically the part neither of them reaches: the narrow counters. -use bouncycastle_aes::hazmat::AES128Internal; -use bouncycastle_cipher::Encrypting; -use bouncycastle_cipher::modes::Ctr; +use bouncycastle_aes::AES128Key; +use bouncycastle_cipher::modes::CTR; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{StreamCipherEncryptor, SymmetricCipherEncryptor}; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +use bouncycastle_core::traits::StreamCipherEncryptor; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; /// The AES-128 key used for every vector in this file: SP 800-38A Appendix F's first key. const KEY: &str = "2b7e151628aed2a6abf7158809cf4f3c"; -fn key() -> KeyMaterial<16> { +/// The key under CTR at the given nonce length. +fn key() -> AES128Key> { let raw = hex::decode(KEY).expect("valid hex"); - KeyMaterial::<16>::from_bytes_as_type(&raw, KeyType::SymmetricCipherKey).expect("a valid key") + let material = KeyMaterial::<16>::from_bytes_as_type(&raw, KeyType::SymmetricCipherKey) + .expect("a valid key"); + AES128Key::from_key_material(&material).expect("a valid key") } /// Produces `blocks` blocks of keystream by encrypting zeros under the given nonce. fn keystream(nonce_hex: &str, blocks: usize) -> Vec { let nonce: [u8; NONCE_LEN] = hex::decode(nonce_hex).expect("valid hex").try_into().expect("nonce length"); - let (mut enc, got) = Ctr::::do_encrypt_init_rng( - &key(), - &mut FixedSeedRNG::::new(nonce), - ) - .expect("encrypt init"); + let (mut enc, got) = key::() + .create_encryptor(&mut FixedSeedRNG::::new(nonce)) + .expect("encrypt init"); assert_eq!(got, nonce, "the pinned RNG should reproduce the nonce"); let mut data = vec![0u8; blocks * 16]; @@ -151,11 +152,7 @@ fn three_byte_counter_matches_bc_java() { fn the_counter_limit_falls_where_bc_java_throws() { let nonce: [u8; 15] = hex::decode("5a5b5c5d5e5f606162636465666768").unwrap().try_into().unwrap(); - let (mut enc, _) = Ctr::::do_encrypt_init_rng( - &key(), - &mut FixedSeedRNG::<15>::new(nonce), - ) - .unwrap(); + let (mut enc, _) = key::<15>().create_encryptor(&mut FixedSeedRNG::<15>::new(nonce)).unwrap(); // BC Java encrypts 4096 bytes under this IV without complaint. let mut data = vec![0u8; 4096]; diff --git a/crypto/aes/tests/ctr_vector_tests.rs b/crypto/aes/tests/ctr_vector_tests.rs index d98452531..fe9d4071f 100644 --- a/crypto/aes/tests/ctr_vector_tests.rs +++ b/crypto/aes/tests/ctr_vector_tests.rs @@ -26,16 +26,13 @@ //! the counter starting at zero, so the two line up exactly when the IV's low four bytes are zero, //! which is why the IV above ends in `00000000`. See the [`Ctr`] module docs. -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Ctr; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CTR; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -100,11 +97,15 @@ fn key_material(hex_str: &str) -> KeyMaterial { /// through every path rather than only the batched one. const CHUNKINGS: [usize; 6] = [1, 5, 16, 17, 33, 69]; -fn check(name: &str, key_hex: &str, expected_hex: &str) +fn check(name: &str, key_hex: &str, expected_hex: &str) where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + + SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { - let key = key_material::(key_hex); + let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let nonce: [u8; NONCE_LEN] = unhex(NONCE).try_into().expect("a 12-byte nonce"); let plaintext = unhex(PLAINTEXT); let expected = unhex(expected_hex); @@ -114,11 +115,7 @@ where // Encryption, in one call and in every chunking. for chunk in [plaintext.len()].into_iter().chain(CHUNKINGS) { let (mut enc, got) = - Ctr::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(nonce), - ) - .expect("encrypt init"); + key.create_encryptor(&mut FixedSeedRNG::::new(nonce)).expect("encrypt init"); assert_eq!(got, nonce, "{name}: the pinned RNG should reproduce the nonce"); let mut data = plaintext.clone(); @@ -130,9 +127,7 @@ where // Decryption, likewise. for chunk in [expected.len()].into_iter().chain(CHUNKINGS) { - let mut dec = - Ctr::::do_decrypt_init(&key, &nonce) - .expect("decrypt init"); + let mut dec = key.create_decryptor(&nonce); let mut data = expected.clone(); for piece in data.chunks_mut(chunk) { dec.do_decrypt_inplace(piece).expect("decryption"); @@ -142,24 +137,23 @@ where // ...and the one-shot. let mut data = expected.clone(); - Ctr::::decrypt_inplace(&key, &nonce, &mut data) - .expect("one-shot decryption"); + key.decrypt_inplace(&nonce, &mut data).expect("one-shot decryption"); assert_eq!(data, plaintext, "{name}: one-shot"); } #[test] fn aes128_ctr_matches_openssl() { - check::("AES-128", KEY_128, CT_128); + check::>, 16>("AES-128", KEY_128, CT_128); } #[test] fn aes192_ctr_matches_openssl() { - check::("AES-192", KEY_192, CT_192); + check::>, 24>("AES-192", KEY_192, CT_192); } #[test] fn aes256_ctr_matches_openssl() { - check::("AES-256", KEY_256, CT_256); + check::>, 32>("AES-256", KEY_256, CT_256); } /// The vectors must actually depend on the counter advancing: the second block of ciphertext must diff --git a/crypto/aes/tests/gcm_tests.rs b/crypto/aes/tests/gcm_tests.rs index b2a445611..1fc0023b5 100644 --- a/crypto/aes/tests/gcm_tests.rs +++ b/crypto/aes/tests/gcm_tests.rs @@ -1,53 +1,41 @@ //! AES-GCM through the shared conformance suites. //! -//! What is checked here is that all three key lengths reach the shared `AEADCipherEncryptor` / -//! `AEADCipherDecryptor` conformance suite (`TestFrameworkAEADCipher`, which runs the -//! `SymmetricCipherEncryptor` / `SymmetricCipherDecryptor` suite first), that the inline +//! What is checked here is that all three key lengths reach the shared `AeadEncryptionKey` / +//! `AeadDecryptionKey` conformance suite (`TestFrameworkAEADCipher`), that the inline //! `ciphertext || tag` layout holds at every byte-boundary edge (`TestFrameworkAEADTaggedLayout`), //! and that a fresh nonce is generated per encryption. Algorithm correctness itself is pinned by //! the ACVP and bc-java known-answer suites beside this file. -use bouncycastle_aes::{AES_GCM_128, AES_GCM_192, AES_GCM_256}; -use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::GCM; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core_test_framework::aead::TestFrameworkAEADCipher; use bouncycastle_core_test_framework::aead::TestFrameworkAEADTaggedLayout; +use bouncycastle_rng::HashDRBG_SHA512; -fn key() -> KeyMaterial { +fn material() -> KeyMaterial { let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") } -/// All three key lengths satisfy the shared AEAD conformance suite, which includes the -/// symmetric-cipher suite the padding adapters and the stream modes run. +fn key>() -> K { + K::from_key_material(&material::()).expect("a valid key") +} + +fn os_rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +/// All three key lengths satisfy the shared AEAD conformance suite, which reaches the no-AAD +/// symmetric contract as the AEAD one with an empty AAD. #[test] fn all_three_key_lengths_conform_to_the_aead_suite() { let framework = TestFrameworkAEADCipher::new(); - framework.test_encryptor_decryptor::< - 16, - 12, - 16, - 16, - AES_GCM_128, - AES_GCM_128, - >(); - framework.test_encryptor_decryptor::< - 24, - 12, - 16, - 16, - AES_GCM_192, - AES_GCM_192, - >(); - framework.test_encryptor_decryptor::< - 32, - 12, - 16, - 16, - AES_GCM_256, - AES_GCM_256, - >(); + let mut rng = os_rng(); + framework.test_key::<16, 12, 16, 16, _>(&key::<16, AES128Key>(), &mut rng); + framework.test_key::<24, 12, 16, 16, _>(&key::<24, AES192Key>(), &mut rng); + framework.test_key::<32, 12, 16, 16, _>(&key::<32, AES256Key>(), &mut rng); } /// The inline `ciphertext || tag` layout -- where the tag lands, what the decryptor holds back, @@ -57,8 +45,8 @@ fn all_three_key_lengths_conform_to_the_aead_suite() { #[test] fn the_inline_tag_layout_conforms_at_every_edge() { let framework = TestFrameworkAEADTaggedLayout::new(); - framework.test::<16, 12, 16, 16, AES_GCM_128, AES_GCM_128>(); - framework.test::<32, 12, 16, 16, AES_GCM_256, AES_GCM_256>(); + framework.test_key::<16, 12, 16, 16, _>(&key::<16, AES128Key>()); + framework.test_key::<32, 12, 16, 16, _>(&key::<32, AES256Key>()); } /// The nonce is generated per encryption, so the same plaintext gives different ciphertext, and @@ -66,23 +54,15 @@ fn the_inline_tag_layout_conforms_at_every_edge() { #[test] fn each_encryption_gets_a_fresh_nonce() { let data = *b"the quick brown fox jumps over the lazy dog!!!"; + let key = key::<16, AES128Key>(); + let mut rng = os_rng(); let mut seen = std::collections::BTreeSet::new(); for _ in 0..16 { let mut ct = [0u8; 46]; - let (nonce, _, tag) = - AES_GCM_128::::encrypt_detached_out(&key::<16>(), b"aad", &data, &mut ct) - .unwrap(); + let (_, nonce, tag) = key.encrypt_detached_out(&mut rng, b"aad", &data, &mut ct).unwrap(); assert!(seen.insert(nonce), "nonce repeated across encryptions"); let mut pt = [0u8; 46]; - AES_GCM_128::::decrypt_detached_out( - &key::<16>(), - &nonce, - b"aad", - &ct, - &tag, - &mut pt, - ) - .unwrap(); + key.decrypt_detached_out(&nonce, b"aad", &ct, &tag, &mut pt).unwrap(); assert_eq!(pt, data); } } diff --git a/crypto/aes/tests/mode_key_tests.rs b/crypto/aes/tests/mode_key_tests.rs new file mode 100644 index 000000000..f007d5e24 --- /dev/null +++ b/crypto/aes/tests/mode_key_tests.rs @@ -0,0 +1,180 @@ +//! The mode-bound AES keys for the block, padded, stream and fixed-frame modes: the construction +//! suite and the shared framework's key runner for each, and what tells the modes apart on the +//! key path -- an `ECB` key takes no RNG, a padded ECB key's init data is empty, and a +//! `CBC` key's in-place one-shot is the `CBC` key's `encrypt_out`. +//! +//! The GCM, CCM, CMAC and GMAC keys have their own files. + +use bouncycastle_aes::hazmat::{AES128Internal, AES256Internal}; +use bouncycastle_aes::{AES128Key, AES256Key}; +use bouncycastle_cipher::modes::hazmat::ecb::ECB; +use bouncycastle_cipher::modes::{Blocks, CBC, CCMFrame, CFB, CFB8, CTR, ModeNames}; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::hazmat::{EcbEncryptionKey, Mode}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, + SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::BlockCipherEncryptor; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::aead::TestFrameworkAEADCipher; +use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_core_test_framework::symmetric_ciphers::{ + TestFrameworkStreamCipher, TestFrameworkSymmetricCipher, +}; +use bouncycastle_rng::HashDRBG_SHA512; + +/// A 32-byte frame for the fixed-frame CCM key, within the framework's seed buffer. +type Frame = CCMFrame<12, 16, 64, 32>; + +fn material() -> KeyMaterial { + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +fn key128>() -> AES128Key { + AES128Key::from_key_material(&material::<16>()).expect("a valid key") +} + +fn key256>() -> AES256Key { + AES256Key::from_key_material(&material::<32>()).expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +/// The construction suite -- key type, binding, strength and `generate` -- for the mode `M` at +/// both key lengths, against the name the permutation gives that mode. +fn construction_suite(name_128: &'static str, name_256: &'static str) +where + M: Mode + Mode, +{ + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::<16, AES128Key>(name_128, SecurityStrength::_128bit); + framework.test::<32, AES256Key>(name_256, SecurityStrength::_256bit); +} + +#[test] +fn every_mode_key_enforces_the_material_policy() { + construction_suite::>(AES128Internal::CBC_ALG_NAME, AES256Internal::CBC_ALG_NAME); + construction_suite::>(AES128Internal::CBC_ALG_NAME, AES256Internal::CBC_ALG_NAME); + construction_suite::>( + AES128Internal::CBC_ALG_NAME, + AES256Internal::CBC_ALG_NAME, + ); + construction_suite::>(AES128Internal::ECB_ALG_NAME, AES256Internal::ECB_ALG_NAME); + construction_suite::>(AES128Internal::ECB_ALG_NAME, AES256Internal::ECB_ALG_NAME); + construction_suite::(AES128Internal::CFB_ALG_NAME, AES256Internal::CFB_ALG_NAME); + construction_suite::(AES128Internal::CFB8_ALG_NAME, AES256Internal::CFB8_ALG_NAME); + construction_suite::(AES128Internal::CTR_ALG_NAME, AES256Internal::CTR_ALG_NAME); + construction_suite::(AES128Internal::CCM_ALG_NAME, AES256Internal::CCM_ALG_NAME); +} + +#[test] +fn cbc_blocks_keys_pass_the_block_cipher_framework() { + let framework = TestFrameworkBlockCipher::new(); + framework.test_key::<16, 16, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 16, 16, _>(&key256::>(), &mut rng()); +} + +#[test] +fn ecb_blocks_keys_pass_the_block_cipher_framework() { + let framework = TestFrameworkBlockCipher::new(); + framework.test_ecb_key::<16, 16, _>(&key128::>()); + framework.test_ecb_key::<32, 16, _>(&key256::>()); +} + +#[test] +fn padded_keys_pass_the_symmetric_cipher_framework() { + let framework = TestFrameworkSymmetricCipher::new(); + framework.test_key::<16, 16, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 16, 16, _>(&key256::>(), &mut rng()); + framework.test_key::<16, 0, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 0, 16, _>(&key256::>(), &mut rng()); +} + +/// `NoPadding` turns the adapters into an alignment check: the framework is told so, and asserts +/// that every other length is refused. +#[test] +fn no_padding_keys_pass_the_symmetric_cipher_framework() { + let mut framework = TestFrameworkSymmetricCipher::new(); + framework.required_alignment = 16; + framework.test_key::<16, 16, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 16, 16, _>(&key256::>(), &mut rng()); +} + +#[test] +fn stream_mode_keys_pass_the_stream_cipher_framework() { + let framework = TestFrameworkStreamCipher::new(); + framework.test_key::<16, 16, _>(&key128::(), &mut rng()); + framework.test_key::<32, 16, _>(&key256::(), &mut rng()); + framework.test_key::<16, 16, _>(&key128::(), &mut rng()); + framework.test_key::<32, 16, _>(&key256::(), &mut rng()); + framework.test_key::<16, 12, _>(&key128::(), &mut rng()); + framework.test_key::<32, 12, _>(&key256::(), &mut rng()); +} + +/// The fixed-frame key's workers and one-shots take exactly the frame, so the framework is told +/// the one length they accept. +#[test] +fn ccm_frame_keys_pass_the_aead_framework() { + let mut framework = TestFrameworkAEADCipher::new(); + framework.fixed_message_len = Some(32); + framework.test_key::<16, 12, 16, 16, _>(&key128::(), &mut rng()); + framework.test_key::<32, 12, 16, 16, _>(&key256::(), &mut rng()); +} + +/// ECB has no init data, so the `ECB` key makes its workers with no RNG and nothing to +/// return, and the `ECB` key, on the symmetric path, takes an RNG it never draws from and +/// returns `[u8; 0]`. The padded ciphertext is the block pair's encryption of the padded block. +#[test] +fn ecb_keys_take_no_init_data() { + let key = key128::>(); + let mut padded_hello = *b"hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"; + let mut enc = key.create_encryptor(); + enc.do_encrypt_inplace(&mut padded_hello).unwrap(); + let mut again = *b"hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"; + assert_eq!(key.encrypt_inplace(&mut again).unwrap(), 16); + assert_eq!(again, padded_hello, "the worker and the one-shot are the same codebook"); + + let padded_key = key128::>(); + let mut out = [0u8; 16]; + // A seedless RNG panics on any draw, so finishing is the proof that none is made. + let (written, init): (usize, [u8; 0]) = + padded_key.encrypt_out(&mut FixedSeedRNG::<0>::new([]), b"hello", &mut out).unwrap(); + assert_eq!((written, init), (16, [])); + assert_eq!(out, padded_hello, "PKCS7 over ECB is ECB of the padded block"); + assert_eq!(padded_key.decrypt(&init, &out).unwrap(), b"hello"); +} + +/// Under a pinned IV, the `CBC` key's in-place one-shot and the `CBC` key's +/// `encrypt_out` are the same bytes: `NoPadding` is an alignment check over the same mode. +#[test] +fn cbc_blocks_in_place_agrees_with_cbc_no_padding_out() { + let iv = [0xA5u8; 16]; + let msg: [u8; 48] = core::array::from_fn(|i| (i * 7 + 3) as u8); + + let mut in_place = msg; + let (n, iv_blocks) = key128::>() + .encrypt_inplace(&mut FixedSeedRNG::<16>::new(iv), &mut in_place) + .unwrap(); + assert_eq!((n, iv_blocks), (48, iv)); + + let mut out = [0u8; 48]; + let (m, iv_np) = key128::>() + .encrypt_out(&mut FixedSeedRNG::<16>::new(iv), &msg, &mut out) + .unwrap(); + assert_eq!((m, iv_np), (48, iv)); + assert_eq!(out, in_place); + + // ...and each key's decryptor opens the other's ciphertext. + key128::>().decrypt_inplace(&iv, &mut out).unwrap(); + assert_eq!(out, msg); + let mut back = [0u8; 64]; + let k = key128::>().decrypt_out(&iv, &in_place, &mut back).unwrap(); + assert_eq!(&back[..k], &msg[..]); +} diff --git a/crypto/aes/tests/padded_alias_tests.rs b/crypto/aes/tests/padded_alias_tests.rs index 4dc7c5ffc..9536cad02 100644 --- a/crypto/aes/tests/padded_alias_tests.rs +++ b/crypto/aes/tests/padded_alias_tests.rs @@ -1,35 +1,47 @@ -//! Tests for the padded AES-CBC and AES-ECB aliases. +//! The CBC and ECB aliases with their padding parameter. //! //! The aliases are only type aliases, so what is worth testing is that both parameters actually //! select: the direction picks the encryptor or the decryptor, and the padding scheme changes the //! behaviour rather than being decorative. The mode and the padding layer are tested in their own -//! crates; this checks the wiring between them. Which type each alias resolves to is pinned in -//! `alias_tests.rs`. +//! crates; this checks the wiring between them, through the keys that hand the aliases out. Which +//! type each alias resolves to is pinned in `alg_name_tests.rs`. -use bouncycastle_aes::hazmat::{AES_ECB_128, AES_ECB_192, AES_ECB_256}; -use bouncycastle_aes::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CBC; +use bouncycastle_cipher::modes::hazmat::ecb::ECB; use bouncycastle_cipher::padding::{NoPadding, PKCS7}; -use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle_rng::HashDRBG_SHA512; -fn key() -> KeyMaterial { +fn material() -> KeyMaterial { let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") } -/// Every key length round-trips through its alias, at lengths that need padding and lengths that -/// do not. +fn key_for>() -> K { + K::from_key_material(&material::()).expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + #[test] fn every_key_length_round_trips() { - fn check(name: &str) + fn check(name: &str) where - Enc: SymmetricCipherEncryptor, - Dec: SymmetricCipherDecryptor, + K: FromKeyMaterial + + SymmetricEncryptionKey + + SymmetricDecryptionKey, { + let key = key_for::(); for len in [0usize, 1, 15, 16, 17, 63, 64] { let plaintext: Vec = (0..len).map(|i| (i * 11 + 3) as u8).collect(); - let (iv, ciphertext) = Enc::encrypt(&key::(), &plaintext).expect("encryption"); + let (iv, ciphertext) = key.encrypt(&mut rng(), &plaintext).expect("encryption"); // PKCS#7 always adds at least one byte, and rounds up to a whole block. assert_eq!( @@ -38,125 +50,110 @@ fn every_key_length_round_trips() { "{name}, len {len}: PKCS7 pads up to the next whole block" ); - let recovered = Dec::decrypt(&key::(), &iv, &ciphertext).expect("decryption"); + let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); assert_eq!(recovered, plaintext, "{name}, len {len}: round trip"); } } - check::<16, 16, AES_CBC_128, AES_CBC_128>("CBC-128"); - check::<24, 16, AES_CBC_192, AES_CBC_192>("CBC-192"); - check::<32, 16, AES_CBC_256, AES_CBC_256>("CBC-256"); - check::<16, 0, AES_ECB_128, AES_ECB_128>("ECB-128"); - check::<24, 0, AES_ECB_192, AES_ECB_192>("ECB-192"); - check::<32, 0, AES_ECB_256, AES_ECB_256>("ECB-256"); + check::<16, 16, AES128Key>>("CBC-128"); + check::<24, 16, AES192Key>>("CBC-192"); + check::<32, 16, AES256Key>>("CBC-256"); + check::<16, 0, AES128Key>>("ECB-128"); + check::<24, 0, AES192Key>>("ECB-192"); + check::<32, 0, AES256Key>>("ECB-256"); } -/// The padding parameter must actually select the scheme, not merely be carried around. -/// -/// `PKCS7` accepts any length and always grows the message; `NoPadding` accepts only whole blocks -/// and never grows it. Checking both against the same alias, key and plaintext is what proves the -/// parameter reaches the behaviour. #[test] fn the_padding_parameter_selects_the_scheme() { - fn check(name: &str) + fn check(name: &str) where - Pkcs7Enc: SymmetricCipherEncryptor<16, IV_LEN, 16>, - NoPadEnc: SymmetricCipherEncryptor<16, IV_LEN, 16>, + Pkcs7Key: FromKeyMaterial<16> + SymmetricEncryptionKey<16, IV_LEN, 16>, + NoPadKey: FromKeyMaterial<16> + SymmetricEncryptionKey<16, IV_LEN, 16>, { + let pkcs7_key = key_for::<16, Pkcs7Key>(); + let nopad_key = key_for::<16, NoPadKey>(); + // A whole block: both schemes accept it, and they disagree about the length. let aligned = [0x5Au8; 16]; - let (_, pkcs7) = Pkcs7Enc::encrypt(&key::<16>(), &aligned).expect("PKCS7 accepts it"); - let (_, nopad) = NoPadEnc::encrypt(&key::<16>(), &aligned).expect("NoPadding accepts it"); + let (_, pkcs7) = pkcs7_key.encrypt(&mut rng(), &aligned).expect("PKCS7 accepts it"); + let (_, nopad) = nopad_key.encrypt(&mut rng(), &aligned).expect("NoPadding accepts it"); assert_eq!(pkcs7.len(), 32, "{name}: PKCS7 adds a whole block of padding to aligned data"); assert_eq!(nopad.len(), 16, "{name}: NoPadding adds nothing"); // Five bytes: PKCS7 pads it, NoPadding refuses rather than silently padding. let unaligned = b"hello"; - assert!(Pkcs7Enc::encrypt(&key::<16>(), unaligned).is_ok(), "{name}: PKCS7 pads it"); + assert!(pkcs7_key.encrypt(&mut rng(), unaligned).is_ok(), "{name}: PKCS7 pads it"); assert!( - NoPadEnc::encrypt(&key::<16>(), unaligned).is_err(), + nopad_key.encrypt(&mut rng(), unaligned).is_err(), "{name}: NoPadding must refuse a message that is not a whole number of blocks" ); } - check::<16, AES_CBC_128, AES_CBC_128>("CBC"); - check::<0, AES_ECB_128, AES_ECB_128>("ECB"); + check::<16, AES128Key>, AES128Key>>("CBC"); + check::<0, AES128Key>, AES128Key>>("ECB"); } -/// A ciphertext made under one scheme must not decrypt cleanly under the other. -/// -/// This is the practical reason the scheme is named in the type: the two are not interchangeable, -/// and without the type parameter nothing would stop a caller pairing them. #[test] fn the_two_schemes_are_not_interchangeable() { let aligned = [0x5Au8; 16]; let (iv, pkcs7) = - AES_CBC_128::::encrypt(&key::<16>(), &aligned).expect("encryption"); + key_for::<16, AES128Key>>().encrypt(&mut rng(), &aligned).expect("encryption"); // NoPadding will hand back the padded block as if it were data, so it "succeeds" with the // wrong answer -- exactly the silent mismatch the type parameter is there to prevent. - let as_nopad = AES_CBC_128::::decrypt(&key::<16>(), &iv, &pkcs7) + let as_nopad = key_for::<16, AES128Key>>() + .decrypt(&iv, &pkcs7) .expect("NoPadding cannot tell that the trailing block is padding"); assert_ne!(as_nopad, aligned, "the recovered data must not match the original"); assert_eq!(as_nopad.len(), 32, "it keeps the padding block as data"); // ...and the matching scheme gets it right. - let correct = - AES_CBC_128::::decrypt(&key::<16>(), &iv, &pkcs7).expect("decryption"); + let correct = key_for::<16, AES128Key>>().decrypt(&iv, &pkcs7).expect("decryption"); assert_eq!(correct, aligned); } -/// The IV is generated per encryption, so the same plaintext gives different ciphertext. #[test] fn each_cbc_encryption_gets_a_fresh_iv() { + let key = key_for::<16, AES128Key>>(); + let mut rng = rng(); let plaintext = [0x77u8; 32]; let mut seen = std::collections::BTreeSet::new(); for _ in 0..16 { - let (iv, ct) = AES_CBC_128::::encrypt(&key::<16>(), &plaintext).unwrap(); + let (iv, ct) = key.encrypt(&mut rng, &plaintext).unwrap(); assert!(seen.insert(iv), "IV repeated across encryptions"); - let back = AES_CBC_128::::decrypt(&key::<16>(), &iv, &ct).unwrap(); + let back = key.decrypt(&iv, &ct).unwrap(); assert_eq!(back, plaintext); } } -/// ECB has no IV, so the init data is an empty array and the ciphertext is exactly the padded -/// plaintext with nothing prepended. That is the difference from the CBC aliases, and it comes from -/// the `INIT_DATA_LEN = 0` the projection is given. #[test] fn ecb_has_no_iv() { - let (no_iv, ciphertext) = - AES_ECB_128::::encrypt(&key::<16>(), b"hello").expect("encryption"); + let key = key_for::<16, AES128Key>>(); + let (no_iv, ciphertext) = key.encrypt(&mut rng(), b"hello").expect("encryption"); assert_eq!(no_iv, [0u8; 0], "ECB has no IV, so the init data is empty"); assert_eq!(ciphertext.len(), 16, "five bytes padded to one block, nothing prepended"); - let recovered = - AES_ECB_128::::decrypt(&key::<16>(), &no_iv, &ciphertext).unwrap(); + let recovered = key.decrypt(&no_iv, &ciphertext).unwrap(); assert_eq!(recovered, b"hello"); } -/// Padding does not fix ECB: identical plaintext blocks still give identical ciphertext blocks, and -/// the same message under the same key always gives the same ciphertext. The aliases carry the -/// warning; this is the test that it is warranted. #[test] fn padding_does_not_hide_the_ecb_codebook_property() { // Two identical blocks give two identical ciphertext blocks. let (_, ciphertext) = - AES_ECB_128::::encrypt(&key::<16>(), &[0x5Au8; 32]).unwrap(); + key_for::<16, AES128Key>>().encrypt(&mut rng(), &[0x5Au8; 32]).unwrap(); assert_eq!(ciphertext[..16], ciphertext[16..], "ECB is a codebook, padded or not"); // ...and encryption is deterministic, there being no IV to vary. - let (_, a) = AES_ECB_128::::encrypt(&key::<16>(), b"hello").unwrap(); - let (_, b) = AES_ECB_128::::encrypt(&key::<16>(), b"hello").unwrap(); + let key = key_for::<16, AES128Key>>(); + let (_, a) = key.encrypt(&mut rng(), b"hello").unwrap(); + let (_, b) = key.encrypt(&mut rng(), b"hello").unwrap(); assert_eq!(a, b, "the same message encrypts the same way every time"); } -/// The allocating streaming wrappers `do_encrypt` / `do_decrypt` return exactly what their `_out` -/// counterparts write -- `do_*_out_len` bytes, including none when a piece is wholly buffered or -/// held back -- and a message streamed through them round-trips. #[test] fn the_allocating_streaming_wrappers_match_the_out_versions() { - type Enc = AES_CBC_128; - type Dec = AES_CBC_128; + let key = key_for::<16, AES128Key>>(); let plaintext: Vec = (0u8..40).collect(); // Piece lengths 5, 11, 1, 20, 0, 3: CBC releases a block only once it is full, so these @@ -164,7 +161,7 @@ fn the_allocating_streaming_wrappers_match_the_out_versions() { let pieces = [0..5, 5..16, 16..17, 17..37, 37..37, 37..40]; let expected_released = [0, 16, 0, 16, 0, 0]; - let (mut enc, iv) = Enc::do_encrypt_init(&key::<16>()).unwrap(); + let (mut enc, iv) = key.create_encryptor(&mut rng()).unwrap(); let mut ciphertext = Vec::new(); for (range, expected) in pieces.into_iter().zip(expected_released) { let piece = &plaintext[range]; @@ -180,11 +177,11 @@ fn the_allocating_streaming_wrappers_match_the_out_versions() { let (last, last_len) = enc.do_encrypt_final().unwrap(); ciphertext.extend_from_slice(&last[..last_len]); assert_eq!(ciphertext.len(), 48); - assert_eq!(Dec::decrypt(&key::<16>(), &iv, &ciphertext).unwrap(), plaintext); + assert_eq!(key.decrypt(&iv, &ciphertext).unwrap(), plaintext); // Decrypt the same ciphertext through `do_decrypt` in uneven pieces: the decryptor holds back // the block that might carry the padding, so some pieces release nothing. - let mut dec = Dec::do_decrypt_init(&key::<16>(), &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut recovered = Vec::new(); let mut saw_empty = false; for range in [0..16, 16..17, 17..48] { diff --git a/crypto/aes/tests/sp800_38a_cbc_tests.rs b/crypto/aes/tests/sp800_38a_cbc_tests.rs index f3a566e88..86b1681ef 100644 --- a/crypto/aes/tests/sp800_38a_cbc_tests.rs +++ b/crypto/aes/tests/sp800_38a_cbc_tests.rs @@ -11,16 +11,16 @@ //! # Driving the IV //! //! There is no API for supplying an IV -- see the crate docs. Encryption is therefore driven -//! through [`BlockCipherEncryptor::do_encrypt_init_rng`] with a [`FixedSeedRNG`] whose stream is -//! the vector's IV, and the test asserts the returned init data really is that IV before comparing -//! any ciphertext. Decryption takes the IV directly, as init data. - -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Cbc; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +//! through the key's [`BlockEncryptionKey::create_encryptor`] with a [`FixedSeedRNG`] whose +//! stream is the vector's IV, and the test asserts the returned init data really is that IV before +//! comparing any ciphertext. Decryption takes the IV directly, as init data. + +use bouncycastle_aes::hazmat::AES128Internal; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::{Blocks, CBC}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -92,32 +92,24 @@ fn key_material(hex_str: &str) -> KeyMaterial { /// /// Checks the whole message in one call, then again one block at a time -- the vector should not /// care how the calls are grouped. -fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) +fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + BlockEncryptionKey, { - let key = key_material::(key_hex); + let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); let pt = blocks(&PLAINTEXTS); let ct = blocks(expected); // All four blocks in one call. - let (mut enc, got_iv) = Cbc::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(iv), - ) - .unwrap(); + let (mut enc, got_iv) = key.create_encryptor(&mut FixedSeedRNG::::new(iv)).unwrap(); assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); let mut data = flat(&PLAINTEXTS); enc.do_encrypt_blocks_inplace(data.as_chunks_mut().0).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( - &key, - &mut FixedSeedRNG::::new(iv), - ) - .unwrap(); + let (mut enc, _) = key.create_encryptor(&mut FixedSeedRNG::::new(iv)).unwrap(); for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { let mut got = *p; enc.do_encrypt_blocks_inplace(got.as_chunks_mut().0).unwrap(); @@ -129,25 +121,23 @@ where /// /// 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_inplace`. -fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) +fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + BlockDecryptionKey, { - let key = key_material::(key_hex); + let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); let pt = blocks(&PLAINTEXTS); let ct = blocks(ciphertext); - type Dec = Cbc; - // All four blocks in one call (two pairs, no remainder). - let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut data = flat(ciphertext); dec.do_decrypt_blocks_inplace(data.as_chunks_mut().0).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(); + let mut dec = key.create_decryptor(&iv); for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { let mut got = *c; dec.do_decrypt_blocks_inplace(got.as_chunks_mut().0).unwrap(); @@ -155,7 +145,7 @@ where } // 3 + 1: one pair plus a remainder, then a lone block. - let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); dec.do_decrypt_blocks_inplace(three.as_chunks_mut().0).unwrap(); let mut one = ct[3]; @@ -166,32 +156,32 @@ where #[test] fn f_2_1_cbc_aes128_encrypt() { - check_encrypt::("F.2.1", KEY_128, &CIPHERTEXTS_128); + check_encrypt::>, 16>("F.2.1", KEY_128, &CIPHERTEXTS_128); } #[test] fn f_2_2_cbc_aes128_decrypt() { - check_decrypt::("F.2.2", KEY_128, &CIPHERTEXTS_128); + check_decrypt::>, 16>("F.2.2", KEY_128, &CIPHERTEXTS_128); } #[test] fn f_2_3_cbc_aes192_encrypt() { - check_encrypt::("F.2.3", KEY_192, &CIPHERTEXTS_192); + check_encrypt::>, 24>("F.2.3", KEY_192, &CIPHERTEXTS_192); } #[test] fn f_2_4_cbc_aes192_decrypt() { - check_decrypt::("F.2.4", KEY_192, &CIPHERTEXTS_192); + check_decrypt::>, 24>("F.2.4", KEY_192, &CIPHERTEXTS_192); } #[test] fn f_2_5_cbc_aes256_encrypt() { - check_encrypt::("F.2.5", KEY_256, &CIPHERTEXTS_256); + check_encrypt::>, 32>("F.2.5", KEY_256, &CIPHERTEXTS_256); } #[test] fn f_2_6_cbc_aes256_decrypt() { - check_decrypt::("F.2.6", KEY_256, &CIPHERTEXTS_256); + check_decrypt::>, 32>("F.2.6", KEY_256, &CIPHERTEXTS_256); } /// The one-shot API must agree with the vectors too, on the decrypt side where the IV is an input. @@ -209,18 +199,15 @@ 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( + >::encrypt_block( &>::new(&key).unwrap(), &mut ecb, ); assert_eq!(ecb, block("3ad77bb40d7a3660a89ecaf32466ef97"), "F.1.1 block #1"); // CBC's C1 = CIPH_K(P1 XOR IV) is the F.2.1 answer, and differs. - let (mut enc, _) = Cbc::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::<16>::new(iv), - ) - .unwrap(); + let key_object = AES128Key::>::from_key_material(&key).unwrap(); + let (mut enc, _) = key_object.create_encryptor(&mut FixedSeedRNG::<16>::new(iv)).unwrap(); let mut cbc = block(PLAINTEXTS[0]); enc.do_encrypt_blocks_inplace(cbc.as_chunks_mut().0).unwrap(); assert_eq!(cbc, block(CIPHERTEXTS_128[0]), "F.2.1 block #1"); diff --git a/crypto/aes/tests/sp800_38a_cfb8_tests.rs b/crypto/aes/tests/sp800_38a_cfb8_tests.rs index 194ca6141..185436de3 100644 --- a/crypto/aes/tests/sp800_38a_cfb8_tests.rs +++ b/crypto/aes/tests/sp800_38a_cfb8_tests.rs @@ -26,20 +26,19 @@ //! # Driving the IV //! //! There is no API for supplying an IV -- see the crate docs. Encryption is therefore driven -//! through [`SymmetricCipherEncryptor::do_encrypt_init_rng`] with a [`FixedSeedRNG`] whose stream is -//! the vector's IV, and the test asserts the returned init data really is that IV before comparing -//! any ciphertext. Decryption takes the IV directly, as init data. - -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Cfb8; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; +//! through the key's [`SymmetricEncryptionKey::create_encryptor`] with a [`FixedSeedRNG`] whose +//! stream is the vector's IV, and the test asserts the returned init data really is that IV before +//! comparing any ciphertext. Decryption takes the IV directly, as init data. + +use bouncycastle_aes::hazmat::AES128Internal; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CFB8; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -135,22 +134,20 @@ const CHUNKINGS: [usize; 7] = [1, 3, 4, 8, 9, 17, 18]; /// /// Checks the whole message in one call, then in every chunking above -- the vector should not care /// how the calls are grouped. -fn check_encrypt(section: &str, key_hex: &str, expected_hex: &str) +fn check_encrypt(section: &str, key_hex: &str, expected_hex: &str) where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + SymmetricEncryptionKey, + K::Encryptor: StreamCipherEncryptor, { - let key = key_material::(key_hex); + let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); let plaintext = bytes(PLAINTEXT); let expected = bytes(expected_hex); assert_eq!(plaintext.len(), 18, "{section}: the CFB8 subsections use 18 one-byte segments"); for chunk in [plaintext.len()].into_iter().chain(CHUNKINGS) { - let (mut enc, got_iv) = Cfb8::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(iv), - ) - .unwrap(); + let (mut enc, got_iv) = + key.create_encryptor(&mut FixedSeedRNG::::new(iv)).unwrap(); assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); let mut data = plaintext.clone(); @@ -162,18 +159,18 @@ where } /// Runs one Appendix F.3 CFB8 decrypt subsection. -fn check_decrypt(section: &str, key_hex: &str, ciphertext_hex: &str) +fn check_decrypt(section: &str, key_hex: &str, ciphertext_hex: &str) where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + SymmetricDecryptionKey, + K::Decryptor: StreamCipherDecryptor, { - let key = key_material::(key_hex); + let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); let plaintext = bytes(PLAINTEXT); let ciphertext = bytes(ciphertext_hex); for chunk in [ciphertext.len()].into_iter().chain(CHUNKINGS) { - let mut dec = - Cfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut data = ciphertext.clone(); for piece in data.chunks_mut(chunk) { dec.do_decrypt_inplace(piece).unwrap(); @@ -183,38 +180,38 @@ where // ...and the one-shot, where the IV is an input. let mut data = ciphertext.clone(); - Cfb8::::decrypt_inplace(&key, &iv, &mut data).unwrap(); + key.decrypt_inplace(&iv, &mut data).unwrap(); assert_eq!(data, plaintext, "{section}: one-shot"); } #[test] fn f_3_7_cfb8_aes128_encrypt() { - check_encrypt::("F.3.7", KEY_128, CIPHERTEXT_128); + check_encrypt::, 16>("F.3.7", KEY_128, CIPHERTEXT_128); } #[test] fn f_3_8_cfb8_aes128_decrypt() { - check_decrypt::("F.3.8", KEY_128, CIPHERTEXT_128); + check_decrypt::, 16>("F.3.8", KEY_128, CIPHERTEXT_128); } #[test] fn f_3_9_cfb8_aes192_encrypt() { - check_encrypt::("F.3.9", KEY_192, CIPHERTEXT_192); + check_encrypt::, 24>("F.3.9", KEY_192, CIPHERTEXT_192); } #[test] fn f_3_10_cfb8_aes192_decrypt() { - check_decrypt::("F.3.10", KEY_192, CIPHERTEXT_192); + check_decrypt::, 24>("F.3.10", KEY_192, CIPHERTEXT_192); } #[test] fn f_3_11_cfb8_aes256_encrypt() { - check_encrypt::("F.3.11", KEY_256, CIPHERTEXT_256); + check_encrypt::, 32>("F.3.11", KEY_256, CIPHERTEXT_256); } #[test] fn f_3_12_cfb8_aes256_decrypt() { - check_decrypt::("F.3.12", KEY_256, CIPHERTEXT_256); + check_decrypt::, 32>("F.3.12", KEY_256, CIPHERTEXT_256); } /// The spec's tabulated **Input Blocks** are the shift register and its **Output Blocks** are diff --git a/crypto/aes/tests/sp800_38a_cfb_tests.rs b/crypto/aes/tests/sp800_38a_cfb_tests.rs index 0242b8e84..e956aaabb 100644 --- a/crypto/aes/tests/sp800_38a_cfb_tests.rs +++ b/crypto/aes/tests/sp800_38a_cfb_tests.rs @@ -28,20 +28,19 @@ //! # Driving the IV //! //! There is no API for supplying an IV -- see the crate docs. Encryption is therefore driven -//! through [`SymmetricCipherEncryptor::do_encrypt_init_rng`] with a [`FixedSeedRNG`] whose stream is -//! the vector's IV, and the test asserts the returned init data really is that IV before comparing -//! any ciphertext. Decryption takes the IV directly, as init data. +//! through the key's [`SymmetricEncryptionKey::create_encryptor`] with a [`FixedSeedRNG`] whose +//! stream is the vector's IV, and the test asserts the returned init data really is that IV before +//! comparing any ciphertext. Decryption takes the IV directly, as init data. use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Cfb; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::CFB; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{ - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -136,21 +135,18 @@ const ODD_CHUNKS: [usize; 3] = [5, 23, 63]; /// /// Checks the whole message in one call, then again one segment at a time, then again in chunks /// that straddle the segments -- the vector should not care how the calls are grouped. -fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) +fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + SymmetricEncryptionKey, + K::Encryptor: StreamCipherEncryptor, { - let key = key_material::(key_hex); + let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); let pt = blocks(&PLAINTEXTS); let ct = blocks(expected); let init = || { - let (enc, got_iv) = Cfb::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::::new(iv), - ) - .unwrap(); + let (enc, got_iv) = key.create_encryptor(&mut FixedSeedRNG::::new(iv)).unwrap(); assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); enc }; @@ -185,25 +181,24 @@ where /// Checks one call, one segment at a time, the odd grouping `3 + 1` -- which is the grouping that /// leaves a one-block remainder after the pair loop in `do_decrypt_inplace` -- and chunks that /// straddle the segments. -fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) +fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + SymmetricDecryptionKey, + K::Decryptor: StreamCipherDecryptor, { - let key = key_material::(key_hex); + let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); let pt = blocks(&PLAINTEXTS); let ct = blocks(ciphertext); - type Dec = Cfb; - // All four segments in one call (two pairs, no remainder). - let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut data = flat(ciphertext); dec.do_decrypt_inplace(&mut data).unwrap(); assert_eq!(data, flat(&PLAINTEXTS), "{section}: four segments in one call"); // One segment at a time (never takes the pair path). - let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { let mut got = *c; dec.do_decrypt_inplace(&mut got).unwrap(); @@ -211,7 +206,7 @@ where } // 3 + 1: one pair plus a remainder, then a lone block. - let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); dec.do_decrypt_inplace(&mut three).unwrap(); let mut one = ct[3]; @@ -221,7 +216,7 @@ where // In chunks that cut across the segments. for chunk in ODD_CHUNKS { - let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut dec = key.create_decryptor(&iv); let mut data = flat(ciphertext); for piece in data.chunks_mut(chunk) { dec.do_decrypt_inplace(piece).unwrap(); @@ -232,32 +227,32 @@ where #[test] fn f_3_13_cfb128_aes128_encrypt() { - check_encrypt::("F.3.13", KEY_128, &CIPHERTEXTS_128); + check_encrypt::, 16>("F.3.13", KEY_128, &CIPHERTEXTS_128); } #[test] fn f_3_14_cfb128_aes128_decrypt() { - check_decrypt::("F.3.14", KEY_128, &CIPHERTEXTS_128); + check_decrypt::, 16>("F.3.14", KEY_128, &CIPHERTEXTS_128); } #[test] fn f_3_15_cfb128_aes192_encrypt() { - check_encrypt::("F.3.15", KEY_192, &CIPHERTEXTS_192); + check_encrypt::, 24>("F.3.15", KEY_192, &CIPHERTEXTS_192); } #[test] fn f_3_16_cfb128_aes192_decrypt() { - check_decrypt::("F.3.16", KEY_192, &CIPHERTEXTS_192); + check_decrypt::, 24>("F.3.16", KEY_192, &CIPHERTEXTS_192); } #[test] fn f_3_17_cfb128_aes256_encrypt() { - check_encrypt::("F.3.17", KEY_256, &CIPHERTEXTS_256); + check_encrypt::, 32>("F.3.17", KEY_256, &CIPHERTEXTS_256); } #[test] fn f_3_18_cfb128_aes256_decrypt() { - check_decrypt::("F.3.18", KEY_256, &CIPHERTEXTS_256); + check_decrypt::, 32>("F.3.18", KEY_256, &CIPHERTEXTS_256); } /// The one-shot API must agree with the vectors too, on the decrypt side where the IV is an input. @@ -283,7 +278,7 @@ fn check_output_blocks( ciphertexts: &[&str; 4], output_blocks: &[&str; 4], ) where - P: ElectronicCodeBook + ModeNames, + P: ElectronicCodeBook, { let key = key_material::(key_hex); let perm = P::new(&key).expect("a valid key"); @@ -348,13 +343,9 @@ fn cfb128_agrees_with_ofb_on_the_first_block_only() { "F.3.13 and F.4.1 must tabulate the same O1 = CIPH_K(IV)" ); - let key = key_material::<16>(KEY_128); + let key = AES128Key::::from_key_material(&key_material::<16>(KEY_128)).unwrap(); let iv = block(IV); - let (mut enc, got_iv) = Cfb::::do_encrypt_init_rng( - &key, - &mut FixedSeedRNG::<16>::new(iv), - ) - .unwrap(); + let (mut enc, got_iv) = key.create_encryptor(&mut FixedSeedRNG::<16>::new(iv)).unwrap(); assert_eq!(got_iv, iv); let mut c1 = block(PLAINTEXTS[0]); diff --git a/crypto/aes/tests/sp800_38c_tests.rs b/crypto/aes/tests/sp800_38c_tests.rs index a68b86bd4..307eca370 100644 --- a/crypto/aes/tests/sp800_38c_tests.rs +++ b/crypto/aes/tests/sp800_38c_tests.rs @@ -17,30 +17,51 @@ //! decryption-verification process of CCM is straightforward to construct"), so the decryption //! direction is checked by round-tripping each vector's own `C` back to its `P`. -use bouncycastle_aes::AES128Key; use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::{CCM, CcmDecryptor, CcmEncryptor, ModeNames}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::{CCM, CCMFrame, CcmDecryptor, CcmEncryptor, ModeNames}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::symmetric_key::FromKeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, + FromKeyMaterial, +}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::aead::TestFrameworkAEADCipher; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_hex as hex; +use bouncycastle_rng::HashDRBG_SHA512; /// Appendix C's key, the same in all four examples: `40414243 44454647 48494a4b 4c4d4e4f`. const APPENDIX_C_KEY: &str = "404142434445464748494a4b4c4d4e4f"; -fn key(hex_key: &str) -> KeyMaterial { +fn material(hex_key: &str) -> KeyMaterial { let bytes = hex::decode(hex_key).expect("valid hex key"); assert_eq!(bytes.len(), N, "key length must match the parameter set"); KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) .expect("a symmetric cipher key") } +/// The appendix key bound to the fixed-frame pair: its streams are [`CcmEncryptor`] / +/// [`CcmDecryptor`] at `(NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN)`, and its one-shots take exactly +/// `DATA_LEN` bytes. +type FrameKey = + AES128Key>; + +fn frame_key() +-> FrameKey { + FrameKey::from_key_material(&material::<16>(APPENDIX_C_KEY)).expect("the appendix key") +} + +fn os_rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + fn buffer_len_error(r: Result) -> Option { match r { Err(SymmetricCipherError::OutputBufferTooSmall(needed)) => Some(needed), @@ -49,17 +70,17 @@ fn buffer_len_error(r: Result) -> Option { } /// Drives one Appendix C example through every entry point: the mode's in-place one-shots, the -/// fixed-frame pair's any-length one-shots in the appendix's own inline layout, and, if `sweep`, -/// the pair's streams at the example's frame `DATA_LEN` in several chunkings. +/// fixed-frame key's one-shots in the appendix's own inline layout, and, if `sweep`, the key's +/// streams in several chunkings. The frame is the example's own `Alen` and `Plen`, since the +/// key's one-shots are bound to it; the appendix is AES-128 only. /// /// `c` is the appendix's whole `C` string; it is split at `plaintext.len()` into the ciphertext and /// the tag, so a mistake in either half is caught, and so is a mistake in where the split belongs. fn check_vector< - const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, + const AAD_LEN: usize, const DATA_LEN: usize, - P: ElectronicCodeBook + ModeNames + Clone, >( name: &str, key_hex: &str, @@ -72,16 +93,15 @@ fn check_vector< sweep: bool, ) { type Mode = CCM; - type Enc = - CcmEncryptor; - type Dec = - CcmDecryptor; - let k = key::(key_hex); - let perm = P::new(&k).expect("the appendix key"); + let k = material::<16>(key_hex); + let perm = AES128Internal::new(&k).expect("the appendix key"); + let key = FrameKey::::from_key_material(&k) + .expect("the appendix key"); let nonce_bytes = hex::decode(nonce_hex).expect("valid hex nonce"); let nonce: [u8; NONCE_LEN] = nonce_bytes.try_into().expect("nonce length matches NONCE_LEN"); let plaintext = hex::decode(plaintext_hex).expect("valid hex plaintext"); + assert_eq!(aad.len(), AAD_LEN, "{name}: the frame's AAD capacity is the example's Alen"); assert_eq!(plaintext.len(), DATA_LEN, "{name}: the frame is the example's Plen"); let c = hex::decode(c_hex).expect("valid hex C"); @@ -104,19 +124,14 @@ fn check_vector< .expect("decryption"); assert_eq!(buf, plaintext, "{name}: detached round trip"); - // --- the appendix's own inline `ciphertext || tag` layout, through the pair's one-shots --- + // --- the appendix's own inline `ciphertext || tag` layout, through the key's one-shots --- let mut inline = vec![0u8; plaintext.len() + TAG_LEN]; - let n = Enc::::encrypt_with_aad_nonce_out( - &k, &nonce, aad, &plaintext, &mut inline, - ) - .expect("encryption"); + let n = + key.encrypt_with_aad_nonce_out(&nonce, aad, &plaintext, &mut inline).expect("encryption"); assert_eq!(n, c.len(), "{name}: inline output length"); assert_eq!(inline, c, "{name}: the whole C string of Appendix C"); let mut recovered = vec![0u8; plaintext.len()]; - let n = Dec::::decrypt_with_aad_out( - &k, &nonce, aad, &c, &mut recovered, - ) - .expect("decryption"); + let n = key.decrypt_with_aad_out(&nonce, aad, &c, &mut recovered).expect("decryption"); assert_eq!(n, plaintext.len()); assert_eq!(recovered, plaintext, "{name}: inline round trip"); @@ -125,9 +140,7 @@ fn check_vector< // Sec 3 says CCM is not a streaming mode, and the pair handles that by fixing the frame in // the type; given that, the chunking must be invisible, exactly as for the other modes. for chunk in [1usize, 2, 3, 7, 16, 17] { - let mut enc = - Enc::::do_encrypt_init_nonce(&k, &nonce) - .expect("streaming init"); + let mut enc = key.create_encryptor_nonce(&nonce); enc.do_update_aad(aad).expect("aad"); let mut streamed = vec![0u8; DATA_LEN]; let mut written = 0; @@ -138,9 +151,7 @@ fn check_vector< assert_eq!(streamed, want_ct, "{name}: ciphertext, streamed in {chunk}-byte chunks"); assert_eq!(&streamed_tag, want_tag, "{name}: tag, streamed in {chunk}-byte chunks"); - let mut dec = - Dec::::do_decrypt_init(&k, &nonce) - .expect("streaming init"); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(aad).expect("aad"); let mut opened = vec![0u8; DATA_LEN]; let mut written = 0; @@ -158,7 +169,7 @@ fn check_vector< /// `n = 7`, so `q = 8`: the widest length field A.1 allows, and the shortest permitted tag. #[test] fn appendix_c1() { - check_vector::<16, 7, 4, 4, AES128Internal>( + check_vector::<7, 4, 8, 4>( "C.1", APPENDIX_C_KEY, "10111213141516", @@ -176,7 +187,7 @@ fn appendix_c1() { /// "minimum number of '0' bits, possibly none" is none. #[test] fn appendix_c2() { - check_vector::<16, 8, 6, 16, AES128Internal>( + check_vector::<8, 6, 16, 16>( "C.2", APPENDIX_C_KEY, "1011121314151617", @@ -194,7 +205,7 @@ fn appendix_c2() { /// the payload spans two counter blocks. #[test] fn appendix_c3() { - check_vector::<16, 12, 8, 24, AES128Internal>( + check_vector::<12, 8, 20, 24>( "C.3", APPENDIX_C_KEY, "101112131415161718191a1b", @@ -226,7 +237,7 @@ fn appendix_c4() { } assert_eq!(aad.len(), 65536, "Alen = 524288 bits"); - check_vector::<16, 13, 14, 32, AES128Internal>( + check_vector::<13, 14, 65536, 32>( "C.4", APPENDIX_C_KEY, "101112131415161718191a1b1c", @@ -254,74 +265,44 @@ fn framework(data_len: usize) -> TestFrameworkAEADCipher { framework } -/// The whole [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] contract, through the shared -/// framework, for the fixed-frame [`CcmEncryptor`] / [`CcmDecryptor`] pair. +/// The whole [`AeadEncryptionKey`] / [`AeadDecryptionKey`] contract, through the shared +/// framework, for a key bound to [`CCMFrame`], whose streams are the fixed-frame +/// [`CcmEncryptor`] / [`CcmDecryptor`] pair. /// /// `DATA_LEN` is 240, well above the few multiples of the tag the suite's one-shots try, so the -/// one-shots are exercised at every length up to it and the streaming methods at exactly it. -/// `AAD_LEN` is 64, above the suite's 20-byte AAD. `FINAL_LEN` is the tag. +/// one-shots and the streaming methods are exercised at exactly it and refused at every other +/// length up to it. `AAD_LEN` is 64, above the suite's 20-byte AAD. `FINAL_LEN` is the tag. #[test] fn framework_streaming_contract() { - framework(240).test_encryptor_decryptor::< - 16, - 12, - 16, - 16, - CcmEncryptor, - CcmDecryptor, - >(); + framework(240).test_key::<16, 12, 16, 16, _>(&frame_key::<12, 16, 64, 240>(), &mut os_rng()); } -/// The same, for the other two AES key lengths and a short tag, so the framework's error and -/// key-policy checks run against every parameterization the CLI and the aliases expose. +/// The same, for the other two AES key lengths and a short tag, so the framework's error checks +/// run against every parameterization the CLI and the aliases expose. #[test] fn framework_streaming_contract_other_parameter_sets() { - framework(240).test_encryptor_decryptor::< - 24, - 12, - 16, - 16, - CcmEncryptor, - CcmDecryptor, - >(); - framework(240).test_encryptor_decryptor::< - 32, - 12, - 16, - 16, - CcmEncryptor, - CcmDecryptor, - >(); + let mut rng = os_rng(); + framework(240).test_key::<24, 12, 16, 16, _>( + &AES192Key::>::generate(&mut rng).expect("a generated key"), + &mut rng, + ); + framework(240).test_key::<32, 12, 16, 16, _>( + &AES256Key::>::generate(&mut rng).expect("a generated key"), + &mut rng, + ); // A 13-byte nonce (q = 2) with an 8-byte tag: the parameterization IEEE 802.11 CCMP uses, and // the one A.1's narrowest length field applies to. - framework(240).test_encryptor_decryptor::< - 16, - 13, - 8, - 8, - CcmEncryptor, - CcmDecryptor, - >(); + framework(240).test_key::<16, 13, 8, 8, _>(&frame_key::<13, 8, 64, 240>(), &mut rng); // The empty frame: a message that is nothing but its AAD and tag. - framework(0).test_encryptor_decryptor::< - 16, - 12, - 16, - 16, - CcmEncryptor, - CcmDecryptor, - >(); + framework(0).test_key::<16, 12, 16, 16, _>(&frame_key::<12, 16, 64, 0>(), &mut rng); } /// The fixed-frame pair's streams reproduce Appendix C.3 byte for byte, driven with the -/// appendix's nonce through `do_encrypt_init_rng` and a fixed-output RNG. C.3's payload is 24 +/// appendix's nonce through `create_encryptor` and a fixed-output RNG. C.3's payload is 24 /// bytes and its AAD 20, so that is the frame. #[test] fn the_fixed_frame_pair_agrees_with_the_direct_api_on_appendix_c3() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 8, 20, 24>(); let nonce_bytes = hex::decode("101112131415161718191a1b").unwrap(); let aad = hex::decode("000102030405060708090a0b0c0d0e0f10111213").unwrap(); let plaintext = hex::decode("202122232425262728292a2b2c2d2e2f3031323334353637").unwrap(); @@ -329,11 +310,11 @@ fn the_fixed_frame_pair_agrees_with_the_direct_api_on_appendix_c3() { hex::decode("e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5484392fbc1b09951").unwrap(); let (want_ct, want_tag) = c.split_at(plaintext.len()); - // The trait generates the nonce; feed it Appendix C.3's so the answer is comparable, and check - // it came back, so an implementation that ignored the RNG could not pass silently. + // The key draws the nonce; feed it Appendix C.3's so the answer is comparable, and check it + // came back, so an implementation that ignored the RNG could not pass silently. let nonce_seed: [u8; 12] = nonce_bytes.clone().try_into().expect("12-byte nonce"); let mut rng = FixedSeedRNG::<12>::new(nonce_seed); - let (mut enc, nonce) = Enc::do_encrypt_init_rng(&k, &mut rng).expect("init"); + let (mut enc, nonce) = key.create_encryptor(&mut rng).expect("init"); assert_eq!(&nonce[..], &nonce_bytes[..], "the generated nonce must come from the RNG"); // Chunk both phases, and check `update_out_len`'s promise that every byte is released at once. @@ -353,7 +334,7 @@ fn the_fixed_frame_pair_agrees_with_the_direct_api_on_appendix_c3() { assert_eq!(&ct[..], want_ct, "C.3 ciphertext via the trait"); assert_eq!(&tag[..], want_tag, "C.3 tag via the trait"); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(&aad).expect("aad"); let mut pt = Vec::new(); for piece in want_ct.chunks(5) { @@ -373,14 +354,14 @@ fn the_fixed_frame_pair_agrees_with_the_direct_api_on_appendix_c3() { // `ciphertext || tag`, with the tag as the final's output, and the decryptor takes the tag // back off its end. let mut rng = FixedSeedRNG::<12>::new(nonce_seed); - let (mut enc, nonce) = Enc::do_encrypt_init_rng(&k, &mut rng).expect("init"); + let (mut enc, nonce) = key.create_encryptor(&mut rng).expect("init"); enc.do_update_aad(&aad).expect("aad"); let mut inline = vec![0u8; 24]; enc.do_encrypt_out(&plaintext, &mut inline).expect("update"); let (last, last_len) = enc.do_encrypt_final().expect("final"); inline.extend_from_slice(&last[..last_len]); assert_eq!(&inline[..], &c[..], "C.3 `C` via the inline do_encrypt_final"); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(&aad).expect("aad"); let mut pt = Vec::new(); for piece in c.chunks(5) { @@ -398,11 +379,9 @@ fn the_fixed_frame_pair_agrees_with_the_direct_api_on_appendix_c3() { /// AAD past `AAD_LEN`. #[test] fn the_adapters_refuse_more_than_the_declared_lengths() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 32, 32>(); - let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + let (mut enc, _) = key.create_encryptor(&mut os_rng()).expect("init"); let mut out = [0xEEu8; 33]; match enc.do_encrypt_out(&[0u8; 33], &mut out) { Err(SymmetricCipherError::StateError(msg)) => assert!( @@ -414,14 +393,14 @@ fn the_adapters_refuse_more_than_the_declared_lengths() { assert_eq!(out, [0u8; 33], "a refused update must leave the output buffer zeroed"); // In two calls that together overflow, the first must succeed and the second be refused. - let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + let (mut enc, _) = key.create_encryptor(&mut os_rng()).expect("init"); assert_eq!(enc.do_encrypt_out(&[0u8; 20], &mut out).expect("fits"), 20); assert!(matches!( enc.do_encrypt_out(&[0u8; 13], &mut out), Err(SymmetricCipherError::StateError(_)) )); - let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + let (mut enc, _) = key.create_encryptor(&mut os_rng()).expect("init"); match enc.do_update_aad(&[0u8; 33]) { Err(SymmetricCipherError::GenericError(msg)) => { assert!(msg.contains("AAD_LEN"), "the refusal must name AAD_LEN, got: {msg}") @@ -431,7 +410,7 @@ fn the_adapters_refuse_more_than_the_declared_lengths() { // The decryptor's bound is `DATA_LEN + TAG_LEN`, the frame with an inline tag. let nonce = [0x24u8; 12]; - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); let mut pt = [0xEEu8; 49]; match dec.do_decrypt_out(&[0u8; 49], &mut pt) { Err(SymmetricCipherError::StateError(msg)) => assert!( @@ -441,13 +420,13 @@ fn the_adapters_refuse_more_than_the_declared_lengths() { other => panic!("expected StateError, got {other:?}"), } assert_eq!(pt, [0u8; 49], "a refused update must leave the output buffer zeroed"); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); assert_eq!(dec.do_decrypt_out(&[0u8; 40], &mut pt).expect("fits"), 32); assert!(matches!( dec.do_decrypt_out(&[0u8; 9], &mut pt), Err(SymmetricCipherError::StateError(_)) )); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); assert!(matches!(dec.do_update_aad(&[0u8; 33]), Err(SymmetricCipherError::GenericError(_)))); } @@ -458,13 +437,11 @@ fn the_adapters_refuse_more_than_the_declared_lengths() { /// call starts the data phase. #[test] fn an_empty_update_does_not_close_the_aad_phase() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 48, 7>(); let aad = b"header"; let message = b"payload"; - let (mut enc, nonce) = Enc::do_encrypt_init(&k).expect("init"); + let (mut enc, nonce) = key.create_encryptor(&mut os_rng()).expect("init"); let mut sealed = [0u8; 7]; enc.do_encrypt_out(&[], &mut sealed).expect("an empty update is a no-op"); enc.do_update_aad(aad).expect("the AAD phase is still open after an empty update"); @@ -479,13 +456,12 @@ fn an_empty_update_does_not_close_the_aad_phase() { // The AAD really was absorbed: the one-shot with the same AAD must agree, and the // decryptor, given the same empty-then-AAD sequence, must verify it. let mut expected = [0u8; 7 + 16]; - let n = - Enc::encrypt_with_aad_nonce_out(&k, &nonce, aad, message, &mut expected).expect("one-shot"); + let n = key.encrypt_with_aad_nonce_out(&nonce, aad, message, &mut expected).expect("one-shot"); assert_eq!(n, 23); assert_eq!(&sealed[..], &expected[..7]); assert_eq!(&tag[..], &expected[7..]); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); let mut opened = [0u8; 7]; dec.do_decrypt_out(&[], &mut opened).expect("an empty update is a no-op"); dec.do_update_aad(aad).expect("the AAD phase is still open after an empty update"); @@ -505,15 +481,13 @@ fn an_empty_update_does_not_close_the_aad_phase() { /// its own bookkeeping goes wrong. #[test] fn exact_lengths_are_accepted_whole_and_split() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 32, 32>(); let aad = [0x11u8; 32]; let message = [0x5Au8; 32]; let nonce_seed = [0x24u8; 12]; let (mut enc, nonce) = - Enc::do_encrypt_init_rng(&k, &mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); + key.create_encryptor(&mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); enc.do_update_aad(&aad).expect("AAD exactly filling the capacity is accepted"); let mut sealed = [0u8; 48]; assert_eq!(enc.do_encrypt_out(&message, &mut sealed).expect("exactly DATA_LEN"), 32); @@ -521,7 +495,7 @@ fn exact_lengths_are_accepted_whole_and_split() { sealed[32..].copy_from_slice(&tag); let (mut enc, _) = - Enc::do_encrypt_init_rng(&k, &mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); + key.create_encryptor(&mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); enc.do_update_aad(&aad[..20]).expect("part"); enc.do_update_aad(&aad[20..]).expect("exactly fills the remaining capacity"); let mut split = [0u8; 48]; @@ -531,14 +505,14 @@ fn exact_lengths_are_accepted_whole_and_split() { split[32..].copy_from_slice(&tag); assert_eq!(split, sealed, "the chunking must not change the answer"); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(&aad).expect("aad"); let mut opened = [0u8; 32]; assert_eq!(dec.do_decrypt_out(&sealed, &mut opened).expect("the whole frame"), 32); dec.do_decrypt_final().expect("tag check"); assert_eq!(opened, message); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(&aad[..20]).expect("part"); dec.do_update_aad(&aad[20..]).expect("the rest"); let mut opened = [0u8; 32]; @@ -558,12 +532,10 @@ fn exact_lengths_are_accepted_whole_and_split() { /// wants, and the AAD may fall short of its capacity, including all the way to none. #[test] fn the_aad_capacity_and_the_payload_length_are_independent() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 8, 64>(); // AAD past AAD_LEN is refused, although it would fit in DATA_LEN. - let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + let (mut enc, _) = key.create_encryptor(&mut os_rng()).expect("init"); assert!(matches!(enc.do_update_aad(&[0u8; 9]), Err(SymmetricCipherError::GenericError(_)))); // A full AAD_LEN of AAD and a full DATA_LEN of payload together, far more than AAD_LEN alone, @@ -571,7 +543,7 @@ fn the_aad_capacity_and_the_payload_length_are_independent() { // none, each agreeing with the direct API. let message = [0x5Au8; 64]; for aad in [&[0x11u8; 8][..], &[0x11u8; 3][..], &[][..]] { - let (mut enc, nonce) = Enc::do_encrypt_init(&k).expect("init"); + let (mut enc, nonce) = key.create_encryptor(&mut os_rng()).expect("init"); enc.do_update_aad(aad).expect("within AAD_LEN"); let mut sealed = [0u8; 64]; enc.do_encrypt_out(&message, &mut sealed).expect("exactly DATA_LEN"); @@ -579,11 +551,10 @@ fn the_aad_capacity_and_the_payload_length_are_independent() { let mut direct = [0u8; 64]; let (_, direct_tag) = - Enc::encrypt_detached_nonce_out(&k, &nonce, aad, &message, &mut direct) - .expect("one-shot"); + key.encrypt_detached_nonce_out(&nonce, aad, &message, &mut direct).expect("one-shot"); assert_eq!((sealed, tag), (direct, direct_tag), "aad of {} bytes", aad.len()); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(aad).expect("within AAD_LEN"); let mut opened = [0u8; 64]; dec.do_decrypt_out(&sealed, &mut opened).expect("exactly DATA_LEN"); @@ -597,19 +568,17 @@ fn the_aad_capacity_and_the_payload_length_are_independent() { /// excess ciphertext if it says detached. Nothing is released by either final. #[test] fn the_decryptor_releases_the_payload_and_holds_back_only_the_tag() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 32, 32>(); let message = [0x5Au8; 32]; - let (mut enc, nonce) = Enc::do_encrypt_init(&k).expect("init"); + let (mut enc, nonce) = key.create_encryptor(&mut os_rng()).expect("init"); let mut inline = [0u8; 48]; enc.do_encrypt_out(&message, &mut inline).expect("the frame"); let (tag, _) = enc.do_encrypt_final().expect("final"); inline[32..].copy_from_slice(&tag); // Inline: 40 bytes release the 32 of payload and hold 8 of tag; the last 8 release nothing. - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); let mut out = [0u8; 32]; assert_eq!(dec.do_decrypt_out_len(40), 32); assert_eq!( @@ -622,7 +591,7 @@ fn the_decryptor_releases_the_payload_and_holds_back_only_the_tag() { assert_eq!(n, 0, "nothing is left to release"); // One byte past the frame with its tag is refused, and the final still verifies. - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_decrypt_out(&inline, &mut out).expect("the whole frame"); assert!(matches!( dec.do_decrypt_out(&[0u8; 1], &mut []), @@ -632,7 +601,7 @@ fn the_decryptor_releases_the_payload_and_holds_back_only_the_tag() { // Detached, the 16 bytes held back after the payload have nowhere to go: the frame is // exactly DATA_LEN, so this `C` is malformed. - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_decrypt_out(&inline, &mut out).expect("the whole frame"); let mut nothing = [0xEEu8; 16]; assert!(matches!( @@ -642,7 +611,7 @@ fn the_decryptor_releases_the_payload_and_holds_back_only_the_tag() { assert_eq!(nothing, [0u8; 16], "the detached final only zeroes its buffer"); // ...and exactly DATA_LEN is the detached frame. - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); let mut out = [0u8; 32]; dec.do_decrypt_out(&inline[..32], &mut out).expect("the frame"); assert_eq!(dec.do_decrypt_final_detached_out(&tag, &mut nothing).expect("tag check"), 0); @@ -650,93 +619,127 @@ fn the_decryptor_releases_the_payload_and_holds_back_only_the_tag() { assert_eq!(out, message); } -/// The one-shots take the whole message, so `DATA_LEN` and `AAD_LEN` do not bind them: at the -/// frame's length they agree with the mode's in-place one-shot byte for byte, and at any other -/// length, with AAD past the capacity, they still round trip and agree, on every one of the nine -/// forms -- detached and inline, with and without a caller's RNG, with and without AAD, and the -/// three decrypting forms. A failed tag leaves the output zeroized. The streaming methods stay -/// bound; see `the_adapters_refuse_more_than_the_declared_lengths`. +/// The frame key's one-shots are written over its streams, so `DATA_LEN` and `AAD_LEN` bind +/// them: at the frame's length, with AAD at and under the capacity, they agree with the packet +/// key's in-place one-shots byte for byte, on every form -- detached and inline, drawing and +/// supplied-nonce, with and without AAD, and the three decrypting forms -- and at any other +/// length, or with AAD past the capacity, every form is refused. A failed tag leaves the output +/// zeroized. The packet key, [`CCM`], is the one that takes any length. #[test] -fn trait_one_shots_take_any_length_and_agree_with_the_in_place_one_shot() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); - let perm = AES128Internal::new(&k).unwrap(); +fn frame_key_one_shots_take_the_frame_and_agree_with_the_packet_key() { + let key = frame_key::<12, 16, 48, 48>(); + let packet = AES128Key::>::from_key_material(&material::<16>(APPENDIX_C_KEY)) + .expect("the appendix key"); let rng = || FixedSeedRNG::<12>::new([0x24u8; 12]); - // The frame, one byte either side of it, nothing, and well past it; AAD at, under and past - // the capacity. - for (msg_len, aad_len) in [(48usize, 48usize), (47, 0), (49, 49), (0, 5), (1000, 200)] { + // The frame, with AAD at and under the capacity, and with none. + for (msg_len, aad_len) in [(48usize, 48usize), (48, 3), (48, 0)] { let msg = vec![0xA5u8; msg_len]; let aad = vec![0x3Cu8; aad_len]; let what = format!("{msg_len}-byte payload, {aad_len}-byte AAD"); let mut detached = vec![0u8; msg_len]; - let (nonce, written, tag) = - Enc::encrypt_detached_rng_out(&k, &mut rng(), &aad, &msg, &mut detached) - .unwrap_or_else(|e| panic!("{what}: {e:?}")); + let (written, nonce, tag) = key + .encrypt_detached_out(&mut rng(), &aad, &msg, &mut detached) + .unwrap_or_else(|e| panic!("{what}: {e:?}")); assert_eq!(written, msg_len); let mut direct = msg.clone(); let direct_tag = - CCM::<12, 16>::encrypt_detached_in_place_nonce(&perm, &nonce, &aad, &mut direct) - .expect("in place"); + packet.encrypt_detached_in_place_nonce(&nonce, &aad, &mut direct).expect("in place"); assert_eq!((&detached, tag), (&direct, direct_tag), "{what}: detached agrees"); let mut inline = vec![0u8; msg_len + 16]; - let (nonce_i, written_i) = - Enc::encrypt_with_aad_rng_out(&k, &mut rng(), &aad, &msg, &mut inline) - .unwrap_or_else(|e| panic!("{what}: {e:?}")); + let (written_i, nonce_i) = key + .encrypt_with_aad_out(&mut rng(), &aad, &msg, &mut inline) + .unwrap_or_else(|e| panic!("{what}: {e:?}")); assert_eq!((nonce_i, written_i), (nonce, msg_len + 16)); assert_eq!(&inline[..msg_len], &detached[..], "{what}: inline is detached plus tag"); assert_eq!(&inline[msg_len..], &tag); let mut pt = vec![0xEEu8; msg_len]; assert_eq!( - Dec::decrypt_detached_out(&k, &nonce, &aad, &detached, &tag, &mut pt).expect("open"), + key.decrypt_detached_out(&nonce, &aad, &detached, &tag, &mut pt).expect("open"), msg_len ); assert_eq!(pt, msg, "{what}: detached round trip"); pt.fill(0xEE); assert_eq!( - Dec::decrypt_with_aad_out(&k, &nonce, &aad, &inline, &mut pt).expect("open"), + key.decrypt_with_aad_out(&nonce, &aad, &inline, &mut pt).expect("open"), msg_len ); assert_eq!(pt, msg, "{what}: inline round trip"); - - // The default-DRBG forms and the no-AAD forms, each opened by the matching decrypt. - let (n2, w2, t2) = Enc::encrypt_detached_out(&k, &aad, &msg, &mut detached).unwrap(); - assert_eq!(w2, msg_len); - Dec::decrypt_detached_out(&k, &n2, &aad, &detached, &t2, &mut pt).expect("open"); - let (n3, w3) = Enc::encrypt_with_aad_out(&k, &aad, &msg, &mut inline).unwrap(); + packet.decrypt_detached_in_place(&nonce, &aad, &mut direct, &tag).expect("in place"); + assert_eq!(direct, msg, "{what}: in-place round trip"); + + // The supplied-nonce forms, each opened by the matching decrypt. + let (w2, t2) = key.encrypt_detached_nonce_out(&nonce, &aad, &msg, &mut detached).unwrap(); + assert_eq!((w2, t2), (msg_len, tag)); + key.decrypt_detached_out(&nonce, &aad, &detached, &t2, &mut pt).expect("open"); + let w3 = key.encrypt_with_aad_nonce_out(&nonce, &aad, &msg, &mut inline).unwrap(); assert_eq!(w3, msg_len + 16); - Dec::decrypt_with_aad_out(&k, &n3, &aad, &inline, &mut pt).expect("open"); - let (n4, w4) = Enc::encrypt_rng_out(&k, &mut rng(), &msg, &mut inline).unwrap(); - assert_eq!((n4, w4), (nonce, msg_len + 16)); - assert_eq!(Dec::decrypt_out_len(w4), msg_len); - pt.fill(0xEE); - assert_eq!(Dec::decrypt_out(&k, &n4, &inline, &mut pt).expect("open"), msg_len); - assert_eq!(pt, msg, "{what}: no-AAD round trip"); - let (n5, w5) = Enc::encrypt_out(&k, &msg, &mut inline).unwrap(); - assert_eq!(w5, msg_len + 16); - Dec::decrypt_out(&k, &n5, &inline, &mut pt).expect("open"); + key.decrypt_with_aad_out(&nonce, &aad, &inline, &mut pt).expect("open"); + } + + // One byte either side of the frame, nothing, well past it, and AAD past the capacity: every + // form refuses, and the packet key does not. + for (msg_len, aad_len) in [(47usize, 0usize), (49, 0), (0, 5), (1000, 200), (48, 49)] { + let msg = vec![0xA5u8; msg_len]; + let aad = vec![0x3Cu8; aad_len]; + let what = format!("{msg_len}-byte payload, {aad_len}-byte AAD"); + let nonce = [0x24u8; 12]; + + let mut detached = vec![0u8; msg_len]; + assert!( + key.encrypt_detached_out(&mut rng(), &aad, &msg, &mut detached).is_err(), + "{what}: encrypt_detached_out" + ); + assert!( + key.encrypt_detached_nonce_out(&nonce, &aad, &msg, &mut detached).is_err(), + "{what}: encrypt_detached_nonce_out" + ); + let mut inline = vec![0u8; msg_len + 16]; + assert!( + key.encrypt_with_aad_out(&mut rng(), &aad, &msg, &mut inline).is_err(), + "{what}: encrypt_with_aad_out" + ); + assert!( + key.encrypt_with_aad_nonce_out(&nonce, &aad, &msg, &mut inline).is_err(), + "{what}: encrypt_with_aad_nonce_out" + ); + + let mut direct = msg.clone(); + let tag = packet + .encrypt_detached_in_place_nonce(&nonce, &aad, &mut direct) + .unwrap_or_else(|e| panic!("{what}: the packet key takes any length: {e:?}")); + let n = packet.encrypt_with_aad_nonce_out(&nonce, &aad, &msg, &mut inline).unwrap(); + assert_eq!(n, msg_len + 16); + let mut pt = vec![0xEEu8; msg_len]; + assert!( + key.decrypt_detached_out(&nonce, &aad, &direct, &tag, &mut pt).is_err(), + "{what}: decrypt_detached_out" + ); + assert!( + key.decrypt_with_aad_out(&nonce, &aad, &inline, &mut pt).is_err(), + "{what}: decrypt_with_aad_out" + ); } // A failed tag leaves nothing behind, on all three decrypting forms. let msg = [0xA5u8; 48]; let mut inline = [0u8; 64]; - let (nonce, _) = Enc::encrypt_with_aad_out(&k, b"hdr", &msg, &mut inline).unwrap(); + let (_, nonce) = key.encrypt_with_aad_out(&mut os_rng(), b"hdr", &msg, &mut inline).unwrap(); let mut forged = inline; forged[0] ^= 1; let mut pt = [0xEEu8; 48]; assert!(matches!( - Dec::decrypt_with_aad_out(&k, &nonce, b"hdr", &forged, &mut pt), + key.decrypt_with_aad_out(&nonce, b"hdr", &forged, &mut pt), Err(SymmetricCipherError::AEADTagCheckFailed) )); assert_eq!(pt, [0u8; 48]); pt.fill(0xEE); assert!( matches!( - Dec::decrypt_out(&k, &nonce, &inline, &mut pt), + key.decrypt_with_aad_out(&nonce, &[], &inline, &mut pt), Err(SymmetricCipherError::AEADTagCheckFailed) ), "the AAD is missing" @@ -744,10 +747,16 @@ fn trait_one_shots_take_any_length_and_agree_with_the_in_place_one_shot() { assert_eq!(pt, [0u8; 48]); pt.fill(0xEE); assert!(matches!( - Dec::decrypt_detached_out(&k, &nonce, b"hdr", &inline[..48], &[0u8; 16], &mut pt), + key.decrypt_detached_out(&nonce, b"hdr", &inline[..48], &[0u8; 16], &mut pt), Err(SymmetricCipherError::AEADTagCheckFailed) )); assert_eq!(pt, [0u8; 48]); + let mut buf = inline[..48].to_vec(); + assert!(matches!( + packet.decrypt_detached_in_place(&nonce, b"hdr", &mut buf, &[0u8; 16]), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(buf, [0u8; 48]); } /// `B0` commits to `DATA_LEN`, so every final must check that exactly that much payload was @@ -762,9 +771,7 @@ fn trait_one_shots_take_any_length_and_agree_with_the_in_place_one_shot() { /// succeeding on the right amount, so a matcher that passed for the wrong reason would show. #[test] fn every_final_refuses_a_payload_of_the_wrong_length() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 16, 8>(); let aad = b"header"; let frame = [0x5Au8; 8]; let nonce_seed = [0x24u8; 12]; @@ -772,7 +779,7 @@ fn every_final_refuses_a_payload_of_the_wrong_length() { // An encryptor fed `supplied` of the 8 declared bytes. let enc = |supplied: usize| { let (mut enc, _) = - Enc::do_encrypt_init_rng(&k, &mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); + key.create_encryptor(&mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); enc.do_update_aad(aad).expect("aad"); let mut out = [0u8; 8]; enc.do_encrypt_out(&frame[..supplied], &mut out).expect("update"); @@ -825,7 +832,7 @@ fn every_final_refuses_a_payload_of_the_wrong_length() { let mut ct = [0u8; 8]; { let (mut e, _) = - Enc::do_encrypt_init_rng(&k, &mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); + key.create_encryptor(&mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); e.do_update_aad(aad).expect("aad"); e.do_encrypt_out(&frame, &mut ct).expect("update"); e.do_encrypt_final().expect("final"); @@ -836,7 +843,7 @@ fn every_final_refuses_a_payload_of_the_wrong_length() { // A decryptor fed the first `supplied` bytes of `inline`. let dec = |supplied: usize| { - let mut dec = Dec::do_decrypt_init(&k, &nonce_seed).expect("init"); + let mut dec = key.create_decryptor(&nonce_seed); dec.do_update_aad(aad).expect("aad"); let mut out = [0u8; 8]; dec.do_decrypt_out(&inline[..supplied], &mut out).expect("update"); @@ -908,8 +915,8 @@ fn every_final_refuses_a_payload_of_the_wrong_length() { /// A `C` of exactly `TAG_LEN` octets is *not* too short: it is the empty payload of Sec 5.3's /// footnote, and must authenticate. /// -/// Both inline entry points -- the decryptor's `decrypt_with_aad_out` and its `do_decrypt_final` -/// -- must report the same malformed input with the same variant, +/// Both inline entry points -- the key's `decrypt_with_aad_out` and the decryptor's +/// `do_decrypt_final` -- must report the same malformed input with the same variant, /// [`SymmetricCipherError::DecryptionFailed`], which is what /// [`SymmetricCipherDecryptor::do_decrypt_final`] specifies for a malformed ciphertext; a caller /// telling "malformed" from "inauthentic" must not get a different answer depending on which one it @@ -917,9 +924,7 @@ fn every_final_refuses_a_payload_of_the_wrong_length() { #[test] fn an_inline_ciphertext_shorter_than_the_tag_is_rejected() { // The empty frame, so that every byte of a short `C` is a (missing) tag byte. - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 48, 0>(); let nonce = [0u8; 12]; let mut out = [0u8; 16]; let mut nothing = [0u8; 0]; @@ -928,12 +933,12 @@ fn an_inline_ciphertext_shorter_than_the_tag_is_rejected() { let short = vec![0u8; len]; assert!( matches!( - Dec::decrypt_with_aad_out(&k, &nonce, &[], &short, &mut out), + key.decrypt_with_aad_out(&nonce, &[], &short, &mut out), Err(SymmetricCipherError::DecryptionFailed) ), "a {len}-byte C cannot carry a 16-byte tag (decrypt_with_aad_out)" ); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_decrypt_out(&short, &mut nothing).expect("held back as a possible tag"); assert!( matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), @@ -943,32 +948,33 @@ fn an_inline_ciphertext_shorter_than_the_tag_is_rejected() { // Exactly TAG_LEN: an empty payload plus its tag, which must verify, on both. let mut inline = [0u8; 16]; - let n = Enc::encrypt_with_aad_nonce_out(&k, &nonce, &[], &[], &mut inline).expect("encryption"); + let n = key.encrypt_with_aad_nonce_out(&nonce, &[], &[], &mut inline).expect("encryption"); assert_eq!(n, 16); - assert_eq!( - Dec::decrypt_with_aad_out(&k, &nonce, &[], &inline, &mut out).expect("decryption"), - 0 - ); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + assert_eq!(key.decrypt_with_aad_out(&nonce, &[], &inline, &mut out).expect("decryption"), 0); + let mut dec = key.create_decryptor(&nonce); assert_eq!(dec.do_decrypt_out(&inline, &mut nothing).expect("the tag"), 0); assert_eq!(dec.do_decrypt_final().expect("an empty frame still verifies").1, 0); } -/// A truncated `C` on a frame that is not empty, where the streaming final and the one-shots -/// part ways. The streaming decryptor knows the frame: `do_decrypt_out` releases the payload as -/// it arrives, up to `DATA_LEN`, holds back only what follows as the tag, and the final reports -/// anything short of `DATA_LEN + TAG_LEN` as malformed. The one-shots take the whole message and -/// know no frame: a `C` shorter than the tag is malformed, and anything longer is a shorter -/// message whose tag does not verify. `decrypt_out_len` is `C` less a tag, as for any AEAD. +/// A truncated `C` on a frame that is not empty, where the frame key's stream and the packet +/// key's one-shots part ways. The streaming decryptor knows the frame: `do_decrypt_out` releases +/// the payload as it arrives, up to `DATA_LEN`, holds back only what follows as the tag, and the +/// final reports anything short of `DATA_LEN + TAG_LEN` as malformed. The packet key's one-shots +/// take the whole message and know no frame: a `C` shorter than the tag is malformed, and +/// anything longer is a shorter message whose tag does not verify. `decrypt_out_len` is `C` less +/// a tag, as for any AEAD. #[test] fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { const DATA_LEN: usize = 32; type Enc = CcmEncryptor; type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 16, DATA_LEN>(); + let packet = AES128Key::>::from_key_material(&material::<16>(APPENDIX_C_KEY)) + .expect("the appendix key"); let frame = [0x5Au8; DATA_LEN]; let mut sealed = vec![0u8; Enc::encrypt_out_len(DATA_LEN)]; - let (nonce, n) = Enc::encrypt_with_aad_out(&k, b"hdr", &frame, &mut sealed).expect("seal"); + let (n, nonce) = + key.encrypt_with_aad_out(&mut os_rng(), b"hdr", &frame, &mut sealed).expect("seal"); assert_eq!(n, DATA_LEN + 16); assert_eq!(Dec::decrypt_out_len(DATA_LEN + 16), DATA_LEN); @@ -984,17 +990,11 @@ fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { }; let mut pt = vec![0u8; Dec::decrypt_out_len(len)]; assert!( - one_shot_says(Dec::decrypt_with_aad_out(&k, &nonce, b"hdr", short, &mut pt)), + one_shot_says(packet.decrypt_with_aad_out(&nonce, b"hdr", short, &mut pt)), "a {len}-byte C is too short for a tag, or a shorter message with the wrong one \ (decrypt_with_aad_out)" ); - let mut pt = vec![0u8; Dec::decrypt_out_len(len)]; - assert!( - one_shot_says(Dec::decrypt_out(&k, &nonce, short, &mut pt)), - "a {len}-byte C is too short for a tag, or a shorter message with the wrong one \ - (decrypt_out)" - ); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"hdr").expect("aad"); let mut pt = vec![0u8; dec.do_decrypt_out_len(len)]; dec.do_decrypt_out(short, &mut pt).expect("the payload is released, the rest held"); @@ -1004,83 +1004,55 @@ fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { ); } - // ...and the accepted length, through the same three, so the loop's bound is not off by one. + // ...and the accepted length, through both, so the loop's bound is not off by one. let mut pt = vec![0u8; Dec::decrypt_out_len(sealed.len())]; - assert_eq!(Dec::decrypt_with_aad_out(&k, &nonce, b"hdr", &sealed, &mut pt).expect("open"), 32); + assert_eq!(packet.decrypt_with_aad_out(&nonce, b"hdr", &sealed, &mut pt).expect("open"), 32); + assert_eq!(&pt[..], &frame[..]); + assert_eq!(key.decrypt_with_aad_out(&nonce, b"hdr", &sealed, &mut pt).expect("open"), 32); assert_eq!(&pt[..], &frame[..]); } /// An output buffer that is too short is refused with the length required, before any work. #[test] fn undersized_output_buffers_are_refused() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 0, 24>(); let nonce = [0u8; 12]; let plaintext = [0xAAu8; 24]; let mut too_small = [0u8; 23]; assert_eq!( - buffer_len_error(Enc::encrypt_detached_nonce_out( - &k, - &nonce, - &[], - &plaintext, - &mut too_small - )), + buffer_len_error(key.encrypt_detached_nonce_out(&nonce, &[], &plaintext, &mut too_small)), Some(24) ); let mut too_small = [0u8; 39]; assert_eq!( - buffer_len_error(Enc::encrypt_with_aad_nonce_out( - &k, - &nonce, - &[], - &plaintext, - &mut too_small - )), + buffer_len_error(key.encrypt_with_aad_nonce_out(&nonce, &[], &plaintext, &mut too_small)), Some(40) ); let mut ct = [0u8; 40]; - Enc::encrypt_with_aad_nonce_out(&k, &nonce, &[], &plaintext, &mut ct).expect("encryption"); + key.encrypt_with_aad_nonce_out(&nonce, &[], &plaintext, &mut ct).expect("encryption"); let mut too_small = [0u8; 23]; assert_eq!( - buffer_len_error(Dec::decrypt_with_aad_out(&k, &nonce, &[], &ct, &mut too_small)), + buffer_len_error(key.decrypt_with_aad_out(&nonce, &[], &ct, &mut too_small)), Some(24) ); } -/// A key of the wrong [`KeyType`] is rejected by every entry point, in both directions, and by -/// the mode-bound key's constructor. +/// The construction suite for both keys: the type, binding and strength policy of +/// `from_key_material`, which is the one place a key of the wrong [`KeyType`] is refused. #[test] -fn a_non_cipher_key_is_rejected() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let wrong = - KeyMaterial::<16>::from_bytes_as_type(&[0x11; 16], KeyType::MACKey).expect("a MAC key"); - let mut out = [0u8; 16]; - assert!(matches!( - Enc::encrypt_detached_nonce_out(&wrong, &[0u8; 12], &[], &[], &mut out), - Err(SymmetricCipherError::KeyMaterialError(_)) - )); - assert!(matches!( - Enc::do_encrypt_init_nonce(&wrong, &[0u8; 12]), - Err(SymmetricCipherError::KeyMaterialError(_)) - )); - assert!(matches!( - Dec::decrypt_with_aad_out(&wrong, &[0u8; 12], &[], &[0u8; 16], &mut out), - Err(SymmetricCipherError::KeyMaterialError(_)) - )); - assert!(matches!( - Dec::do_decrypt_init(&wrong, &[0u8; 12]), - Err(SymmetricCipherError::KeyMaterialError(_)) - )); - assert!(matches!( - AES128Key::>::from_key_material(&wrong), - Err(SymmetricCipherError::KeyMaterialError(_)) - )); +fn the_keys_enforce_the_material_policy() { + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::<16, AES128Key>>( + AES128Internal::CCM_ALG_NAME, + SecurityStrength::_128bit, + ); + framework.test::<16, FrameKey<12, 16, 0, 0>>( + AES128Internal::CCM_ALG_NAME, + SecurityStrength::_128bit, + ); } /// The direction is in the type, so the wrong direction's method is a **compile** error rather @@ -1092,18 +1064,16 @@ fn a_non_cipher_key_is_rejected() { /// `compile_fail` cannot express. #[test] fn each_direction_has_its_own_methods() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::<12, 16, 8, 4>(); let nonce = [0x55u8; 12]; - let mut enc = Enc::do_encrypt_init_nonce(&k, &nonce).expect("encrypt init"); + let mut enc = key.create_encryptor_nonce(&nonce); enc.do_update_aad(b"aad").expect("aad"); let mut data = [0u8; 4]; enc.do_encrypt_out(&[1u8, 2, 3, 4], &mut data).expect("encrypt update"); let (_, _, tag) = enc.do_encrypt_final_detached().expect("encrypt final"); - let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("decrypt init"); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"aad").expect("aad"); let mut back = [0u8; 4]; dec.do_decrypt_out(&data, &mut back).expect("decrypt update"); @@ -1153,7 +1123,7 @@ fn sizes_match_the_documented_memory_table() { fn payload_longer_than_the_q_limit_is_refused() { type Mode = CCM<13, 14>; assert_eq!(Mode::MAX_PAYLOAD_LEN, 65535, "2^16 - 1 is the largest payload q = 2 can encode"); - let perm = AES128Internal::new(&key::<16>(APPENDIX_C_KEY)).unwrap(); + let perm = AES128Internal::new(&material::<16>(APPENDIX_C_KEY)).unwrap(); let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; let mut buf = vec![0u8; 65536]; assert!( @@ -1187,19 +1157,14 @@ fn check_progressive_aad< c: &str, chunk: usize, ) { - type Enc = - CcmEncryptor; - type Dec = - CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); + let key = frame_key::(); let nonce: [u8; NONCE_LEN] = hex::decode(nonce).unwrap().try_into().unwrap(); let plaintext = hex::decode(plaintext).unwrap(); let c = hex::decode(c).unwrap(); let (want_ct, want_tag) = c.split_at(plaintext.len()); let want_tag: &[u8; TAG_LEN] = want_tag.try_into().unwrap(); - let mut enc = - Enc::::do_encrypt_init_nonce(&k, &nonce).unwrap(); + let mut enc = key.create_encryptor_nonce(&nonce); for piece in aad.chunks(chunk) { enc.do_update_aad(piece).unwrap(); } @@ -1209,8 +1174,7 @@ fn check_progressive_aad< assert_eq!(data, want_ct, "{label}: ciphertext, AAD in {chunk}-byte pieces"); assert_eq!(&tag, want_tag, "{label}: tag, AAD in {chunk}-byte pieces"); - let mut dec = - Dec::::do_decrypt_init(&k, &nonce).unwrap(); + let mut dec = key.create_decryptor(&nonce); for piece in aad.chunks(chunk) { dec.do_update_aad(piece).unwrap(); } @@ -1254,22 +1218,19 @@ fn progressive_aad_matches_appendix_c() { /// The supplied-nonce forms take the caller's nonce to the same Sec 6.1 as the mode's in-place /// one-shot, so they agree with it byte for byte. They are also how a protocol with its own -/// nonce discipline reaches CCM through the generic traits: IEEE 802.11 CCMP's 13-byte nonce and -/// 8-byte tag sit below the 12-byte floor a generated nonce needs, and compile here because -/// nothing draws one. +/// nonce discipline reaches CCM through the key: IEEE 802.11 CCMP's 13-byte nonce and 8-byte +/// tag. #[test] fn supplied_nonce_forms_agree_with_the_in_place_one_shot_at_any_a1_nonce_length() { - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; - let k = key::<16>(APPENDIX_C_KEY); - let perm = AES128Internal::new(&k).unwrap(); + let key = frame_key::<13, 8, 32, 48>(); + let perm = AES128Internal::new(&material::<16>(APPENDIX_C_KEY)).unwrap(); let nonce = [0x11u8; 13]; let aad = [0x3Cu8; 32]; let frame = [0xA5u8; 48]; let mut detached = [0u8; 48]; let (n, tag) = - Enc::encrypt_detached_nonce_out(&k, &nonce, &aad, &frame, &mut detached).expect("seal"); + key.encrypt_detached_nonce_out(&nonce, &aad, &frame, &mut detached).expect("seal"); assert_eq!(n, 48); let mut direct = frame; let direct_tag = @@ -1278,14 +1239,14 @@ fn supplied_nonce_forms_agree_with_the_in_place_one_shot_at_any_a1_nonce_length( assert_eq!((detached, tag), (direct, direct_tag), "the two routes to Sec 6.1 agree"); let mut inline = [0u8; 56]; assert_eq!( - Enc::encrypt_with_aad_nonce_out(&k, &nonce, &aad, &frame, &mut inline).expect("seal"), + key.encrypt_with_aad_nonce_out(&nonce, &aad, &frame, &mut inline).expect("seal"), 56 ); assert_eq!(&inline[..48], &detached); assert_eq!(&inline[48..], &tag); // The streaming constructor, on the frame. - let mut enc = Enc::do_encrypt_init_nonce(&k, &nonce).expect("init"); + let mut enc = key.create_encryptor_nonce(&nonce); enc.do_update_aad(&aad).expect("aad"); let mut streamed = [0u8; 48]; assert_eq!(enc.do_encrypt_out(&frame, &mut streamed).expect("released"), 48); @@ -1295,16 +1256,13 @@ fn supplied_nonce_forms_agree_with_the_in_place_one_shot_at_any_a1_nonce_length( // Opened by the decryptor at the same parameters, and not under another nonce. let mut opened = [0u8; 48]; assert_eq!( - Dec::decrypt_detached_out(&k, &nonce, &aad, &detached, &tag, &mut opened).expect("open"), + key.decrypt_detached_out(&nonce, &aad, &detached, &tag, &mut opened).expect("open"), 48 ); assert_eq!(opened, frame); - assert_eq!( - Dec::decrypt_with_aad_out(&k, &nonce, &aad, &inline, &mut opened).expect("open"), - 48 - ); + assert_eq!(key.decrypt_with_aad_out(&nonce, &aad, &inline, &mut opened).expect("open"), 48); assert!(matches!( - Dec::decrypt_detached_out(&k, &[0x12u8; 13], &aad, &detached, &tag, &mut opened), + key.decrypt_detached_out(&[0x12u8; 13], &aad, &detached, &tag, &mut opened), Err(SymmetricCipherError::AEADTagCheckFailed) )); assert_eq!(opened, [0u8; 48]); diff --git a/crypto/aes/tests/suspend_tests.rs b/crypto/aes/tests/suspend_tests.rs index 3a0e529ac..b278c00b8 100644 --- a/crypto/aes/tests/suspend_tests.rs +++ b/crypto/aes/tests/suspend_tests.rs @@ -1,39 +1,55 @@ //! Suspend-and-resume round trips through the AES aliases. //! //! The impls live on the generic modes in `bouncycastle-cipher`, where they are tested over a -//! toy permutation; what is checked here is that each alias, at each key length, reaches them with -//! the right key type. The engine itself has no state and nothing to suspend: a resumed mode -//! rebuilds it from the re-supplied key. The test does part of an operation, suspends a clone, -//! resumes it, and finishes both the same way. +//! toy permutation; what is checked here is that each alias, at each key length, made from its +//! key, reaches them and resumes from the key's state. The engine itself has no state and nothing +//! to suspend: a resumed mode rebuilds it from the re-supplied key. The test does part of an +//! operation, suspends a clone, resumes it, and finishes both the same way. use bouncycastle_aes::hazmat::{AES_ECB_128, AES_ECB_192, AES_ECB_256}; use bouncycastle_aes::{ AES_CBC_128, AES_CBC_192, AES_CBC_256, AES_CCM_128, AES_CCM_192, AES_CCM_256, AES_CFB_128, AES_CFB_192, AES_CFB_256, AES_CFB8_128, AES_CFB8_192, AES_CFB8_256, AES_CTR_128, AES_CTR_192, - AES_CTR_256, AES_GCM_128, AES_GCM_192, AES_GCM_256, + AES_CTR_256, AES_GCM_128, AES_GCM_192, AES_GCM_256, AES128Key, AES192Key, AES256Key, }; use bouncycastle_cipher::Encrypting; +use bouncycastle_cipher::modes::hazmat::ecb::ECB; +use bouncycastle_cipher::modes::{CBC, CCM_Packet, CFB, CFB8, CTR, GCM}; use bouncycastle_cipher::padding::PKCS7; +use bouncycastle_core::hazmat::ModeKey; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + AeadEncryptionKey, FromKeyMaterial, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{ AEADCipherEncryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; +use bouncycastle_rng::HashDRBG_SHA512; -fn key() -> KeyMaterial { +fn material() -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&[0x42; K], KeyType::SymmetricCipherKey).unwrap() } -fn round_trip( +/// The key under a mode, at key length `K`; its workers borrow it, and resume takes its state. +fn key_for>() -> Key { + Key::from_key_material(&material::()).expect("a valid key") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +fn round_trip<'k, const N: usize, C>( cipher: C, + key: &'k C::Key, finish: impl Fn(C) -> Vec, ) -> Vec where - C: SuspendableKeyed> + Clone, + C: SuspendableKeyed<'k, N> + Clone, { - let key = key::(); - TestFrameworkSuspendableKeyedState::new().test(&cipher, &key); - let resumed = C::from_suspended(cipher.clone().suspend(), &key).unwrap(); + TestFrameworkSuspendableKeyedState::new().test(&cipher, key); + let resumed = C::from_suspended(cipher.clone().suspend(), key).unwrap(); let original_output = finish(cipher); assert_eq!(original_output, finish(resumed), "the resumed cipher must continue identically"); original_output @@ -43,65 +59,76 @@ where /// and each needs its own `SUSPENDED_STATE_LEN` as a const argument, which a generic function /// cannot take from a type parameter; so the per-size repetition is a macro. macro_rules! every_alias_family_is_suspendable { - ($k:literal, $cbc:ident, $ecb:ident, $cfb:ident, $cfb8:ident, $ctr:ident, $gcm:ident, $ccm:ident) => {{ - type CbcEnc = $cbc; - let (mut cbc, _) = CbcEnc::do_encrypt_init(&key::<$k>()).unwrap(); + ($k:literal, $key:ident, $cbc:ident, $ecb:ident, $cfb:ident, $cfb8:ident, $ctr:ident, $gcm:ident, $ccm:ident) => {{ + type CbcEnc<'k> = $cbc<'k, Encrypting, PKCS7>; + let key = key_for::<$k, $key>>(); + let (mut cbc, _) = key.create_encryptor(&mut rng()).unwrap(); cbc.do_encrypt_out(&[0x11u8; 20], &mut [0u8; 16]).unwrap(); - round_trip::<{ CbcEnc::SUSPENDED_STATE_LEN }, $k, _>(cbc, |mut e| { + round_trip::<{ CbcEnc::SUSPENDED_STATE_LEN }, _>(cbc, key.state(), |mut e| { let mut out = [0u8; 16]; e.do_encrypt_out(&[0x22u8; 12], &mut out).unwrap(); let (last, n) = e.do_encrypt_final().unwrap(); [out.as_slice(), &last[..n]].concat() }); - type EcbEnc = $ecb; - let (mut ecb, _) = EcbEnc::do_encrypt_init(&key::<$k>()).unwrap(); + type EcbEnc<'k> = $ecb<'k, Encrypting, PKCS7>; + let key = key_for::<$k, $key>>(); + let (mut ecb, _) = key.create_encryptor(&mut rng()).unwrap(); ecb.do_encrypt_out(&[0x11u8; 20], &mut [0u8; 16]).unwrap(); - round_trip::<{ EcbEnc::SUSPENDED_STATE_LEN }, $k, _>(ecb, |e| { + round_trip::<{ EcbEnc::SUSPENDED_STATE_LEN }, _>(ecb, key.state(), |e| { let (last, n) = e.do_encrypt_final().unwrap(); last[..n].to_vec() }); - let (mut cfb, _) = $cfb::::do_encrypt_init(&key::<$k>()).unwrap(); + let key = key_for::<$k, $key>(); + let (mut cfb, _) = key.create_encryptor(&mut rng()).unwrap(); cfb.do_encrypt_inplace(&mut [0x11u8; 7]).unwrap(); - round_trip::<{ $cfb::::SUSPENDED_STATE_LEN }, $k, _>(cfb, |mut e| { + round_trip::<{ $cfb::::SUSPENDED_STATE_LEN }, _>(cfb, key.state(), |mut e| { let mut data = [0x22u8; 25]; e.do_encrypt_inplace(&mut data).unwrap(); data.to_vec() }); - let (mut cfb8, _) = $cfb8::::do_encrypt_init(&key::<$k>()).unwrap(); + let key = key_for::<$k, $key>(); + let (mut cfb8, _) = key.create_encryptor(&mut rng()).unwrap(); cfb8.do_encrypt_inplace(&mut [0x11u8; 7]).unwrap(); - round_trip::<{ $cfb8::::SUSPENDED_STATE_LEN }, $k, _>(cfb8, |mut e| { - let mut data = [0x22u8; 9]; - e.do_encrypt_inplace(&mut data).unwrap(); - data.to_vec() - }); + round_trip::<{ $cfb8::::SUSPENDED_STATE_LEN }, _>( + cfb8, + key.state(), + |mut e| { + let mut data = [0x22u8; 9]; + e.do_encrypt_inplace(&mut data).unwrap(); + data.to_vec() + }, + ); - let (mut ctr, _) = $ctr::::do_encrypt_init(&key::<$k>()).unwrap(); + let key = key_for::<$k, $key>(); + let (mut ctr, _) = key.create_encryptor(&mut rng()).unwrap(); ctr.do_encrypt_inplace(&mut [0x11u8; 7]).unwrap(); - round_trip::<{ $ctr::::SUSPENDED_STATE_LEN }, $k, _>(ctr, |mut e| { + round_trip::<{ $ctr::::SUSPENDED_STATE_LEN }, _>(ctr, key.state(), |mut e| { let mut data = [0x22u8; 25]; e.do_encrypt_inplace(&mut data).unwrap(); data.to_vec() }); - let (mut gcm, _) = $gcm::::do_encrypt_init(&key::<$k>()).unwrap(); + let key = key_for::<$k, $key>(); + let (mut gcm, _) = key.create_encryptor(&mut rng()).unwrap(); gcm.do_update_aad(b"header").unwrap(); gcm.do_encrypt_out(&[0x11u8; 7], &mut [0u8; 7]).unwrap(); - round_trip::<{ $gcm::::SUSPENDED_STATE_LEN }, $k, _>(gcm, |mut e| { + round_trip::<{ $gcm::::SUSPENDED_STATE_LEN }, _>(gcm, key.state(), |mut e| { let mut out = [0u8; 25]; e.do_encrypt_out(&[0x22u8; 25], &mut out).unwrap(); let (tag, n) = e.do_encrypt_final().unwrap(); [out.as_slice(), &tag[..n]].concat() }); - type CcmEnc = $ccm; - let mut ccm = CcmEnc::do_encrypt_init_nonce(&key::<$k>(), &[0x24u8; 12]).unwrap(); + type CcmEnc<'k> = $ccm<'k, Encrypting, 12, 16, 64, 32>; + let key = key_for::<$k, $key>>(); + let mut ccm = key.create_encryptor_nonce(&[0x24u8; 12]); ccm.do_update_aad(b"header").unwrap(); let mut head = [0u8; 7]; ccm.do_encrypt_out(&[0x11u8; 7], &mut head).unwrap(); - round_trip::<{ CcmEnc::SUSPENDED_STATE_LEN }, $k, _>(ccm, |mut e| { + round_trip::<{ CcmEnc::SUSPENDED_STATE_LEN }, _>(ccm, key.state(), |mut e| { let mut out = [0u8; 25]; e.do_encrypt_out(&[0x22u8; 25], &mut out).unwrap(); let (tag, n) = e.do_encrypt_final().unwrap(); @@ -113,23 +140,23 @@ macro_rules! every_alias_family_is_suspendable { #[test] fn every_aes128_alias_family_is_suspendable() { every_alias_family_is_suspendable!( - 16, AES_CBC_128, AES_ECB_128, AES_CFB_128, AES_CFB8_128, AES_CTR_128, AES_GCM_128, - AES_CCM_128 + 16, AES128Key, AES_CBC_128, AES_ECB_128, AES_CFB_128, AES_CFB8_128, AES_CTR_128, + AES_GCM_128, AES_CCM_128 ); } #[test] fn every_aes192_alias_family_is_suspendable() { every_alias_family_is_suspendable!( - 24, AES_CBC_192, AES_ECB_192, AES_CFB_192, AES_CFB8_192, AES_CTR_192, AES_GCM_192, - AES_CCM_192 + 24, AES192Key, AES_CBC_192, AES_ECB_192, AES_CFB_192, AES_CFB8_192, AES_CTR_192, + AES_GCM_192, AES_CCM_192 ); } #[test] fn every_aes256_alias_family_is_suspendable() { every_alias_family_is_suspendable!( - 32, AES_CBC_256, AES_ECB_256, AES_CFB_256, AES_CFB8_256, AES_CTR_256, AES_GCM_256, - AES_CCM_256 + 32, AES256Key, AES_CBC_256, AES_ECB_256, AES_CFB_256, AES_CFB8_256, AES_CTR_256, + AES_GCM_256, AES_CCM_256 ); } diff --git a/crypto/core-test-framework/src/block_cipher.rs b/crypto/core-test-framework/src/block_cipher.rs index 7eb83ac4f..b1e3b4765 100644 --- a/crypto/core-test-framework/src/block_cipher.rs +++ b/crypto/core-test-framework/src/block_cipher.rs @@ -5,7 +5,7 @@ use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey}; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey}; use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, RNG}; diff --git a/mem_usage_benches/src/bench_ccm_mem_usage.rs b/mem_usage_benches/src/bench_ccm_mem_usage.rs index b659a3a8f..7fa5262d0 100644 --- a/mem_usage_benches/src/bench_ccm_mem_usage.rs +++ b/mem_usage_benches/src/bench_ccm_mem_usage.rs @@ -28,15 +28,18 @@ #![allow(dead_code)] #![allow(unused_imports)] +use bouncycastle::aes::AES128Key; use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle::cipher::modes::{CCM, CcmDecryptor, CcmEncryptor}; +use bouncycastle::cipher::modes::{CCM, CCMFrame, CcmDecryptor, CcmEncryptor}; use bouncycastle::core::hazmat::{ AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook, }; use bouncycastle::core::key_material::{KeyMaterial, KeyType}; +use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle::core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; +use bouncycastle::rng::HashDRBG_SHA512; /// The parameters the ACVP vectors and most protocols use: 12-byte nonce, 16-byte tag. const NONCE_LEN: usize = 12; @@ -53,6 +56,8 @@ const MESSAGE_LEN: usize = DATA_LEN; /// The mode's in-place one-shots over the keyed permutation: the direct path. type Aes128CcmMode = CCM; +/// The key bound to the fixed frame, whose streams are the pair below. +type Aes128CcmFrameKey = AES128Key>; type Aes128CcmEncryptor = CcmEncryptor; type Aes128CcmDecryptor = @@ -66,6 +71,10 @@ fn perm() -> AES128Internal { AES128Internal::new(&key::<16>()).unwrap() } +fn frame_key() -> Aes128CcmFrameKey { + Aes128CcmFrameKey::from_key_material(&key::<16>()).unwrap() +} + /// The message every bench processes, filled at run time and then only ever reached through a /// `black_box`ed reference, so that it is a whole stack array in every bench alike. Without that, /// a `[0xA5; N]` literal is a constant the compiler may keep in read-only data in one bench, or @@ -135,20 +144,21 @@ fn bench_in_place_encrypt() { print!("{:x?}", &tag); } -/// The same message through the trait encryptor's **streaming** methods: `do_encrypt_init` -/// builds the value, `do_update_out` writes each chunk's ciphertext straight out, and the final -/// returns the tag. The caller's two arrays are the whole of the stack that scales. +/// The same message through the frame key's **streaming** encryptor: `create_encryptor` builds +/// the value under a drawn nonce, `do_encrypt_out` writes each chunk's ciphertext straight out, +/// and the final returns the tag. The caller's two arrays are the whole of the stack that scales. #[inline(never)] fn bench_streaming_encrypt() { eprintln!( - "CcmEncryptor do_encrypt_init/do_update_out/do_encrypt_final_detached_out, {MESSAGE_LEN} B in 1 KiB chunks" + "CcmEncryptor create_encryptor/do_encrypt_out/do_encrypt_final_detached_out, {MESSAGE_LEN} B in 1 KiB chunks" ); - let k = key::<16>(); + let k = frame_key(); + let mut rng = HashDRBG_SHA512::new_from_os(); let plaintext = message(); let plaintext = core::hint::black_box(&plaintext); let mut ciphertext = [0u8; MESSAGE_LEN]; - let (mut enc, _nonce) = Aes128CcmEncryptor::do_encrypt_init(&k).unwrap(); + let (mut enc, _nonce) = k.create_encryptor(&mut rng).unwrap(); let mut written = 0; for chunk in plaintext.chunks(1024) { written += enc.do_encrypt_out(chunk, &mut ciphertext[written..]).unwrap(); @@ -166,10 +176,10 @@ fn bench_streaming_encrypt() { #[inline(never)] fn bench_streaming_decrypt() { eprintln!( - "CcmDecryptor do_decrypt_init/do_update_out/do_decrypt_final, {MESSAGE_LEN} B in 1 KiB chunks" + "CcmDecryptor create_decryptor/do_decrypt_out/do_decrypt_final, {MESSAGE_LEN} B in 1 KiB chunks" ); - let k = key::<16>(); + let k = frame_key(); let nonce = [0x24u8; NONCE_LEN]; let mut sealed = [0u8; MESSAGE_LEN + TAG_LEN]; sealed[..MESSAGE_LEN].fill(core::hint::black_box(0xA5)); @@ -184,7 +194,7 @@ fn bench_streaming_decrypt() { let sealed = core::hint::black_box(&sealed); let mut opened = [0u8; MESSAGE_LEN]; - let mut dec = Aes128CcmDecryptor::do_decrypt_init(&k, &nonce).unwrap(); + let mut dec = k.create_decryptor(&nonce); let mut written = 0; for chunk in sealed.chunks(1024) { written += dec.do_decrypt_out(chunk, &mut opened[written..]).unwrap(); @@ -193,20 +203,19 @@ fn bench_streaming_decrypt() { print!("{}", written + m); } -/// The trait encryptor's **one-shot**, which runs the packet one-shot into the caller's output -/// buffer under a generated nonce, so it should measure what `bench_in_place_encrypt` measures -/// plus a second array and the DRBG the nonce is drawn from -- no adapter value and no `AAD_LEN` -/// buffer. +/// The frame key's **one-shot**, written over its stream, into the caller's output buffer under +/// a nonce drawn from the caller's DRBG, so it should measure what `bench_streaming_encrypt` +/// measures plus that DRBG. #[inline(never)] fn bench_oneshot_encrypt_out_detached() { - eprintln!("CcmEncryptor::encrypt_detached_out, {MESSAGE_LEN} B"); + eprintln!("AES128Key::encrypt_detached_out, {MESSAGE_LEN} B"); - let k = key::<16>(); + let k = frame_key(); + let mut rng = HashDRBG_SHA512::new_from_os(); let plaintext = message(); let plaintext = core::hint::black_box(&plaintext); let mut ciphertext = [0u8; MESSAGE_LEN]; - let (_, _, tag) = - Aes128CcmEncryptor::encrypt_detached_out(&k, &[], plaintext, &mut ciphertext).unwrap(); + let (_, _, tag) = k.encrypt_detached_out(&mut rng, &[], plaintext, &mut ciphertext).unwrap(); print!("{:x?}", &tag); } From 32d67f83a0b79b6c2aa34b99d96f6345359c81f0 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 00:39:00 +1100 Subject: [PATCH 12/54] cli: the cipher commands build key objects (prototype) The block, stream and GCM helpers take the key, or the worker it made, in place of the key material: load_key_object builds the AES key type a command's mode wants, the encrypt side draws its IV or nonce from the OS DRBG through create_encryptor, and the decrypt side's create_decryptor is infallible. The Ascon command builds an AsconAEAD128Key the same way. Output is unchanged: every cli/tests script passes. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/aes_cbc_cmd.rs | 37 +++++---- cli/src/aes_cfb8_cmd.rs | 35 ++++++-- cli/src/aes_cfb_cmd.rs | 35 ++++++-- cli/src/aes_ctr_cmd.rs | 36 ++++++-- cli/src/aes_ecb_cmd.rs | 49 +++++------ cli/src/aes_gcm_cmd.rs | 34 ++++---- cli/src/ascon_cmd.rs | 54 ++++++------ cli/src/helpers/aead_cipher_helpers.rs | 53 ++++++------ cli/src/helpers/block_mode_helpers.rs | 109 +++++++++++++++---------- cli/src/helpers/stream_mode_helpers.rs | 59 ++++++------- 10 files changed, 286 insertions(+), 215 deletions(-) diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs index 8f2d8bd2e..f5026eb86 100644 --- a/cli/src/aes_cbc_cmd.rs +++ b/cli/src/aes_cbc_cmd.rs @@ -10,13 +10,13 @@ //! separately. use crate::helpers::block_mode_helpers::{ - BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key, + BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key_object, + start_encryptor, }; -use bouncycastle::aes::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::cipher::modes::CBC; use bouncycastle::cipher::padding::PKCS7; -use bouncycastle::cipher::{Decrypting, Encrypting}; -use bouncycastle::core::key_material::KeyMaterial; -use bouncycastle::core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; pub(crate) fn aes128_cbc_cmd( action: &CipherDirection, @@ -26,9 +26,9 @@ pub(crate) fn aes128_cbc_cmd( iv_file: &Option, output_hex: bool, ) { - run::, AES_CBC_128, 16>( + run::<_, 16>( action, - &load_key::<16>(key, key_file, "AES-128"), + &load_key_object::>, 16>(key, key_file, "AES-128"), load_iv::(iv, iv_file), output_hex, ); @@ -42,9 +42,9 @@ pub(crate) fn aes192_cbc_cmd( iv_file: &Option, output_hex: bool, ) { - run::, AES_CBC_192, 24>( + run::<_, 24>( action, - &load_key::<24>(key, key_file, "AES-192"), + &load_key_object::>, 24>(key, key_file, "AES-192"), load_iv::(iv, iv_file), output_hex, ); @@ -58,30 +58,31 @@ pub(crate) fn aes256_cbc_cmd( iv_file: &Option, output_hex: bool, ) { - run::, AES_CBC_256, 32>( + run::<_, 32>( action, - &load_key::<32>(key, key_file, "AES-256"), + &load_key_object::>, 32>(key, key_file, "AES-256"), load_iv::(iv, iv_file), output_hex, ); } -/// Dispatches to the shared streaming loops. -fn run( +/// Dispatches to the shared streaming loops under the key's padded CBC mode. +fn run( action: &CipherDirection, - key: &KeyMaterial, + key: &K, iv: Option<[u8; BLOCK_LEN]>, output_hex: bool, ) where - E: SymmetricCipherEncryptor, - D: SymmetricCipherDecryptor, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, { match action { CipherDirection::Encrypt => { - encrypt_stream::(key, iv, output_hex, None) + let (enc, generated_iv) = start_encryptor(iv, |rng| key.create_encryptor(rng)); + encrypt_stream::<_, BLOCK_LEN>(enc, generated_iv.as_ref(), output_hex, None) } CipherDirection::Decrypt => { - decrypt_stream::(key, iv, output_hex, None) + decrypt_stream::<_, BLOCK_LEN>(iv, |iv| key.create_decryptor(iv), output_hex, None) } } } diff --git a/cli/src/aes_cfb8_cmd.rs b/cli/src/aes_cfb8_cmd.rs index ee6690cdb..450b8eb1c 100644 --- a/cli/src/aes_cfb8_cmd.rs +++ b/cli/src/aes_cfb8_cmd.rs @@ -25,10 +25,12 @@ //! plaintext byte, corrupts the following 16 bytes, and then decryption resynchronises. Do not //! decrypt data you have not authenticated separately. -use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key}; +use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; -use bouncycastle::aes::{AES_CFB8_128, AES_CFB8_192, AES_CFB8_256}; -use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::cipher::modes::CFB8; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; pub(crate) fn aes128_cfb8_cmd( action: &CipherDirection, @@ -38,9 +40,9 @@ pub(crate) fn aes128_cfb8_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CFB8_128, 16, BLOCK_LEN>( + run::<_, 16>( action, - &load_key::<16>(key, key_file, "AES-128"), + &load_key_object::, 16>(key, key_file, "AES-128"), load_iv::(iv, iv_file), output_hex, ); @@ -54,9 +56,9 @@ pub(crate) fn aes192_cfb8_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CFB8_192, 24, BLOCK_LEN>( + run::<_, 24>( action, - &load_key::<24>(key, key_file, "AES-192"), + &load_key_object::, 24>(key, key_file, "AES-192"), load_iv::(iv, iv_file), output_hex, ); @@ -70,10 +72,25 @@ pub(crate) fn aes256_cfb8_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CFB8_256, 32, BLOCK_LEN>( + run::<_, 32>( action, - &load_key::<32>(key, key_file, "AES-256"), + &load_key_object::, 32>(key, key_file, "AES-256"), load_iv::(iv, iv_file), output_hex, ); } + +/// Dispatches to the shared streaming loops with `Cfb8` filled in as the mode. +fn run<'k, K, const KEY_LEN: usize>( + action: &CipherDirection, + key: &'k K, + iv: Option<[u8; BLOCK_LEN]>, + output_hex: bool, +) where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + run_stream_mode::(action, key, iv, output_hex) +} diff --git a/cli/src/aes_cfb_cmd.rs b/cli/src/aes_cfb_cmd.rs index 9cc88a6e2..d59398684 100644 --- a/cli/src/aes_cfb_cmd.rs +++ b/cli/src/aes_cfb_cmd.rs @@ -26,10 +26,12 @@ //! of the plaintext in the *same* block, so an attacker edits the block they aimed at, at the cost //! of randomising the next one. Do not decrypt data you have not authenticated separately. -use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key}; +use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; -use bouncycastle::aes::{AES_CFB_128, AES_CFB_192, AES_CFB_256}; -use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::cipher::modes::CFB; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; pub(crate) fn aes128_cfb_cmd( action: &CipherDirection, @@ -39,9 +41,9 @@ pub(crate) fn aes128_cfb_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CFB_128, 16, BLOCK_LEN>( + run::<_, 16>( action, - &load_key::<16>(key, key_file, "AES-128"), + &load_key_object::, 16>(key, key_file, "AES-128"), load_iv::(iv, iv_file), output_hex, ); @@ -55,9 +57,9 @@ pub(crate) fn aes192_cfb_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CFB_192, 24, BLOCK_LEN>( + run::<_, 24>( action, - &load_key::<24>(key, key_file, "AES-192"), + &load_key_object::, 24>(key, key_file, "AES-192"), load_iv::(iv, iv_file), output_hex, ); @@ -71,10 +73,25 @@ pub(crate) fn aes256_cfb_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CFB_256, 32, BLOCK_LEN>( + run::<_, 32>( action, - &load_key::<32>(key, key_file, "AES-256"), + &load_key_object::, 32>(key, key_file, "AES-256"), load_iv::(iv, iv_file), output_hex, ); } + +/// Dispatches to the shared streaming loops with `Cfb` filled in as the mode. +fn run<'k, K, const KEY_LEN: usize>( + action: &CipherDirection, + key: &'k K, + iv: Option<[u8; BLOCK_LEN]>, + output_hex: bool, +) where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + run_stream_mode::(action, key, iv, output_hex) +} diff --git a/cli/src/aes_ctr_cmd.rs b/cli/src/aes_ctr_cmd.rs index 742f333fc..138eab6e5 100644 --- a/cli/src/aes_ctr_cmd.rs +++ b/cli/src/aes_ctr_cmd.rs @@ -33,10 +33,13 @@ //! nonce is drawn from the OS-backed DRBG for exactly that reason, and there is no way to supply //! one. -use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; -use bouncycastle::aes::{AES_CTR_128, AES_CTR_192, AES_CTR_256, CTR_NONCE_LEN}; -use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::aes::CTR_NONCE_LEN; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::cipher::modes::CTR; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; pub(crate) fn aes128_ctr_cmd( action: &CipherDirection, @@ -46,9 +49,9 @@ pub(crate) fn aes128_ctr_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CTR_128, 16, CTR_NONCE_LEN>( + run::<_, 16>( action, - &load_key::<16>(key, key_file, "AES-128"), + &load_key_object::>, 16>(key, key_file, "AES-128"), load_iv::(iv, iv_file), output_hex, ); @@ -62,9 +65,9 @@ pub(crate) fn aes192_ctr_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CTR_192, 24, CTR_NONCE_LEN>( + run::<_, 24>( action, - &load_key::<24>(key, key_file, "AES-192"), + &load_key_object::>, 24>(key, key_file, "AES-192"), load_iv::(iv, iv_file), output_hex, ); @@ -78,10 +81,25 @@ pub(crate) fn aes256_ctr_cmd( iv_file: &Option, output_hex: bool, ) { - run_stream_mode::, AES_CTR_256, 32, CTR_NONCE_LEN>( + run::<_, 32>( action, - &load_key::<32>(key, key_file, "AES-256"), + &load_key_object::>, 32>(key, key_file, "AES-256"), load_iv::(iv, iv_file), output_hex, ); } + +/// Dispatches to the shared streaming loops with `Ctr` filled in as the mode. +fn run<'k, K, const KEY_LEN: usize>( + action: &CipherDirection, + key: &'k K, + iv: Option<[u8; CTR_NONCE_LEN]>, + output_hex: bool, +) where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + run_stream_mode::(action, key, iv, output_hex) +} diff --git a/cli/src/aes_ecb_cmd.rs b/cli/src/aes_ecb_cmd.rs index 63b7d43f5..4451abc35 100644 --- a/cli/src/aes_ecb_cmd.rs +++ b/cli/src/aes_ecb_cmd.rs @@ -16,13 +16,12 @@ //! `aes*-cbc` or `aes*-cfb` under separate authentication, or better an AEAD. use crate::helpers::block_mode_helpers::{ - BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_key, + BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_key_object, start_encryptor, }; -use bouncycastle::aes::hazmat::{AES_ECB_128, AES_ECB_192, AES_ECB_256}; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::cipher::modes::hazmat::ecb::ECB; use bouncycastle::cipher::padding::NoPadding; -use bouncycastle::cipher::{Decrypting, Encrypting}; -use bouncycastle::core::key_material::KeyMaterial; -use bouncycastle::core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; /// Names the mode in error messages. const MODE: &str = "ECB"; @@ -33,9 +32,9 @@ pub(crate) fn aes128_ecb_cmd( key_file: &Option, output_hex: bool, ) { - run::, AES_ECB_128, 16>( + run::<_, 16>( action, - &load_key::<16>(key, key_file, "AES-128"), + &load_key_object::>, 16>(key, key_file, "AES-128"), output_hex, ); } @@ -46,9 +45,9 @@ pub(crate) fn aes192_ecb_cmd( key_file: &Option, output_hex: bool, ) { - run::, AES_ECB_192, 24>( + run::<_, 24>( action, - &load_key::<24>(key, key_file, "AES-192"), + &load_key_object::>, 24>(key, key_file, "AES-192"), output_hex, ); } @@ -59,29 +58,31 @@ pub(crate) fn aes256_ecb_cmd( key_file: &Option, output_hex: bool, ) { - run::, AES_ECB_256, 32>( + run::<_, 32>( action, - &load_key::<32>(key, key_file, "AES-256"), + &load_key_object::>, 32>(key, key_file, "AES-256"), output_hex, ); } -/// Dispatches to the shared streaming loops. `INIT_DATA_LEN` is 0, so the loops write and read no -/// IV. -fn run( - action: &CipherDirection, - key: &KeyMaterial, - output_hex: bool, -) where - E: SymmetricCipherEncryptor, - D: SymmetricCipherDecryptor, +/// Dispatches to the shared streaming loops under the key's ECB mode. `INIT_DATA_LEN` is 0, so +/// the loops write and read no IV, and `NoPadding` makes them refuse a partial final block. +fn run(action: &CipherDirection, key: &K, output_hex: bool) +where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, { match action { CipherDirection::Encrypt => { - encrypt_stream::(key, None, output_hex, Some(MODE)) - } - CipherDirection::Decrypt => { - decrypt_stream::(key, None, output_hex, Some(MODE)) + // ECB has no IV: the constructor's RNG draws nothing, and nothing is written. + let (enc, _) = start_encryptor(None, |rng| key.create_encryptor(rng)); + encrypt_stream::<_, 0>(enc, None, output_hex, Some(MODE)) } + CipherDirection::Decrypt => decrypt_stream::<_, 0>( + None, + |no_iv| key.create_decryptor(no_iv), + output_hex, + Some(MODE), + ), } } diff --git a/cli/src/aes_gcm_cmd.rs b/cli/src/aes_gcm_cmd.rs index c6907aebd..5575c8d6d 100644 --- a/cli/src/aes_gcm_cmd.rs +++ b/cli/src/aes_gcm_cmd.rs @@ -14,11 +14,10 @@ //! recover the hash subkey (Appendix A). use crate::helpers::aead_cipher_helpers::{decrypt_gcm, encrypt_gcm, load_aad}; -use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key}; -use bouncycastle::aes::{AES_GCM_128, AES_GCM_192, AES_GCM_256}; -use bouncycastle::cipher::{Decrypting, Encrypting}; -use bouncycastle::core::key_material::KeyMaterial; -use bouncycastle::core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::cipher::modes::GCM; +use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; pub(crate) fn aes128_gcm_cmd( action: &CipherDirection, @@ -30,9 +29,9 @@ pub(crate) fn aes128_gcm_cmd( aad_file: &Option, output_hex: bool, ) { - run::, AES_GCM_128, 16>( + run::, 16>( action, - &load_key::<16>(key, key_file, "AES-128"), + &load_key_object::<_, 16>(key, key_file, "AES-128"), load_iv::<12>(iv, iv_file), &load_aad(aad, aad_file), output_hex, @@ -49,9 +48,9 @@ pub(crate) fn aes192_gcm_cmd( aad_file: &Option, output_hex: bool, ) { - run::, AES_GCM_192, 24>( + run::, 24>( action, - &load_key::<24>(key, key_file, "AES-192"), + &load_key_object::<_, 24>(key, key_file, "AES-192"), load_iv::<12>(iv, iv_file), &load_aad(aad, aad_file), output_hex, @@ -68,28 +67,27 @@ pub(crate) fn aes256_gcm_cmd( aad_file: &Option, output_hex: bool, ) { - run::, AES_GCM_256, 32>( + run::, 32>( action, - &load_key::<32>(key, key_file, "AES-256"), + &load_key_object::<_, 32>(key, key_file, "AES-256"), load_iv::<12>(iv, iv_file), &load_aad(aad, aad_file), output_hex, ); } -/// Dispatches to the shared AEAD streaming loops with GCM's 128-bit tag. -fn run( +/// Dispatches to the shared AEAD streaming loops with `Gcm`'s 128-bit tag. +fn run( action: &CipherDirection, - key: &KeyMaterial, + key: &K, nonce: Option<[u8; 12]>, aad: &[u8], output_hex: bool, ) where - E: AEADCipherEncryptor, - D: AEADCipherDecryptor, + K: AeadEncryptionKey + AeadDecryptionKey, { match action { - CipherDirection::Encrypt => encrypt_gcm::(key, nonce, aad, output_hex), - CipherDirection::Decrypt => decrypt_gcm::(key, nonce, aad, output_hex), + CipherDirection::Encrypt => encrypt_gcm::(key, nonce, aad, output_hex), + CipherDirection::Decrypt => decrypt_gcm::(key, nonce, aad, output_hex), } } diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs index 7656b1f4c..a51ba01b5 100644 --- a/cli/src/ascon_cmd.rs +++ b/cli/src/ascon_cmd.rs @@ -1,15 +1,15 @@ use std::io::{self, Read}; use std::process::exit; -use bouncycastle::ascon::Ascon_AEAD128; +use bouncycastle::ascon::AsconAEAD128Key; use bouncycastle::ascon::AsconCXof128; use bouncycastle::ascon::AsconHash256; use bouncycastle::ascon::AsconXof128; -use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::core::errors::SymmetricCipherError; use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::core::security_strength::SecurityStrength; +use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle::core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; @@ -56,9 +56,9 @@ fn require_16(bytes: Vec, label: &str) -> [u8; 16] { }) } -/// Build a `KeyMaterial<16>` for the AEAD key, warning (and forcing usable metadata) only if the -/// key turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does. -fn load_key_material(key_bytes: &[u8; 16]) -> KeyMaterial<16> { +/// Build the AEAD key, warning (and forcing usable metadata on the material) only if the key +/// turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does. +fn load_aead_key(key_bytes: &[u8; 16]) -> AsconAEAD128Key { let mut key = KeyMaterial::<16>::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey).unwrap(); if key.key_type() == KeyType::Zeroized || key.security_strength() < SecurityStrength::_128bit { @@ -71,7 +71,10 @@ fn load_key_material(key_bytes: &[u8; 16]) -> KeyMaterial<16> { }) .unwrap(); } - key + AsconAEAD128Key::from_key_material(&key).unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }) } /// Ascon-Hash256 of stdin. Streaming update; 256-bit digest. @@ -123,7 +126,7 @@ pub(crate) fn aead128_cmd( decrypt: bool, output_hex: bool, ) { - let key = load_key_material(&require_16(load_bytes(key, key_file, "key"), "key")); + let key = load_aead_key(&require_16(load_bytes(key, key_file, "key"), "key")); let nonce = load_optional_bytes(nonce, nonce_file, "nonce").map(|bytes| require_16(bytes, "nonce")); let ad_bytes = match ad { @@ -142,25 +145,26 @@ pub(crate) fn aead128_cmd( } } -/// Drives [`Ascon_AEAD128`] in the inline `ciphertext || tag` layout (the inherited -/// [`SymmetricCipherEncryptor::do_encrypt_final_out`]). With no nonce given one is generated and -/// written ahead of the stream; a caller-supplied nonce is not written, since the caller has it. +/// Drives the key's encryptor in the inline `ciphertext || tag` layout (the inherited +/// [`SymmetricCipherEncryptor::do_encrypt_final_out`]). With no nonce given one is drawn from the +/// OS-backed DRBG and written ahead of the stream; a caller-supplied nonce is not written, since +/// the caller has it. fn aead128_encrypt_stream( - key: &KeyMaterial<16>, + key: &AsconAEAD128Key, nonce: Option<&[u8; 16]>, ad_opt: Option<&[u8]>, output_hex: bool, ) { - let start = match nonce { - Some(nonce) => { - Ascon_AEAD128::::do_encrypt_init_nonce(key, nonce).map(|c| (c, None)) - } - None => Ascon_AEAD128::::do_encrypt_init(key).map(|(c, n)| (c, Some(n))), + let (mut cipher, generated_nonce) = match nonce { + Some(nonce) => (key.create_encryptor_nonce(nonce), None), + None => key + .create_encryptor(&mut bouncycastle::rng::HashDRBG_SHA512::new_from_os()) + .map(|(c, n)| (c, Some(n))) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't start encryption: {e:?}"); + exit(-1); + }), }; - let (mut cipher, generated_nonce) = start.unwrap_or_else(|e| { - eprintln!("Error: couldn't start encryption: {e:?}"); - exit(-1); - }); if let Some(ad) = ad_opt { // infallible: `do_update_aad` only refuses AAD once plaintext has been fed in, and none // has been yet. @@ -193,11 +197,11 @@ fn aead128_encrypt_stream( } /// Decrypts a stream whose final 16 bytes are the tag, which is only known once EOF is reached. -/// [`Ascon_AEAD128`] holds the last 16 bytes it has seen back itself, releasing everything +/// The key's decryptor holds the last 16 bytes it has seen back itself, releasing everything /// before them as soon as it is known not to be part of the tag; at EOF /// [`SymmetricCipherDecryptor::do_decrypt_final`] checks what it held back as the tag. fn aead128_decrypt_stream( - key: &KeyMaterial<16>, + key: &AsconAEAD128Key, nonce: Option<&[u8; 16]>, ad_opt: Option<&[u8]>, output_hex: bool, @@ -218,11 +222,7 @@ fn aead128_decrypt_stream( } }; - let mut cipher = - Ascon_AEAD128::::do_decrypt_init(key, &nonce).unwrap_or_else(|e| { - eprintln!("Error: couldn't start decryption: {e:?}"); - exit(-1); - }); + let mut cipher = key.create_decryptor(&nonce); if let Some(ad) = ad_opt { // infallible: as on the encrypt side, no ciphertext has been fed in yet. cipher.do_update_aad(ad).unwrap(); diff --git a/cli/src/helpers/aead_cipher_helpers.rs b/cli/src/helpers/aead_cipher_helpers.rs index e54b36fb8..c4bc5769c 100644 --- a/cli/src/helpers/aead_cipher_helpers.rs +++ b/cli/src/helpers/aead_cipher_helpers.rs @@ -1,10 +1,10 @@ //! Shared plumbing for the AEAD subcommands: `aes{128,192,256}-gcm`. //! -//! Parallel to [`crate::helpers::stream_mode_helpers`], but for [`bouncycastle::cipher::modes::Gcm`] rather than a -//! [`StreamCipherEncryptor`](bouncycastle::core::traits::StreamCipherEncryptor) mode: GCM carries -//! additional authenticated data and a tag, neither of which that trait has room for, so this -//! module drives it through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] instead, which add -//! `do_update_aad` to the symmetric-cipher streaming methods. +//! Parallel to [`crate::helpers::stream_mode_helpers`], but for a key bound to +//! [`bouncycastle::cipher::modes::GCM`] rather than a stream mode: GCM carries additional +//! authenticated data and a tag, neither of which the stream traits have room for, so this module +//! drives the key's AEAD workers through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], which +//! add `do_update_aad` to the symmetric-cipher streaming methods. //! //! # On-the-wire format: `IV || ciphertext || tag` //! @@ -39,8 +39,10 @@ //! other GCM implementation given the same file. use crate::helpers::{flush_stdout, read_from_file_raw, write_bytes_or_hex, write_stdout}; -use bouncycastle::core::key_material::KeyMaterial; -use bouncycastle::core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; +use bouncycastle::core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; use bouncycastle::hex; use std::io; use std::io::{Read, Write}; @@ -72,25 +74,27 @@ pub fn load_aad(aad: &Option, aad_file: &Option) -> Vec { /// Encrypts stdin to stdout under GCM: writes the generated IV (unless `nonce` supplies one), /// then the ciphertext as it streams, then the tag. -pub fn encrypt_gcm( - key: &KeyMaterial, +pub fn encrypt_gcm( + key: &K, nonce: Option<[u8; GCM_NONCE_LEN]>, aad: &[u8], output_hex: bool, ) where - E: AEADCipherEncryptor, + K: AeadEncryptionKey, { let mut enc = match nonce { - Some(nonce) => E::do_encrypt_init_nonce(key, &nonce), - None => E::do_encrypt_init(key).map(|(enc, nonce)| { + Some(nonce) => key.create_encryptor_nonce(&nonce), + None => { + let (enc, nonce) = key + .create_encryptor(&mut bouncycastle::rng::HashDRBG_SHA512::new_from_os()) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't start encryption: {e:?}"); + exit(-1); + }); write_bytes_or_hex(&nonce, output_hex); enc - }), - } - .unwrap_or_else(|e| { - eprintln!("Error: couldn't start encryption: {e:?}"); - exit(-1); - }); + } + }; enc.do_update_aad(aad).unwrap_or_else(|e| { eprintln!("Error: couldn't absorb the additional authenticated data: {e:?}"); @@ -116,7 +120,7 @@ pub fn encrypt_gcm( } // The detached final flushes nothing for GCM and returns the tag, written last. - let (_, _, tag) = enc.do_encrypt_final_detached().unwrap_or_else(|e| { + let (_, tag) = enc.do_encrypt_final_detached_out(&mut [0u8; TAG_LEN]).unwrap_or_else(|e| { eprintln!("Error: encryption failed: {e:?}"); exit(-1); }); @@ -128,13 +132,13 @@ pub fn encrypt_gcm( /// first, streams the rest through the inline decryptor, and checks the tag on /// `do_decrypt_final`. See the module docs for why plaintext may already be written to stdout by /// the time a tag failure is reported. -pub fn decrypt_gcm( - key: &KeyMaterial, +pub fn decrypt_gcm( + key: &K, nonce: Option<[u8; GCM_NONCE_LEN]>, aad: &[u8], output_hex: bool, ) where - D: AEADCipherDecryptor, + K: AeadDecryptionKey, { let nonce = nonce.unwrap_or_else(|| { let mut nonce = [0u8; GCM_NONCE_LEN]; @@ -148,10 +152,7 @@ pub fn decrypt_gcm( nonce }); - let mut dec = D::do_decrypt_init(key, &nonce).unwrap_or_else(|e| { - eprintln!("Error: couldn't start decryption: {e:?}"); - exit(-1); - }); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(aad).unwrap_or_else(|e| { eprintln!("Error: couldn't absorb the additional authenticated data: {e:?}"); exit(-1); diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index c68dcc85b..cc3b9ec71 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -3,8 +3,8 @@ //! Everything here is mode-independent -- key loading, stdin framing, block-alignment enforcement, //! output formatting -- and is generic over the mode via [`SymmetricCipherEncryptor`] / //! [`SymmetricCipherDecryptor`]. `aes_cbc_cmd` and `aes_ecb_cmd` are thin dispatchers over it, -//! instantiating it with the `AES_CBC_*` types under `PKCS7` and the `AES_ECB_*` types under -//! `NoPadding`, so the commands cannot drift apart on the parts that matter for correctness. +//! driving it with the workers their keys hand out, `CBC` and `ECB`, so the +//! commands cannot drift apart on the parts that matter for correctness. //! //! The CFB and CTR commands are stream ciphers and live in [`crate::helpers::stream_mode_helpers`] instead; //! they share @@ -28,9 +28,9 @@ //! Every mode with an IV or nonce (CBC, CFB, CFB8, CTR, GCM) also takes `--iv` / `--iv-file`, //! loaded by [`load_iv`], for a format that carries the IV itself and for reproducing published //! vectors. With it, `encrypt` uses the supplied value and writes **only the ciphertext**, and -//! `decrypt` takes it from the flag and reads **only ciphertext** from stdin. The modes have no -//! IV-taking constructor, so it reaches them through `do_encrypt_init_rng` and a [`FixedIv`] -//! that hands out exactly those bytes (GCM has `do_encrypt_init_nonce` and uses that). +//! `decrypt` takes it from the flag and reads **only ciphertext** from stdin. The keys' constructors +//! draw the IV from an RNG, so it reaches them through [`start_encryptor`] and a [`FixedIv`] that +//! hands out exactly those bytes (GCM keys have `create_encryptor_nonce` and use that). //! Unpredictability, and for CTR and GCM uniqueness, is then the caller's responsibility. CCM //! keeps its own `--nonce`, which is variable-length and was never generated. //! @@ -54,16 +54,18 @@ //! cat cipher.hex | bc-rust hex-decode | bc-rust aes256-cbc decrypt --key-file k.bin //! ``` -use crate::helpers; use crate::helpers::{ - flush_stdout, read_from_file, strip_trailing_newline, write_bytes_or_hex, write_stdout, + flush_stdout, read_from_file, read_from_file_raw, strip_trailing_newline, write_bytes_or_hex, + write_stdout, }; -use bouncycastle::core::errors::RNGError; +use bouncycastle::core::errors::{RNGError, SymmetricCipherError}; use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::core::security_strength::SecurityStrength; +use bouncycastle::core::symmetric_key::FromKeyMaterial; use bouncycastle::core::traits::{RNG, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; use bouncycastle::hex; +use bouncycastle::rng::HashDRBG_SHA512; use clap::ValueEnum; use std::io; use std::io::{Read, Write}; @@ -147,8 +149,21 @@ pub(crate) fn load_key( key } -/// An [`RNG`] that yields the caller's IV once, so `--iv` can reach a mode whose only -/// constructors are `do_encrypt_init` and `do_encrypt_init_rng`. A second draw, or one of the +/// The key object for a command: the material from `--key` / `--key-file`, checked and expanded +/// once. `K` is the key type the command's mode wants, `AES128Key>` and the like. +pub(crate) fn load_key_object, const KEY_LEN: usize>( + key: &Option, + key_file: &Option, + alg: &str, +) -> K { + K::from_key_material(&load_key::(key, key_file, alg)).unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }) +} + +/// An [`RNG`] that yields the caller's IV once, so `--iv` can reach a key whose only +/// constructor draws the IV from an RNG. A second draw, or one of the /// wrong length, is an error rather than a repeat: the IV must be consumed exactly as given. pub(crate) struct FixedIv { iv: [u8; N], @@ -205,7 +220,7 @@ pub(crate) fn load_iv( iv_file: &Option, ) -> Option<[u8; INIT_DATA_LEN]> { let bytes = if let Some(file) = iv_file { - let bytes = helpers::read_from_file_raw(file); + let bytes = read_from_file_raw(file); if bytes.last() == Some(&b'\n') { eprintln!( "Warning: IV file '{file}' ends with a newline byte (0x0a), which is used as part \ @@ -230,33 +245,44 @@ pub(crate) fn load_iv( })) } -/// Encrypts stdin to stdout under the mode `E`. With no `iv`, the init data (the IV) is generated -/// and written first; with one, it is used and the output is ciphertext only. -/// -/// `INIT_DATA_LEN` is the mode's: one block for CBC, 0 for ECB, in which case nothing is written -/// ahead of the ciphertext. `aligned_mode` is `Some(mode name)` for a mode that pads nothing, whose -/// input must therefore be a whole number of blocks; the name appears in the error message. -pub(crate) fn encrypt_stream( - key: &KeyMaterial, +/// Starts an encryptor from `create`, the key's constructor: under `iv` when one was supplied, +/// through a [`FixedIv`], and otherwise under a fresh IV from the OS-backed DRBG. A generated IV +/// comes back for the caller to write ahead of the ciphertext; a supplied one does not, since it +/// is the caller's to carry. +pub(crate) fn start_encryptor( iv: Option<[u8; INIT_DATA_LEN]>, - output_hex: bool, - aligned_mode: Option<&str>, -) where - E: SymmetricCipherEncryptor, -{ - let init = match iv { - Some(iv) => E::do_encrypt_init_rng(key, &mut FixedIv::new(iv)), - None => E::do_encrypt_init(key), + create: impl FnOnce(&mut dyn RNG) -> Result<(E, [u8; INIT_DATA_LEN]), SymmetricCipherError>, +) -> (E, Option<[u8; INIT_DATA_LEN]>) { + let started = match iv { + Some(iv) => create(&mut FixedIv::new(iv)).map(|(enc, _)| (enc, None)), + None => create(&mut HashDRBG_SHA512::new_from_os()).map(|(enc, iv)| (enc, Some(iv))), }; - let (mut enc, generated_iv) = init.unwrap_or_else(|e| { + started.unwrap_or_else(|e| { eprintln!("Error: couldn't start encryption: {e:?}"); exit(-1); - }); + }) +} - // A generated IV goes out ahead of the ciphertext, so `decrypt` can pick it up. (Empty for - // ECB.) A supplied one is the caller's to carry. - if INIT_DATA_LEN > 0 && iv.is_none() { - write_bytes_or_hex(&generated_iv, output_hex); +/// Encrypts stdin to stdout through `enc`, a worker the command's key has already made, writing +/// `generated_iv` first when there is one. +/// +/// `INIT_DATA_LEN` is the mode's: one block for CBC, 0 for ECB. `generated_iv` is `None` when the +/// IV was supplied with `--iv`, or when the mode has none, and then nothing is written ahead of +/// the ciphertext; see [`start_encryptor`]. `aligned_mode` names a mode whose input must be a +/// whole number of blocks, for the error that reports a partial one (ECB under `NoPadding`); it is +/// `None` for a padded mode, which takes any length. +pub(crate) fn encrypt_stream( + mut enc: E, + generated_iv: Option<&[u8; INIT_DATA_LEN]>, + output_hex: bool, + aligned_mode: Option<&str>, +) where + E: SymmetricCipherEncryptor, +{ + // A generated IV goes out ahead of the ciphertext, so `decrypt` can pick it up. A supplied + // one is the caller's to carry. (Nothing for ECB.) + if let Some(iv) = generated_iv { + write_bytes_or_hex(iv, output_hex); } let mut out = [0u8; CHUNK_LEN]; @@ -281,16 +307,16 @@ pub(crate) fn encrypt_stream( - key: &KeyMaterial, +/// Decrypts stdin to stdout through the worker `make_decryptor` gives for the init data (the IV): +/// `iv` if it was supplied, else the first `INIT_DATA_LEN` bytes of input -- one block for CBC, +/// nothing for ECB. `aligned_mode` is as for [`encrypt_stream`]. +pub(crate) fn decrypt_stream( iv: Option<[u8; INIT_DATA_LEN]>, + make_decryptor: impl FnOnce(&[u8; INIT_DATA_LEN]) -> D, output_hex: bool, aligned_mode: Option<&str>, ) where - D: SymmetricCipherDecryptor, + D: SymmetricCipherDecryptor, { let iv = iv.unwrap_or_else(|| { // The leading bytes are the IV, not ciphertext. (None for ECB: the read is skipped.) @@ -307,10 +333,7 @@ pub(crate) fn decrypt_stream( - key: &KeyMaterial, +pub(crate) fn encrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>( + key: &'k K, iv: Option<[u8; INIT_DATA_LEN]>, output_hex: bool, ) where - E: StreamCipherEncryptor, + K: SymmetricEncryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, { - let init = match iv { - Some(iv) => E::do_encrypt_init_rng(key, &mut FixedIv::new(iv)), - None => E::do_encrypt_init(key), - }; - let (mut enc, generated_iv) = init.unwrap_or_else(|e| { - eprintln!("Error: couldn't start encryption: {e:?}"); - exit(-1); - }); + let (mut enc, generated_iv) = start_encryptor(iv, |rng| key.create_encryptor(rng)); // A generated IV goes out ahead of the ciphertext, so `decrypt` can pick it up. A supplied // one is the caller's to carry. - if iv.is_none() { - write_bytes_or_hex(&generated_iv, output_hex); + if let Some(iv) = generated_iv { + write_bytes_or_hex(&iv, output_hex); } // The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out. @@ -77,14 +71,15 @@ pub(crate) fn encrypt_stream( - key: &KeyMaterial, +pub(crate) fn decrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>( + key: &'k K, iv: Option<[u8; INIT_DATA_LEN]>, output_hex: bool, ) where - D: StreamCipherDecryptor, + K: SymmetricDecryptionKey, + K::Decryptor<'k>: StreamCipherDecryptor, { let iv = iv.unwrap_or_else(|| { // The leading bytes are the IV, not ciphertext. @@ -99,10 +94,7 @@ pub(crate) fn decrypt_stream( +/// Runs one direction of a stream mode under its key. The `run` dispatchers in `aes_cfb_cmd`, +/// `aes_cfb8_cmd` and `aes_ctr_cmd` differ only in which key type they load, so the match lives +/// here. +pub(crate) fn run_stream_mode<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>( action: &CipherDirection, - key: &KeyMaterial, + key: &'k K, iv: Option<[u8; INIT_DATA_LEN]>, output_hex: bool, ) where - E: StreamCipherEncryptor, - D: StreamCipherDecryptor, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, { match action { CipherDirection::Encrypt => { - encrypt_stream::(key, iv, output_hex) + encrypt_stream::(key, iv, output_hex) } CipherDirection::Decrypt => { - decrypt_stream::(key, iv, output_hex) + decrypt_stream::(key, iv, output_hex) } } } From 16c3497210b7c7c22e4df7c38ad2b94114b34bbe Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 01:02:44 +1100 Subject: [PATCH 13/54] core: the cipher and MAC traits lose their constructors and key-taking one-shots (prototype) SymmetricCipherEncryptor / Decryptor, AEADCipherEncryptor / Decryptor, BlockCipher* and StreamCipher* now describe a worker that already exists: do_encrypt_init, do_encrypt_init_rng, do_encrypt_init_nonce, do_decrypt_init and every static one-shot over them are gone, and with them the KEY_LEN parameter, which was only ever the material's. MAC loses new and new_allow_weak_key. Construction is a key object's (create_encryptor / create_decryptor / create_generator) and the one-shots are the role traits' in core::symmetric_key; the temporary AeadEncryptWorker / AeadDecryptWorker traits fold back into the AEAD cipher traits, which the role and mode traits bound on directly. Downstream, the impls go from the cipher modes, the padding adapters, the stream wrapper, Ascon-AEAD128, HMAC, KMAC and CMAC; the CCM packet workers and GMAC's material route are rewritten over the key state; the test framework drops its E/D-generic runners and test_mac; the HMAC callers in the hmac, sha2, sha3 and sm3 crates (tests, benches, docs) move onto HmacKey; the last GCM and CCM test helpers in aes move onto the keys. Dropped with no key-path twin: GCM's verify-before-decrypt one-shot override (the provided one-shots zeroize on failure), and the comparisons against the worker constructors, which now compare against the key-state route or an RFC reference. QUALITY_AND_STYLE.md's API and naming rules say where construction lives, and CLAUDE.md records the lessons. quality_stats.sh, ./crypto, before and after the branch: unwraps in core code 827 -> 798, Err() in core code 474 -> 446, test ratio .95 -> .98. Assisted-by: Claude:claude-mythos-5-1 --- CLAUDE.md | 41 +- QUALITY_AND_STYLE.md | 24 +- crypto/aes/tests/ccm_key_tests.rs | 33 +- crypto/aes/tests/cfb8_bc-test-data.rs | 4 +- crypto/aes/tests/cfb_bc-test-data.rs | 4 +- crypto/aes/tests/common/acvp_gcm_helpers.rs | 85 +- crypto/aes/tests/ctr_bc-test-data.rs | 4 +- crypto/aes/tests/ctr_vector_tests.rs | 4 +- crypto/aes/tests/gcm_bc_java_tests.rs | 38 +- crypto/aes/tests/gcm_wycheproof.rs | 45 +- crypto/aes/tests/gmac_key_tests.rs | 16 +- crypto/aes/tests/key_tests.rs | 123 +- crypto/aes/tests/sp800_38a_cfb8_tests.rs | 4 +- crypto/aes/tests/sp800_38a_cfb_tests.rs | 4 +- crypto/ascon/src/ascon_aead128.rs | 151 +- crypto/cipher/src/modes/cbc.rs | 179 +-- crypto/cipher/src/modes/ccm.rs | 1289 +++++------------ crypto/cipher/src/modes/cfb.rs | 146 +- crypto/cipher/src/modes/cfb8.rs | 133 +- crypto/cipher/src/modes/cmac.rs | 29 +- crypto/cipher/src/modes/gcm.rs | 462 ++---- crypto/cipher/src/modes/gmac.rs | 31 +- crypto/cipher/src/modes/hazmat/ecb.rs | 164 +-- .../cipher/src/padding/padded_block_cipher.rs | 149 +- crypto/cipher/src/stream.rs | 124 +- crypto/cipher/tests/modes/cbc_tests.rs | 25 +- crypto/cipher/tests/modes/cfb8_tests.rs | 6 +- crypto/cipher/tests/modes/cfb_tests.rs | 8 +- crypto/cipher/tests/modes/ctr_tests.rs | 6 +- crypto/cipher/tests/modes/ecb_tests.rs | 67 +- .../tests/modes/symmetric_cipher_api_tests.rs | 8 +- crypto/core-test-framework/src/aead.rs | 1053 +------------- .../core-test-framework/src/block_cipher.rs | 207 +-- crypto/core-test-framework/src/mac.rs | 146 +- .../src/symmetric_ciphers.rs | 681 +-------- crypto/core/src/hazmat/ecb_key.rs | 4 +- crypto/core/src/hazmat/mode.rs | 25 +- crypto/core/src/provided.rs | 35 +- crypto/core/src/symmetric_key.rs | 138 +- crypto/core/src/traits.rs | 811 +---------- crypto/core/tests/aead_buffering_toy_tests.rs | 103 +- crypto/factory/src/mac_factory.rs | 10 - crypto/hmac/src/lib.rs | 33 +- crypto/hmac/tests/hmac_key_tests.rs | 48 +- crypto/hmac/tests/hmac_tests.rs | 324 +++-- crypto/hmac/tests/hmac_wycheproof.rs | 57 +- crypto/sha2/benches/hmac_sha2_benches.rs | 20 +- crypto/sha2/src/hkdf.rs | 4 +- crypto/sha2/src/hmac.rs | 130 +- crypto/sha3/benches/hmac_sha3_benches.rs | 20 +- crypto/sha3/src/hmac.rs | 128 +- crypto/sha3/src/kmac.rs | 27 +- crypto/sm3/benches/hmac_sm3_benches.rs | 13 +- crypto/sm3/src/hmac.rs | 15 +- .../src/bench_ascon_mem_usage.rs | 16 +- 55 files changed, 1850 insertions(+), 5604 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 908439d61..35484c1f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,10 +173,12 @@ taught, as mechanics to apply next time a cipher type is shaped or reshaped: struct under the spec-spelled name (`Ascon_AEAD128`, `#[allow(non_camel_case_types)]`) is the whole public type, with no CamelCase twin and no alias to it. - **The trait pair is the API.** Do not keep an inherent streaming or one-shot API beside the - `AEADCipher*` / `SymmetricCipher*` impls; everything a caller needs, including a supplied nonce - (`do_encrypt_init_nonce`, `encrypt_with_aad_nonce_out`), is on the traits. The only inherent - methods a cipher adds are for what the traits lack, in-place streaming (`do_*_inplace`), and - their decrypt form keeps the same tag hold-back as `do_decrypt_out` so the two interleave. + `AEADCipher*` / `SymmetricCipher*` impls; everything a caller needs is on the traits, and + everything that takes a key or draws a nonce, including a supplied nonce + (`create_encryptor_nonce`, `encrypt_with_aad_nonce_out`), is on the key object. The only + inherent methods a cipher adds are for what the traits lack, in-place streaming + (`do_*_inplace`), and their decrypt form keeps the same tag hold-back as `do_decrypt_out` so + the two interleave. - **Private helpers must not share a trait method's name.** An inherent `do_encrypt_final` with a different signature from the trait's is what forced the wrapper newtypes; name the core helpers for what they do (`absorb_aad`, `encrypt_bytes`, `finish_tag`, `verify_tag`). @@ -197,6 +199,37 @@ taught, as mechanics to apply next time a cipher type is shaped or reshaped: Ascon; the CCM packet type still has its namesake. Note the follow-up rather than widening the change. +## Lessons from the key-object rework (October 2026) + +The symmetric cipher and MAC traits lost their constructors and key-taking one-shots; keys took them. +What that taught, as mechanics for the next trait that sheds a constructor: + +- **The trait describes a worker that exists.** `SymmetricCipherEncryptor`, `AEADCipherEncryptor`, + `BlockCipherEncryptor`, `StreamCipherEncryptor`, their decrypting halves and `MAC` carry only + streaming methods and length helpers. Construction is a key object's (`create_encryptor(rng)`, + `create_decryptor(init)`, `create_generator()`), and the one-shots are provided on the role + traits in `core::symmetric_key` over those. `KEY_LEN` is on the role and mode traits, never on + a worker trait. +- **Mode types carry the role.** A key is `AESKey`; which role pairs it implements is + decided by `M`'s mode-trait impls in `core::hazmat::mode` (`SymmetricEncryptMode`, + `BlockEncryptMode`, `EcbEncryptMode`, `AeadEncryptMode`, `AeadPacketEncryptMode`, `MacMode`). + Padding and frame parameters live in the mode type (`CBC`, `CBC`, + `ECB`, `CTR<12>`, `CCMFrame<12, 16, 64, 1024>`), not on a worker alias. +- **A key that needs the permutation's key length cannot be generic over it.** `Mode

` cannot + carry `KEY_LEN` while `ElectronicCodeBook` does, so the GCM, GMAC and CMAC + mode impls are written per AES key length; the cipher crate's tests use local `ToyGCM` / + `ToyCMAC` marker types for the same reason. A `BlockPermutation` split is the open + follow-up. +- **Test the key, not the worker pair.** The framework runners are `test_key` over a role pair + and the construction suites in `core_test_framework::from_key_material`; a test of "a key of the + wrong type is rejected" belongs to the construction suite, once, not to each mode's tests. + Known-answer suites keep pinning their IV through `create_encryptor` and a `FixedSeedRNG`. +- **Resume still takes the material.** `SuspendableKeyed::Key` stays `KeyMaterial`; + resuming from a key object is a follow-up, not something to fold into the deletion. +- **What has no key-path twin is dropped, and said so.** `new_allow_weak_key` on a cipher-based + MAC, a wrong-length `KeyMaterial512` into a fixed-length key, GCM's verify-before-decrypt + one-shot override: each went with a line in the commit message rather than a shim. + ## Working from specifications QUALITY_AND_STYLE.md is where the requirement for spec-corresponding comments and justified deviations lives. This diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index 458177530..603a99634 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -88,7 +88,11 @@ In addition, some library-specific naming conventions: ## APIs Where possible, primitives should expose "one-shot APIs" that simply take data and return a result as a static member -function that does not require object instantiation. +function that does not require object instantiation. The exception is anything keyed: a symmetric cipher or MAC is +reached through a key object (`AES128Key`, `HmacKey`, `KMAC128Key`, `AsconAEAD128Key`), built once from +`KeyMaterial` by `FromKeyMaterial` / `FromAnyKeyMaterial`, and its one-shots and `create_*` constructors are methods on +that key. The cipher and MAC traits in `core::traits` describe a worker that already exists and carry no constructor +and no key-taking method, so a key's validation and expansion happen once, where the key is made, rather than per call. Other version of Bouncy Castle have a design pattern where stateful objects follow a pattern of new () -> init () -> do_update () -> do_final (), and then optionally reset () that sets the object back to an unitialized state. Instead, @@ -123,17 +127,21 @@ Take, for example a one-shot API `fn encrypt(plaintext: &[u8]) -> Result The following prefixes can be applied: * `do_`: this implies that it is part of a stateful streaming API, will typically take `&mut self`, and is likely - accompanied by a `do_encrypt_init()` and `do_encrypt_final()`. + accompanied by a `do_encrypt_final()`; the object comes from a key's `create_encryptor()`. +* `create_`: a key object's constructor for a worker -- `create_encryptor(&self, rng)`, `create_decryptor(&self, + init_data)`, `create_generator(&self)`, `create_verifier(&self)`. Whatever draws randomness takes a `&mut dyn RNG` + explicitly, so a key has no `_rng` twins and no method that reaches for a default RNG. The following suffixes can be applied -* `_init / _update / _final`: indicates phase of a stateful streaming API. Other verbs can be used here as appropriate - to the primitive, such as `absorb / squeeze`, `encrypt / decrypt`, etc. `_init` is typically a static constructor - (though exceptions may exist), and `_final` indicates that this function call renders the object unusable afterwards - by consuming `self` via a move: `_final(self, ..)`. +* `_update / _final`: indicates phase of a stateful streaming API. Other verbs can be used here as appropriate to the + primitive, such as `absorb / squeeze`, `encrypt / decrypt`, etc. `_final` indicates that this function call renders + the object unusable afterwards by consuming `self` via a move: `_final(self, ..)`. * `_rng`: indicates that this version of the function sources its random numbers from a provided `&mut dyn RNG` instead - of - from the default library RNG. `_rng(.., &mut dyn RNG)`. + of from the default library RNG. `_rng(.., &mut dyn RNG)`. Keyed primitives do not have it: their constructors and + one-shots take the RNG, full stop. +* `_nonce`: on an AEAD key, the twin that takes the nonce from the caller instead of drawing it from the RNG, and so + does not return one. * `_out / _inplace`: indicates that the function works in the provided buffer. `_out` indicates that the function takes an output buffer, which may be oversized, and returns the number of bytes written to it: `_out(.., out: &mut [u8] -> Result`. `_inplace` indicates that the input and output are diff --git a/crypto/aes/tests/ccm_key_tests.rs b/crypto/aes/tests/ccm_key_tests.rs index 4dca681ec..008edfd50 100644 --- a/crypto/aes/tests/ccm_key_tests.rs +++ b/crypto/aes/tests/ccm_key_tests.rs @@ -4,14 +4,17 @@ //! Only the worker traits are in scope here, since the pair's cipher-trait methods share their //! names. -use bouncycastle_aes::{AES_CCM_128, AES128Key, AES192Key}; -use bouncycastle_cipher::Encrypting; +use bouncycastle_aes::hazmat::AES128Internal; +use bouncycastle_aes::{AES128Key, AES192Key}; use bouncycastle_cipher::modes::CCM; use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::{AeadPacketEncryptMode, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{ - AeadDecryptWorker, AeadEncryptWorker, AeadPacketDecryptionKey, AeadPacketEncryptionKey, - FromKeyMaterial, + AeadPacketDecryptionKey, AeadPacketEncryptionKey, FromKeyMaterial, +}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; @@ -31,15 +34,12 @@ fn message(len: usize) -> Vec { (0..len).map(|i| (i * 31 % 251) as u8).collect() } -/// The pair's any-length `_nonce` one-shot, the reference for every form below. +/// The mode's in-place `_nonce` one-shot over the bare permutation (the hazmat mode-trait +/// route), the reference for every form below. fn reference(km: &KeyMaterial<16>, nonce: &[u8; 12], msg: &[u8]) -> (Vec, [u8; 16]) { - use bouncycastle_core::traits::AEADCipherEncryptor; - let mut ct = vec![0u8; msg.len()]; - let (n, tag) = AES_CCM_128::::encrypt_detached_nonce_out( - km, nonce, AAD, msg, &mut ct, - ) - .unwrap(); - assert_eq!(n, msg.len()); + let perm = AES128Internal::new(km).unwrap(); + let mut ct = msg.to_vec(); + let tag = CCM::<12, 16>::encrypt_detached_in_place_nonce(&perm, nonce, AAD, &mut ct).unwrap(); (ct, tag) } @@ -180,12 +180,9 @@ fn a_short_nonce_is_reachable_through_the_supplied_nonce_forms() { let mut buf = msg.clone(); let tag = key.encrypt_detached_in_place_nonce(&nonce, AAD, &mut buf).unwrap(); let (ct, tag_p) = { - use bouncycastle_core::traits::AEADCipherEncryptor; - let mut ct = vec![0u8; msg.len()]; - let (_, t) = AES_CCM_128::::encrypt_detached_nonce_out( - &km, &nonce, AAD, &msg, &mut ct, - ) - .unwrap(); + let perm = AES128Internal::new(&km).unwrap(); + let mut ct = msg.clone(); + let t = CCM::<8, 8>::encrypt_detached_in_place_nonce(&perm, &nonce, AAD, &mut ct).unwrap(); (ct, t) }; assert_eq!((ct, tag_p), (buf.clone(), tag)); diff --git a/crypto/aes/tests/cfb8_bc-test-data.rs b/crypto/aes/tests/cfb8_bc-test-data.rs index b54536c96..3f54f4a57 100644 --- a/crypto/aes/tests/cfb8_bc-test-data.rs +++ b/crypto/aes/tests/cfb8_bc-test-data.rs @@ -94,8 +94,8 @@ where K: FromKeyMaterial + SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); diff --git a/crypto/aes/tests/cfb_bc-test-data.rs b/crypto/aes/tests/cfb_bc-test-data.rs index 35be56106..a4704d66d 100644 --- a/crypto/aes/tests/cfb_bc-test-data.rs +++ b/crypto/aes/tests/cfb_bc-test-data.rs @@ -99,8 +99,8 @@ where K: FromKeyMaterial + SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); diff --git a/crypto/aes/tests/common/acvp_gcm_helpers.rs b/crypto/aes/tests/common/acvp_gcm_helpers.rs index 4dc660446..76badc75a 100644 --- a/crypto/aes/tests/common/acvp_gcm_helpers.rs +++ b/crypto/aes/tests/common/acvp_gcm_helpers.rs @@ -9,15 +9,12 @@ #![allow(dead_code)] -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Gcm; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::GCM; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::KeyMaterial; -use bouncycastle_core::traits::{ - AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, -}; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_core::traits::{AEADCipherDecryptor, SymmetricCipherDecryptor}; use bouncycastle_core_test_framework::FixedSeedRNG; /// The nonce length these vectors use; every group in the ACVP AES-GCM/GMAC sets has `ivLen = 96`. @@ -41,46 +38,42 @@ pub fn run_encrypt_case( expected_tag: &[u8], ) { macro_rules! dispatch { - ($p:ty, $klen:literal) => {{ + ($k:ident, $klen:literal) => {{ let key = cipher_key::<$klen>(key_bytes); let mut data = pt.to_vec(); match tag_len { - 12 => run_encrypt::<$p, $klen, 12>(&key, iv, aad, &mut data, expected_tag), - 13 => run_encrypt::<$p, $klen, 13>(&key, iv, aad, &mut data, expected_tag), - 14 => run_encrypt::<$p, $klen, 14>(&key, iv, aad, &mut data, expected_tag), - 15 => run_encrypt::<$p, $klen, 15>(&key, iv, aad, &mut data, expected_tag), - 16 => run_encrypt::<$p, $klen, 16>(&key, iv, aad, &mut data, expected_tag), + 12 => run_encrypt::<$k>, $klen, 12>(&key, iv, aad, &mut data, expected_tag), + 13 => run_encrypt::<$k>, $klen, 13>(&key, iv, aad, &mut data, expected_tag), + 14 => run_encrypt::<$k>, $klen, 14>(&key, iv, aad, &mut data, expected_tag), + 15 => run_encrypt::<$k>, $klen, 15>(&key, iv, aad, &mut data, expected_tag), + 16 => run_encrypt::<$k>, $klen, 16>(&key, iv, aad, &mut data, expected_tag), other => panic!("unsupported ACVP tagLen {other} bytes"), } assert_eq!(data, expected_ct); }}; } match key_bytes.len() { - 16 => dispatch!(AES128Internal, 16), - 24 => dispatch!(AES192Internal, 24), - 32 => dispatch!(AES256Internal, 32), + 16 => dispatch!(AES128Key, 16), + 24 => dispatch!(AES192Key, 24), + 32 => dispatch!(AES256Key, 32), other => panic!("unexpected AES key length {other}"), } } -fn run_encrypt( +fn run_encrypt( key: &KeyMaterial, iv: [u8; GCM_NONCE_LEN], aad: &[u8], data: &mut [u8], expected_tag: &[u8], ) where - P: bouncycastle_core::hazmat::ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + AeadEncryptionKey, { + let key = K::from_key_material(key).expect("a valid key"); let mut ct = vec![0u8; data.len()]; - let (got_iv, written, tag) = Gcm::::encrypt_detached_rng_out( - key, - &mut FixedSeedRNG::::new(iv), - aad, - data, - &mut ct, - ) - .expect("encrypt"); + let (written, got_iv, tag) = key + .encrypt_detached_out(&mut FixedSeedRNG::::new(iv), aad, data, &mut ct) + .expect("encrypt"); assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); assert_eq!(written, data.len(), "GCM ciphertext is as long as the plaintext"); assert_eq!(&tag[..], expected_tag, "tag mismatch"); @@ -99,27 +92,27 @@ pub fn run_decrypt_case( expected_pt: Option<&[u8]>, ) { macro_rules! dispatch { - ($p:ty, $klen:literal) => {{ + ($k:ident, $klen:literal) => {{ let key = cipher_key::<$klen>(key_bytes); match tag.len() { - 12 => run_decrypt::<$p, $klen, 12>(&key, iv, aad, ct, tag, expected_pt), - 13 => run_decrypt::<$p, $klen, 13>(&key, iv, aad, ct, tag, expected_pt), - 14 => run_decrypt::<$p, $klen, 14>(&key, iv, aad, ct, tag, expected_pt), - 15 => run_decrypt::<$p, $klen, 15>(&key, iv, aad, ct, tag, expected_pt), - 16 => run_decrypt::<$p, $klen, 16>(&key, iv, aad, ct, tag, expected_pt), + 12 => run_decrypt::<$k>, $klen, 12>(&key, iv, aad, ct, tag, expected_pt), + 13 => run_decrypt::<$k>, $klen, 13>(&key, iv, aad, ct, tag, expected_pt), + 14 => run_decrypt::<$k>, $klen, 14>(&key, iv, aad, ct, tag, expected_pt), + 15 => run_decrypt::<$k>, $klen, 15>(&key, iv, aad, ct, tag, expected_pt), + 16 => run_decrypt::<$k>, $klen, 16>(&key, iv, aad, ct, tag, expected_pt), other => panic!("unsupported ACVP tagLen {other} bytes"), } }}; } match key_bytes.len() { - 16 => dispatch!(AES128Internal, 16), - 24 => dispatch!(AES192Internal, 24), - 32 => dispatch!(AES256Internal, 32), + 16 => dispatch!(AES128Key, 16), + 24 => dispatch!(AES192Key, 24), + 32 => dispatch!(AES256Key, 32), other => panic!("unexpected AES key length {other}"), } } -fn run_decrypt( +fn run_decrypt( key: &KeyMaterial, iv: [u8; GCM_NONCE_LEN], aad: &[u8], @@ -127,24 +120,20 @@ fn run_decrypt( tag: &[u8], expected_pt: Option<&[u8]>, ) where - P: bouncycastle_core::hazmat::ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + AeadDecryptionKey, { + let key = K::from_key_material(key).expect("a valid key"); let tag_arr: [u8; TAG_LEN] = tag.try_into().expect("tag length matches TAG_LEN"); - // The detached one-shot: AAD-capable, and never releases plaintext before the tag checks out. + // The key's detached one-shot: AAD-capable, and its buffer is zeroized on a failed tag check. let mut data = vec![0xEEu8; ct.len()]; - let one_shot_result = Gcm::::decrypt_detached_out( - key, &iv, aad, ct, &tag_arr, &mut data, - ); + let one_shot_result = key.decrypt_detached_out(&iv, aad, ct, &tag_arr, &mut data); // The inline `SymmetricCipherDecryptor` streaming view, `ciphertext || tag` through - // `do_update_out`/`do_decrypt_final`, with AAD fed via `do_update_aad` first. Note this is - // *not* the AAD-less static `decrypt_out` one-shot (which has no AAD parameter at all, so it - // cannot run the cases that carry AAD, and most do): the streaming path is where the inline - // layout meets AAD support, and unlike the one-shot it releases plaintext before the tag is - // checked -- see `gcm_tests.rs` for that distinction pinned with empty AAD. - let mut dec = - Gcm::::do_decrypt_init(key, &iv).expect("decrypt init"); + // `do_update_out`/`do_decrypt_final`, with AAD fed via `do_update_aad` first; unlike the + // one-shot it releases plaintext before the tag is checked -- see `gcm_tests.rs` for that + // distinction pinned with empty AAD. + let mut dec = key.create_decryptor(&iv); dec.do_update_aad(aad).expect("aad"); let mut inline_ct = ct.to_vec(); inline_ct.extend_from_slice(tag); diff --git a/crypto/aes/tests/ctr_bc-test-data.rs b/crypto/aes/tests/ctr_bc-test-data.rs index be26dd0d5..9615083f9 100644 --- a/crypto/aes/tests/ctr_bc-test-data.rs +++ b/crypto/aes/tests/ctr_bc-test-data.rs @@ -99,8 +99,8 @@ where K: FromKeyMaterial + SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); diff --git a/crypto/aes/tests/ctr_vector_tests.rs b/crypto/aes/tests/ctr_vector_tests.rs index fe9d4071f..fd84238ca 100644 --- a/crypto/aes/tests/ctr_vector_tests.rs +++ b/crypto/aes/tests/ctr_vector_tests.rs @@ -102,8 +102,8 @@ where K: FromKeyMaterial + SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let nonce: [u8; NONCE_LEN] = unhex(NONCE).try_into().expect("a 12-byte nonce"); diff --git a/crypto/aes/tests/gcm_bc_java_tests.rs b/crypto/aes/tests/gcm_bc_java_tests.rs index 7c6703485..fabaa3f76 100644 --- a/crypto/aes/tests/gcm_bc_java_tests.rs +++ b/crypto/aes/tests/gcm_bc_java_tests.rs @@ -9,12 +9,10 @@ //! transcribed below, verified against the bc-java source read this session, with all-zero fields //! built programmatically rather than typed out (a zero key or plaintext cannot be mistyped). -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::Gcm; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::GCM; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -180,9 +178,11 @@ fn cases() -> Vec { ] } -fn run(case: &Case) +fn run(case: &Case) where - P: bouncycastle_core::hazmat::ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + + AeadEncryptionKey + + AeadDecryptionKey, { let key_bytes = hex::decode(&case.key).expect("valid hex key"); // `KeyMaterial` tags an all-zero buffer as `KeyType::Zeroized` regardless of the type @@ -208,15 +208,11 @@ where let expected_ct = hex::decode(&case.ct).expect("valid hex ct"); let expected_tag = hex::decode(case.tag).expect("valid hex tag"); + let key = K::from_key_material(&key).expect("a valid key"); let mut data = vec![0u8; pt.len()]; - let (got_iv, _, tag) = Gcm::::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::<12>::new(iv), - &aad, - &pt, - &mut data, - ) - .expect("encrypt"); + let (_, got_iv, tag) = key + .encrypt_detached_out(&mut FixedSeedRNG::<12>::new(iv), &aad, &pt, &mut data) + .expect("encrypt"); assert_eq!(got_iv, iv, "{}: the pinned RNG should reproduce the vector's IV", case.name); assert_eq!(data, expected_ct, "{}: ciphertext mismatch", case.name); @@ -224,10 +220,8 @@ where let tag_arr: [u8; 16] = expected_tag.try_into().expect("16-byte tag"); let mut recovered = vec![0u8; data.len()]; - Gcm::::decrypt_detached_out( - &key, &iv, &aad, &data, &tag_arr, &mut recovered, - ) - .unwrap_or_else(|e| panic!("{}: decrypt should have verified, got {e:?}", case.name)); + key.decrypt_detached_out(&iv, &aad, &data, &tag_arr, &mut recovered) + .unwrap_or_else(|e| panic!("{}: decrypt should have verified, got {e:?}", case.name)); assert_eq!(recovered, pt, "{}: decrypted plaintext mismatch", case.name); } @@ -237,9 +231,9 @@ fn bc_java_test_vectors_with_a_96_bit_iv() { for case in cases() { let key_len_bytes = case.key.len() / 2; match key_len_bytes { - 16 => run::(&case), - 24 => run::(&case), - 32 => run::(&case), + 16 => run::, 16>(&case), + 24 => run::, 24>(&case), + 32 => run::, 32>(&case), other => panic!("{}: unexpected key length {other} bytes", case.name), } checked += 1; diff --git a/crypto/aes/tests/gcm_wycheproof.rs b/crypto/aes/tests/gcm_wycheproof.rs index 2a02a1523..d200f67a2 100644 --- a/crypto/aes/tests/gcm_wycheproof.rs +++ b/crypto/aes/tests/gcm_wycheproof.rs @@ -19,10 +19,10 @@ //! # Ciphertext and tag are separate fields //! //! Wycheproof's AEAD schema (`aead_test_schema_v1`) carries `ct` and `tag` as distinct fields, so -//! these cases go through the detached pair, [`AEADCipherEncryptor::encrypt_detached_rng_out`] / -//! [`AEADCipherDecryptor::decrypt_detached_out`]. [`Gcm`] generates its own nonce, so the vector's -//! `iv` is supplied through a `FixedSeedRNG` and the returned nonce is asserted to be exactly that -//! IV, the same technique as `gcm_bc-test-data.rs`. +//! these cases go through the key's detached pair, `encrypt_detached_out` / +//! `decrypt_detached_out`. The key draws its own nonce, so the vector's `iv` is supplied through +//! a `FixedSeedRNG` and the returned nonce is asserted to be exactly that IV, the same technique +//! as `gcm_bc-test-data.rs`. //! //! # Only the 96-bit-IV groups can be dispatched to, by design //! @@ -36,16 +36,13 @@ //! 128-bit tag, which is within the `12..=16` bytes `Gcm` accepts, so the tag size never rules a //! case out. -use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::ModeNames; -use bouncycastle_cipher::modes::{GCM_NONCE_LEN, Gcm}; -use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; @@ -74,7 +71,7 @@ fn cipher_key(bytes: &[u8]) -> KeyMaterial { /// reason to reproduce a deliberately corrupted tag -- and it must fail the tag check rather than /// return a payload, leaving the caller's buffer zeroized as the trait contract requires. #[allow(clippy::too_many_arguments)] -fn run_case( +fn run_case( tc_id: u64, key_bytes: &[u8], iv: [u8; GCM_NONCE_LEN], @@ -84,22 +81,18 @@ fn run_case( expected_tag: &[u8], valid: bool, ) where - P: ElectronicCodeBook + ModeNames, + K: FromKeyMaterial + + AeadEncryptionKey + + AeadDecryptionKey, { - let key = cipher_key::(key_bytes); + let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a wycheproof key"); let tag: [u8; TAG_LEN] = expected_tag.try_into().unwrap_or_else(|_| panic!("tcId {tc_id}: bad tag length")); if valid { let mut ct = vec![0u8; msg.len()]; - let (got_iv, written, got_tag) = - Gcm::::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::::new(iv), - aad, - msg, - &mut ct, - ) + let (written, got_iv, got_tag) = key + .encrypt_detached_out(&mut FixedSeedRNG::::new(iv), aad, msg, &mut ct) .unwrap_or_else(|e| panic!("tcId {tc_id}: valid case failed to encrypt: {e:?}")); assert_eq!(got_iv, iv, "tcId {tc_id}: the seeded RNG must reproduce the vector's IV"); assert_eq!(written, msg.len(), "tcId {tc_id}: encrypt_detached writes exactly msg.len()"); @@ -110,9 +103,7 @@ fn run_case( // Pre-filled with a non-zero byte, so a buffer the decryptor never wrote is not mistaken for // one it zeroized. let mut plaintext = vec![0xEEu8; expected_ct.len()]; - match Gcm::::decrypt_detached_out( - &key, &iv, aad, expected_ct, &tag, &mut plaintext, - ) { + match key.decrypt_detached_out(&iv, aad, expected_ct, &tag, &mut plaintext) { Ok(n) => { assert!(valid, "tcId {tc_id}: an invalid vector decrypted and verified anyway"); plaintext.truncate(n); @@ -152,13 +143,13 @@ fn dispatch( return false; } match key_bytes.len() { - 16 => run_case::<16, 16, AES128Internal>( + 16 => run_case::<16, 16, AES128Key>( tc_id, key_bytes, iv, aad, msg, expected_ct, expected_tag, valid, ), - 24 => run_case::<24, 16, AES192Internal>( + 24 => run_case::<24, 16, AES192Key>( tc_id, key_bytes, iv, aad, msg, expected_ct, expected_tag, valid, ), - 32 => run_case::<32, 16, AES256Internal>( + 32 => run_case::<32, 16, AES256Key>( tc_id, key_bytes, iv, aad, msg, expected_ct, expected_tag, valid, ), _ => return false, diff --git a/crypto/aes/tests/gmac_key_tests.rs b/crypto/aes/tests/gmac_key_tests.rs index 9505b85d8..9bc0f6e18 100644 --- a/crypto/aes/tests/gmac_key_tests.rs +++ b/crypto/aes/tests/gmac_key_tests.rs @@ -10,8 +10,7 @@ use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::symmetric_key::FromKeyMaterial; -use bouncycastle_core::traits::AEADCipherEncryptor; +use bouncycastle_core::symmetric_key::{AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core_test_framework::FixedSeedRNG; const NONCE: [u8; GCM_NONCE_LEN] = [0x5A; GCM_NONCE_LEN]; @@ -61,14 +60,11 @@ fn the_key_and_the_other_routes_agree_at_every_key_length() { agree!(AES256Key, AES_GMAC_256, 32, 0x33); // And GCM over the same bytes as a cipher key, with the message as AAD and no payload. - let (nonce, _, from_gcm) = AES_GCM_128::::encrypt_detached_rng_out( - &material::<16>(0x11, KeyType::SymmetricCipherKey), - &mut rng(), - MSG, - &[], - &mut [], - ) - .unwrap(); + let (_, nonce, from_gcm) = + AES128Key::::from_key_material(&material::<16>(0x11, KeyType::SymmetricCipherKey)) + .unwrap() + .encrypt_detached_out(&mut rng(), MSG, &[], &mut []) + .unwrap(); assert_eq!((nonce, from_gcm), (NONCE, tag128)); } diff --git a/crypto/aes/tests/key_tests.rs b/crypto/aes/tests/key_tests.rs index 45a1fe80c..d733f555a 100644 --- a/crypto/aes/tests/key_tests.rs +++ b/crypto/aes/tests/key_tests.rs @@ -1,11 +1,13 @@ //! The mode-bound AES key objects: validation at construction, and `key.create_encryptor(rng)` -//! producing exactly what the mode's own `do_encrypt_init_rng` produces from the same material +//! producing exactly what the mode's worker built from the bare key state produces from the same material //! and RNG. -use bouncycastle_aes::{AES_GCM_128, AES_GCM_192, AES_GCM_256, AES128Key, AES192Key, AES256Key}; -use bouncycastle_cipher::modes::GCM; +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_aes::{AES_GCM_128, AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::{GCM, Gcm, GcmKeyState, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; @@ -14,12 +16,6 @@ use bouncycastle_core::traits::{ }; use bouncycastle_core_test_framework::FixedSeedRNG; -/// The worker traits, kept out of scope: a GCM worker implements both them and the cipher -/// traits under the same method names, so the generic helpers name them by path. -mod w { - pub use bouncycastle_core::symmetric_key::{AeadDecryptWorker, AeadEncryptWorker}; -} - fn material() -> KeyMaterial { let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") @@ -38,11 +34,10 @@ where K: AeadEncryptionKey, { let (mut enc, nonce) = key.create_encryptor(&mut rng()).expect("nonce draw"); - w::AeadEncryptWorker::do_update_aad(&mut enc, AAD).unwrap(); + enc.do_update_aad(AAD).unwrap(); let mut ct = vec![0u8; MSG.len()]; - w::AeadEncryptWorker::do_encrypt_out(&mut enc, MSG, &mut ct).unwrap(); - let (_, tag) = - w::AeadEncryptWorker::do_encrypt_final_detached_out(enc, &mut [0u8; 16]).unwrap(); + enc.do_encrypt_out(MSG, &mut ct).unwrap(); + let (_, tag) = enc.do_encrypt_final_detached_out(&mut [0u8; 16]).unwrap(); (nonce, ct, tag) } @@ -59,11 +54,11 @@ where { // Decryption setup is infallible: no Result to unwrap. let mut dec = key.create_decryptor(nonce); - w::AeadDecryptWorker::do_update_aad(&mut dec, AAD)?; + dec.do_update_aad(AAD)?; let mut pt = vec![0u8; ct.len() + 16]; - let n = w::AeadDecryptWorker::do_decrypt_out(&mut dec, ct, &mut pt)?; + let n = dec.do_decrypt_out(ct, &mut pt)?; let mut last = [0u8; 16]; - let m = w::AeadDecryptWorker::do_decrypt_final_detached_out(dec, tag, &mut last)?; + let m = dec.do_decrypt_final_detached_out(tag, &mut last)?; Ok([&pt[..n], &last[..m]].concat()) } @@ -93,28 +88,31 @@ fn the_key_type_a_mode_requires_is_the_constructor_constant() { } /// The key-side encryptor is the mode's own type built from the same material, so with the same -/// RNG it must produce the same nonce, ciphertext and tag as the static constructor. +/// RNG it must produce the same nonce, ciphertext and tag as the worker built from the bare key +/// state (the `from_key_state` route a mode impl uses). #[test] -fn create_encryptor_matches_do_encrypt_init_rng() { +fn create_encryptor_matches_the_key_state_route() { let km = material::<32>(); let key = AES256Key::::from_key_material(&km).expect("a valid key"); + let state = GcmKeyState::new(AES256Internal::new(&km).unwrap()); - let (mut enc_static, nonce_static) = - AES_GCM_256::::do_encrypt_init_rng(&km, &mut rng()).expect("static init"); + let (mut enc_state, nonce_state) = + Gcm::::from_key_state(&state, &mut rng()) + .expect("key-state init"); let (mut enc_key, nonce_key) = key.create_encryptor(&mut rng()).expect("key-side init"); - assert_eq!(nonce_static, nonce_key); + assert_eq!(nonce_state, nonce_key); - let mut ct_static = vec![0u8; MSG.len()]; + let mut ct_state = vec![0u8; MSG.len()]; let mut ct_key = vec![0u8; MSG.len()]; - enc_static.do_update_aad(AAD).unwrap(); + enc_state.do_update_aad(AAD).unwrap(); enc_key.do_update_aad(AAD).unwrap(); - enc_static.do_encrypt_out(MSG, &mut ct_static).unwrap(); + enc_state.do_encrypt_out(MSG, &mut ct_state).unwrap(); enc_key.do_encrypt_out(MSG, &mut ct_key).unwrap(); - let (_, _, tag_static) = enc_static.do_encrypt_final_detached().unwrap(); + let (_, _, tag_state) = enc_state.do_encrypt_final_detached().unwrap(); let (_, _, tag_key) = enc_key.do_encrypt_final_detached().unwrap(); - assert_eq!(ct_static, ct_key); - assert_eq!(tag_static, tag_key); + assert_eq!(ct_state, ct_key); + assert_eq!(tag_state, tag_key); } #[test] @@ -134,35 +132,33 @@ fn create_decryptor_round_trips_and_detects_tampering() { } /// The one-shots on the role traits are written over `create_encryptor` and the streaming -/// methods, so they must match the cipher traits' key-taking one-shots byte for byte. +/// methods, so they must match the message streamed by hand byte for byte, in both layouts. #[test] -fn key_one_shots_match_the_static_one_shots() { +fn key_one_shots_match_the_streaming_methods() { let km = material::<16>(); let key = AES128Key::::from_key_material(&km).expect("a valid key"); - type E = AES_GCM_128; - type D = AES_GCM_128; + type E<'k> = AES_GCM_128<'k, Encrypting>; + type D<'k> = AES_GCM_128<'k, Decrypting>; + + // The reference: streamed by hand, detached, under the pinned nonce. + let (nonce_s, ct_s, tag_s) = encrypt_via_key::<16, _>(&key); + let mut inline_s = ct_s.clone(); + inline_s.extend_from_slice(&tag_s); // Inline tag. - let mut ct_static = vec![0u8; E::encrypt_out_len(MSG.len())]; - let (nonce_s, n_s) = - E::encrypt_with_aad_rng_out(&km, &mut rng(), AAD, MSG, &mut ct_static).unwrap(); let mut ct_key = vec![0u8; E::encrypt_out_len(MSG.len())]; let (n_k, nonce_k) = key.encrypt_with_aad_out(&mut rng(), AAD, MSG, &mut ct_key).unwrap(); - assert_eq!((nonce_s, n_s), (nonce_k, n_k)); - assert_eq!(ct_static, ct_key); + assert_eq!((nonce_s, inline_s.len()), (nonce_k, n_k)); + assert_eq!(inline_s, ct_key); let mut pt = vec![0u8; D::decrypt_out_len(ct_key.len())]; let m = key.decrypt_with_aad_out(&nonce_k, AAD, &ct_key, &mut pt).unwrap(); assert_eq!(&pt[..m], MSG); - assert_eq!(D::decrypt_with_aad(&km, &nonce_k, AAD, &ct_key).unwrap(), MSG); assert_eq!(key.decrypt_with_aad(&nonce_k, AAD, &ct_key).unwrap(), MSG); // Detached tag. - let mut ct_s = vec![0u8; E::encrypt_detached_out_len(MSG.len())]; - let (nonce_s, n_s, tag_s) = - E::encrypt_detached_rng_out(&km, &mut rng(), AAD, MSG, &mut ct_s).unwrap(); let (nonce_k, ct_k, tag_k) = key.encrypt_detached(&mut rng(), AAD, MSG).unwrap(); - assert_eq!((nonce_s, n_s), (nonce_k, ct_k.len())); + assert_eq!((nonce_s, ct_s.len()), (nonce_k, ct_k.len())); assert_eq!((ct_s, tag_s), (ct_k.clone(), tag_k)); let mut pt = vec![0u8; D::decrypt_detached_out_len(ct_k.len())]; @@ -188,34 +184,46 @@ fn key_one_shots_match_the_static_one_shots() { )); } -/// The decryptor built from the key accepts what the static encryptor produced, and vice versa, -/// at every key length: one more check that the two paths are the same cipher. +/// The decryptor built from the key accepts what a worker built from the bare key state +/// produced, and vice versa, at every key length: one more check that the two routes are the +/// same cipher. #[test] -fn key_side_and_static_paths_interoperate_at_every_key_length() { - fn check(key: &K) +fn the_key_and_the_key_state_route_interoperate_at_every_key_length() { + fn check(key: &K) where + P: ElectronicCodeBook + ModeNames + Clone, K: AeadEncryptionKey + AeadDecryptionKey, - E: AEADCipherEncryptor, - D: AEADCipherDecryptor, { - let km = material::(); - - let (nonce, ct, tag) = E::encrypt_detached(&km, AAD, MSG).expect("static one-shot"); + let state = GcmKeyState::new(P::new(&material::()).unwrap()); + + let (mut enc, nonce) = + Gcm::::from_key_state(&state, &mut rng()).unwrap(); + enc.do_update_aad(AAD).unwrap(); + let mut ct = vec![0u8; MSG.len()]; + enc.do_encrypt_out(MSG, &mut ct).unwrap(); + let (_, _, tag) = enc.do_encrypt_final_detached().unwrap(); let pt = decrypt_via_key::(key, &nonce, &ct, &tag).expect("a valid tag"); assert_eq!(pt, MSG); let (nonce, ct, tag) = encrypt_via_key::(key); - let pt = D::decrypt_detached(&km, &nonce, AAD, &ct, &tag).expect("static one-shot"); + let mut dec = Gcm::::from_key_state(&state, &nonce); + dec.do_update_aad(AAD).unwrap(); + let mut pt = vec![0u8; ct.len()]; + let n = dec.do_decrypt_out(&ct, &mut pt).unwrap(); + let mut last = [0u8; 16]; + let m = dec.do_decrypt_final_detached_out(&tag, &mut last).unwrap(); + pt.truncate(n); + pt.extend_from_slice(&last[..m]); assert_eq!(pt, MSG); } - check::<16, _, AES_GCM_128, AES_GCM_128>( + check::<16, AES128Internal, _>( &AES128Key::::from_key_material(&material::<16>()).unwrap(), ); - check::<24, _, AES_GCM_192, AES_GCM_192>( + check::<24, AES192Internal, _>( &AES192Key::::from_key_material(&material::<24>()).unwrap(), ); - check::<32, _, AES_GCM_256, AES_GCM_256>( + check::<32, AES256Internal, _>( &AES256Key::::from_key_material(&material::<32>()).unwrap(), ); } @@ -365,11 +373,10 @@ fn supplied_nonce_forms_match_the_drawing_forms() { assert_eq!(buf, ct); let mut enc = key.create_encryptor_nonce(&nonce); - w::AeadEncryptWorker::do_update_aad(&mut enc, AAD).unwrap(); + enc.do_update_aad(AAD).unwrap(); let mut streamed = vec![0u8; msg.len()]; - let n = w::AeadEncryptWorker::do_encrypt_out(&mut enc, msg, &mut streamed).unwrap(); - let (_, tag_s) = - w::AeadEncryptWorker::do_encrypt_final_detached_out(enc, &mut [0u8; 16]).unwrap(); + let n = enc.do_encrypt_out(msg, &mut streamed).unwrap(); + let (_, tag_s) = enc.do_encrypt_final_detached_out(&mut [0u8; 16]).unwrap(); assert_eq!((&streamed[..n], tag_s), (&ct[..], tag)); assert_eq!(key.decrypt_detached(&nonce, AAD, &ct, &tag).unwrap(), msg); diff --git a/crypto/aes/tests/sp800_38a_cfb8_tests.rs b/crypto/aes/tests/sp800_38a_cfb8_tests.rs index 185436de3..78b637226 100644 --- a/crypto/aes/tests/sp800_38a_cfb8_tests.rs +++ b/crypto/aes/tests/sp800_38a_cfb8_tests.rs @@ -137,7 +137,7 @@ const CHUNKINGS: [usize; 7] = [1, 3, 4, 8, 9, 17, 18]; fn check_encrypt(section: &str, key_hex: &str, expected_hex: &str) where K: FromKeyMaterial + SymmetricEncryptionKey, - K::Encryptor: StreamCipherEncryptor, + K::Encryptor: StreamCipherEncryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); @@ -162,7 +162,7 @@ where fn check_decrypt(section: &str, key_hex: &str, ciphertext_hex: &str) where K: FromKeyMaterial + SymmetricDecryptionKey, - K::Decryptor: StreamCipherDecryptor, + K::Decryptor: StreamCipherDecryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); diff --git a/crypto/aes/tests/sp800_38a_cfb_tests.rs b/crypto/aes/tests/sp800_38a_cfb_tests.rs index e956aaabb..d17b10587 100644 --- a/crypto/aes/tests/sp800_38a_cfb_tests.rs +++ b/crypto/aes/tests/sp800_38a_cfb_tests.rs @@ -138,7 +138,7 @@ const ODD_CHUNKS: [usize; 3] = [5, 23, 63]; fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) where K: FromKeyMaterial + SymmetricEncryptionKey, - K::Encryptor: StreamCipherEncryptor, + K::Encryptor: StreamCipherEncryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); @@ -184,7 +184,7 @@ where fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) where K: FromKeyMaterial + SymmetricDecryptionKey, - K::Decryptor: StreamCipherDecryptor, + K::Decryptor: StreamCipherDecryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 50998b1ea..0fc3fbd2e 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -27,7 +27,6 @@ use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_utils::ct::ct_eq_bytes; use bouncycastle_utils::secret::Secret; use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; @@ -173,19 +172,6 @@ impl

Clone for Ascon_AEAD128 { } } -/// Draw a fresh, unique 128-bit nonce from the library's default OS-seeded DRBG. -/// -/// The one-shot APIs of main's cipher framework generate the init data / nonce internally, so -/// Ascon's per-encryption nonce-uniqueness requirement (SP 800-232 R3) is satisfied by sourcing -/// each nonce from a CSPRNG. A caller who manages its own nonces uses -/// [`AEADCipherEncryptor::do_encrypt_init_nonce`] instead. -fn fresh_nonce() -> Result<[u8; NONCE_LEN], SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - let mut nonce = [0u8; NONCE_LEN]; - rng.next_bytes_out(&mut nonce)?; - Ok(nonce) -} - impl Ascon_AEAD128 { /// Validate a [`KeyMaterial`] for use with Ascon-AEAD128 and return its key words. /// The key must be tagged as a [`KeyType::SymmetricCipherKey`] and carry at least the @@ -210,16 +196,6 @@ impl Ascon_AEAD128 { Ok([load_u64_le(bytes, 0), load_u64_le(bytes, 8)]) } - /// SP 800-232 Sec 4.1.1 / 4.1.2 step 1, the same in both directions: validates `key` as a - /// [`KeyType::SymmetricCipherKey`] of at least 128-bit strength and initialises the state - /// under `nonce`. The direction is whatever type this is called on. - fn new( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - ) -> Result { - Ok(Self::from_key_words(&Self::checked_key(key)?, nonce)) - } - /// Step 1 from already-validated key words: what [`AsconAEAD128Key`] calls per message. fn from_key_words(key_words: &[u64; 2], nonce: &[u8; NONCE_LEN]) -> Self { let mut key_secret: Secret<[u64; 2]> = Secret::new(); @@ -455,23 +431,7 @@ impl Algorithm for Ascon_AEAD128 { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } -impl SymmetricCipherEncryptor for Ascon_AEAD128 { - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { - let nonce = fresh_nonce()?; - Ok((Self::new(key, &nonce)?, nonce)) - } - - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { - let mut nonce = [0u8; NONCE_LEN]; - rng.next_bytes_out(&mut nonce)?; - Ok((Self::new(key, &nonce)?, nonce)) - } - +impl SymmetricCipherEncryptor for Ascon_AEAD128 { /// Ascon-AEAD128 never buffers: every byte given is a byte returned. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -503,18 +463,11 @@ impl SymmetricCipherEncryptor for Ascon_AEAD128 for Ascon_AEAD128 { +impl AEADCipherEncryptor for Ascon_AEAD128 { fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { self.absorb_aad(aad) } - fn do_encrypt_init_nonce( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - ) -> Result { - Self::new(key, nonce) - } - /// Nothing is ever held back to flush, so `ciphertext` is left zeroed. fn do_encrypt_final_detached_out( self, @@ -525,14 +478,7 @@ impl AEADCipherEncryptor for Ascon_AEAD128 } } -impl SymmetricCipherDecryptor for Ascon_AEAD128 { - fn do_decrypt_init( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - ) -> Result { - Self::new(key, nonce) - } - +impl SymmetricCipherDecryptor for Ascon_AEAD128 { /// Everything but the last `TAG_LEN` bytes seen so far is released. fn do_decrypt_out_len(&self, input_len: usize) -> usize { (self.held_len + input_len).saturating_sub(TAG_LEN) @@ -588,7 +534,7 @@ impl SymmetricCipherDecryptor for Ascon_AEAD128 for Ascon_AEAD128 { +impl AEADCipherDecryptor for Ascon_AEAD128 { /// # Errors /// [`SymmetricCipherError::StateError`] if `aad` is non-empty and /// [`SymmetricCipherDecryptor::do_decrypt_out`] has already been called. @@ -734,95 +680,6 @@ impl AeadDecryptionKey for AsconAEAD128Key } } -// The worker traits the key's role traits are bounded on, forwarded to the cipher traits by -// path; they fold into those traits once the trait constructors go. -impl bouncycastle_core::symmetric_key::AeadEncryptWorker - for Ascon_AEAD128 -{ - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - AEADCipherEncryptor::do_update_aad(self, aad) - } - - fn do_encrypt_out_len(&self, input_len: usize) -> usize { - SymmetricCipherEncryptor::do_encrypt_out_len(self, input_len) - } - - fn do_encrypt_out( - &mut self, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result { - SymmetricCipherEncryptor::do_encrypt_out(self, plaintext, ciphertext) - } - - fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - SymmetricCipherEncryptor::do_encrypt_final(self) - } - - fn do_encrypt_final_detached_out( - self, - ciphertext: &mut [u8; TAG_LEN], - ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { - AEADCipherEncryptor::do_encrypt_final_detached_out(self, ciphertext) - } - - fn encrypt_out_len(plaintext_len: usize) -> usize { - >::encrypt_out_len( - plaintext_len, - ) - } - - fn encrypt_detached_out_len(plaintext_len: usize) -> usize { - >::encrypt_detached_out_len( - plaintext_len, - ) - } -} - -impl bouncycastle_core::symmetric_key::AeadDecryptWorker - for Ascon_AEAD128 -{ - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - AEADCipherDecryptor::do_update_aad(self, aad) - } - - fn do_decrypt_out_len(&self, input_len: usize) -> usize { - SymmetricCipherDecryptor::do_decrypt_out_len(self, input_len) - } - - fn do_decrypt_out( - &mut self, - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - SymmetricCipherDecryptor::do_decrypt_out(self, ciphertext, plaintext) - } - - fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - SymmetricCipherDecryptor::do_decrypt_final(self) - } - - fn do_decrypt_final_detached_out( - self, - tag: &[u8; TAG_LEN], - plaintext: &mut [u8; TAG_LEN], - ) -> Result { - AEADCipherDecryptor::do_decrypt_final_detached_out(self, tag, plaintext) - } - - fn decrypt_out_len(ciphertext_len: usize) -> usize { - >::decrypt_out_len( - ciphertext_len, - ) - } - - fn decrypt_detached_out_len(ciphertext_len: usize) -> usize { - >::decrypt_detached_out_len( - ciphertext_len, - ) - } -} - impl Debug for Ascon_AEAD128 { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "Ascon-AEAD128 (key/state masked)") diff --git a/crypto/cipher/src/modes/cbc.rs b/crypto/cipher/src/modes/cbc.rs index eb45fe26f..f5f10e459 100644 --- a/crypto/cipher/src/modes/cbc.rs +++ b/crypto/cipher/src/modes/cbc.rs @@ -9,15 +9,15 @@ //! the forward cipher operations cannot be performed in parallel". //! //! This implementation uses that: decryption walks the ciphertext four blocks at a time through -//! [`ElectronicCodeBook::decrypt_4blocks`], then any remaining pair through -//! [`ElectronicCodeBook::decrypt_2blocks`], then the last block singly. A bit-sliced engine +//! [`BlockPermutation::decrypt_4blocks`], then any remaining pair through +//! [`BlockPermutation::decrypt_2blocks`], then the last block singly. A bit-sliced engine //! computes two or four blocks (AES, on `u32` or `u64` planes) for barely more than the cost of //! one. Encryption cannot, and does not. //! //! # Usage Examples //! -//! The direction is part of the type: [`Cbc`](Cbc) implements -//! [`BlockCipherEncryptor`] and nothing else, and [`Cbc`](Cbc) implements +//! The direction is part of the type: [`Cbc<'k, P, Encrypting, ..>`](Cbc) implements +//! [`BlockCipherEncryptor`] and nothing else, and [`Cbc<'k, P, Decrypting, ..>`](Cbc) implements //! [`BlockCipherDecryptor`] and nothing else. //! The IV is drawn from the RNG and returned; there is no API for supplying one. //! @@ -88,8 +88,8 @@ //! # Suspending and resuming execution //! //! [`Cbc`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte -//! array and resumed later with the re-supplied key. The state is the chaining block; the -//! permutation is rebuilt from the key. The array length is `Cbc::SUSPENDED_STATE_LEN`; see [the +//! array and resumed later over the re-supplied key state. The state is the chaining block; the +//! permutation is the key's. The array length is `Cbc::SUSPENDED_STATE_LEN`; see [the //! crate docs](crate#suspending-and-resuming-execution) for an example. //! //! # 🚨 Security Considerations 🚨 @@ -112,16 +112,15 @@ use crate::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ - BlockDecryptMode, BlockEncryptMode, ElectronicCodeBook, Mode, SymmetricDecryptMode, - SymmetricEncryptMode, + BlockDecryptMode, BlockEncryptMode, BlockPermutation, ElectronicCodeBook, Mode, + SymmetricDecryptMode, SymmetricEncryptMode, }; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding, RNG, SuspendableKeyed, }; -use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_utils::suspendable_state::{ LIB_VERSION_LEN, SuspendableComponent, resume_component, suspend_component, }; @@ -150,20 +149,17 @@ where P: ElectronicCodeBook + ModeNames + Clone, Pad: BlockCipherPadding, { - type Encryptor = PaddedBlockCipherEncryptor< - Cbc, - Pad, - KEY_LEN, - BLOCK_LEN, - BLOCK_LEN, - >; + type Encryptor<'k> + = PaddedBlockCipherEncryptor, Pad, BLOCK_LEN, BLOCK_LEN> + where + P: 'k; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; BLOCK_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { let iv = random_iv::(rng)?; - Ok((PaddedBlockCipherEncryptor::from_inner(Cbc::from_perm(state.clone(), iv)), iv)) + Ok((PaddedBlockCipherEncryptor::from_inner(Cbc::from_perm(state, iv)), iv)) } } @@ -173,16 +169,16 @@ where P: ElectronicCodeBook + ModeNames + Clone, Pad: BlockCipherPadding, { - type Decryptor = PaddedBlockCipherDecryptor< - Cbc, - Pad, - KEY_LEN, - BLOCK_LEN, - BLOCK_LEN, - >; + type Decryptor<'k> + = PaddedBlockCipherDecryptor, Pad, BLOCK_LEN, BLOCK_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState, init_data: &[u8; BLOCK_LEN]) -> Self::Decryptor { - PaddedBlockCipherDecryptor::from_inner(Cbc::from_perm(state.clone(), *init_data)) + fn decrypt_init<'k>( + state: &'k Self::KeyState, + init_data: &[u8; BLOCK_LEN], + ) -> Self::Decryptor<'k> { + PaddedBlockCipherDecryptor::from_inner(Cbc::from_perm(state, *init_data)) } } @@ -191,14 +187,17 @@ impl where P: ElectronicCodeBook + ModeNames + Clone, { - type Encryptor = Cbc; + type Encryptor<'k> + = Cbc<'k, P, Encrypting, BLOCK_LEN> + where + P: 'k; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; BLOCK_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { let iv = random_iv::(rng)?; - Ok((Cbc::from_perm(state.clone(), iv), iv)) + Ok((Cbc::from_perm(state, iv), iv)) } } @@ -207,17 +206,23 @@ impl where P: ElectronicCodeBook + ModeNames + Clone, { - type Decryptor = Cbc; + type Decryptor<'k> + = Cbc<'k, P, Decrypting, BLOCK_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState, init_data: &[u8; BLOCK_LEN]) -> Self::Decryptor { - Cbc::from_perm(state.clone(), *init_data) + fn decrypt_init<'k>( + state: &'k Self::KeyState, + init_data: &[u8; BLOCK_LEN], + ) -> Self::Decryptor<'k> { + Cbc::from_perm(state, *init_data) } } /// 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 +/// former and [`BlockCipherDecryptor`] only for the latter, so a `Cbc<'k, _, Encrypting, _, _>` has no /// decryption methods at all -- using one in the wrong direction is a compile error rather than a /// runtime check. /// @@ -225,30 +230,30 @@ where /// /// # State /// -/// Two fields: the permutation (which owns the key schedule, and is responsible for keeping it in -/// a zeroize-on-drop wrapper) and one block of chaining value. The chaining value is an IV or a +/// Two fields: a reference to the permutation, whose key schedule stays in the key, and one block +/// of chaining value. The chaining value is an IV or a /// ciphertext block, both of which are public, so it is deliberately not wrapped in a `Secret`. #[derive(Clone)] -pub struct Cbc +pub struct Cbc<'k, P, Dir, const BLOCK_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - perm: P, + perm: &'k P, /// `Cj-1`, initialised to the IV. See the module docs on why there is only one field for both. chain: [u8; BLOCK_LEN], _dir: PhantomData, } -impl Cbc +impl<'k, P, Dir, const BLOCK_LEN: usize> Cbc<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// The `N` of this type's [`SuspendableKeyed`] impl: the version header and the chaining + /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the version header and the chaining /// block. See [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN + BLOCK_LEN; /// `C0 = IV`, over an already-keyed permutation: what the [`CBC`] mode impls build. - pub(crate) fn from_perm(perm: P, iv: [u8; BLOCK_LEN]) -> Self { + pub(crate) fn from_perm(perm: &'k P, iv: [u8; BLOCK_LEN]) -> Self { Self { perm, chain: iv, _dir: PhantomData } } @@ -275,7 +280,7 @@ where self.chain = cj; } - /// Decrypts two consecutive blocks with one [`ElectronicCodeBook::decrypt_2blocks`] call. + /// Decrypts two consecutive blocks with one [`BlockPermutation::decrypt_2blocks`] call. /// /// Writing the pair as `Cj, Cj+1` with `Cj-1` the incoming chaining value, Sec 6.2 gives /// @@ -304,7 +309,7 @@ where self.chain = cj1; } - /// Decrypts four consecutive blocks with one [`ElectronicCodeBook::decrypt_4blocks`] call. + /// Decrypts four consecutive blocks with one [`BlockPermutation::decrypt_4blocks`] call. /// /// The same argument as [`Self::decrypt_pair`], four wide: `Pj+k = CIPH^-1_K(Cj+k) XOR Cj+k-1` /// for `k = 0..4`, with `Cj-1` the incoming chaining value. No inverse cipher depends on @@ -327,10 +332,9 @@ where } } -impl Algorithm - for Cbc +impl<'k, P, Dir, const BLOCK_LEN: usize> Algorithm for Cbc<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// The permutation's name for this mode, [`ModeNames::CBC_ALG_NAME`]. const ALG_NAME: &'static str = P::CBC_ALG_NAME; @@ -338,29 +342,13 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl - BlockCipherEncryptor for Cbc +impl<'k, P, const BLOCK_LEN: usize> BlockCipherEncryptor + for Cbc<'k, P, Encrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Begins an encryption flow, generating the IV from the library's default OS-backed DRBG. - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - Self::do_encrypt_init_rng(key, &mut rng) - } - - /// As [`BlockCipherEncryptor::do_encrypt_init`], but takes the IV from the provided RNG. - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { - let perm = P::new(key)?; - let iv = random_iv::(rng)?; - Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) - } - + /// The implementor hook. + /// /// Strictly serial: `Cj` is the input to block `j + 1`, so there is no pair path here. See the /// module docs. Never fails: CBC has no per-IV data limit. fn do_encrypt_blocks_inplace( @@ -374,21 +362,13 @@ where } } -impl - BlockCipherDecryptor for Cbc +impl<'k, P, const BLOCK_LEN: usize> BlockCipherDecryptor + for Cbc<'k, P, Decrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Begins a decryption flow from the IV returned by - /// [`BlockCipherEncryptor::do_encrypt_init`]. - fn do_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; BLOCK_LEN], - ) -> Result { - let perm = P::new(key)?; - Ok(Self { perm, chain: *init_data, _dir: PhantomData }) - } - + /// The implementor hook. + /// /// Walks the input in fours through `decrypt_4blocks`, then pairs through `decrypt_2blocks`, /// then the at-most-one block left over: Sec 6.2's parallelism, in the units the permutation /// offers. `as_chunks_mut` splits into exactly those shapes with no runtime length check and no @@ -413,22 +393,21 @@ where } } -/// The suspended state is the chaining block `Cj-1`, in both directions; the permutation is -/// rebuilt from the re-supplied key. See [`bouncycastle_utils::suspendable_state`]. -impl SuspendableComponent - for Cbc +/// The suspended state is the chaining block `Cj-1`, in both directions; the permutation is the +/// key's, re-supplied by reference. See [`bouncycastle_utils::suspendable_state`]. +impl<'k, P, Dir, const BLOCK_LEN: usize> SuspendableComponent<'k> for Cbc<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = BLOCK_LEN; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { out.copy_from_slice(&self.chain); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { - let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { + let perm = key; let mut chain = [0u8; BLOCK_LEN]; chain.copy_from_slice(state); Ok(Self { perm, chain, _dir: PhantomData }) @@ -436,18 +415,18 @@ where } /// `N` must be [`Cbc::SUSPENDED_STATE_LEN`]; anything else is a compile error. -impl SuspendableKeyed - for Cbc +impl<'k, P, Dir, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> + for Cbc<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - type Key = KeyMaterial; + type Key = P; fn suspend(self) -> [u8; N] { suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 373d53410..7e05acc31 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -13,9 +13,9 @@ //! # Usage Examples //! //! CCM must know the payload length before it starts (Sec 3). A key bound to [`CCM`] takes the -//! lengths per message through the AEAD *packet* role pair, and its in-place one-shots take any -//! length with no second buffer; a key bound to [`CCMFrame`] fixes them in the type, as the -//! [`CcmEncryptor`] / [`CcmDecryptor`] streams it hands out do: `DATA_LEN` bytes of payload +//! lengths per message through the AEAD *declared-length* role pair, and its in-place one-shots take any +//! length with no second buffer; a key bound to [`CCM_Packet`] fixes them in the type, as the +//! [`CcmPacketEncryptor`] / [`CcmPacketDecryptor`] streams it hands out do: `DATA_LEN` bytes of payload //! exactly, up to `AAD_LEN` of AAD. The nonce is drawn unless a `_nonce` form is used; CCM needs //! it unique, not random (Sec 5.3), so a caller with a counter supplies its own. The AAD is //! authenticated but not encrypted, and the tag is the extra output. @@ -27,7 +27,7 @@ //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; //! use bouncycastle_core::errors::SymmetricCipherError; -//! use bouncycastle_core::hazmat::{AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook, Mode}; +//! use bouncycastle_core::hazmat::{AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, ElectronicCodeBook, Mode}; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_cipher::modes::CCM; //! @@ -76,7 +76,7 @@ //! //! # Suspending and resuming execution //! -//! [`CcmEncryptor`] and [`CcmDecryptor`] implement [`SuspendableKeyed`], so a message in progress +//! [`CcmPacketEncryptor`] and [`CcmPacketDecryptor`] implement [`SuspendableKeyed`], so a message in progress //! can be suspended to a byte array and resumed later with the re-supplied key. The state is the //! CTR half, the CBC-MAC chaining value, the payload still owed and the AAD held back so far; the //! permutation is rebuilt from the key. The array length is each type's `SUSPENDED_STATE_LEN`; @@ -109,17 +109,15 @@ use crate::modes::iv::random_iv; use crate::stream::StreamCipher; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ - AeadDecryptMode, AeadEncryptMode, AeadPacketDecryptMode, AeadPacketEncryptMode, - ElectronicCodeBook, KeyStream, Mode, + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, + BlockPermutation, ElectronicCodeBook, KeyStream, Mode, }; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::symmetric_key::{AeadDecryptWorker, AeadEncryptWorker}; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_utils::ct::ct_eq_bytes; use bouncycastle_utils::secret::Secret; use bouncycastle_utils::suspendable_state::{ @@ -133,33 +131,27 @@ use crate::{Decrypting, Encrypting}; /// CCM (SP 800-38C) over any [`ElectronicCodeBook`] with a 128-bit block, for Sec 3's "packet /// environment, i.e., when all of the data is available in storage before CCM is applied": the /// AAD and payload lengths are declared per message, and the nonce is the caller's. Crate-private: -/// the trait-implementing pair, [`CcmEncryptor`] / [`CcmDecryptor`], is built on it, and the +/// the trait-implementing pair, [`CcmPacketEncryptor`] / [`CcmPacketDecryptor`], is built on it, and the /// [`CCM`] mode's in-place one-shots run it over a message's own length. /// -/// `Dir` is [`Encrypting`] or [`Decrypting`]: `CcmPacket` has Sec 6.1's -/// methods and nothing else, and `CcmPacket` has Sec 6.2's. A.1's shape +/// `Dir` is [`Encrypting`] or [`Decrypting`]: `Ccm<'k, P, Encrypting, ..>` has Sec 6.1's +/// methods and nothing else, and `Ccm<'k, P, Decrypting, ..>` has Sec 6.2's. A.1's shape /// conditions on `NONCE_LEN` and `TAG_LEN` are asserted at compile time in `check_shape`, which /// every constructor runs; the pair's docs carry the `compile_fail` cases. #[derive(Clone)] -pub(crate) struct CcmPacket< +pub(crate) struct Ccm< + 'k, P, Dir, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, > where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { // The CTR half (Sec 6.1 steps 5-8): the payload keystream `S1 || S2 || ...`, and the key // schedule, which the CBC-MAC half below shares (Sec 5.2). - ctr: StreamCipher< - CcmKeyStream, - Dir, - KEY_LEN, - NONCE_LEN, - BLOCK_LEN, - >, + ctr: StreamCipher, Dir, NONCE_LEN, BLOCK_LEN>, // The CBC-MAC chaining value: `Y0` once the constructor has absorbed `B0` (Sec 6.1 step 2), // then `Yi` as further blocks arrive (step 3). Bytes are XORed into it in place, so part-way // through a block it holds `Yi-1 XOR (the part of Bi seen so far)`. @@ -183,28 +175,21 @@ pub(crate) struct CcmPacket< _dir: PhantomData, } -impl< - P, - Dir, - const KEY_LEN: usize, - const BLOCK_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, -> CcmPacket +impl<'k, P, Dir, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize> + Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// The spec's `q`: the octet length of the payload-length field `Q`. A.1 requires `n + q = 15`. - const Q_LEN: usize = CcmKeyStream::::Q_LEN; + const Q_LEN: usize = CcmKeyStream::::Q_LEN; /// The CTR half's share of the suspended state. const CTR_STATE_LEN: usize = , + CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN>, Dir, - KEY_LEN, NONCE_LEN, BLOCK_LEN, - > as SuspendableComponent>::STATE_LEN; + > as SuspendableComponent<'k>>::STATE_LEN; /// The largest payload this parameterization can carry, from A.1's "by definition, p<2^8q". /// @@ -242,48 +227,13 @@ where }; } - /// Begins a CCM flow: formats `B0`, absorbs it and all of `A` into the CBC-MAC, and readies the - /// counter blocks. Everything after this streams without buffering. - /// - /// `payload_len` is declared here because Appendix A.2.1 puts the payload length inside `B0`, - /// the first block the CBC-MAC absorbs. An AAD that is not all in hand at once goes through - /// the adapters, which hold it back until it is. - /// - /// * `key` must be a [`KeyType::SymmetricCipherKey`](bouncycastle_core::key_material::KeyType::SymmetricCipherKey) - /// of at least the permutation's strength. - /// * `nonce` **must not** repeat under `key`; see the module's security considerations. - /// * `aad` is authenticated but not encrypted, and may be empty. - /// * `payload_len` is the exact number of payload bytes that will follow. Supplying any other - /// amount is refused, at the update or at finalization. - /// - /// # Errors - /// [`SymmetricCipherError::KeyMaterialError`] for a key of the wrong type or strength, and - /// [`SymmetricCipherError::GenericError`] if `payload_len` exceeds A.1's `2^8q - 1`; see - /// `CcmPacket` for the table. - pub(crate) fn new( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - payload_len: usize, - ) -> Result { - // The shape check and the payload-limit check both belong to `from_perm_with_lengths`, - // which is the one path every construction goes through; duplicating them here would be - // two more `Err` sites that could drift apart from it. `P::new`'s own `KeyType`/strength - // checks are the only key validation needed, exactly as for every other mode in this crate. - let perm = P::new(key)?; - let mut ccm = Self::from_perm_with_lengths(perm, nonce, aad.len(), payload_len)?; - // Exactly the length just declared, so nothing is left owed. - ccm.absorb_aad(aad); - Ok(ccm) - } - - /// As [`Self::new`], from a key schedule that has already been expanded: the constructor a - /// mode-bound key's in-place one-shots use, where the key was validated once at construction. + /// From a key schedule that has already been expanded: the constructor a mode-bound key's + /// in-place one-shots use, where the key was validated once at construction. /// /// # Errors /// [`SymmetricCipherError::GenericError`] if `payload_len` exceeds A.1's `2^8q - 1`. pub(crate) fn from_perm( - perm: P, + perm: &'k P, nonce: &[u8; NONCE_LEN], aad: &[u8], payload_len: usize, @@ -350,7 +300,7 @@ where /// than the AAD itself: formats and absorbs `B0` and, if there is any AAD, the encoding of /// its length, leaving the AAD itself to [`Self::absorb_aad`]. fn from_perm_with_lengths( - perm: P, + perm: &'k P, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, @@ -369,12 +319,12 @@ where /// The state *before* Sec 6.1 step 1: the keystream is positioned at `S1` and `payload_len` /// is owed, but nothing has been absorbed into the CBC-MAC, not even `B0`. /// - /// Crate-private, for [`CcmEncryptor`] / [`CcmDecryptor`]: the trait constructor they + /// Crate-private, for [`CcmPacketEncryptor`] / [`CcmPacketDecryptor`]: the trait constructor they /// implement is handed a key and no AAD, and `B0`'s Adata bit (A.2.2: "'0' if a=0 and '1' if /// a>0") cannot be set until the AAD is complete. They call [`Self::format_header`] when it /// is, and nothing else may touch the MAC before that. `payload_len` must already be at most /// `Self::MAX_PAYLOAD_LEN`; the adapters assert theirs at compile time. - fn from_perm_unformatted(perm: P, nonce: &[u8; NONCE_LEN], payload_len: usize) -> Self { + fn from_perm_unformatted(perm: &'k P, nonce: &[u8; NONCE_LEN], payload_len: usize) -> Self { Self::check_shape(); Self { ctr: StreamCipher::from_keystream(CcmKeyStream::from_perm(perm, nonce)), @@ -482,7 +432,7 @@ where | ((((TAG_LEN - 2) / 2) as u8) << 3) | ((Self::Q_LEN - 1) as u8); b0[1..1 + NONCE_LEN].copy_from_slice(nonce); - CcmKeyStream::::put_q_field(&mut b0, payload_len); + CcmKeyStream::::put_q_field(&mut b0, payload_len); b0 } @@ -557,9 +507,7 @@ where // rather than a plain local that outlives this function's stack frame unzeroed. let keystream = self.ctr.keystream(); let mut s0: Secret<[u8; BLOCK_LEN]> = Secret::new(); - *s0 = CcmKeyStream::::counter_block( - &keystream.ctr_template, 0, - ); + *s0 = CcmKeyStream::::counter_block(&keystream.ctr_template, 0); keystream.perm.encrypt_block(&mut s0); // `MSB_Tlen` of a byte-aligned value is its first `TAG_LEN` bytes; A.1 makes `t` an octet @@ -574,10 +522,10 @@ where /// Sec 6.1, the generation-encryption process. Present only on the encrypting direction, so a /// decryptor cannot be asked to produce a tag. -impl - CcmPacket +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize> + Ccm<'k, P, Encrypting, BLOCK_LEN, NONCE_LEN, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Encrypts `data` in place and authenticates it. /// @@ -617,70 +565,14 @@ where } Ok(self.finish_mac()) } - - /// One-shot generation-encryption with a **detached** tag (Sec 6.1). - /// - /// Writes `plaintext.len()` bytes of ciphertext into `ciphertext` and returns that count with - /// the tag. The entire output buffer is zeroized before the ciphertext is written, so any bytes - /// past that count will be 0. For the spec's own inline `ciphertext || tag` string, use - /// [`Self::encrypt_out`]. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, plus - /// [`Self::new`]'s errors. - pub(crate) fn encrypt_detached_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { - ciphertext.fill(0); - if ciphertext.len() < plaintext.len() { - return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); - } - let mut ccm = Self::new(key, nonce, aad, plaintext.len())?; - let out = &mut ciphertext[..plaintext.len()]; - out.copy_from_slice(plaintext); - ccm.do_encrypt_inplace(out)?; - let tag = ccm.do_encrypt_final()?; - Ok((plaintext.len(), tag)) - } - - /// One-shot generation-encryption producing the spec's own output string (Sec 6.1 step 8): - /// `C = (P XOR MSB_Plen(S)) || (T XOR MSB_Tlen(S0))`, i.e. `ciphertext || tag` inline. - /// - /// `ciphertext` needs `plaintext.len() + TAG_LEN` bytes; the return is how many were written. - /// The entire output buffer is zeroized before the output is written, so any bytes past that - /// count will be 0. - /// - /// # Errors - /// As [`Self::encrypt_detached_out`]. - pub(crate) fn encrypt_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result { - ciphertext.fill(0); - let needed = plaintext.len() + TAG_LEN; - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let (data, tag_out) = ciphertext[..needed].split_at_mut(plaintext.len()); - let (_, tag) = Self::encrypt_detached_out(key, nonce, aad, plaintext, data)?; - tag_out.copy_from_slice(&tag); - Ok(needed) - } } /// Sec 6.2, the decryption-verification process. Present only on the decrypting direction, so an /// encryptor cannot be asked to verify a tag. -impl - CcmPacket +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize> + Ccm<'k, P, Decrypting, BLOCK_LEN, NONCE_LEN, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Decrypts `data` in place and authenticates the recovered plaintext. /// @@ -733,83 +625,12 @@ where Err(SymmetricCipherError::AEADTagCheckFailed) } } - - /// One-shot decryption-verification with a **detached** tag (Sec 6.2). - /// - /// Returns the number of plaintext bytes written. The entire output buffer is zeroized before - /// the plaintext is written, so any bytes past that count will be 0. On failure `plaintext` is zeroized before the error is returned, so Sec 6.2's "the payload P - /// and the MAC T shall not be revealed" holds even for a caller who ignores the `Result`. - /// - /// # Errors - /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify, - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, plus - /// [`Self::new`]'s errors. - pub(crate) fn decrypt_detached_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - if plaintext.len() < ciphertext.len() { - return Err(SymmetricCipherError::OutputBufferTooSmall(ciphertext.len())); - } - let mut ccm = Self::new(key, nonce, aad, ciphertext.len())?; - let out = &mut plaintext[..ciphertext.len()]; - out.copy_from_slice(ciphertext); - ccm.do_decrypt_inplace(out)?; - match ccm.do_decrypt_final(tag) { - Ok(()) => Ok(ciphertext.len()), - Err(e) => { - // Sec 6.2: on INVALID the payload "shall not be revealed". A plain `fill` because - // this crate is `#![forbid(unsafe_code)]`; the store is to the caller's own buffer, - // which the caller may read after this returns, so it is not a dead store the - // optimizer is entitled to drop. - out.fill(0); - Err(e) - } - } - } - - /// One-shot decryption-verification of the spec's own output string (Sec 6.2), splitting the - /// trailing `TAG_LEN` bytes off `ciphertext` as the tag -- step 6's `LSB_Tlen(C)`. Returns the - /// number of plaintext bytes written. The entire output buffer is zeroized before the plaintext - /// is written, so any bytes past that count will be 0. - /// - /// # Errors - /// [`SymmetricCipherError::DecryptionFailed`] for Sec 6.2 step 1, "If Clen <= Tlen, then - /// return INVALID": a malformed input rather than a failed check, reported with the variant - /// [`SymmetricCipherDecryptor::do_decrypt_final`] specifies for a malformed ciphertext so that - /// every inline entry point -- this one, [`CcmDecryptor::do_decrypt_final`] and - /// [`CcmDecryptor::decrypt_with_aad_out`](AEADCipherDecryptor::decrypt_with_aad_out) -- agrees - /// on the same input. Otherwise as [`Self::decrypt_detached_out`]. - pub(crate) fn decrypt_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - let Some((data, tag)) = ciphertext.split_last_chunk::() else { - return Err(SymmetricCipherError::DecryptionFailed); - }; - Self::decrypt_detached_out(key, nonce, aad, data, tag, plaintext) - } } -impl< - P, - Dir, - const KEY_LEN: usize, - const BLOCK_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, -> Algorithm for CcmPacket +impl<'k, P, Dir, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize> Algorithm + for Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// The permutation's name for this mode, [`ModeNames::CCM_ALG_NAME`]. const ALG_NAME: &'static str = P::CCM_ALG_NAME; @@ -821,14 +642,14 @@ where /// over counter blocks formatted as A.3 specifies. /// /// Crate-private: CCM's CTR half alone is an unauthenticated cipher, and is only reachable -/// through `CcmPacket`. It shares its key schedule with the CBC-MAC half, which reaches it through +/// through `Ccm`. It shares its key schedule with the CBC-MAC half, which reaches it through /// [`StreamCipher::keystream`]. #[derive(Clone)] -struct CcmKeyStream +struct CcmKeyStream<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - perm: P, + perm: &'k P, // `Ctr_i` with its counter field zeroed (A.3, Table 3): the flags octet and the nonce, which // are the same in every counter block. Public data -- flags and nonce travel in the clear -- // so deliberately not a `Secret`. @@ -838,10 +659,10 @@ where next_ctr: u64, } -impl - CcmKeyStream +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> + CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// The spec's `q`: the octet length of the payload-length field `Q`, which is also the width /// of the counter field. A.1 requires `n + q = 15`. @@ -853,7 +674,7 @@ where if Self::Q_LEN >= 8 { u64::MAX } else { (1u64 << (8 * Self::Q_LEN)) - 1 }; /// Formats the counter template and positions the keystream at `S1`. - fn from_perm(perm: P, nonce: &[u8; NONCE_LEN]) -> Self { + fn from_perm(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { // A.3, Tables 3 and 4: `Ctr_i` is `Flags || N || [i]_8q`, and its flags octet has both // reserved bits and bits 3, 4 and 5 zero -- "to ensure that all the counter blocks are // distinct from B0", whose bits 3..5 encode `t` and so cannot all be zero -- leaving bits @@ -866,7 +687,7 @@ where /// The nonce `N`, read back out of the counter template: A.3 Table 3 puts it in octets /// `1 ... 15-q`, which is `1 ... NONCE_LEN` since `n + q = 15`. It is the same `N` that - /// `B0` carries (A.2.1 Table 2), so `CcmPacket::format_header` needs no second copy of it. + /// `B0` carries (A.2.1 Table 2), so `Ccm::format_header` needs no second copy of it. fn nonce(&self) -> [u8; NONCE_LEN] { let mut nonce = [0u8; NONCE_LEN]; nonce.copy_from_slice(&self.ctr_template[1..1 + NONCE_LEN]); @@ -877,8 +698,8 @@ where /// Table 2) and the counter field of `Ctr_i` (A.3, Table 3), which occupy the same octets. /// /// `Q_LEN <= 8`, so the low `Q_LEN` bytes of a big-endian `u64` are exactly `[x]_8q`. Nothing - /// is ever truncated in a way that matters: `CcmPacket::new` refuses a payload above - /// `CcmPacket::MAX_PAYLOAD_LEN`, and the counter cannot pass that either, since there is one + /// is ever truncated in a way that matters: `Ccm::new` refuses a payload above + /// `Ccm::MAX_PAYLOAD_LEN`, and the counter cannot pass that either, since there is one /// counter block per `BLOCK_LEN` payload bytes. #[inline] fn put_q_field(block: &mut [u8; BLOCK_LEN], x: u64) { @@ -897,29 +718,22 @@ where } } -impl Algorithm - for CcmKeyStream +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> Algorithm + for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const ALG_NAME: &'static str = P::CCM_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl - KeyStream for CcmKeyStream +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> KeyStream + for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - fn new( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - ) -> Result { - Ok(Self::from_perm(P::new(key)?, nonce)) - } - /// Every counter value from `next_ctr` to `2^8q - 1`. Never the binding limit in practice: - /// `CcmPacket::new` caps the payload at `2^8q - 1` bytes, far fewer than that many blocks. + /// `Ccm::new` caps the payload at `2^8q - 1` bytes, far fewer than that many blocks. fn remaining_blocks(&self) -> u64 { Self::MAX_COUNTER - self.next_ctr + 1 } @@ -929,7 +743,7 @@ where fn apply_blocks(&mut self, blocks: &mut [[u8; BLOCK_LEN]]) { let template = &self.ctr_template; apply_counter_blocks( - &self.perm, + self.perm, &mut self.next_ctr, |j| Self::counter_block(template, j), blocks, @@ -937,7 +751,7 @@ where } } -/// What [`CcmEncryptor`] and [`CcmDecryptor`] share: the `CcmPacket` state, which the trait +/// What [`CcmPacketEncryptor`] and [`CcmPacketDecryptor`] share: the `Ccm` state, which the trait /// constructor builds before it has seen any AAD, and the AAD itself, held back until it is /// complete. /// @@ -948,22 +762,22 @@ where /// buffer in either adapter, and `AAD_LEN` is its capacity: header-sized, by the caller's choice. /// /// `DATA_LEN` is an exact length, not a capacity, and is committed to `B0` here: the payload -/// then streams through `CcmPacket`'s own `owed` accounting, which refuses more and whose finals +/// then streams through `Ccm`'s own `owed` accounting, which refuses more and whose finals /// refuse less. #[derive(Clone)] struct CcmAdapter< + 'k, P, Dir, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, > where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - ccm: CcmPacket, + ccm: Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN>, // Associated data is authenticated but not encrypted, and travels in the clear, so it is not // secret and is not wrapped. aad: [u8; AAD_LEN], @@ -973,33 +787,32 @@ struct CcmAdapter< } impl< + 'k, P, Dir, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> CcmAdapter +> CcmAdapter<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// The compile-time check for the trait adapters, run from both [`CcmEncryptor`]'s and - /// [`CcmDecryptor`]'s constructors and from their one-shots, which never build an adapter, so + /// The compile-time check for the trait adapters, run from both [`CcmPacketEncryptor`]'s and + /// [`CcmPacketDecryptor`]'s constructors and from their one-shots, which never build an adapter, so /// that a parameter set either names a usable pair or does not compile at all. - /// `CcmPacket::check_shape`'s own checks run as well, from the `CcmPacket` constructor underneath, - /// and the floor for a generated nonce is `CcmEncryptor::check_random_nonce`'s, run only from + /// `Ccm::check_shape`'s own checks run as well, from the `Ccm` constructor underneath, + /// and the floor for a generated nonce is `CcmPacketEncryptor::check_random_nonce`'s, run only from /// the forms that draw one: every A.1 nonce length from 7 through 13 is usable here under a /// supplied nonce. #[inline] fn check_shape() { const { // `B0` could not carry it (A.1's `p < 2^8q`), and this is the one place the length is - // known at compile time, so it is a compile error rather than `CcmPacket::new`'s `Err`. + // known at compile time, so it is a compile error rather than `Ccm::new`'s `Err`. assert!( - DATA_LEN as u64 - <= CcmPacket::::MAX_PAYLOAD_LEN, + DATA_LEN as u64 <= Ccm::::MAX_PAYLOAD_LEN, "CCM: DATA_LEN exceeds the payload limit 2^8q - 1 that NONCE_LEN implies (A.1)" ); } @@ -1007,10 +820,10 @@ where /// Readies the keystream and commits `DATA_LEN` as the payload length; the CBC-MAC absorbs /// nothing until [`Self::begin_data`]. - fn new(perm: P, nonce: &[u8; NONCE_LEN]) -> Self { + fn new(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { Self::check_shape(); Self { - ccm: CcmPacket::from_perm_unformatted(perm, nonce, DATA_LEN), + ccm: Ccm::from_perm_unformatted(perm, nonce, DATA_LEN), aad: [0u8; AAD_LEN], aad_len: 0, formatted: false, @@ -1048,7 +861,7 @@ where /// Ends the AAD phase, the first time it is called: runs Sec 6.1 step 1 -- `B0`, the encoding /// of `a`, the AAD and its zero pad -- over the AAD held so far, which is now known to be all - /// of it. Called before any payload byte goes through `CcmPacket`, and from every final, so a + /// of it. Called before any payload byte goes through `Ccm`, and from every final, so a /// message with no payload at all still gets its header. Later calls do nothing. fn begin_data(&mut self) { if !self.formatted { @@ -1060,10 +873,10 @@ where } } -/// Adapts `CcmPacket` to [`AEADCipherEncryptor`] and, through it, [`SymmetricCipherEncryptor`], for a +/// Adapts `Ccm` to [`AEADCipherEncryptor`] and, through it, [`SymmetricCipherEncryptor`], for a /// payload of exactly `DATA_LEN` bytes. /// -/// [`SymmetricCipherEncryptor::do_encrypt_init`] is handed a key and nothing else, but CCM cannot +/// An AEAD key's `create_encryptor` is handed a nonce and nothing else, but CCM cannot /// form `B0` -- and so cannot authenticate anything -- until it knows the payload length /// (Appendix A.2.1; see the module docs). Rather than hold the message until the final call /// reveals that length, this type fixes the payload length as the const parameter `DATA_LEN`. @@ -1074,7 +887,7 @@ where /// `DATA_LEN` bytes of payload -- the fixed frame of SP 800-38C Sec 3's "packet environment" -- /// and refuse any other amount, more at the update that would exceed it and less at the final. /// The one-shots are not bound by either parameter: with the whole message in hand there is -/// nothing to declare up front and nothing to hold back, so they are `CcmPacket`'s own one-shots +/// nothing to declare up front and nothing to hold back, so they are `Ccm`'s own one-shots /// under a generated nonce, for a payload and AAD of any length, and the AAD buffer is never /// built. `DATA_LEN` and `AAD_LEN` govern the streaming methods only. /// @@ -1089,11 +902,11 @@ where /// use bouncycastle_core::hazmat::{AeadDecryptMode, AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; /// use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_cipher::modes::CCM_Packet; /// use bouncycastle_rng::HashDRBG_SHA512; /// /// // Frames of exactly 40 payload bytes, with up to 16 bytes of header, as a key is bound to them. -/// type Frame = CCMFrame<12, 16, 16, 40>; +/// type Frame = CCM_Packet<12, 16, 16, 40>; /// /// let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) /// .expect("a 16-byte symmetric cipher key"); @@ -1129,8 +942,8 @@ where /// /// The forms that generate a nonce need `NONCE_LEN` of at least 12, to keep the random-collision /// bound useful; the check runs only when one of them is instantiated. The forms that take the -/// caller's nonce -- the `_nonce` constructor and one-shots of a key bound to [`CCMFrame`] -- and -/// [`CcmDecryptor`] accept every A.1 nonce length from 7 through 13, as the inherent `CcmPacket` +/// caller's nonce -- the `_nonce` constructor and one-shots of a key bound to [`CCM_Packet`] -- and +/// [`CcmPacketDecryptor`] accept every A.1 nonce length from 7 through 13, as the inherent `Ccm` /// API does, with uniqueness the caller's to guarantee. So a short nonce compiles when it is /// supplied: /// @@ -1140,13 +953,13 @@ where /// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::{AEADCipherEncryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_cipher::modes::CCM_Packet; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // n = 8 is permitted by A.1; the caller's counter supplies it. /// let counter = 7u64.to_be_bytes(); -/// let mut enc = CCMFrame::<8, 16, 64, 256>::encrypt_init_nonce(&state, &counter); +/// let mut enc = CCM_Packet::<8, 16, 64, 256>::encrypt_init_nonce(&state, &counter); /// enc.do_update_aad(b"hdr").unwrap(); /// let mut sealed = [0u8; 256 + 16]; /// let n = enc.do_encrypt_out(&[0x5A; 256], &mut sealed).unwrap(); @@ -1162,13 +975,13 @@ where /// # use toy::ToyBlockCipher; /// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_cipher::modes::CCM_Packet; /// use bouncycastle_rng::HashDRBG_SHA512; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // n = 8 is permitted by A.1, but too short for a random draw. -/// let _ = CCMFrame::<8, 16, 64, 256>::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()); +/// let _ = CCM_Packet::<8, 16, 64, 256>::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()); /// ``` /// /// Nor does a `DATA_LEN` that `B0` could not carry under this `NONCE_LEN` (A.1's `p < 2^8q`): @@ -1178,18 +991,18 @@ where /// # use toy::ToyBlockCipher; /// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_cipher::modes::CCM_Packet; /// use bouncycastle_rng::HashDRBG_SHA512; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // n = 13 leaves q = 2, so the payload is at most 65535 bytes. -/// let _ = CCMFrame::<13, 16, 64, 65536>::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()); +/// let _ = CCM_Packet::<13, 16, 64, 65536>::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()); /// ``` /// /// # Memory /// -/// A value is the fixed-size `CcmPacket` state, the `AAD_LEN`-byte AAD buffer and two words of +/// A value is the fixed-size `Ccm` state, the `AAD_LEN`-byte AAD buffer and two words of /// bookkeeping, independent of `DATA_LEN`; the finals return and write only the tag. Nothing /// scales with the message. /// @@ -1200,13 +1013,13 @@ where /// # use toy::ToyBlockCipher; /// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_cipher::modes::CCM_Packet; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) /// .unwrap(); -/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // n = 6 is not in {7, ..., 13}: it would make q = 9, which A.1 does not allow. -/// let _ = CCMFrame::<6, 16, 64, 256>::encrypt_init_nonce(&state, &[0u8; 6]); +/// let _ = CCM_Packet::<6, 16, 64, 256>::encrypt_init_nonce(&state, &[0u8; 6]); /// ``` /// /// Nor does an odd tag length: @@ -1216,54 +1029,54 @@ where /// # use toy::ToyBlockCipher; /// use bouncycastle_core::hazmat::{AeadEncryptMode, ElectronicCodeBook, Mode}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_cipher::modes::CCMFrame; +/// use bouncycastle_cipher::modes::CCM_Packet; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) /// .unwrap(); -/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); +/// let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); /// // t = 15 is not in {4, 6, 8, 10, 12, 14, 16}. -/// let _ = CCMFrame::<12, 15, 64, 256>::encrypt_init_nonce(&state, &[0u8; 12]); +/// let _ = CCM_Packet::<12, 15, 64, 256>::encrypt_init_nonce(&state, &[0u8; 12]); /// ``` #[derive(Clone)] -pub struct CcmEncryptor< +pub struct CcmPacketEncryptor< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, ->(CcmAdapter) +>(CcmAdapter<'k, P, Encrypting, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>) where - P: ElectronicCodeBook + ModeNames; + P: BlockPermutation + ModeNames; impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> Algorithm for CcmEncryptor +> Algorithm for CcmPacketEncryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const ALG_NAME: &'static str = P::CCM_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> CcmEncryptor +> CcmPacketEncryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Every final comes here: Sec 6.1 steps 4 and 8, the tag. The header goes in first if no /// payload call put it there, which is the `DATA_LEN = 0` message. @@ -1271,20 +1084,19 @@ where /// # Errors /// [`SymmetricCipherError::StateError`] if fewer than `DATA_LEN` payload bytes were supplied: /// `B0` committed to `DATA_LEN`, so a shorter message would get a tag no verifier could - /// reproduce, and `CcmPacket::do_encrypt_final` refuses to produce one. + /// reproduce, and `Ccm::do_encrypt_final` refuses to produce one. fn finish(mut self) -> Result<[u8; TAG_LEN], SymmetricCipherError> { self.0.begin_data(); self.0.ccm.do_encrypt_final() } /// Begins an encryption from a key schedule that has already been expanded, under a nonce - /// drawn from `rng`: the constructor a mode-bound key's `encrypt_init` uses. The result is - /// identical to [`SymmetricCipherEncryptor::do_encrypt_init_rng`] from the same key and RNG. + /// drawn from `rng`: the constructor a mode-bound key's `encrypt_init` uses. /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. pub fn from_perm( - perm: P, + perm: &'k P, rng: &mut dyn RNG, ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { Self::check_random_nonce(); @@ -1294,7 +1106,7 @@ where /// As [`Self::from_perm`] under a nonce the caller supplies, at any A.1 length: the /// constructor a mode-bound key's `encrypt_init_nonce` uses. - pub fn from_perm_nonce(perm: P, nonce: &[u8; NONCE_LEN]) -> Self { + pub fn from_perm_nonce(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { Self(CcmAdapter::new(perm, nonce)) } @@ -1308,43 +1120,25 @@ where const { assert!( NONCE_LEN >= 12, - "CCM: a generated nonce needs NONCE_LEN >= 12; supply the nonce (do_encrypt_init_nonce, the _nonce one-shots, or CcmPacket) for a shorter one" + "CCM: a generated nonce needs NONCE_LEN >= 12; supply the nonce (do_encrypt_init_nonce, the _nonce one-shots, or Ccm) for a shorter one" ); } } } impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> SymmetricCipherEncryptor - for CcmEncryptor +> SymmetricCipherEncryptor + for CcmPacketEncryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - Self::do_encrypt_init_rng(key, &mut rng) - } - - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { - // `P::new`'s own checks are the only key validation needed, exactly as for `CcmPacket` itself - // and every other mode in this crate; `random_iv` is CBC/CFB's same OS-backed draw -- - // Sec 5.3 asks only for uniqueness, not CBC/CFB's unpredictability, but a CSPRNG draw is - // the only way to be unique without state `do_encrypt_init` does not have. - Self::from_perm(P::new(key)?, rng) - } - /// The identity: nothing is held back, since `B0` is already committed to `DATA_LEN`. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -1374,7 +1168,7 @@ where // Before the length check, so that a refused oversized call still closes the AAD phase: // the phase order is about call history, and this call happened. self.0.begin_data(); - // `CcmPacket::do_encrypt_inplace` would refuse this too, but only after the plaintext had been copied + // `Ccm::do_encrypt_inplace` would refuse this too, but only after the plaintext had been copied // into the caller's output buffer, and a refused call must not leave plaintext there. if plaintext.len() > self.0.ccm.owed { return Err(SymmetricCipherError::StateError( @@ -1399,50 +1193,22 @@ where fn encrypt_out_len(plaintext_len: usize) -> usize { plaintext_len + TAG_LEN } - - /// The AEAD one-shot with no AAD; see - /// [`encrypt_with_aad_out`](AEADCipherEncryptor::encrypt_with_aad_out). - fn encrypt_out( - key: &KeyMaterial, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { - >::encrypt_with_aad_out( - key, - &[], - plaintext, - ciphertext, - ) - } - - /// The AEAD one-shot with no AAD; see - /// [`encrypt_with_aad_rng_out`](AEADCipherEncryptor::encrypt_with_aad_rng_out). - fn encrypt_rng_out( - key: &KeyMaterial, - rng: &mut dyn RNG, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { - >::encrypt_with_aad_rng_out( - key, rng, &[], plaintext, ciphertext, - ) - } } /// The AEAD view, with `FINAL_LEN = TAG_LEN`: the encryptor holds nothing back, so the detached /// final flushes nothing and returns only the tag. impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> AEADCipherEncryptor - for CcmEncryptor +> AEADCipherEncryptor + for CcmPacketEncryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Holds back `aad` until the payload begins; see `CcmAdapter`. /// @@ -1454,53 +1220,6 @@ where self.0.do_update_aad(aad) } - /// [`do_encrypt_init_rng`](SymmetricCipherEncryptor::do_encrypt_init_rng) with the caller's - /// nonce in place of the draw, which is Sec 5.3's own model -- "the nonce is not required to - /// be random" -- and takes any A.1 nonce length, since nothing is drawn. - fn do_encrypt_init_nonce( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - ) -> Result { - let perm = P::new(key)?; - Ok(Self(CcmAdapter::new(perm, nonce))) - } - - /// `CcmPacket::encrypt_detached_out` itself, for a `plaintext` and `aad` of any length. - /// - /// # Errors - /// As [`encrypt_detached_rng_out`](AEADCipherEncryptor::encrypt_detached_rng_out), less the - /// RNG's. - fn encrypt_detached_nonce_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { - CcmAdapter::::check_shape(); - CcmPacket::::encrypt_detached_out( - key, nonce, aad, plaintext, ciphertext, - ) - } - - /// `CcmPacket::encrypt_out` itself, for a `plaintext` and `aad` of any length. - /// - /// # Errors - /// As [`encrypt_with_aad_rng_out`](AEADCipherEncryptor::encrypt_with_aad_rng_out), less the - /// RNG's. - fn encrypt_with_aad_nonce_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result { - CcmAdapter::::check_shape(); - CcmPacket::::encrypt_out( - key, nonce, aad, plaintext, ciphertext, - ) - } - /// Sec 6.1 steps 4 and 8: the tag. Nothing is held back, so `ciphertext` is left zeroed. /// /// # Errors @@ -1512,94 +1231,10 @@ where ciphertext.fill(0); Ok((0, self.finish()?)) } - - /// `CcmPacket::encrypt_detached_out` under a nonce drawn from the default DRBG, as - /// [`do_encrypt_init`](SymmetricCipherEncryptor::do_encrypt_init) draws it; see - /// [`encrypt_detached_rng_out`](AEADCipherEncryptor::encrypt_detached_rng_out). - fn encrypt_detached_out( - key: &KeyMaterial, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - Self::encrypt_detached_rng_out(key, &mut rng, aad, plaintext, ciphertext) - } - - /// `CcmPacket::encrypt_detached_out` under a nonce drawn from `rng`, for a `plaintext` and `aad` - /// of any length: neither `DATA_LEN` nor `AAD_LEN` applies to a one-shot (see the type docs). - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is shorter than - /// `plaintext`, checked before anything is drawn from `rng`; plus `CcmPacket::new`'s errors and - /// the RNG's. - fn encrypt_detached_rng_out( - key: &KeyMaterial, - rng: &mut dyn RNG, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { - ciphertext.fill(0); - if ciphertext.len() < plaintext.len() { - return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); - } - Self::check_random_nonce(); - CcmAdapter::::check_shape(); - let nonce = random_iv::(rng)?; - let (written, tag) = - CcmPacket::::encrypt_detached_out( - key, &nonce, aad, plaintext, ciphertext, - )?; - Ok((nonce, written, tag)) - } - - /// `CcmPacket::encrypt_out` under a nonce drawn from the default DRBG; see - /// [`encrypt_with_aad_rng_out`](AEADCipherEncryptor::encrypt_with_aad_rng_out). - fn encrypt_with_aad_out( - key: &KeyMaterial, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - Self::encrypt_with_aad_rng_out(key, &mut rng, aad, plaintext, ciphertext) - } - - /// `CcmPacket::encrypt_out` under a nonce drawn from `rng`, for a `plaintext` and `aad` of any - /// length, as [`encrypt_detached_rng_out`](AEADCipherEncryptor::encrypt_detached_rng_out). - /// - /// # Errors - /// As [`encrypt_detached_rng_out`](AEADCipherEncryptor::encrypt_detached_rng_out), with - /// `ciphertext` needing `plaintext.len() + TAG_LEN` bytes. - fn encrypt_with_aad_rng_out( - key: &KeyMaterial, - rng: &mut dyn RNG, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { - ciphertext.fill(0); - let needed = - >::encrypt_out_len( - plaintext.len(), - ); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - Self::check_random_nonce(); - CcmAdapter::::check_shape(); - let nonce = random_iv::(rng)?; - let written = - CcmPacket::::encrypt_out( - key, &nonce, aad, plaintext, ciphertext, - )?; - Ok((nonce, written)) - } } -/// Adapts `CcmPacket` to [`AEADCipherDecryptor`] and, through it, [`SymmetricCipherDecryptor`], for -/// a payload of exactly `DATA_LEN` bytes; the mirror of [`CcmEncryptor`], and see it for the +/// Adapts `Ccm` to [`AEADCipherDecryptor`] and, through it, [`SymmetricCipherDecryptor`], for +/// a payload of exactly `DATA_LEN` bytes; the mirror of [`CcmPacketEncryptor`], and see it for the /// parameters, the nonce floor and the memory. /// /// Knowing the payload length has a consequence no other decryptor in this crate enjoys: there is @@ -1608,7 +1243,7 @@ where /// can only be an inline tag (Sec 6.2 step 6's `LSB_Tlen(C)`), and only those bytes -- at most /// `TAG_LEN` -- are held back for the final to check. A `C` of any other length than `DATA_LEN` /// (detached) or `DATA_LEN + TAG_LEN` (inline) is refused as malformed. The one-shots are -/// `CcmPacket`'s own and take a `C` of any length; see [`CcmEncryptor`]. +/// `Ccm`'s own and take a `C` of any length; see [`CcmPacketEncryptor`]. /// /// # 🚨 Security Considerations 🚨 /// @@ -1617,22 +1252,22 @@ where /// recovered rather than hold the whole frame back, so a forged ciphertext yields attacker-chosen /// bytes that only the final's [`SymmetricCipherError::AEADTagCheckFailed`] disowns. That is /// [`AEADCipherDecryptor`]'s general streaming caveat, and the inherent -/// `CcmPacket::do_decrypt_inplace` has it too. Sec 6.2's "the payload P and the MAC T shall not be +/// `Ccm::do_decrypt_inplace` has it too. Sec 6.2's "the payload P and the MAC T shall not be /// revealed" on INVALID is honoured by the one-shots, which zeroize what they wrote before /// returning the error. #[derive(Clone)] -pub struct CcmDecryptor< +pub struct CcmPacketDecryptor< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, > where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - inner: CcmAdapter, + inner: CcmAdapter<'k, P, Decrypting, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, // The bytes past the `DATA_LEN`th, as they arrive: an inline tag, if the final says the // layout is inline, and excess ciphertext if it says detached. Public either way (the tag // travels in the clear), so not wrapped. @@ -1641,48 +1276,48 @@ pub struct CcmDecryptor< } impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> Algorithm for CcmDecryptor +> Algorithm for CcmPacketDecryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const ALG_NAME: &'static str = P::CCM_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> CcmDecryptor +> CcmPacketDecryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Begins a decryption from a key schedule that has already been expanded: the constructor a /// mode-bound key's `decrypt_init` uses. Infallible, as [`AeadDecryptMode::decrypt_init`] is. /// /// [`AeadDecryptMode::decrypt_init`]: bouncycastle_core::hazmat::AeadDecryptMode::decrypt_init - pub fn from_perm(perm: P, nonce: &[u8; NONCE_LEN]) -> Self { + pub fn from_perm(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { Self { inner: CcmAdapter::new(perm, nonce), tag: [0u8; TAG_LEN], tag_len: 0 } } /// Every final comes here once it has settled which bytes are the tag: Sec 6.2 step 10, - /// through `CcmPacket::do_decrypt_final`. The header goes in first if no payload call put it + /// through `Ccm::do_decrypt_final`. The header goes in first if no payload call put it /// there, which is the `DATA_LEN = 0` message. /// /// # Errors /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. The callers have - /// already refused a short payload, so `CcmPacket::do_decrypt_final`'s own refusal of one is not + /// already refused a short payload, so `Ccm::do_decrypt_final`'s own refusal of one is not /// reachable from here; it stays as the backstop it is for the inherent API. fn finish(mut self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { self.inner.begin_data(); @@ -1691,26 +1326,18 @@ where } impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> SymmetricCipherDecryptor - for CcmDecryptor +> SymmetricCipherDecryptor + for CcmPacketDecryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - fn do_decrypt_init( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - ) -> Result { - // `P::new`'s own checks are the only key validation needed; see the encryptor. - Ok(Self::from_perm(P::new(key)?, nonce)) - } - /// Every byte of `input_len` that is still inside the declared payload; the rest can only be /// the tag, and is held back. fn do_decrypt_out_len(&self, input_len: usize) -> usize { @@ -1751,7 +1378,7 @@ where declares plus an inline tag", )); } - // `release` is within what is owed, so `CcmPacket` cannot refuse it. + // `release` is within what is owed, so `Ccm` cannot refuse it. plaintext[..release].copy_from_slice(data); self.inner.ccm.do_decrypt_inplace(&mut plaintext[..release])?; self.tag[self.tag_len..self.tag_len + tail.len()].copy_from_slice(tail); @@ -1783,41 +1410,24 @@ where fn decrypt_out_len(ciphertext_len: usize) -> usize { ciphertext_len.saturating_sub(TAG_LEN) } - - /// The AEAD one-shot with no AAD; see - /// [`decrypt_with_aad_out`](AEADCipherDecryptor::decrypt_with_aad_out). - fn decrypt_out( - key: &KeyMaterial, - init_data: &[u8; NONCE_LEN], - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - >::decrypt_with_aad_out( - key, - init_data, - &[], - ciphertext, - plaintext, - ) - } } -/// The AEAD view, with `FINAL_LEN = TAG_LEN`. The one-shots are `CcmPacket`'s own, for a `C` and -/// AAD of any length; see [`CcmEncryptor`]. +/// The AEAD view, with `FINAL_LEN = TAG_LEN`. The one-shots are `Ccm`'s own, for a `C` and +/// AAD of any length; see [`CcmPacketEncryptor`]. impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> AEADCipherDecryptor - for CcmDecryptor +> AEADCipherDecryptor + for CcmPacketDecryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// As [`CcmEncryptor::do_update_aad`](AEADCipherEncryptor::do_update_aad); the concatenation + /// As [`CcmPacketEncryptor::do_update_aad`](AEADCipherEncryptor::do_update_aad); the concatenation /// must match the encryptor's byte for byte or the tag check fails. fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { self.inner.do_update_aad(aad) @@ -1843,78 +1453,18 @@ where self.finish(tag)?; Ok(0) } - - /// `CcmPacket::decrypt_detached_out`, for a `ciphertext` and `aad` of any length: neither - /// `DATA_LEN` nor `AAD_LEN` applies to a one-shot (see [`CcmEncryptor`]). Zeroizes - /// `plaintext` on a failed tag, as `CcmPacket`'s does. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is shorter than - /// `ciphertext`; [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify; - /// plus `CcmPacket::new`'s errors. - fn decrypt_detached_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - if plaintext.len() < ciphertext.len() { - return Err(SymmetricCipherError::OutputBufferTooSmall(ciphertext.len())); - } - CcmAdapter::::check_shape(); - CcmPacket::::decrypt_detached_out( - key, nonce, aad, ciphertext, tag, plaintext, - ) - } - - /// `CcmPacket::decrypt_out`, for a `ciphertext` and `aad` of any length, as - /// [`decrypt_detached_out`](AEADCipherDecryptor::decrypt_detached_out). - /// - /// # Errors - /// As [`decrypt_detached_out`](AEADCipherDecryptor::decrypt_detached_out), with `plaintext` - /// needing [`decrypt_out_len`](SymmetricCipherDecryptor::decrypt_out_len) bytes, and - /// [`SymmetricCipherError::DecryptionFailed`] for a `C` shorter than the tag it must end with. - fn decrypt_with_aad_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - let needed = - >::decrypt_out_len( - ciphertext.len(), - ); - if plaintext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - CcmAdapter::::check_shape(); - CcmPacket::::decrypt_out( - key, nonce, aad, ciphertext, plaintext, - ) - } } /// The suspended state is the CTR half, the CBC-MAC chaining value, how much of its current /// block has gone in, and how much AAD and payload are still owed. The chaining value is /// key-dependent MAC state, which is why the state must be protected; see [`bouncycastle_utils::suspendable_state`]. -impl< - P, - Dir, - const KEY_LEN: usize, - const BLOCK_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, -> SuspendableComponent for CcmPacket +impl<'k, P, Dir, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize> + SuspendableComponent<'k> for Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = Self::CTR_STATE_LEN + BLOCK_LEN + 8 + 8 + 8; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { let (ctr, rest) = out.split_at_mut(Self::CTR_STATE_LEN); @@ -1927,7 +1477,7 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { Self::check_shape(); let (ctr, rest) = state.split_at(Self::CTR_STATE_LEN); let ctr = StreamCipher::read_state(ctr, key)?; @@ -1948,15 +1498,15 @@ where } /// The suspended state is the counter template and the next counter index; the permutation is -/// rebuilt from the re-supplied key. Crate-private like the type, reachable only through -/// `CcmPacket`'s state. -impl SuspendableComponent - for CcmKeyStream +/// the key's, re-supplied by reference. Crate-private like the type, reachable only through +/// `Ccm`'s state. +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> SuspendableComponent<'k> + for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = BLOCK_LEN + 8; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { let mut w = CursorMut::new(out); @@ -1965,8 +1515,8 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { - let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { + let perm = key; let mut r = Cursor::new(state); let ctr_template = r.array::(); // A.3: the flags octet is `[q-1]_3` and nothing else, and the counter field is zero in @@ -1994,7 +1544,7 @@ mod tests { //! the AAD length encoding against the document rather than against this implementation. use super::*; - use bouncycastle_core::key_material::KeyType; + use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// A stand-in permutation: the identity. `B0` and `Ctr_i` are formatted *before* any cipher /// call, so the identity is enough to read them back out of the state, and it keeps these @@ -2022,6 +1572,9 @@ mod tests { fn new(_key: &KeyMaterial<16>) -> Result { Ok(Identity) } + } + + impl BlockPermutation<16> for Identity { fn encrypt_block(&self, _block: &mut [u8; 16]) {} fn decrypt_block(&self, _block: &mut [u8; 16]) {} fn encrypt_2blocks(&self, _blocks: &mut [[u8; 16]; 2]) {} @@ -2053,7 +1606,7 @@ mod tests { fn c1_b0_matches_the_spec() { let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; assert_eq!( - CcmPacket::::format_b0(&nonce, true, 4), + Ccm::::format_b0(&nonce, true, 4), [0x4f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0, 0, 0, 0, 0, 0, 0, 4], "C.1 B0: flags 0x4f = Adata 1 | [(4-2)/2]_3 = 001 | [8-1]_3 = 111, then Q = [4]_64" ); @@ -2064,14 +1617,14 @@ mod tests { #[test] fn adata_flag_is_bit_6_of_the_flags_octet() { let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; - let with = CcmPacket::::format_b0(&nonce, true, 4); - let without = CcmPacket::::format_b0(&nonce, false, 4); + let with = Ccm::::format_b0(&nonce, true, 4); + let without = Ccm::::format_b0(&nonce, false, 4); assert_eq!(without[0], 0x0f, "a = 0 clears bit 6, leaving the t and q fields alone"); assert_eq!(with[0] ^ without[0], 1 << 6, "Adata is bit 6 and nothing else"); assert_eq!(with[1..], without[1..], "the flag must not disturb N or Q"); } - /// The constructor really does absorb the `B0` that [`CcmPacket::format_b0`] built. With the identity + /// The constructor really does absorb the `B0` that [`Ccm::format_b0`] built. With the identity /// permutation the CBC-MAC chaining value after one block is that block itself, so a /// no-AAD, no-payload construction leaves `B0` sitting in `y`. /// @@ -2079,12 +1632,11 @@ mod tests { #[test] fn the_constructor_absorbs_b0() { let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + let identity = Identity::new(&key()).unwrap(); let ccm = - CcmPacket::::new(&key(), &nonce, &[], 4).unwrap(); - assert_eq!( - *ccm.y, - CcmPacket::::format_b0(&nonce, false, 4) - ); + Ccm::::from_perm_with_lengths(&identity, &nonce, 0, 4) + .unwrap(); + assert_eq!(*ccm.y, Ccm::::format_b0(&nonce, false, 4)); assert_eq!(ccm.mac_pos, 0, "a whole block was absorbed, so nothing is part-filled"); } @@ -2097,7 +1649,7 @@ mod tests { fn c4_b0_matches_the_spec() { let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; assert_eq!( - CcmPacket::::format_b0(&nonce, true, 32), + Ccm::::format_b0(&nonce, true, 32), [ 0x71, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x00, 0x20 @@ -2114,9 +1666,9 @@ mod tests { #[test] fn counter_blocks_match_the_spec() { let nonce_c1 = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; - let mut ks = CcmKeyStream::::from_perm(Identity, &nonce_c1); + let mut ks = CcmKeyStream::::from_perm(&Identity, &nonce_c1); // `Ctr0` is the template with a zero counter field. - let ctr0 = CcmKeyStream::::counter_block(&ks.ctr_template, 0); + let ctr0 = CcmKeyStream::::counter_block(&ks.ctr_template, 0); assert_eq!( ctr0, [0x07, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0, 0, 0, 0, 0, 0, 0, 0], @@ -2132,9 +1684,9 @@ mod tests { let nonce_c4 = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; - let ks4 = CcmKeyStream::::from_perm(Identity, &nonce_c4); + let ks4 = CcmKeyStream::::from_perm(&Identity, &nonce_c4); assert_eq!( - CcmKeyStream::::counter_block(&ks4.ctr_template, 0), + CcmKeyStream::::counter_block(&ks4.ctr_template, 0), [ 0x01, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x00, 0x00 @@ -2151,10 +1703,10 @@ mod tests { #[test] fn a_fresh_keystream_has_every_counter_but_ctr0_left() { // n = 13, so q = 2: counters 1 ..= 65535. - let ks = CcmKeyStream::::from_perm(Identity, &[0u8; 13]); + let ks = CcmKeyStream::::from_perm(&Identity, &[0u8; 13]); assert_eq!(ks.remaining_blocks(), 65535); // n = 7, so q = 8: counters 1 ..= 2^64 - 1, which is `u64::MAX` of them. - let ks = CcmKeyStream::::from_perm(Identity, &[0u8; 7]); + let ks = CcmKeyStream::::from_perm(&Identity, &[0u8; 7]); assert_eq!(ks.remaining_blocks(), u64::MAX); } @@ -2168,9 +1720,14 @@ mod tests { } use bouncycastle_core_test_framework::key_stream::TestFrameworkKeyStream; use toy::ToyBlockCipher; + let perm = ToyBlockCipher::new(&key()).unwrap(); let framework = TestFrameworkKeyStream::new(); - framework.test::<16, 7, 16, CcmKeyStream>(); - framework.test::<16, 13, 16, CcmKeyStream>(); + framework.test::<7, 16, _>(|nonce| { + CcmKeyStream::::from_perm(&perm, nonce) + }); + framework.test::<13, 16, _>(|nonce| { + CcmKeyStream::::from_perm(&perm, nonce) + }); } /// A.2.2's three AAD length encodings, at and around both boundaries. @@ -2184,7 +1741,7 @@ mod tests { /// which through the public API would need a 4 GiB AAD. #[test] fn aad_length_encoding_matches_a_2_2() { - type Mode = CcmPacket; + type Mode<'k> = Ccm<'k, Identity, Encrypting, 16, 7, 4>; // Case 1: 0 < a < 2^16 - 2^8, two octets, `[a]_16`. assert_eq!( @@ -2237,18 +1794,20 @@ mod tests { } } - /// The constructor really uses `CcmPacket::encode_aad_len`, and puts it *before* the AAD. + /// The constructor really uses `Ccm::encode_aad_len`, and puts it *before* the AAD. /// /// With the identity permutation the CBC-MAC is `y = B0 ^ B1 ^ ... ^ Br`, so with a one-block /// all-zero AAD the only nonzero contributions are `B0` and the length encoding. That makes the /// encoding readable back out, which is what pins the ordering rather than just the value. #[test] fn the_constructor_prefixes_the_aad_with_its_length() { - type Mode = CcmPacket; + type Mode<'k> = Ccm<'k, Identity, Encrypting, 16, 7, 4>; let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; // 14 zero bytes of AAD: the 2-byte length plus 14 bytes is exactly one 16-byte block, so // there is no padding to reason about. - let ccm = Mode::new(&key(), &nonce, &[0u8; 14], 0).unwrap(); + let identity = Identity::new(&key()).unwrap(); + let mut ccm = Mode::from_perm_with_lengths(&identity, &nonce, 14, 0).unwrap(); + ccm.absorb_aad(&[0u8; 14]); let b0 = Mode::format_b0(&nonce, true, 0); let mut b1 = [0u8; 16]; @@ -2262,9 +1821,9 @@ mod tests { /// /// CCM must commit to the AAD and payload lengths before its first byte (`B0` carries the /// payload length, A.2.2 length-prefixes the AAD), so the key's constructors take them: `CCM` -/// implements the packet mode traits, and its workers, [`CcmPacketEncryptor`] / -/// [`CcmPacketDecryptor`], then take exactly those amounts in any chunking and hold nothing -/// back. The generic-trait pair with the frame in its type, [`CcmEncryptor`] / [`CcmDecryptor`], +/// implements the declared-length mode traits, and its workers, [`CcmEncryptor`] / +/// [`CcmDecryptor`], then take exactly those amounts in any chunking and hold nothing +/// back. The generic-trait pair with the frame in its type, [`CcmPacketEncryptor`] / [`CcmPacketDecryptor`], /// is the other way onto the same mode. pub struct CCM; @@ -2278,35 +1837,39 @@ impl CCM { /// The encrypting worker a `CCM` key produces: Sec 6.1 over the lengths declared at /// construction, released as it is processed. The direction is in the type. -pub struct CcmPacketEncryptor( - CcmPacket, +pub struct CcmEncryptor<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize>( + Ccm<'k, P, Encrypting, 16, NONCE_LEN, TAG_LEN>, ) where - P: ElectronicCodeBook + ModeNames; + P: BlockPermutation<16> + ModeNames; /// The decrypting worker a `CCM` key produces: Sec 6.2 over the lengths declared at /// construction. The payload is released as it is recovered, up to the declared length; what /// follows can only be the inline tag, and is held back for the final. -pub struct CcmPacketDecryptor +pub struct CcmDecryptor<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - ccm: CcmPacket, + ccm: Ccm<'k, P, Decrypting, 16, NONCE_LEN, TAG_LEN>, // Public either way (the tag travels in the clear), so not wrapped. tag: [u8; TAG_LEN], tag_len: usize, } -impl - AeadEncryptWorker - for CcmPacketEncryptor +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> Algorithm + for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - self.0.do_update_aad(aad) - } + const ALG_NAME: &'static str = P::CCM_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> + SymmetricCipherEncryptor for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ /// The identity: nothing is held back, since `B0` is already committed to the length. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -2335,6 +1898,20 @@ where Ok((self.0.do_encrypt_final()?, TAG_LEN)) } + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + TAG_LEN + } +} + +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> + AEADCipherEncryptor for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.0.do_update_aad(aad) + } + fn do_encrypt_final_detached_out( self, ciphertext: &mut [u8; TAG_LEN], @@ -2343,25 +1920,25 @@ where Ok((0, self.0.do_encrypt_final()?)) } - fn encrypt_out_len(plaintext_len: usize) -> usize { - plaintext_len + TAG_LEN - } - fn encrypt_detached_out_len(plaintext_len: usize) -> usize { plaintext_len } } -impl - AeadDecryptWorker - for CcmPacketDecryptor +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> Algorithm + for CcmDecryptor<'k, P, NONCE_LEN, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - self.ccm.do_update_aad(aad) - } + const ALG_NAME: &'static str = P::CCM_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> + SymmetricCipherDecryptor for CcmDecryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ /// Every byte still inside the declared payload; the rest can only be the tag, and is held. fn do_decrypt_out_len(&self, input_len: usize) -> usize { input_len.min(self.ccm.owed) @@ -2400,6 +1977,20 @@ where Ok(([0u8; TAG_LEN], 0)) } + fn decrypt_out_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TAG_LEN) + } +} + +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> + AEADCipherDecryptor for CcmDecryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.ccm.do_update_aad(aad) + } + /// Detached: the ciphertext must be exactly the declared payload, with nothing after it. fn do_decrypt_final_detached_out( self, @@ -2414,20 +2005,18 @@ where Ok(0) } - fn decrypt_out_len(ciphertext_len: usize) -> usize { - ciphertext_len.saturating_sub(TAG_LEN) - } - fn decrypt_detached_out_len(ciphertext_len: usize) -> usize { ciphertext_len } } // The mode impls are generic over the permutation: `Mode

` needs no `KEY_LEN`, since CCM keeps -// nothing per key but the schedule, and the packet mode traits carry what else they need. The -// in-place one-shots override the provided bodies to run the packet directly over the buffer. +// nothing per key but the schedule, and the declared-length mode traits carry what else they need. The +// in-place one-shots override the provided bodies to run `Ccm` directly over the buffer. -impl Mode

for CCM { +impl Mode

+ for CCM +{ type KeyState = P; const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; @@ -2437,34 +2026,32 @@ impl Mode

for CCM - AeadPacketEncryptMode for CCM + AeadDeclaredEncryptMode for CCM where P: ElectronicCodeBook + ModeNames + Clone, { - type Encryptor = CcmPacketEncryptor; + type Encryptor<'k> + = CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> + where + P: 'k; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, aad_len: usize, payload_len: usize, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { - let nonce = CcmPacket::::random_nonce(rng)?; + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { + let nonce = Ccm::::random_nonce(rng)?; Ok((Self::encrypt_init_nonce(state, &nonce, aad_len, payload_len)?, nonce)) } - fn encrypt_init_nonce( - state: &Self::KeyState, + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result { - Ok(CcmPacketEncryptor(CcmPacket::from_perm_with_lengths( - state.clone(), - nonce, - aad_len, - payload_len, - )?)) + ) -> Result, SymmetricCipherError> { + Ok(CcmEncryptor(Ccm::from_perm_with_lengths(state, nonce, aad_len, payload_len)?)) } fn encrypt_detached_in_place_nonce( @@ -2473,32 +2060,31 @@ where aad: &[u8], buf: &mut [u8], ) -> Result<[u8; TAG_LEN], SymmetricCipherError> { - let mut ccm = CcmPacket::::from_perm( - state.clone(), - nonce, - aad, - buf.len(), - )?; + let mut ccm = + Ccm::::from_perm(state, nonce, aad, buf.len())?; ccm.do_encrypt_inplace(buf)?; ccm.do_encrypt_final() } } impl - AeadPacketDecryptMode for CCM + AeadDeclaredDecryptMode for CCM where P: ElectronicCodeBook + ModeNames + Clone, { - type Decryptor = CcmPacketDecryptor; + type Decryptor<'k> + = CcmDecryptor<'k, P, NONCE_LEN, TAG_LEN> + where + P: 'k; - fn decrypt_init( - state: &Self::KeyState, + fn decrypt_init<'k>( + state: &'k Self::KeyState, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result { - Ok(CcmPacketDecryptor { - ccm: CcmPacket::from_perm_with_lengths(state.clone(), nonce, aad_len, payload_len)?, + ) -> Result, SymmetricCipherError> { + Ok(CcmDecryptor { + ccm: Ccm::from_perm_with_lengths(state, nonce, aad_len, payload_len)?, tag: [0u8; TAG_LEN], tag_len: 0, }) @@ -2513,12 +2099,8 @@ where buf: &mut [u8], tag: &[u8; TAG_LEN], ) -> Result<(), SymmetricCipherError> { - let mut ccm = CcmPacket::::from_perm( - state.clone(), - nonce, - aad, - buf.len(), - )?; + let mut ccm = + Ccm::::from_perm(state, nonce, aad, buf.len())?; let result = ccm.do_decrypt_inplace(buf).and_then(|()| ccm.do_decrypt_final(tag)); if result.is_err() { buf.fill(0); @@ -2528,12 +2110,13 @@ where } /// The mode type of the fixed-frame pair: the `M` of a mode-bound key such as -/// `AES128Key>`, on the AEAD key path with [`CcmEncryptor`] / -/// [`CcmDecryptor`] as its workers, which take exactly `DATA_LEN` bytes of payload and up to +/// `AES128Key>`, on the AEAD key path with [`CcmPacketEncryptor`] / +/// [`CcmPacketDecryptor`] as its workers, which take exactly `DATA_LEN` bytes of payload and up to /// `AAD_LEN` of AAD. The one-shots a key provides are written over those workers, so they take /// the frame length too; a message of any length goes through [`CCM`], whose constructors take /// the lengths. The key state is the keyed permutation alone, as for [`CCM`]. -pub struct CCMFrame< +#[allow(non_camel_case_types)] +pub struct CCM_Packet< const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, @@ -2546,7 +2129,7 @@ impl< const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> Mode

for CCMFrame +> Mode

for CCM_Packet { type KeyState = P; const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; @@ -2564,21 +2147,27 @@ impl< const AAD_LEN: usize, const DATA_LEN: usize, > AeadEncryptMode - for CCMFrame + for CCM_Packet where P: ElectronicCodeBook + ModeNames + Clone, { - type Encryptor = CcmEncryptor; + type Encryptor<'k> + = CcmPacketEncryptor<'k, P, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> + where + P: 'k; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { - CcmEncryptor::from_perm(state.clone(), rng) + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { + CcmPacketEncryptor::from_perm(state, rng) } - fn encrypt_init_nonce(state: &Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor { - CcmEncryptor::from_perm_nonce(state.clone(), nonce) + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; NONCE_LEN], + ) -> Self::Encryptor<'k> { + CcmPacketEncryptor::from_perm_nonce(state, nonce) } } @@ -2590,156 +2179,52 @@ impl< const AAD_LEN: usize, const DATA_LEN: usize, > AeadDecryptMode - for CCMFrame + for CCM_Packet where P: ElectronicCodeBook + ModeNames + Clone, { - type Decryptor = CcmDecryptor; + type Decryptor<'k> + = CcmPacketDecryptor<'k, P, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor { - CcmDecryptor::from_perm(state.clone(), nonce) + fn decrypt_init<'k>(state: &'k Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { + CcmPacketDecryptor::from_perm(state, nonce) } } // The key-produced worker traits for the fixed-frame pair, forwarded to the cipher traits' own // methods while those still exist; named by path as `gcm.rs` names them. -impl< - P, - const KEY_LEN: usize, - const BLOCK_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const AAD_LEN: usize, - const DATA_LEN: usize, -> AeadEncryptWorker - for CcmEncryptor -where - P: ElectronicCodeBook + ModeNames, -{ - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - AEADCipherEncryptor::do_update_aad(self, aad) - } - - fn do_encrypt_out_len(&self, input_len: usize) -> usize { - SymmetricCipherEncryptor::do_encrypt_out_len(self, input_len) - } - - fn do_encrypt_out( - &mut self, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result { - SymmetricCipherEncryptor::do_encrypt_out(self, plaintext, ciphertext) - } - - fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - SymmetricCipherEncryptor::do_encrypt_final(self) - } - - fn do_encrypt_final_detached_out( - self, - ciphertext: &mut [u8; TAG_LEN], - ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { - AEADCipherEncryptor::do_encrypt_final_detached_out(self, ciphertext) - } - - fn encrypt_out_len(plaintext_len: usize) -> usize { - >::encrypt_out_len( - plaintext_len, - ) - } - - fn encrypt_detached_out_len(plaintext_len: usize) -> usize { - >::encrypt_detached_out_len( - plaintext_len, - ) - } -} - -impl< - P, - const KEY_LEN: usize, - const BLOCK_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const AAD_LEN: usize, - const DATA_LEN: usize, -> AeadDecryptWorker - for CcmDecryptor -where - P: ElectronicCodeBook + ModeNames, -{ - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - AEADCipherDecryptor::do_update_aad(self, aad) - } - - fn do_decrypt_out_len(&self, input_len: usize) -> usize { - SymmetricCipherDecryptor::do_decrypt_out_len(self, input_len) - } - - fn do_decrypt_out( - &mut self, - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - SymmetricCipherDecryptor::do_decrypt_out(self, ciphertext, plaintext) - } - - fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - SymmetricCipherDecryptor::do_decrypt_final(self) - } - - fn do_decrypt_final_detached_out( - self, - tag: &[u8; TAG_LEN], - plaintext: &mut [u8; TAG_LEN], - ) -> Result { - AEADCipherDecryptor::do_decrypt_final_detached_out(self, tag, plaintext) - } - - fn decrypt_out_len(ciphertext_len: usize) -> usize { - >::decrypt_out_len( - ciphertext_len, - ) - } - - fn decrypt_detached_out_len(ciphertext_len: usize) -> usize { - >::decrypt_detached_out_len( - ciphertext_len, - ) - } -} - // ---- suspending the pair ----------------------------------------------------------------- -/// The adapter's suspended state is the packet's, then the held-back AAD, its length and the +/// The adapter's suspended state is the `Ccm` state's, then the held-back AAD, its length and the /// phase flag. A state that says the header is not yet formatted must have absorbed nothing, /// and the payload still owed can be no more than the frame. impl< + 'k, P, Dir, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> SuspendableComponent - for CcmAdapter +> SuspendableComponent<'k> + for CcmAdapter<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = - as SuspendableComponent>::STATE_LEN + as SuspendableComponent<'k>>::STATE_LEN + AAD_LEN + 8 + 1; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { let (ccm, rest) = out.split_at_mut( - as SuspendableComponent>::STATE_LEN, + as SuspendableComponent<'k>>::STATE_LEN, ); self.ccm.write_state(ccm); let mut w = CursorMut::new(rest); @@ -2749,12 +2234,12 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { Self::check_shape(); let (ccm, rest) = state.split_at( - as SuspendableComponent>::STATE_LEN, + as SuspendableComponent<'k>>::STATE_LEN, ); - let ccm = CcmPacket::read_state(ccm, key)?; + let ccm = Ccm::read_state(ccm, key)?; let mut r = Cursor::new(rest); let aad: [u8; AAD_LEN] = r.array(); let aad_len = bounded_usize(r.u64(), AAD_LEN)?; @@ -2774,81 +2259,80 @@ where } impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> SuspendableComponent - for CcmEncryptor +> SuspendableComponent<'k> + for CcmPacketEncryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = as SuspendableComponent>::STATE_LEN; - type Key = KeyMaterial; + > as SuspendableComponent<'k>>::STATE_LEN; + type Key = P; fn write_state(&self, out: &mut [u8]) { self.0.write_state(out); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { Ok(Self(CcmAdapter::read_state(state, key)?)) } } /// The decryptor adds the bytes it is holding back past the frame and how many there are. impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> SuspendableComponent - for CcmDecryptor +> SuspendableComponent<'k> + for CcmPacketDecryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = as SuspendableComponent>::STATE_LEN + > as SuspendableComponent<'k>>::STATE_LEN + TAG_LEN + 8; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { - let (inner, rest) = - out.split_at_mut( - as SuspendableComponent>::STATE_LEN, - ); + let (inner, rest) = out.split_at_mut( + as SuspendableComponent<'k>>::STATE_LEN, + ); self.inner.write_state(inner); let mut w = CursorMut::new(rest); w.bytes(&self.tag); @@ -2856,20 +2340,19 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { - let (inner, rest) = - state.split_at( - as SuspendableComponent>::STATE_LEN, - ); + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { + let (inner, rest) = state.split_at( + as SuspendableComponent<'k>>::STATE_LEN, + ); let inner = CcmAdapter::read_state(inner, key)?; let mut r = Cursor::new(rest); let tag: [u8; TAG_LEN] = r.array(); @@ -2880,88 +2363,90 @@ where } impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> CcmEncryptor +> CcmPacketEncryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the mode's + /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the version header, the mode's /// state, the `AAD_LEN`-byte AAD buffer, its length and the phase flag. See /// [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = - LIB_VERSION_LEN + ::STATE_LEN; + LIB_VERSION_LEN + >::STATE_LEN; } impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, -> CcmDecryptor +> CcmPacketDecryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// The `N` of this type's [`SuspendableKeyed`] impl: the encryptor's, plus the held-back + /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the encryptor's, plus the held-back /// tag and its length. See [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = - LIB_VERSION_LEN + ::STATE_LEN; + LIB_VERSION_LEN + >::STATE_LEN; } -/// `N` must be [`CcmEncryptor::SUSPENDED_STATE_LEN`]; anything else is a compile error. +/// `N` must be [`CcmPacketEncryptor::SUSPENDED_STATE_LEN`]; anything else is a compile error. impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, const N: usize, -> SuspendableKeyed for CcmEncryptor +> SuspendableKeyed<'k, N> + for CcmPacketEncryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - type Key = KeyMaterial; + type Key = P; fn suspend(self) -> [u8; N] { suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } -/// `N` must be [`CcmDecryptor::SUSPENDED_STATE_LEN`]; anything else is a compile error. +/// `N` must be [`CcmPacketDecryptor::SUSPENDED_STATE_LEN`]; anything else is a compile error. impl< + 'k, P, - const KEY_LEN: usize, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, const N: usize, -> SuspendableKeyed for CcmDecryptor +> SuspendableKeyed<'k, N> + for CcmPacketDecryptor<'k, P, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - type Key = KeyMaterial; + type Key = P; fn suspend(self) -> [u8; N] { suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } diff --git a/crypto/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs index a37d38724..5c05d0061 100644 --- a/crypto/cipher/src/modes/cfb.rs +++ b/crypto/cipher/src/modes/cfb.rs @@ -26,8 +26,8 @@ //! As a stream cipher producing an XOR key stream, the forward (encryption) and reverse (decryption) //! are the same: both directions apply `CIPH_K`. //! -//! So [`Cfb`](Cfb) is implemented over a permutation that impls [`ElectronicCodeBook`], -//! but never calls [`ElectronicCodeBook::decrypt_block`]. A permutation that implements only the +//! So [`Cfb<'k, P, Decrypting, ..>`](Cfb) is implemented over a permutation that impls [`ElectronicCodeBook`], +//! but never calls [`BlockPermutation::decrypt_block`]. A permutation that implements only the //! forward direction would still work here. The two directions differ only in which of the two //! values -- the byte that came in, or the byte that went out -- is the ciphertext to be fed back into //! the next block. @@ -40,15 +40,15 @@ //! decryption, the required forward cipher operations can be performed in parallel if the input //! blocks are first constructed (in series) from the IV and the ciphertext." //! -//! This implementation follows: encryption handles blocks singly via [`ElectronicCodeBook::encrypt_block`], +//! This implementation follows: encryption handles blocks singly via [`BlockPermutation::encrypt_block`], //! while decryption can batch-process two or four blocks at a time via -//! [`ElectronicCodeBook::encrypt_2blocks`] or [`ElectronicCodeBook::encrypt_4blocks`], which may +//! [`BlockPermutation::encrypt_2blocks`] or [`BlockPermutation::encrypt_4blocks`], which may //! yield a performance gain, depending on the implementation of the underlying permutation. //! //! # Usage Examples //! -//! The direction is part of the type: [`Cfb`](Cfb) implements -//! [`StreamCipherEncryptor`] and nothing else, and [`Cfb`](Cfb) implements +//! The direction is part of the type: [`Cfb<'k, P, Encrypting, ..>`](Cfb) implements +//! [`StreamCipherEncryptor`] and nothing else, and [`Cfb<'k, P, Decrypting, ..>`](Cfb) implements //! [`StreamCipherDecryptor`] and nothing else. //! //! They take a `&mut [u8]` of any length; there is no padding layer and the ciphertext is exactly @@ -147,15 +147,14 @@ use crate::modes::iv::random_iv; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ - ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, + BlockPermutation, ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, }; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_utils::suspendable_state::{ Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, suspend_component, @@ -186,15 +185,18 @@ impl SymmetricEncryptMode + ModeNames + Clone, { - type Encryptor = Cfb; + type Encryptor<'k> + = Cfb<'k, P, Encrypting, BLOCK_LEN> + where + P: 'k; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; BLOCK_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { // `I1 = IV`. let iv = random_iv::(rng)?; - Ok((Cfb::start(state.clone(), iv), iv)) + Ok((Cfb::start(state, iv), iv)) } } @@ -203,10 +205,16 @@ impl SymmetricDecryptMode + ModeNames + Clone, { - type Decryptor = Cfb; + type Decryptor<'k> + = Cfb<'k, P, Decrypting, BLOCK_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState, init_data: &[u8; BLOCK_LEN]) -> Self::Decryptor { - Cfb::start(state.clone(), *init_data) + fn decrypt_init<'k>( + state: &'k Self::KeyState, + init_data: &[u8; BLOCK_LEN], + ) -> Self::Decryptor<'k> { + Cfb::start(state, *init_data) } } @@ -218,17 +226,17 @@ where /// blocks is handled. /// /// `Dir` is [`Encrypting`] or [`Decrypting`]. [`StreamCipherEncryptor`] is implemented only for the -/// former and [`StreamCipherDecryptor`] only for the latter, so a `Cfb<_, Encrypting, _, _>` has no +/// former and [`StreamCipherDecryptor`] only for the latter, so a `Cfb<'k, _, Encrypting, _, _>` has no /// decryption methods at all -- using one in the wrong direction is a compile error rather than a /// runtime check. /// /// The initialization data is one block, so `INIT_DATA_LEN == BLOCK_LEN`. #[derive(Clone)] -pub struct Cfb +pub struct Cfb<'k, P, Dir, const BLOCK_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - perm: P, + perm: &'k P, /// `buf[..used]` is the ciphertext of the current segment so far, i.e. the head of `I_{j+1}`; /// `buf[used..]` is the unused tail of `Oj`. When `used == BLOCK_LEN` the whole buffer is the /// next input block (initially `I1 = IV`) and no keystream is pending. @@ -250,17 +258,17 @@ where _dir: PhantomData

, } -impl Cfb +impl<'k, P, Dir, const BLOCK_LEN: usize> Cfb<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the segment + /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the version header, the segment /// buffer and the `used` count as a `u64`. See [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN + BLOCK_LEN + 8; /// `I1 = IV`, with no segment open: the first byte in either direction will compute `O1`. #[inline] - fn start(perm: P, iv: [u8; BLOCK_LEN]) -> Self { + fn start(perm: &'k P, iv: [u8; BLOCK_LEN]) -> Self { Self { perm, buf: iv, used: BLOCK_LEN, _dir: PhantomData } } @@ -336,7 +344,7 @@ where self.buf = cj; } - /// Decrypts two consecutive blocks with one [`ElectronicCodeBook::encrypt_2blocks`] call. + /// Decrypts two consecutive blocks with one [`BlockPermutation::encrypt_2blocks`] call. /// /// Writing the pair as `Cj, Cj+1` with `Ij` the incoming input block, the `s = b` equations /// give @@ -370,7 +378,7 @@ where } } - /// Decrypts four consecutive blocks with one [`ElectronicCodeBook::encrypt_4blocks`] call. + /// Decrypts four consecutive blocks with one [`BlockPermutation::encrypt_4blocks`] call. /// /// The same construction as [`Self::decrypt_pair`] widened to four: the input blocks are the /// incoming input block followed by the first three ciphertext blocks, all known before any @@ -405,10 +413,9 @@ where } } -impl Algorithm - for Cfb +impl<'k, P, Dir, const BLOCK_LEN: usize> Algorithm for Cfb<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// The permutation's name for this mode, [`ModeNames::CFB_ALG_NAME`]. const ALG_NAME: &'static str = P::CFB_ALG_NAME; @@ -416,30 +423,11 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl - SymmetricCipherEncryptor for Cfb +impl<'k, P, const BLOCK_LEN: usize> SymmetricCipherEncryptor + for Cfb<'k, P, Encrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Begins an encryption flow, generating the IV from the library's default OS-backed DRBG. - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - Self::do_encrypt_init_rng(key, &mut rng) - } - - /// As [`SymmetricCipherEncryptor::do_encrypt_init`], but takes the IV from the provided RNG. - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { - let perm = P::new(key)?; - // `I1 = IV`. - let iv = random_iv::(rng)?; - Ok((Self::start(perm, iv), iv)) - } - /// Every input byte produces exactly one output byte. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -477,10 +465,10 @@ where } } -impl StreamCipherEncryptor - for Cfb +impl<'k, P, const BLOCK_LEN: usize> StreamCipherEncryptor + for Cfb<'k, P, Encrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Encrypts `data`, of any length, in place. /// @@ -502,22 +490,11 @@ where } } -impl - SymmetricCipherDecryptor for Cfb +impl<'k, P, const BLOCK_LEN: usize> SymmetricCipherDecryptor + for Cfb<'k, P, Decrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Begins a decryption flow from the IV returned by - /// [`SymmetricCipherEncryptor::do_encrypt_init`]. - fn do_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; BLOCK_LEN], - ) -> Result { - let perm = P::new(key)?; - // `I1 = IV`, exactly as on the encrypt side. - Ok(Self::start(perm, *init_data)) - } - /// Nothing is held back, so every input byte can be released immediately. fn do_decrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -555,10 +532,10 @@ where } } -impl StreamCipherDecryptor - for Cfb +impl<'k, P, const BLOCK_LEN: usize> StreamCipherDecryptor + for Cfb<'k, P, Decrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Decrypts `data`, of any length, in place. /// @@ -588,15 +565,14 @@ where } /// The suspended state is `buf` and `used` -- the open segment, which is public ciphertext and -/// the keystream not yet used against it -- in both directions; the permutation is rebuilt from -/// the re-supplied key. See [`bouncycastle_utils::suspendable_state`]. -impl SuspendableComponent - for Cfb +/// the keystream not yet used against it -- in both directions; the permutation is the key's, +/// re-supplied by reference. See [`bouncycastle_utils::suspendable_state`]. +impl<'k, P, Dir, const BLOCK_LEN: usize> SuspendableComponent<'k> for Cfb<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = BLOCK_LEN + 8; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { let mut w = CursorMut::new(out); @@ -605,8 +581,8 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { - let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { + let perm = key; let mut r = Cursor::new(state); let buf = r.array::(); // `used` is `0..=BLOCK_LEN`; anything past the buffer would index out of it. @@ -617,18 +593,18 @@ where } /// `N` must be [`Cfb::SUSPENDED_STATE_LEN`]; anything else is a compile error. -impl SuspendableKeyed - for Cfb +impl<'k, P, Dir, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> + for Cfb<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - type Key = KeyMaterial; + type Key = P; fn suspend(self) -> [u8; N] { suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } diff --git a/crypto/cipher/src/modes/cfb8.rs b/crypto/cipher/src/modes/cfb8.rs index 41dd34c62..36517dd7c 100644 --- a/crypto/cipher/src/modes/cfb8.rs +++ b/crypto/cipher/src/modes/cfb8.rs @@ -57,15 +57,14 @@ use crate::modes::iv::random_iv; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ - ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, + BlockPermutation, ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, }; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_utils::suspendable_state::{ LIB_VERSION_LEN, SuspendableComponent, resume_component, suspend_component, }; @@ -95,15 +94,18 @@ impl SymmetricEncryptMode + ModeNames + Clone, { - type Encryptor = Cfb8; + type Encryptor<'k> + = Cfb8<'k, P, Encrypting, BLOCK_LEN> + where + P: 'k; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; BLOCK_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { // `I1 = IV`. let iv = random_iv::(rng)?; - Ok((Cfb8 { perm: state.clone(), chain: iv, _dir: PhantomData }, iv)) + Ok((Cfb8 { perm: state, chain: iv, _dir: PhantomData }, iv)) } } @@ -112,10 +114,16 @@ impl SymmetricDecryptMode + ModeNames + Clone, { - type Decryptor = Cfb8; + type Decryptor<'k> + = Cfb8<'k, P, Decrypting, BLOCK_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState, init_data: &[u8; BLOCK_LEN]) -> Self::Decryptor { - Cfb8 { perm: state.clone(), chain: *init_data, _dir: PhantomData } + fn decrypt_init<'k>( + state: &'k Self::KeyState, + init_data: &[u8; BLOCK_LEN], + ) -> Self::Decryptor<'k> { + Cfb8 { perm: state, chain: *init_data, _dir: PhantomData } } } @@ -125,7 +133,7 @@ where /// interoperable with [`cfb`], which is `s = b`. /// /// `Dir` is [`Encrypting`] or [`Decrypting`]. [`StreamCipherEncryptor`] is implemented only for the -/// former and [`StreamCipherDecryptor`] only for the latter, so a `Cfb8<_, Encrypting, _, _>` has +/// former and [`StreamCipherDecryptor`] only for the latter, so a `Cfb8<'k, _, Encrypting, _, _>` has /// no decryption methods at all -- using one in the wrong direction is a compile error rather than /// a runtime check. /// @@ -133,30 +141,29 @@ where /// /// # State /// -/// Two fields, the same size as `Cbc`: the permutation (which owns the key schedule, and is -/// responsible for keeping it in a zeroize-on-drop wrapper) and one block holding the shift -/// register `Ij`. `Ij` is built from the IV and ciphertext bytes, both of which are public, so it +/// Two fields, the same size as `Cbc`: a reference to the permutation, whose key schedule stays +/// in the key, and one block holding the shift register `Ij`. `Ij` is built from the IV and ciphertext bytes, both of which are public, so it /// is deliberately not wrapped in a `Secret`. /// /// Note what is *not* stored: the output block `Oj`. It is recomputed from the register on each /// byte and lives only in a local, so no keystream outlives the call that used it. No partial /// segment is stored either, because a segment is one byte. #[derive(Clone)] -pub struct Cfb8 +pub struct Cfb8<'k, P, Dir, const BLOCK_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - perm: P, + perm: &'k P, /// `Ij`: the IV, then the shift register. See the module docs. chain: [u8; BLOCK_LEN], _dir: PhantomData, } -impl Cfb8 +impl<'k, P, Dir, const BLOCK_LEN: usize> Cfb8<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// The `N` of this type's [`SuspendableKeyed`] impl: the version header and the shift + /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the version header and the shift /// register. See [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN + BLOCK_LEN; @@ -214,10 +221,9 @@ where } } -impl Algorithm - for Cfb8 +impl<'k, P, Dir, const BLOCK_LEN: usize> Algorithm for Cfb8<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// The permutation's name for this mode, [`ModeNames::CFB8_ALG_NAME`]. const ALG_NAME: &'static str = P::CFB8_ALG_NAME; @@ -225,30 +231,11 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl - SymmetricCipherEncryptor for Cfb8 +impl<'k, P, const BLOCK_LEN: usize> SymmetricCipherEncryptor + for Cfb8<'k, P, Encrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Begins an encryption flow, generating the IV from the library's default OS-backed DRBG. - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - Self::do_encrypt_init_rng(key, &mut rng) - } - - /// As [`SymmetricCipherEncryptor::do_encrypt_init`], but takes the IV from the provided RNG. - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { - let perm = P::new(key)?; - // `I1 = IV`. - let iv = random_iv::(rng)?; - Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) - } - /// Every input byte produces exactly one output byte. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -286,10 +273,10 @@ where } } -impl StreamCipherEncryptor - for Cfb8 +impl<'k, P, const BLOCK_LEN: usize> StreamCipherEncryptor + for Cfb8<'k, P, Encrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Encrypts `data`, of any length, in place: `Cj = Pj XOR MSB_8(CIPH_K(Ij))` for each byte, /// then `Cj` shifts into the register. @@ -306,22 +293,11 @@ where } } -impl - SymmetricCipherDecryptor for Cfb8 +impl<'k, P, const BLOCK_LEN: usize> SymmetricCipherDecryptor + for Cfb8<'k, P, Decrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Begins a decryption flow from the IV returned by - /// [`SymmetricCipherEncryptor::do_encrypt_init`]. - fn do_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; BLOCK_LEN], - ) -> Result { - let perm = P::new(key)?; - // `I1 = IV`, exactly as on the encrypt side. - Ok(Self { perm, chain: *init_data, _dir: PhantomData }) - } - /// Nothing is held back, so every input byte can be released immediately. fn do_decrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -359,10 +335,10 @@ where } } -impl StreamCipherDecryptor - for Cfb8 +impl<'k, P, const BLOCK_LEN: usize> StreamCipherDecryptor + for Cfb8<'k, P, Decrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Decrypts `data`, of any length, in place: `Pj = Cj XOR MSB_8(CIPH_K(Ij))` for each byte, /// with the *ciphertext* byte -- the one that came in, not the plaintext going out -- shifted @@ -390,22 +366,21 @@ where } } -/// The suspended state is the shift register `Ij`, in both directions; the permutation is -/// rebuilt from the re-supplied key. See [`bouncycastle_utils::suspendable_state`]. -impl SuspendableComponent - for Cfb8 +/// The suspended state is the shift register `Ij`, in both directions; the permutation is the +/// key's, re-supplied by reference. See [`bouncycastle_utils::suspendable_state`]. +impl<'k, P, Dir, const BLOCK_LEN: usize> SuspendableComponent<'k> for Cfb8<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = BLOCK_LEN; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { out.copy_from_slice(&self.chain); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { - let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { + let perm = key; let mut chain = [0u8; BLOCK_LEN]; chain.copy_from_slice(state); Ok(Self { perm, chain, _dir: PhantomData }) @@ -413,18 +388,18 @@ where } /// `N` must be [`Cfb8::SUSPENDED_STATE_LEN`]; anything else is a compile error. -impl SuspendableKeyed - for Cfb8 +impl<'k, P, Dir, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> + for Cfb8<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - type Key = KeyMaterial; + type Key = P; fn suspend(self) -> [u8; N] { suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } diff --git a/crypto/cipher/src/modes/cmac.rs b/crypto/cipher/src/modes/cmac.rs index 4e609d900..3ad63f50e 100644 --- a/crypto/cipher/src/modes/cmac.rs +++ b/crypto/cipher/src/modes/cmac.rs @@ -5,10 +5,10 @@ //! which [`MAC::mac_out`] / [`MAC::do_final_out`] truncate to the caller's buffer, down to //! [`CMAC_MIN_TAG_LEN`] bytes. //! -//! There are two ways onto it. [`MAC::new`] takes the key material for one message and derives -//! the subkeys each time. A mode-bound key holds a [`CmacKeyState`] -- the keyed permutation and -//! the two subkeys, derived once -- and hands out workers through [`CMAC`], the marker a key type -//! such as `bouncycastle_aes::AES128Key` is bound to. +//! The way onto it is a mode-bound key, built with `from_key_material`, which holds a +//! [`CmacKeyState`] -- the keyed permutation and the two subkeys, derived once -- and hands out +//! workers through [`CMAC`], the marker a key type such as `bouncycastle_aes::AES128Key` is +//! bound to. //! //! # Usage Examples //! @@ -61,8 +61,8 @@ //! * **Message span** (Appendix B): limit a key to 2^48 messages with a 128-bit block cipher, and //! 2^21 with a 64-bit one. //! * **Key separation** (Sec 5.2): the key "shall be used exclusively for the CMAC mode of the -//! chosen block cipher", so [`MAC::new`] accepts only a [`KeyType::MACKey`] key (or an all-zero -//! key, as HMAC does), never a [`KeyType::SymmetricCipherKey`]. +//! chosen block cipher", so the key's `from_key_material` accepts only a [`KeyType::MACKey`] +//! key (or an all-zero key, as HMAC does), never a [`KeyType::SymmetricCipherKey`]. //! * **Replay** (Appendix C): a valid tag says nothing about freshness; that is the protocol's job. use super::ModeNames; @@ -249,19 +249,6 @@ impl MAC for Cmac + ModeNames, { - /// # Errors - /// [`KeyMaterialError::InvalidKeyType`] unless the key is a [`KeyType::MACKey`] (or all-zero), - /// [`KeyMaterialError::SecurityStrength`] if it is weaker than the block cipher, and - /// [`KeyMaterialError::InvalidLength`] unless it is exactly `KEY_LEN` bytes. - fn new(key: &impl KeyMaterialTrait) -> Result { - Self::init(key, false) - } - - /// As [`MAC::new`], without the security strength check. - fn new_allow_weak_key(key: &impl KeyMaterialTrait) -> Result { - Self::init(key, true) - } - fn output_len(&self) -> usize { BLOCK_LEN } @@ -370,8 +357,8 @@ where /// # Errors /// [`SuspendableError::IncompatibleVersion`] for a state from an incompatible library version, - /// and [`SuspendableError::InvalidData`] for a malformed state or a key [`MAC::new_allow_weak_key`] - /// would reject. + /// and [`SuspendableError::InvalidData`] for a malformed state or a key the CMAC key's + /// `from_key_material_allow_weak` would reject. fn from_suspended(state: [u8; STATE_LEN], key: &Self::Key) -> Result { const { assert!( diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index 71454d30a..de85bff6d 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -34,11 +34,11 @@ //! use bouncycastle_cipher::{Decrypting, Encrypting}; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGcm = Gcm; +//! type ToyGcm<'k, Dir> = Gcm<'k, ToyBlockCipher, Dir, 16>; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let state = GcmKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); +//! let state = GcmKeyState::new(ToyBlockCipher::new(&key).unwrap()); //! let plaintext: [u8; 16] = *b"attack at dawn!!"; //! //! let (mut enc, nonce) = ToyGcm::::from_key_state(&state, &mut HashDRBG_SHA512::new_from_os()) @@ -82,11 +82,11 @@ //! use bouncycastle_cipher::{Decrypting, Encrypting}; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGcm = Gcm; +//! type ToyGcm<'k, Dir> = Gcm<'k, ToyBlockCipher, Dir, 16>; //! //! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let state = GcmKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); +//! let state = GcmKeyState::new(ToyBlockCipher::new(&key).unwrap()); //! let aad = b"some associated data"; //! let message = b"a message that streams in over more than one call"; //! @@ -122,11 +122,11 @@ //! use bouncycastle_cipher::{Decrypting, Encrypting}; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGcm = Gcm; +//! type ToyGcm<'k, Dir> = Gcm<'k, ToyBlockCipher, Dir, 16>; //! //! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let state = GcmKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); +//! let state = GcmKeyState::new(ToyBlockCipher::new(&key).unwrap()); //! let aad = b"some associated data"; //! let message = b"a message that streams in over more than one call"; //! @@ -150,9 +150,9 @@ //! # Suspending and resuming execution //! //! [`Gcm`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte -//! array and resumed later with the re-supplied key. The state is the CTR half, the running GHASH, -//! the byte counts and whatever a decryptor is holding back as a possible tag; `H` and the tag mask -//! are re-derived from the key. The array length is `Gcm::SUSPENDED_STATE_LEN`; see [the crate +//! array and resumed later over the re-supplied key state. The state is the CTR half, the running +//! GHASH, the byte counts and whatever a decryptor is holding back as a possible tag; `H` is the +//! key state's and the tag mask is re-derived from it. The array length is `Gcm::SUSPENDED_STATE_LEN`; see [the crate //! docs](crate#suspending-and-resuming-execution) for an example. //! //! # 🚨 Security Considerations 🚨 @@ -199,14 +199,15 @@ use crate::modes::ModeNames; use crate::modes::hazmat::ctr_key_stream::CtrKeyStream; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::hazmat::{ + AeadDecryptMode, AeadEncryptMode, BlockPermutation, ElectronicCodeBook, Mode, +}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_utils::ct::ct_eq_bytes; use bouncycastle_utils::secret::Secret; use bouncycastle_utils::suspendable_state::{ @@ -221,12 +222,66 @@ pub const GCM_NONCE_LEN: usize = 12; /// The GCM mode type: the `M` of a mode-bound key such as `AES128Key`. `GCM` is GCM with /// the full 128-bit tag; `GCM<12>` and so on select a shorter tag, within the `12..=16` byte range -/// [`Gcm`] accepts. The [`Mode`] and AEAD mode-trait impls that connect it to a key type are -/// written by each permutation's crate while the cipher traits still carry `KEY_LEN`. -/// -/// [`Mode`]: bouncycastle_core::hazmat::Mode +/// [`Gcm`] accepts. The [`Mode`] and AEAD mode-trait impls below are generic over any +/// [`BlockPermutation`] with a 128-bit block, so a permutation crate's key type needs nothing of +/// its own. pub struct GCM; +impl Mode

for GCM +where + P: BlockPermutation<16> + ModeNames + Clone, +{ + type KeyState = GcmKeyState

; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + GcmKeyState::new(perm) + } +} + +impl + AeadEncryptMode for GCM +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Encryptor<'k> + = Gcm<'k, P, Encrypting, TAG_LEN> + where + P: 'k; + + fn encrypt_init<'k>( + state: &'k Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + Gcm::::from_key_state(state, rng) + } + + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Encryptor<'k> { + Gcm::::from_key_state_nonce(state, nonce) + } +} + +impl + AeadDecryptMode for GCM +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Decryptor<'k> + = Gcm<'k, P, Decrypting, TAG_LEN> + where + P: 'k; + + fn decrypt_init<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Decryptor<'k> { + Gcm::::from_key_state(state, nonce) + } +} + /// What a GCM key keeps per key: the keyed permutation and the hash subkey `H = CIPH_K(0^128)` /// (SP 800-38D Algorithm 4 step 1), derived once here rather than once per message. It is the /// `KeyState` of [`GCM`] over any permutation, and is only built and consumed through the mode @@ -239,9 +294,9 @@ pub struct GcmKeyState

{ impl

GcmKeyState

{ /// Derives `H` from `perm`. - pub fn new(perm: P) -> Self + pub fn new(perm: P) -> Self where - P: ElectronicCodeBook, + P: BlockPermutation<16>, { // Encrypted in place inside a `Secret` so that `H` is never held in an unzeroized stack // array (Sec 5.3; Appendix A on what `H` gives an attacker). @@ -273,15 +328,16 @@ impl Phase { /// Galois/Counter Mode over any [`ElectronicCodeBook`] permutation, direction typed as /// [`Encrypting`] / [`Decrypting`]. See the module docs for the two APIs this type exposes and -/// [`GCM_NONCE_LEN`] / `TAG_LEN` for what is fixed and what is chosen. +/// [`GCM_NONCE_LEN`] / `TAG_LEN` for what is fixed and what is chosen. A worker borrows the key +/// state it was made from for `'k`: the schedule and `H` stay in the key. #[derive(Clone)] -pub struct Gcm +pub struct Gcm<'k, P, Dir, const TAG_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { /// `GCTR_K(inc32(J0), .)`: Algorithm 4 step 3 / Algorithm 5 step 4, started at counter 2 (see /// [`Gcm::setup`]). - ctr: Ctr, + ctr: Ctr<'k, P, Dir, 16, GCM_NONCE_LEN>, /// `GHASH_H` over `A || 0^v || C || 0^u`, Algorithm 4/5 step 5/6. ghash: Ghash, /// `CIPH_K(J0)`, the one-time mask for the tag (step 6's `GCTR_K(J0, S) = S (+) CIPH_K(J0)`, @@ -303,19 +359,19 @@ where _dir: PhantomData

, } -impl Gcm +impl<'k, P, Dir, const TAG_LEN: usize> Gcm<'k, P, Dir, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the CTR state, + /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the version header, the CTR state, /// the GHASH state, the two byte counts, the phase, the held-back tail and its length. See /// [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = - LIB_VERSION_LEN + ::STATE_LEN; + LIB_VERSION_LEN + >::STATE_LEN; /// The CTR half's share of the suspended state. const CTR_STATE_LEN: usize = - as SuspendableComponent>::STATE_LEN; + as SuspendableComponent<'k>>::STATE_LEN; /// The compile-time shape check: `TAG_LEN` must be one of Sec 5.2.1.2's five recommended tag /// lengths in bytes (96, 104, 112, 120, 128 bits -- Appendix C's 32- and 64-bit tags are a @@ -331,14 +387,9 @@ where }; } - /// Algorithm 4 steps 1-2 and the precomputation for step 6's tag mask, from a bare permutation. - fn setup(perm: P, nonce: [u8; GCM_NONCE_LEN]) -> Self { - Self::setup_from_state(GcmKeyState::new::(perm), nonce) - } - /// Algorithm 4 step 2 and the precomputation for step 6's tag mask, over a key state that - /// already holds step 1's `H`. - fn setup_from_state(state: GcmKeyState

, nonce: [u8; GCM_NONCE_LEN]) -> Self { + /// already holds step 1's `H`, which the worker borrows for `'k`. + fn setup_from_state(state: &'k GcmKeyState

, nonce: [u8; GCM_NONCE_LEN]) -> Self { Self::check_shape(); let GcmKeyState { perm, h } = state; @@ -358,7 +409,7 @@ where Self { ctr, - ghash: Ghash::new(&h), + ghash: Ghash::new(h), ek_j0, aad_len: 0, data_len: 0, @@ -434,45 +485,38 @@ where } } -impl Algorithm for Gcm +impl<'k, P, Dir, const TAG_LEN: usize> Algorithm for Gcm<'k, P, Dir, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { const ALG_NAME: &'static str = P::GCM_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl Gcm +impl<'k, P, const TAG_LEN: usize> Gcm<'k, P, Encrypting, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - /// Begins an encryption from a key's stored state under a nonce drawn from `rng`: the - /// encryption constructor a permutation crate's `AeadEncryptMode` impl for [`GCM`] calls. The - /// result is identical to [`SymmetricCipherEncryptor::do_encrypt_init_rng`] from the same key - /// and RNG. + /// Begins an encryption over a key's stored state under a nonce drawn from `rng`: the + /// encryption constructor a permutation crate's `AeadEncryptMode` impl for [`GCM`] calls. + /// Nothing is copied out of `state`; the worker reads the schedule and `H` through the + /// borrow and cannot outlive it. /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. pub fn from_key_state( - state: &GcmKeyState

, + state: &'k GcmKeyState

, rng: &mut dyn RNG, - ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> - where - P: Clone, - { + ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { Self::check_shape(); let nonce = crate::modes::iv::random_iv::(rng)?; - Ok((Self::setup_from_state(state.clone(), nonce), nonce)) + Ok((Self::setup_from_state(state, nonce), nonce)) } /// As [`Self::from_key_state`] under a nonce the caller supplies: the constructor a - /// permutation crate's `AeadEncryptMode::encrypt_init_nonce` impl for [`GCM`] calls. The - /// result is identical to [`AEADCipherEncryptor::do_encrypt_init_nonce`] from the same key. - pub fn from_key_state_nonce(state: &GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self - where - P: Clone, - { - Self::setup_from_state(state.clone(), *nonce) + /// permutation crate's `AeadEncryptMode::encrypt_init_nonce` impl for [`GCM`] calls. + pub fn from_key_state_nonce(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { + Self::setup_from_state(state, *nonce) } /// Encrypts `data` in place (GCTR, Algorithm 4 step 3) and absorbs the resulting ciphertext @@ -501,29 +545,11 @@ where } } -impl - SymmetricCipherEncryptor - for Gcm +impl<'k, P, const TAG_LEN: usize> SymmetricCipherEncryptor + for Gcm<'k, P, Encrypting, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - Self::do_encrypt_init_rng(key, &mut rng) - } - - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - Self::check_shape(); - let perm = P::new(key)?; - let nonce = crate::modes::iv::random_iv::(rng)?; - Ok((Self::setup(perm, nonce), nonce)) - } - /// The identity: GCM's encryptor holds nothing back. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -556,28 +582,15 @@ where /// The AEAD view: [`AEADCipherEncryptor`] over the [`SymmetricCipherEncryptor`] impl above, with /// `FINAL_LEN = TAG_LEN`. The encryptor holds nothing back, so the detached final flushes nothing /// and returns only the tag. -impl - AEADCipherEncryptor - for Gcm +impl<'k, P, const TAG_LEN: usize> AEADCipherEncryptor + for Gcm<'k, P, Encrypting, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { self.absorb_aad(aad) } - /// [`do_encrypt_init_rng`](SymmetricCipherEncryptor::do_encrypt_init_rng) with the caller's - /// nonce in place of the draw; SP 800-38D Sec 8.2.1's deterministic construction is the - /// caller's to follow. - fn do_encrypt_init_nonce( - key: &KeyMaterial, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Result { - Self::check_shape(); - let perm = P::new(key)?; - Ok(Self::setup(perm, *nonce)) - } - /// Algorithm 4 steps 4-6; nothing is held back, so `ciphertext` is left zeroed. fn do_encrypt_final_detached_out( self, @@ -588,18 +601,15 @@ where } } -impl Gcm +impl<'k, P, const TAG_LEN: usize> Gcm<'k, P, Decrypting, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { /// Begins the decryption of a message encrypted under `nonce` from a key's stored state: the /// decryption constructor a permutation crate's `AeadDecryptMode` impl for [`GCM`] calls. /// Infallible: the state is already keyed and the nonce length is in the type. - pub fn from_key_state(state: &GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self - where - P: Clone, - { - Self::setup_from_state(state.clone(), *nonce) + pub fn from_key_state(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { + Self::setup_from_state(state, *nonce) } /// Absorbs `data` (ciphertext) into GHASH, then decrypts it in place. Order matters and is the @@ -633,50 +643,13 @@ where Err(SymmetricCipherError::AEADTagCheckFailed) } } - - /// Shared by the trait one-shots (`decrypt_out`, `decrypt_detached_out`, - /// `decrypt_with_aad_out`): absorbs `aad` and - /// `data` (still ciphertext) into GHASH and checks the tag *before* touching `data`, so no - /// unauthenticated plaintext is ever written to the caller's buffer. The preamble of Sec 7 - /// explicitly permits this: "in Algorithm 5, the verification of the tag may precede the - /// computation of the plaintext". Only on success is `data` decrypted. - fn verify_then_decrypt( - key: &KeyMaterial, - nonce: &[u8; GCM_NONCE_LEN], - aad: &[u8], - data: &mut [u8], - tag: &[u8; TAG_LEN], - ) -> Result<(), SymmetricCipherError> { - Self::check_shape(); - let perm = P::new(key)?; - let mut gcm = Self::setup(perm, *nonce); - gcm.absorb_aad(aad)?; - gcm.absorb_data(data)?; - let mut computed: Secret<[u8; 16]> = Secret::new(); - gcm.tag_block(&mut computed); - if !ct_eq_bytes(&computed[..TAG_LEN], tag) { - return Err(SymmetricCipherError::AEADTagCheckFailed); - } - gcm.ctr.do_decrypt_inplace(data)?; - Ok(()) - } } -impl - SymmetricCipherDecryptor - for Gcm +impl<'k, P, const TAG_LEN: usize> SymmetricCipherDecryptor + for Gcm<'k, P, Decrypting, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - fn do_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; GCM_NONCE_LEN], - ) -> Result { - Self::check_shape(); - let perm = P::new(key)?; - Ok(Self::setup(perm, *init_data)) - } - /// `tail_len + input_len`, minus up to `TAG_LEN` bytes held back because they might be the tag. fn do_decrypt_out_len(&self, input_len: usize) -> usize { (self.tail_len + input_len).saturating_sub(TAG_LEN) @@ -745,35 +718,15 @@ where fn decrypt_out_len(ciphertext_len: usize) -> usize { ciphertext_len.saturating_sub(TAG_LEN) } - - /// Overrides the trait's default (which would stream plaintext out before the tag is checked): - /// verifies the tag first and only then decrypts, so this one-shot never exposes - /// unauthenticated plaintext. The streaming path above, by its nature, still does. - fn decrypt_out( - key: &KeyMaterial, - init_data: &[u8; GCM_NONCE_LEN], - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - >::decrypt_with_aad_out( - key, - init_data, - &[], - ciphertext, - plaintext, - ) - } } /// The AEAD view: [`AEADCipherDecryptor`] over the [`SymmetricCipherDecryptor`] impl above, with /// `FINAL_LEN = TAG_LEN`. The one-shots are overridden, as `decrypt_out` is, to check the tag /// before any plaintext is written. -impl - AEADCipherDecryptor - for Gcm +impl<'k, P, const TAG_LEN: usize> AEADCipherDecryptor + for Gcm<'k, P, Decrypting, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { self.absorb_aad(aad) @@ -797,70 +750,18 @@ where } Ok(n) } - - /// Verifies `tag` before decrypting, so no unauthenticated plaintext reaches `plaintext`; on - /// failure what was written there is zeroized. - fn decrypt_detached_out( - key: &KeyMaterial, - nonce: &[u8; GCM_NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - let len = ciphertext.len(); - if plaintext.len() < len { - return Err(SymmetricCipherError::OutputBufferTooSmall(len)); - } - plaintext[..len].copy_from_slice(ciphertext); - Self::verify_then_decrypt(key, nonce, aad, &mut plaintext[..len], tag).inspect_err( - |_| { - // The buffer holds ciphertext rather than unauthenticated plaintext here, since the - // tag is checked before decryption, but the trait's contract is a zeroized buffer on - // failure, and a caller who ignores the `Result` should find nothing in it at all. - plaintext[..len].fill(0); - }, - )?; - Ok(len) - } - - /// The inline layout with AAD: splits the trailing `TAG_LEN` bytes off as the tag and verifies - /// it before decrypting, as the detached one-shot does, zeroizing `plaintext` on failure. - fn decrypt_with_aad_out( - key: &KeyMaterial, - nonce: &[u8; GCM_NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - let needed = Self::decrypt_out_len(ciphertext.len()); - if plaintext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let Some((data, tag)) = ciphertext.split_last_chunk::() else { - return Err(SymmetricCipherError::DecryptionFailed); - }; - let len = data.len(); - plaintext[..len].copy_from_slice(data); - Self::verify_then_decrypt(key, nonce, aad, &mut plaintext[..len], tag) - .inspect_err(|_| plaintext[..len].fill(0))?; - Ok(len) - } } /// The suspended state is the CTR half, the running GHASH, the two byte counts, the phase, and -/// the up-to-`TAG_LEN` bytes a decryptor holds back. `H` and `CIPH_K(J0)` are not in it: both -/// derive from the key and the nonce, and `setup` re-derives them on resume. See +/// the up-to-`TAG_LEN` bytes a decryptor holds back. `H` and `CIPH_K(J0)` are not in it: `H` is +/// the key state's, and `CIPH_K(J0)` is re-derived from it and the nonce on resume. See /// [`bouncycastle_utils::suspendable_state`]. -impl SuspendableComponent - for Gcm +impl<'k, P, Dir, const TAG_LEN: usize> SuspendableComponent<'k> for Gcm<'k, P, Dir, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { const STATE_LEN: usize = Self::CTR_STATE_LEN + GHASH_STATE_LEN + 8 + 8 + 1 + TAG_LEN + 8; - type Key = KeyMaterial; + type Key = GcmKeyState

; fn write_state(&self, out: &mut [u8]) { let (ctr, rest) = out.split_at_mut(Self::CTR_STATE_LEN); @@ -876,19 +777,18 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { Self::check_shape(); let (ctr, rest) = state.split_at(Self::CTR_STATE_LEN); let (ghash, rest) = rest.split_at(GHASH_STATE_LEN); - // `setup` re-derives `H` and `CIPH_K(J0)` from the key and the nonce, which is the - // leading part of the CTR state. Its fresh CTR and GHASH are then replaced by the - // suspended ones; the CTR read expands the key a second time, a one-off cost at resume. - let nonce = CtrKeyStream::::nonce_from_state(ctr); - let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; - let mut gcm = Self::setup(perm, nonce); - gcm.ctr = as SuspendableComponent>::read_state( - ctr, key, + // `setup_from_state` takes `H` from the key state and re-derives `CIPH_K(J0)` from the + // nonce, which is the leading part of the CTR state. Its fresh CTR and GHASH are then + // replaced by the suspended ones. + let nonce = CtrKeyStream::<'k, P, 16, GCM_NONCE_LEN>::nonce_from_state(ctr); + let mut gcm = Self::setup_from_state(key, nonce); + gcm.ctr = as SuspendableComponent<'k>>::read_state( + ctr, &key.perm, )?; gcm.ghash.restore_state(ghash)?; @@ -904,18 +804,18 @@ where } /// `N` must be [`Gcm::SUSPENDED_STATE_LEN`]; anything else is a compile error. -impl SuspendableKeyed - for Gcm +impl<'k, P, Dir, const TAG_LEN: usize, const N: usize> SuspendableKeyed<'k, N> + for Gcm<'k, P, Dir, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - type Key = KeyMaterial; + type Key = GcmKeyState

; fn suspend(self) -> [u8; N] { suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } @@ -923,100 +823,6 @@ where // The key-produced worker traits, forwarded to the cipher traits' own methods while those still // exist. Named by path rather than imported, since the method names coincide and an import would // make every call in this file ambiguous. - -impl - bouncycastle_core::symmetric_key::AeadEncryptWorker - for Gcm -where - P: ElectronicCodeBook + ModeNames, -{ - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - AEADCipherEncryptor::do_update_aad(self, aad) - } - - fn do_encrypt_out_len(&self, input_len: usize) -> usize { - SymmetricCipherEncryptor::do_encrypt_out_len(self, input_len) - } - - fn do_encrypt_out( - &mut self, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result { - SymmetricCipherEncryptor::do_encrypt_out(self, plaintext, ciphertext) - } - - fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - SymmetricCipherEncryptor::do_encrypt_final(self) - } - - fn do_encrypt_final_detached_out( - self, - ciphertext: &mut [u8; TAG_LEN], - ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { - AEADCipherEncryptor::do_encrypt_final_detached_out(self, ciphertext) - } - - fn encrypt_out_len(plaintext_len: usize) -> usize { - >::encrypt_out_len( - plaintext_len, - ) - } - - fn encrypt_detached_out_len(plaintext_len: usize) -> usize { - >::encrypt_detached_out_len( - plaintext_len, - ) - } -} - -impl - bouncycastle_core::symmetric_key::AeadDecryptWorker - for Gcm -where - P: ElectronicCodeBook + ModeNames, -{ - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - AEADCipherDecryptor::do_update_aad(self, aad) - } - - fn do_decrypt_out_len(&self, input_len: usize) -> usize { - SymmetricCipherDecryptor::do_decrypt_out_len(self, input_len) - } - - fn do_decrypt_out( - &mut self, - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - SymmetricCipherDecryptor::do_decrypt_out(self, ciphertext, plaintext) - } - - fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - SymmetricCipherDecryptor::do_decrypt_final(self) - } - - fn do_decrypt_final_detached_out( - self, - tag: &[u8; TAG_LEN], - plaintext: &mut [u8; TAG_LEN], - ) -> Result { - AEADCipherDecryptor::do_decrypt_final_detached_out(self, tag, plaintext) - } - - fn decrypt_out_len(ciphertext_len: usize) -> usize { - >::decrypt_out_len( - ciphertext_len, - ) - } - - fn decrypt_detached_out_len(ciphertext_len: usize) -> usize { - >::decrypt_detached_out_len( - ciphertext_len, - ) - } -} - mod ghash { //! GHASH: the universal hash function GCM builds its authentication on (NIST SP 800-38D Sec 6.3, //! 6.4), and the GF(2^128) multiplication it is defined over. diff --git a/crypto/cipher/src/modes/gmac.rs b/crypto/cipher/src/modes/gmac.rs index 118cf8c09..23fa0e8ee 100644 --- a/crypto/cipher/src/modes/gmac.rs +++ b/crypto/cipher/src/modes/gmac.rs @@ -83,9 +83,7 @@ use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ElectronicCodeBook, do_hazardous_operations}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{ - AEADCipherEncryptor, Algorithm, RNG, SymmetricCipherDecryptor, SymmetricCipherEncryptor, -}; +use bouncycastle_core::traits::{Algorithm, RNG}; /// The GMAC mode type: the `M` of a mode-bound key such as `AES128Key`. `GMAC` is GMAC with /// the full 128-bit tag; `GMAC<12>` and so on select a shorter tag, within the `12..=16` byte @@ -176,9 +174,12 @@ where pub fn new( key: &KeyMaterial, rng: &mut dyn RNG, - ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - let cipher_key = mac_key_as_cipher_key(key)?; - let (gcm, nonce) = Gcm::do_encrypt_init_rng(&cipher_key, rng)?; + ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> + where + P: Clone, + { + let state = GcmKeyState::new::(P::new(&mac_key_as_cipher_key(key)?)?); + let (gcm, nonce) = Gcm::::from_key_state(&state, rng)?; Ok((Self { gcm }, nonce)) } @@ -189,9 +190,12 @@ where pub fn new_nonce( key: &KeyMaterial, nonce: &[u8; GCM_NONCE_LEN], - ) -> Result { - let cipher_key = mac_key_as_cipher_key(key)?; - Ok(Self { gcm: Gcm::do_encrypt_init_nonce(&cipher_key, nonce)? }) + ) -> Result + where + P: Clone, + { + let state = GcmKeyState::new::(P::new(&mac_key_as_cipher_key(key)?)?); + Ok(Self { gcm: Gcm::from_key_state_nonce(&state, nonce) }) } /// Absorbs more of the message. @@ -232,9 +236,12 @@ where pub fn new( key: &KeyMaterial, nonce: &[u8; GCM_NONCE_LEN], - ) -> Result { - let cipher_key = mac_key_as_cipher_key(key)?; - Ok(Self { gcm: Gcm::do_decrypt_init(&cipher_key, nonce)? }) + ) -> Result + where + P: Clone, + { + let state = GcmKeyState::new::(P::new(&mac_key_as_cipher_key(key)?)?); + Ok(Self { gcm: Gcm::::from_key_state(&state, nonce) }) } /// Absorbs more of the message. diff --git a/crypto/cipher/src/modes/hazmat/ecb.rs b/crypto/cipher/src/modes/hazmat/ecb.rs index a2875253e..08b4cd010 100644 --- a/crypto/cipher/src/modes/hazmat/ecb.rs +++ b/crypto/cipher/src/modes/hazmat/ecb.rs @@ -47,15 +47,15 @@ //! cipher functions can be computed in parallel". //! //! To take advantage of this parallelism, this mode batches both directions through the -//! permutation's four-block and pair methods ([`ElectronicCodeBook::encrypt_4blocks`] / -//! [`ElectronicCodeBook::encrypt_2blocks`] and their inverses), which may represent a speed-up over +//! permutation's four-block and pair methods ([`BlockPermutation::encrypt_4blocks`] / +//! [`BlockPermutation::encrypt_2blocks`] and their inverses), which may represent a speed-up over //! iterating one block at a time, depending on the implementation of the underlying cipher. //! //! # Suspending and resuming execution //! //! [`Ecb`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte //! array and resumed later with the re-supplied key. The state is empty, since nothing carries over -//! between blocks, and resuming is re-expanding the key; it exists so the padded adapters over ECB +//! between blocks, and resuming is taking the key state again; it exists so the padded adapters over ECB //! can be suspended. The array length is `Ecb::SUSPENDED_STATE_LEN`; see [the crate //! docs](crate#suspending-and-resuming-execution) for an example. //! @@ -86,10 +86,10 @@ use crate::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ - EcbDecryptMode, EcbEncryptMode, ElectronicCodeBook, Mode, SymmetricDecryptMode, - SymmetricEncryptMode, + BlockPermutation, EcbDecryptMode, EcbEncryptMode, ElectronicCodeBook, Mode, + SymmetricDecryptMode, SymmetricEncryptMode, }; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding, RNG, @@ -123,10 +123,13 @@ impl EcbEncryptMode + ModeNames + Clone, { - type Encryptor = Ecb; + type Encryptor<'k> + = Ecb<'k, P, Encrypting, BLOCK_LEN> + where + P: 'k; - fn encrypt_init(state: &Self::KeyState) -> Self::Encryptor { - Ecb::from_perm(state.clone()) + fn encrypt_init<'k>(state: &'k Self::KeyState) -> Self::Encryptor<'k> { + Ecb::from_perm(state) } } @@ -135,10 +138,13 @@ impl EcbDecryptMode + ModeNames + Clone, { - type Decryptor = Ecb; + type Decryptor<'k> + = Ecb<'k, P, Decrypting, BLOCK_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState) -> Self::Decryptor { - Ecb::from_perm(state.clone()) + fn decrypt_init<'k>(state: &'k Self::KeyState) -> Self::Decryptor<'k> { + Ecb::from_perm(state) } } @@ -148,21 +154,18 @@ where P: ElectronicCodeBook + ModeNames + Clone, Pad: BlockCipherPadding, { - type Encryptor = PaddedBlockCipherEncryptor< - Ecb, - Pad, - KEY_LEN, - 0, - BLOCK_LEN, - >; + type Encryptor<'k> + = PaddedBlockCipherEncryptor, Pad, 0, BLOCK_LEN> + where + P: 'k; /// ECB has no init data, so nothing is drawn from `rng` and the init data is the empty /// array. - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, _rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; 0]), SymmetricCipherError> { - Ok((PaddedBlockCipherEncryptor::from_inner(Ecb::from_perm(state.clone())), [])) + ) -> Result<(Self::Encryptor<'k>, [u8; 0]), SymmetricCipherError> { + Ok((PaddedBlockCipherEncryptor::from_inner(Ecb::from_perm(state)), [])) } } @@ -172,16 +175,13 @@ where P: ElectronicCodeBook + ModeNames + Clone, Pad: BlockCipherPadding, { - type Decryptor = PaddedBlockCipherDecryptor< - Ecb, - Pad, - KEY_LEN, - 0, - BLOCK_LEN, - >; + type Decryptor<'k> + = PaddedBlockCipherDecryptor, Pad, 0, BLOCK_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState, _init_data: &[u8; 0]) -> Self::Decryptor { - PaddedBlockCipherDecryptor::from_inner(Ecb::from_perm(state.clone())) + fn decrypt_init<'k>(state: &'k Self::KeyState, _init_data: &[u8; 0]) -> Self::Decryptor<'k> { + PaddedBlockCipherDecryptor::from_inner(Ecb::from_perm(state)) } } @@ -191,46 +191,40 @@ where /// Considerations". Provided for interoperability and test vectors. /// /// `Dir` is [`Encrypting`] or [`Decrypting`]. [`BlockCipherEncryptor`] is implemented only for the -/// former and [`BlockCipherDecryptor`] only for the latter, so an `Ecb<_, Encrypting, _, _>` has no +/// former and [`BlockCipherDecryptor`] only for the latter, so an `Ecb<'k, _, Encrypting, _, _>` has no /// decryption methods at all -- using one in the wrong direction is a compile error rather than a /// runtime check. /// /// # State /// Only the permutation, which owns the key schedule and is responsible for keeping it in a /// zeroize-on-drop wrapper. Nothing chains from one block to the next, so unlike `Cbc` and `Cfb` -/// there is no block of chaining value: `size_of::>() == size_of::

()`. +/// there is no block of chaining value: `size_of::>() == size_of::

()`. #[derive(Clone)] -pub struct Ecb +pub struct Ecb<'k, P, Dir, const BLOCK_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - perm: P, + perm: &'k P, _dir: PhantomData

, } -impl Ecb +impl<'k, P, Dir, const BLOCK_LEN: usize> Ecb<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// The `N` of this type's [`SuspendableKeyed`] impl: the version header alone, since ECB + /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the version header alone, since ECB /// has no state between blocks. See [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN; - /// Expands the key. Both `_init` constructors are this; there is nothing else to set up. - fn new(key: &KeyMaterial) -> Result { - Ok(Self::from_perm(P::new(key)?)) - } - /// Over an already-keyed permutation: what the [`ECB`] mode impls build. - pub(crate) fn from_perm(perm: P) -> Self { + pub(crate) fn from_perm(perm: &'k P) -> Self { Self { perm, _dir: PhantomData } } } -impl Algorithm - for Ecb +impl<'k, P, Dir, const BLOCK_LEN: usize> Algorithm for Ecb<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// The permutation's name for this mode, [`ModeNames::ECB_ALG_NAME`]. const ALG_NAME: &'static str = P::ECB_ALG_NAME; @@ -239,37 +233,12 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl BlockCipherEncryptor - for Ecb +impl<'k, P, const BLOCK_LEN: usize> BlockCipherEncryptor<0, BLOCK_LEN> + for Ecb<'k, P, Encrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Expands the key. ECB has no initialization data (SP 800-38A Table D.2 lists the IV column - /// as "Not applicable"), so the returned init data is the empty array. - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; 0]), SymmetricCipherError> { - Ok((Self::new(key)?, [])) - } - - /// Always panics: ECB generates no init data, so there is nothing for an RNG to do. - /// - /// # Panics - /// Unconditionally, as [`BlockCipherEncryptor::do_encrypt_init_rng`] requires of a mode whose - /// `INIT_DATA_LEN` is 0. Reaching for the RNG-taking constructor means the caller expects a - /// randomized mode, and ECB is not one -- SP 800-38A Table D.2 lists its IV column as "Not - /// applicable" -- so silently ignoring the RNG would leave that mistaken expectation - /// undisturbed. Use [`do_encrypt_init`](Self::do_encrypt_init), or a mode that has an IV. - fn do_encrypt_init_rng( - _key: &KeyMaterial, - _rng: &mut dyn RNG, - ) -> Result<(Self, [u8; 0]), SymmetricCipherError> { - unimplemented!( - "ECB has no initialization data, so it draws nothing from an RNG: use do_encrypt_init, \ - or a mode with an IV if a randomized ciphertext was wanted" - ) - } - + /// The implementor hook: /// `Cj = CIPH_K(Pj)` for every block, in place. /// /// Sec 6.1 allows the forward cipher functions to "be computed in parallel", so the blocks go @@ -296,20 +265,12 @@ where } } -impl BlockCipherDecryptor - for Ecb +impl<'k, P, const BLOCK_LEN: usize> BlockCipherDecryptor<0, BLOCK_LEN> + for Ecb<'k, P, Decrypting, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Expands the key. The init data is the empty array [`BlockCipherEncryptor::do_encrypt_init`] - /// returned; there is nothing in it to use. - fn do_decrypt_init( - key: &KeyMaterial, - _init_data: &[u8; 0], - ) -> Result { - Self::new(key) - } - + /// The implementor hook: /// `Pj = CIPH^-1_K(Cj)` for every block, in place -- fours, then pairs, then the remaining /// block, as on the encrypt side. Never fails. fn do_decrypt_blocks_inplace( @@ -333,39 +294,38 @@ where } /// ECB carries nothing from one block to the next, so its suspended state is empty and resuming -/// is re-expanding the key. It is implemented so that the padded adapters over it, which do hold +/// is taking the key state again. It is implemented so that the padded adapters over it, which do hold /// a partial block, can be suspended. See [`bouncycastle_utils::suspendable_state`]. -impl SuspendableComponent - for Ecb +impl<'k, P, Dir, const BLOCK_LEN: usize> SuspendableComponent<'k> for Ecb<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = 0; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { debug_assert!(out.is_empty()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { debug_assert!(state.is_empty()); - Self::new(key).map_err(|_| SuspendableError::InvalidData) + Ok(Self::from_perm(key)) } } /// `N` must be [`Ecb::SUSPENDED_STATE_LEN`]; anything else is a compile error. -impl SuspendableKeyed - for Ecb +impl<'k, P, Dir, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> + for Ecb<'k, P, Dir, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - type Key = KeyMaterial; + type Key = P; fn suspend(self) -> [u8; N] { suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } diff --git a/crypto/cipher/src/padding/padded_block_cipher.rs b/crypto/cipher/src/padding/padded_block_cipher.rs index a6c43bdf1..a037a47ac 100644 --- a/crypto/cipher/src/padding/padded_block_cipher.rs +++ b/crypto/cipher/src/padding/padded_block_cipher.rs @@ -8,11 +8,10 @@ //! all, in which case `do_encrypt_final` reports 0 of the `FINAL_LEN` bytes as output. use bouncycastle_core::errors::{PaddingError, SuspendableError, SymmetricCipherError}; -use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding, RNG, - SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding, SuspendableKeyed, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; use bouncycastle_utils::secret::Secret; use bouncycastle_utils::suspendable_state::{ @@ -28,20 +27,15 @@ const GROUP: usize = 8; /// Encrypts arbitrary-length data with a block cipher `E`, padding the final block with `P`. /// /// Stream with [`SymmetricCipherEncryptor::do_encrypt_out`] then -/// [`SymmetricCipherEncryptor::do_encrypt_final`], or use the one-shot -/// [`SymmetricCipherEncryptor::encrypt_out`]. Output is +/// [`SymmetricCipherEncryptor::do_encrypt_final`], or use the key's one-shot `encrypt_out`. +/// Output is /// `plaintext_len / BLOCK_LEN + 1` blocks for a scheme that always pads (PKCS7), and exactly the /// input length for one that never does (`NoPadding`, which rejects an unaligned input at /// `do_encrypt_final`). The buffered partial plaintext block is held in a [`Secret`]. #[derive(Clone)] -pub struct PaddedBlockCipherEncryptor< - E, - P, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, -> where - E: BlockCipherEncryptor, +pub struct PaddedBlockCipherEncryptor +where + E: BlockCipherEncryptor, P: BlockCipherPadding, { encryptor: E, @@ -51,10 +45,10 @@ pub struct PaddedBlockCipherEncryptor< buf_len: usize, } -impl Algorithm - for PaddedBlockCipherEncryptor +impl Algorithm + for PaddedBlockCipherEncryptor where - E: BlockCipherEncryptor, + E: BlockCipherEncryptor, P: BlockCipherPadding, { /// The inner cipher's name; padding does not change what the algorithm is. @@ -63,10 +57,10 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = E::MAX_SECURITY_STRENGTH; } -impl - PaddedBlockCipherEncryptor +impl + PaddedBlockCipherEncryptor where - E: BlockCipherEncryptor, + E: BlockCipherEncryptor, P: BlockCipherPadding, { /// The adapter over an already-constructed inner cipher, with nothing buffered: what a mode @@ -76,28 +70,13 @@ where } } -impl - SymmetricCipherEncryptor - for PaddedBlockCipherEncryptor +impl + SymmetricCipherEncryptor + for PaddedBlockCipherEncryptor where - E: BlockCipherEncryptor, + E: BlockCipherEncryptor, P: BlockCipherPadding, { - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { - let (encryptor, init_data) = E::do_encrypt_init(key)?; - Ok((Self { encryptor, _padding: PhantomData, buf: Secret::new(), buf_len: 0 }, init_data)) - } - - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { - let (encryptor, init_data) = E::do_encrypt_init_rng(key, rng)?; - Ok((Self { encryptor, _padding: PhantomData, buf: Secret::new(), buf_len: 0 }, init_data)) - } - /// Whole blocks among the buffered bytes plus `input_len`. fn do_encrypt_out_len(&self, input_len: usize) -> usize { (self.buf_len + input_len) / BLOCK_LEN * BLOCK_LEN @@ -197,16 +176,11 @@ where /// /// Only the last block carries padding, so [`do_update_out`](Self::do_decrypt_out) always withholds /// the most recent complete block and [`do_decrypt_final`](Self::do_decrypt_final) unpads it. -/// One-shot: [`decrypt_out`](Self::decrypt_out). +/// One-shot: the key's `decrypt_out`. #[derive(Clone)] -pub struct PaddedBlockCipherDecryptor< - D, - P, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, -> where - D: BlockCipherDecryptor, +pub struct PaddedBlockCipherDecryptor +where + D: BlockCipherDecryptor, P: BlockCipherPadding, { decryptor: D, @@ -218,10 +192,10 @@ pub struct PaddedBlockCipherDecryptor< held: Option<[u8; BLOCK_LEN]>, } -impl Algorithm - for PaddedBlockCipherDecryptor +impl Algorithm + for PaddedBlockCipherDecryptor where - D: BlockCipherDecryptor, + D: BlockCipherDecryptor, P: BlockCipherPadding, { /// The inner cipher's name; padding does not change what the algorithm is. @@ -230,10 +204,10 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = D::MAX_SECURITY_STRENGTH; } -impl - PaddedBlockCipherDecryptor +impl + PaddedBlockCipherDecryptor where - D: BlockCipherDecryptor, + D: BlockCipherDecryptor, P: BlockCipherPadding, { /// The adapter over an already-constructed inner cipher, with nothing buffered or held @@ -243,26 +217,13 @@ where } } -impl - SymmetricCipherDecryptor - for PaddedBlockCipherDecryptor +impl + SymmetricCipherDecryptor + for PaddedBlockCipherDecryptor where - D: BlockCipherDecryptor, + D: BlockCipherDecryptor, P: BlockCipherPadding, { - fn do_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ) -> Result { - Ok(Self { - decryptor: D::do_decrypt_init(key, init_data)?, - _padding: PhantomData, - buf: [0u8; BLOCK_LEN], - buf_len: 0, - held: None, - }) - } - /// All complete blocks but the most recent one are released. fn do_decrypt_out_len(&self, input_len: usize) -> usize { let complete = self.held.is_some() as usize + (self.buf_len + input_len) / BLOCK_LEN; @@ -365,25 +326,25 @@ where } } -impl - PaddedBlockCipherEncryptor +impl<'k, E, P, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize> + PaddedBlockCipherEncryptor where - E: BlockCipherEncryptor + SuspendableComponent, + E: BlockCipherEncryptor + SuspendableComponent<'k>, P: BlockCipherPadding, { /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the inner /// cipher's state, the partial plaintext block and its length as a `u64`. See /// [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = - LIB_VERSION_LEN + ::STATE_LEN; + LIB_VERSION_LEN + >::STATE_LEN; } /// The suspended state is the inner cipher's followed by the buffered partial block. That block /// is plaintext, so the state must be protected; see [`bouncycastle_utils::suspendable_state`]. -impl - SuspendableComponent for PaddedBlockCipherEncryptor +impl<'k, E, P, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize> SuspendableComponent<'k> + for PaddedBlockCipherEncryptor where - E: BlockCipherEncryptor + SuspendableComponent, + E: BlockCipherEncryptor + SuspendableComponent<'k>, P: BlockCipherPadding, { const STATE_LEN: usize = E::STATE_LEN + BLOCK_LEN + 8; @@ -398,7 +359,7 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { let (inner, rest) = state.split_at(E::STATE_LEN); let encryptor = E::read_state(inner, key)?; let mut r = Cursor::new(rest); @@ -413,10 +374,10 @@ where /// `N` must be [`PaddedBlockCipherEncryptor::SUSPENDED_STATE_LEN`]; anything else is a compile /// error. -impl - SuspendableKeyed for PaddedBlockCipherEncryptor +impl<'k, E, P, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, const N: usize> + SuspendableKeyed<'k, N> for PaddedBlockCipherEncryptor where - E: BlockCipherEncryptor + SuspendableComponent, + E: BlockCipherEncryptor + SuspendableComponent<'k>, P: BlockCipherPadding, { type Key = E::Key; @@ -425,30 +386,30 @@ where suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } -impl - PaddedBlockCipherDecryptor +impl<'k, D, P, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize> + PaddedBlockCipherDecryptor where - D: BlockCipherDecryptor + SuspendableComponent, + D: BlockCipherDecryptor + SuspendableComponent<'k>, P: BlockCipherPadding, { /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the inner /// cipher's state, the partial ciphertext block and its length as a `u64`, a flag for /// whether a block is held back, and that block. See [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = - LIB_VERSION_LEN + ::STATE_LEN; + LIB_VERSION_LEN + >::STATE_LEN; } /// The suspended state is the inner cipher's, the buffered partial block, and the withheld /// block if there is one (all ciphertext). See [`bouncycastle_utils::suspendable_state`]. -impl - SuspendableComponent for PaddedBlockCipherDecryptor +impl<'k, D, P, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize> SuspendableComponent<'k> + for PaddedBlockCipherDecryptor where - D: BlockCipherDecryptor + SuspendableComponent, + D: BlockCipherDecryptor + SuspendableComponent<'k>, P: BlockCipherPadding, { const STATE_LEN: usize = D::STATE_LEN + BLOCK_LEN + 8 + 1 + BLOCK_LEN; @@ -473,7 +434,7 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { let (inner, rest) = state.split_at(D::STATE_LEN); let decryptor = D::read_state(inner, key)?; let mut r = Cursor::new(rest); @@ -495,10 +456,10 @@ where /// `N` must be [`PaddedBlockCipherDecryptor::SUSPENDED_STATE_LEN`]; anything else is a compile /// error. -impl - SuspendableKeyed for PaddedBlockCipherDecryptor +impl<'k, D, P, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, const N: usize> + SuspendableKeyed<'k, N> for PaddedBlockCipherDecryptor where - D: BlockCipherDecryptor + SuspendableComponent, + D: BlockCipherDecryptor + SuspendableComponent<'k>, P: BlockCipherPadding, { type Key = D::Key; @@ -507,7 +468,7 @@ where suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } diff --git a/crypto/cipher/src/stream.rs b/crypto/cipher/src/stream.rs index 21d93ccaa..d17110979 100644 --- a/crypto/cipher/src/stream.rs +++ b/crypto/cipher/src/stream.rs @@ -12,7 +12,7 @@ //! # Suspending and resuming execution //! //! [`StreamCipher`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a -//! byte array and resumed later with the re-supplied key. The state is the keystream's own state +//! byte array and resumed later over the re-supplied key state. The state is the keystream's own state //! and the partly used keystream block, for any keystream that implements [`SuspendableComponent`]. //! The array length is `StreamCipher::SUSPENDED_STATE_LEN`; see [the crate //! docs](crate#suspending-and-resuming-execution) for an example. @@ -21,13 +21,11 @@ use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::KeyStream; -use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, + Algorithm, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_utils::secret::Secret; use bouncycastle_utils::suspendable_state::{ Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, @@ -56,14 +54,9 @@ use core::marker::PhantomData; /// against the whole call, so a message is never half-processed before the cipher notices. Past /// that point the keystream would repeat, which is the two-time-pad failure within one message. #[derive(Clone)] -pub struct StreamCipher< - KS, - Dir, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, -> where - KS: KeyStream, +pub struct StreamCipher +where + KS: KeyStream, { keystream: KS, /// The keystream block currently being consumed. Meaningful only while `used < BLOCK_LEN`. @@ -74,18 +67,18 @@ pub struct StreamCipher< _marker: PhantomData, } -impl - StreamCipher +impl + StreamCipher where - KS: KeyStream, + KS: KeyStream, { /// Wraps a keystream that has already been constructed and positioned, such as one that /// starts part-way into its counter space (GCM's GCTR starts at `inc32(J0)`). /// /// # 🚨 Security Considerations 🚨 /// This bypasses init-data generation: the keystream's nonce is whatever it was constructed - /// with, and the caller is responsible for it never repeating under the key. The - /// [`SymmetricCipherEncryptor`] constructors are the safe path. + /// with, and the caller is responsible for it never repeating under the key. A key's + /// `create_encryptor` is the safe path. pub fn from_keystream(keystream: KS) -> Self { Self::check_shape(); Self { keystream, pending: Secret::new(), used: BLOCK_LEN, _marker: PhantomData } @@ -162,10 +155,10 @@ where } } -impl Algorithm - for StreamCipher +impl Algorithm + for StreamCipher where - KS: KeyStream, + KS: KeyStream, { /// The keystream's name. const ALG_NAME: &'static str = KS::ALG_NAME; @@ -173,33 +166,12 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = KS::MAX_SECURITY_STRENGTH; } -impl - SymmetricCipherEncryptor - for StreamCipher +impl + SymmetricCipherEncryptor + for StreamCipher where - KS: KeyStream, + KS: KeyStream, { - /// Begins an encryption flow, drawing the init data from the library's default OS-backed DRBG. - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { - let mut rng = HashDRBG_SHA512::new_from_os(); - Self::do_encrypt_init_rng(key, &mut rng) - } - - /// As [`SymmetricCipherEncryptor::do_encrypt_init`], but draws the init data from `rng`. - /// Never panics: `INIT_DATA_LEN == 0` is ruled out at compile time; see [`StreamCipher`]. - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { - Self::check_shape(); - let mut init_data = [0u8; INIT_DATA_LEN]; - rng.next_bytes_out(&mut init_data)?; - let keystream = KS::new(key, &init_data)?; - Ok((Self::from_keystream(keystream), init_data)) - } - /// Every input byte produces exactly one output byte. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -237,11 +209,10 @@ where } } -impl - StreamCipherEncryptor - for StreamCipher +impl StreamCipherEncryptor + for StreamCipher where - KS: KeyStream, + KS: KeyStream, { /// XORs the next `data.len()` keystream bytes into `data`. /// @@ -253,22 +224,12 @@ where } } -impl - SymmetricCipherDecryptor - for StreamCipher +impl + SymmetricCipherDecryptor + for StreamCipher where - KS: KeyStream, + KS: KeyStream, { - /// Begins a decryption flow from the init data returned by - /// [`SymmetricCipherEncryptor::do_encrypt_init`]. - fn do_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ) -> Result { - Self::check_shape(); - Ok(Self::from_keystream(KS::new(key, init_data)?)) - } - /// Nothing is held back, so every input byte can be released immediately. fn do_decrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -306,11 +267,10 @@ where } } -impl - StreamCipherDecryptor - for StreamCipher +impl StreamCipherDecryptor + for StreamCipher where - KS: KeyStream, + KS: KeyStream, { /// The same XOR as encryption. /// @@ -321,25 +281,25 @@ where } } -impl - StreamCipher +impl<'k, KS, Dir, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize> + StreamCipher where - KS: KeyStream + SuspendableComponent, + KS: KeyStream + SuspendableComponent<'k>, { /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the keystream's /// state, the pending keystream block and the `used` count as a `u64`. See /// [`bouncycastle_utils::suspendable_state`]. pub const SUSPENDED_STATE_LEN: usize = - LIB_VERSION_LEN + ::STATE_LEN; + LIB_VERSION_LEN + >::STATE_LEN; } /// The suspended state is the keystream's own state followed by the pending keystream block and /// how much of it is used. The pending block is live keystream, which is why the whole state /// must be protected and never resumed twice; see [`bouncycastle_utils::suspendable_state`]. -impl - SuspendableComponent for StreamCipher +impl<'k, KS, Dir, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize> SuspendableComponent<'k> + for StreamCipher where - KS: KeyStream + SuspendableComponent, + KS: KeyStream + SuspendableComponent<'k>, { const STATE_LEN: usize = KS::STATE_LEN + BLOCK_LEN + 8; type Key = KS::Key; @@ -353,7 +313,7 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { Self::check_shape(); let (ks, rest) = state.split_at(KS::STATE_LEN); let keystream = KS::read_state(ks, key)?; @@ -369,16 +329,10 @@ where } /// `N` must be [`StreamCipher::SUSPENDED_STATE_LEN`]; anything else is a compile error. -impl< - KS, - Dir, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, - const N: usize, -> SuspendableKeyed for StreamCipher +impl<'k, KS, Dir, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, const N: usize> + SuspendableKeyed<'k, N> for StreamCipher where - KS: KeyStream + SuspendableComponent, + KS: KeyStream + SuspendableComponent<'k>, { type Key = KS::Key; @@ -386,7 +340,7 @@ where suspend_component(&self) } - fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } } diff --git a/crypto/cipher/tests/modes/cbc_tests.rs b/crypto/cipher/tests/modes/cbc_tests.rs index ce7df88eb..3a9a7cb24 100644 --- a/crypto/cipher/tests/modes/cbc_tests.rs +++ b/crypto/cipher/tests/modes/cbc_tests.rs @@ -36,7 +36,7 @@ fn swapped_four_key() -> ToyKey> { /// The implementor hook `do_encrypt_blocks_inplace`, by value, for tests whose data is /// block-shaped. fn enc_blocks( - enc: &mut impl BlockCipherEncryptor, + enc: &mut impl BlockCipherEncryptor, plaintext: &[[u8; TOY_LEN]; N], ) -> [[u8; TOY_LEN]; N] { let mut blocks = *plaintext; @@ -46,7 +46,7 @@ fn enc_blocks( /// The implementor hook `do_decrypt_blocks_inplace`, by value. fn dec_blocks( - dec: &mut impl BlockCipherDecryptor, + dec: &mut impl BlockCipherDecryptor, ciphertext: &[[u8; TOY_LEN]; N], ) -> [[u8; TOY_LEN]; N] { let mut blocks = *ciphertext; @@ -163,11 +163,13 @@ fn the_pair_path_is_really_used() { assert_eq!(dec_blocks(&mut dec, &ct), plaintext); // The swapped-pair toy encrypts identically (encryption is serial and never pairs)... - let (mut enc, iv) = swapped_key().create_encryptor(&mut os_rng()).unwrap(); + let key = swapped_key(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).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 = swapped_key().create_decryptor(&iv); + let key = swapped_key(); + let mut dec = key.create_decryptor(&iv); assert_ne!( dec_blocks(&mut dec, &ct), plaintext, @@ -175,7 +177,8 @@ 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 = swapped_key().create_decryptor(&iv); + let key = swapped_key(); + let mut dec = key.create_decryptor(&iv); let p0 = dec_blocks(&mut dec, &[ct[0]])[0]; let p1 = dec_blocks(&mut dec, &[ct[1]])[0]; assert_eq!([p0, p1], plaintext, "the single-block path must not pair"); @@ -200,20 +203,24 @@ fn the_four_block_path_is_really_used() { assert_eq!(dec_blocks(&mut dec, &ct), plaintext); // The rotated-four toy encrypts identically (encryption is serial and never batches)... - let (mut enc, iv) = swapped_four_key().create_encryptor(&mut os_rng()).unwrap(); + let key = swapped_four_key(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let ct = enc_blocks(&mut enc, &plaintext); // ...but decrypting five together must be wrong, because the first four take the four path. - let mut dec = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut dec = key.create_decryptor(&iv); assert_ne!(dec_blocks(&mut dec, &ct), plaintext, "four blocks must go through decrypt_4blocks"); // Exactly four together is wrong for the same reason. let four: [[u8; TOY_LEN]; 4] = ct[..4].try_into().unwrap(); - let mut dec = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut dec = key.create_decryptor(&iv); assert_ne!(&dec_blocks(&mut dec, &four)[..], &plaintext[..4]); // Two pairs go through the pair path and are correct; so is the fifth block on its own. - let mut dec = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut dec = key.create_decryptor(&iv); let first: [[u8; TOY_LEN]; 2] = ct[..2].try_into().unwrap(); let second: [[u8; TOY_LEN]; 2] = ct[2..4].try_into().unwrap(); assert_eq!( diff --git a/crypto/cipher/tests/modes/cfb8_tests.rs b/crypto/cipher/tests/modes/cfb8_tests.rs index df8fafe5f..d82f90e16 100644 --- a/crypto/cipher/tests/modes/cfb8_tests.rs +++ b/crypto/cipher/tests/modes/cfb8_tests.rs @@ -51,14 +51,14 @@ fn forward_only_key() -> ToyKey { } /// `do_encrypt_inplace`, by value. -fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { +fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { let mut data = plaintext.to_vec(); e.do_encrypt_inplace(&mut data).unwrap(); data } /// `do_decrypt_inplace`, by value. -fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { +fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { let mut data = ciphertext.to_vec(); d.do_decrypt_inplace(&mut data).unwrap(); data @@ -66,7 +66,7 @@ fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) /// `do_decrypt_inplace` in `chunk`-byte calls, by value. The last call may be shorter. fn dec_chunked( - d: &mut impl StreamCipherDecryptor, + d: &mut impl StreamCipherDecryptor, ciphertext: &[u8], chunk: usize, ) -> Vec { diff --git a/crypto/cipher/tests/modes/cfb_tests.rs b/crypto/cipher/tests/modes/cfb_tests.rs index 469bf4579..8f85870e6 100644 --- a/crypto/cipher/tests/modes/cfb_tests.rs +++ b/crypto/cipher/tests/modes/cfb_tests.rs @@ -54,14 +54,14 @@ fn forward_only_key() -> ToyKey { } /// `do_encrypt_inplace`, by value. -fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { +fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { let mut data = plaintext.to_vec(); e.do_encrypt_inplace(&mut data).unwrap(); data } /// `do_decrypt_inplace`, by value. -fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { +fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { let mut data = ciphertext.to_vec(); d.do_decrypt_inplace(&mut data).unwrap(); data @@ -69,7 +69,7 @@ fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) /// `do_encrypt_inplace` in `chunk`-byte calls, by value. The last call may be shorter. fn enc_chunked( - e: &mut impl StreamCipherEncryptor, + e: &mut impl StreamCipherEncryptor, plaintext: &[u8], chunk: usize, ) -> Vec { @@ -82,7 +82,7 @@ fn enc_chunked( /// `do_decrypt_inplace` in `chunk`-byte calls, by value. The last call may be shorter. fn dec_chunked( - d: &mut impl StreamCipherDecryptor, + d: &mut impl StreamCipherDecryptor, ciphertext: &[u8], chunk: usize, ) -> Vec { diff --git a/crypto/cipher/tests/modes/ctr_tests.rs b/crypto/cipher/tests/modes/ctr_tests.rs index 18431107c..be3bd4175 100644 --- a/crypto/cipher/tests/modes/ctr_tests.rs +++ b/crypto/cipher/tests/modes/ctr_tests.rs @@ -79,20 +79,20 @@ fn forward_only_key() -> ToyKey> { /// Capacity of a 1-byte counter, in bytes. const TINY_CAPACITY: usize = 256 * TOY_LEN; -fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { +fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { let mut data = plaintext.to_vec(); e.do_encrypt_inplace(&mut data).unwrap(); data } -fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { +fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { let mut data = ciphertext.to_vec(); d.do_decrypt_inplace(&mut data).unwrap(); data } fn dec_chunked( - d: &mut impl StreamCipherDecryptor, + d: &mut impl StreamCipherDecryptor, ciphertext: &[u8], chunk: usize, ) -> Vec { diff --git a/crypto/cipher/tests/modes/ecb_tests.rs b/crypto/cipher/tests/modes/ecb_tests.rs index 01f8acd36..9c426192d 100644 --- a/crypto/cipher/tests/modes/ecb_tests.rs +++ b/crypto/cipher/tests/modes/ecb_tests.rs @@ -19,7 +19,9 @@ use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{Blocks, CBC, Cbc, ModeNames}; use bouncycastle_cipher::padding::PKCS7; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey, ElectronicCodeBook}; +use bouncycastle_core::hazmat::{ + BlockPermutation, EcbDecryptionKey, EcbEncryptionKey, ElectronicCodeBook, +}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, @@ -33,13 +35,17 @@ use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMat use common::{SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; use toy_key::{ToyKey, os_rng}; -type ToyEcb = Ecb; +type ToyEcb<'k, Dir> = Ecb<'k, Toy, Dir, TOY_LEN>; type ToyEcbKey = ToyKey>; fn ecb_key() -> ToyEcbKey { ToyEcbKey::from_key_material(&toy_key()).expect("the toy key") } +/// The key the worker helpers below hand out borrowed workers over; a worker cannot outlive +/// its key, so the helpers' key lives for the whole test binary. +static KEY: std::sync::LazyLock = std::sync::LazyLock::new(ecb_key); + fn swapped_key() -> ToyKey> { ToyKey::from_key_material(&toy_key()).expect("the toy key") } @@ -51,7 +57,7 @@ fn swapped_four_key() -> ToyKey> { /// The implementor hook `do_encrypt_blocks_inplace`, by value, for tests whose data is /// block-shaped. fn enc_blocks( - enc: &mut impl BlockCipherEncryptor, + enc: &mut impl BlockCipherEncryptor<0, TOY_LEN>, plaintext: &[[u8; TOY_LEN]; N], ) -> [[u8; TOY_LEN]; N] { let mut blocks = *plaintext; @@ -61,7 +67,7 @@ fn enc_blocks( /// The implementor hook `do_decrypt_blocks_inplace`, by value. fn dec_blocks( - dec: &mut impl BlockCipherDecryptor, + dec: &mut impl BlockCipherDecryptor<0, TOY_LEN>, ciphertext: &[[u8; TOY_LEN]; N], ) -> [[u8; TOY_LEN]; N] { let mut blocks = *ciphertext; @@ -69,12 +75,12 @@ fn dec_blocks( blocks } -fn encryptor() -> ToyEcb { - ecb_key().create_encryptor() +fn encryptor() -> ToyEcb<'static, Encrypting> { + KEY.create_encryptor() } -fn decryptor() -> ToyEcb { - ecb_key().create_decryptor() +fn decryptor() -> ToyEcb<'static, Decrypting> { + KEY.create_decryptor() } // ---- the mode against the shared framework ------------------------------------------------ @@ -143,10 +149,8 @@ fn the_mode_matches_the_spec_equations() { // ...and ECB is not CBC: CBC computes CIPH_K(P1 XOR IV), ECB computes CIPH_K(P1). let iv: [u8; TOY_LEN] = core::array::from_fn(|i| 0xF0 ^ (i as u8)); - let (mut cbc, _) = ToyKey::>::from_key_material(&toy_key()) - .unwrap() - .create_encryptor(&mut FixedSeedRNG::::new(iv)) - .unwrap(); + let cbc_key = ToyKey::>::from_key_material(&toy_key()).unwrap(); + let (mut cbc, _) = cbc_key.create_encryptor(&mut FixedSeedRNG::::new(iv)).unwrap(); let mut first = plaintext[0]; cbc.do_encrypt_blocks_inplace(first.as_chunks_mut().0).unwrap(); assert_ne!(first, ct[0], "ECB must not agree with CBC"); @@ -197,25 +201,29 @@ fn the_pair_path_is_used_in_both_directions() { let ct = enc_blocks(&mut encryptor(), &plaintext); // Encryption: a pair goes through encrypt_2blocks, so the swapped toy returns them swapped. - let mut enc = swapped_key().create_encryptor(); + let key = swapped_key(); + let mut enc = key.create_encryptor(); let swapped_ct = enc_blocks(&mut enc, &plaintext); assert_eq!(swapped_ct, [ct[1], ct[0]], "encrypting a pair must go through encrypt_2blocks"); // ...and one block at a time avoids the pair path. - let mut enc = swapped_key().create_encryptor(); + let key = swapped_key(); + let mut enc = key.create_encryptor(); assert_eq!( [enc_blocks(&mut enc, &[plaintext[0]])[0], enc_blocks(&mut enc, &[plaintext[1]])[0]], ct ); // Decryption likewise. - let mut dec = swapped_key().create_decryptor(); + let key = swapped_key(); + let mut dec = key.create_decryptor(); assert_eq!( dec_blocks(&mut dec, &ct), [plaintext[1], plaintext[0]], "decrypting a pair must go through decrypt_2blocks" ); - let mut dec = swapped_key().create_decryptor(); + let key = swapped_key(); + let mut dec = key.create_decryptor(); assert_eq!([dec_blocks(&mut dec, &[ct[0]])[0], dec_blocks(&mut dec, &[ct[1]])[0]], plaintext); } @@ -229,20 +237,27 @@ fn the_four_block_path_is_used_in_both_directions() { let ct = enc_blocks(&mut encryptor(), &plaintext); assert_eq!(dec_blocks(&mut decryptor(), &ct), plaintext); - let mut enc = swapped_four_key().create_encryptor(); + let key = swapped_four_key(); + + let mut enc = key.create_encryptor(); let rotated = enc_blocks(&mut enc, &plaintext); assert_ne!(rotated, ct, "five blocks must go through encrypt_4blocks"); assert_eq!(rotated[4], ct[4], "the fifth block goes through the single path and is right"); assert_eq!(&rotated[..4], &[ct[1], ct[2], ct[3], ct[0]], "four rotated"); - let mut enc = swapped_four_key().create_encryptor(); + let key = swapped_four_key(); + + let mut enc = key.create_encryptor(); let a = enc_blocks(&mut enc, &[plaintext[0], plaintext[1]]); let b = enc_blocks(&mut enc, &[plaintext[2], plaintext[3]]); assert_eq!([a, b].as_flattened(), &ct[..4], "pairs use the pair path only"); - let mut dec = swapped_four_key().create_decryptor(); + let key = swapped_four_key(); + + let mut dec = key.create_decryptor(); assert_ne!(dec_blocks(&mut dec, &ct), plaintext, "five blocks must go through decrypt_4blocks"); - let mut dec = swapped_four_key().create_decryptor(); + let key = swapped_four_key(); + let mut dec = key.create_decryptor(); for (c, p) in ct.iter().zip(plaintext.iter()) { assert_eq!(&dec_blocks(&mut dec, &[*c])[0], p, "the single-block path must not batch"); } @@ -347,8 +362,8 @@ fn the_keys_enforce_the_material_policy() { #[test] fn the_padding_layer_round_trips_every_length() { type Key = ToyKey>; - type Enc = >::Encryptor; - type Dec = >::Decryptor; + type Enc<'k> = >::Encryptor<'k>; + type Dec<'k> = >::Decryptor<'k>; let key = Key::from_key_material(&toy_key()).expect("the toy key"); for len in 0..=(3 * TOY_LEN + 1) { @@ -366,16 +381,16 @@ fn the_padding_layer_round_trips_every_length() { // ---- memory ------------------------------------------------------------------------------ -/// Pins the module docs' claim that an ECB value is exactly the permutation: -/// `size_of::>() == size_of::

()`. +/// Pins the module docs' claim that an ECB value is a reference to the permutation and nothing +/// else. #[test] fn sizes_match_the_documented_memory_table() { use core::mem::size_of; - assert_eq!(size_of::>(), size_of::()); + assert_eq!(size_of::>(), size_of::<&Toy>()); assert_eq!(size_of::>(), size_of::>()); // One block smaller than CBC, which stores a chaining value. assert_eq!( size_of::>() + TOY_LEN, - size_of::>() + size_of::>() ); } diff --git a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs index e1510136e..9a824208c 100644 --- a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs +++ b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs @@ -67,8 +67,8 @@ fn the_two_apis_agree_byte_for_byte() { where K: SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + K::Encryptor: StreamCipherEncryptor, + K::Decryptor: StreamCipherDecryptor, { for len in [0usize, 1, 15, 16, 17, 63, 64, 171] { let plaintext: Vec = (0..len).map(|i| (i * 7 + 1) as u8).collect(); @@ -117,12 +117,12 @@ fn the_length_predictions_are_exact() { let key = key_for::>(); for len in [0usize, 1, 15, 16, 17, 1000] { assert_eq!( - as SymmetricCipherEncryptor>::encrypt_out_len(len), + as SymmetricCipherEncryptor<12, 0>>::encrypt_out_len(len), len, "encrypt_out_len is the identity" ); assert_eq!( - as SymmetricCipherDecryptor>::decrypt_out_len(len), + as SymmetricCipherDecryptor<12, 0>>::decrypt_out_len(len), len, "decrypt_out_len is exact, not an upper bound" ); diff --git a/crypto/core-test-framework/src/aead.rs b/crypto/core-test-framework/src/aead.rs index 94054341b..e5b2aae48 100644 --- a/crypto/core-test-framework/src/aead.rs +++ b/crypto/core-test-framework/src/aead.rs @@ -15,14 +15,13 @@ //! [`SymmetricCipherEncryptor`]: bouncycastle_core::traits::SymmetricCipherEncryptor //! [`SymmetricCipherDecryptor`]: bouncycastle_core::traits::SymmetricCipherDecryptor -use crate::symmetric_ciphers::TestFrameworkSymmetricCipher; use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::symmetric_key::{ - AeadDecryptWorker, AeadDecryptionKey, AeadEncryptWorker, AeadEncryptionKey, +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, RNG, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, }; -use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor, RNG}; /// Instance of the test framework. pub struct TestFrameworkAEADCipher { @@ -41,735 +40,8 @@ impl TestFrameworkAEADCipher { Self { fixed_message_len: None } } - /// Exercises the [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] streaming contract for a - /// paired implementor. The counterpart of [`TestFrameworkBlockCipher::test`] for an - /// authenticated cipher. - /// - /// Checks, in order: - /// * the whole [`TestFrameworkSymmetricCipher::test_encryptor_decryptor`] suite, since an AEAD - /// with no associated data and the tag inline *is* a [`SymmetricCipherEncryptor`] / - /// [`SymmetricCipherDecryptor`] pair, and that `FINAL_LEN` has room for the tag; - /// * the detached one-shot round trip for every message length from 0 to a few times - /// `TAG_LEN`, and that the tag is not the all-zero array; - /// * the inline layout with associated data, one-shot and streaming, is exactly the detached - /// ciphertext with the tag appended, and a stream shorter than the tag is a failed - /// decryption; - /// * streaming in every chunking, of both the AAD and the data, agrees with `update_out_len` - /// on every call and gives the one-shot's ciphertext and tag byte for byte, and decrypts in - /// every chunking; - /// * an empty AAD is a no-op -- it gives what absorbing no AAD at all gives -- and a message - /// with no data still authenticates its AAD; - /// * `do_update_aad` with non-empty AAD after the first `do_update_out` is refused with a - /// [`SymmetricCipherError::StateError`], and the refusal leaves the value usable; - /// * a tampered ciphertext, tag, AAD or nonce all fail the tag check, and the one-shots leave - /// no plaintext behind when they do; - /// * two encryptions under the same key draw different nonces; - /// * every AEAD method that writes into a caller's buffer accepts one larger than needed, - /// returns the number of bytes that call wrote, and zeroes every byte past that count; - /// * a key of the wrong [`KeyType`] is rejected, and the security-strength policy matches - /// [`Algorithm::MAX_SECURITY_STRENGTH`]. - /// - /// `bouncycastle-core`'s own `tests/aead_buffering_toy_tests.rs` separately pins that a cipher - /// which holds back more than the tag is handled correctly by the traits' default one-shots, - /// since `E`/`D` here are supplied by the caller and might not hold anything back. - /// - /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH - /// - /// [`TestFrameworkBlockCipher::test`]: crate::block_cipher::TestFrameworkBlockCipher::test - /// [`TestFrameworkSymmetricCipher::test_encryptor_decryptor`]: crate::symmetric_ciphers::TestFrameworkSymmetricCipher::test_encryptor_decryptor - /// [`SymmetricCipherEncryptor`]: bouncycastle_core::traits::SymmetricCipherEncryptor - /// [`SymmetricCipherDecryptor`]: bouncycastle_core::traits::SymmetricCipherDecryptor - pub fn test_encryptor_decryptor< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const FINAL_LEN: usize, - E: AEADCipherEncryptor, - D: AEADCipherDecryptor, - >( - &self, - ) { - assert!( - FINAL_LEN >= TAG_LEN, - "FINAL_LEN must have room for the inline tag the decryptor holds back" - ); - // No AAD and the tag inline is the plain symmetric-cipher contract. - let mut symmetric = TestFrameworkSymmetricCipher::new(); - symmetric.fixed_message_len = self.fixed_message_len; - symmetric.test_encryptor_decryptor::(); - - let key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - let aad: &[u8] = b"some associated data"; - let pinned = [0xA5u8; NONCE_LEN]; - - // one-shot round trip, every length up to a few times the tag length (and up to the fixed - // length, if there is one, so that the streaming checks inside the loop reach it) - let max_len = (3 * TAG_LEN.max(1) + 5).max(self.fixed_message_len.unwrap_or(0)); - assert!(max_len <= DUMMY_SEED.len(), "the fixed message length must fit the seed buffer"); - for len in 0..=max_len { - let msg = &DUMMY_SEED[..len]; - // a fixed-length pair's streaming methods take only that length; its one-shots, with - // the whole message in hand, take any length and must round trip it - if let Some(fixed) = self.fixed_message_len - && fixed != len - { - let mut ct = vec![0u8; E::encrypt_detached_out_len(len)]; - let (nonce, n, tag) = E::encrypt_detached_out(&key, aad, msg, &mut ct) - .unwrap_or_else(|e| { - panic!( - "fixed length: a {len}-byte detached one-shot must be accepted: {e:?}" - ) - }); - let mut pt = vec![0u8; D::decrypt_detached_out_len(n)]; - let m = - D::decrypt_detached_out(&key, &nonce, aad, &ct[..n], &tag, &mut pt).unwrap(); - assert_eq!(&pt[..m], msg, "fixed length: {len}-byte detached one-shot round trip"); - let mut inline = vec![0u8; E::encrypt_out_len(len)]; - let (nonce, n) = E::encrypt_with_aad_out(&key, aad, msg, &mut inline) - .unwrap_or_else(|e| { - panic!("fixed length: a {len}-byte inline one-shot must be accepted: {e:?}") - }); - let mut pt = vec![0u8; D::decrypt_out_len(n)]; - let m = D::decrypt_with_aad_out(&key, &nonce, aad, &inline[..n], &mut pt).unwrap(); - assert_eq!(&pt[..m], msg, "fixed length: {len}-byte inline one-shot round trip"); - // ...while the streaming methods refuse it, at the update or at the final - let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); - enc.do_update_aad(aad).unwrap(); - let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; - assert!( - enc.do_encrypt_out(msg, &mut buf).is_err() || enc.do_encrypt_final().is_err(), - "fixed length: a {len}-byte stream must be refused" - ); - continue; - } - let mut ct = vec![0u8; E::encrypt_detached_out_len(len)]; - let (nonce, ct_len, tag) = E::encrypt_detached_out(&key, aad, msg, &mut ct).unwrap(); - ct.truncate(ct_len); - assert_ne!(tag, [0u8; TAG_LEN], "len {len}: the tag must not be all zeros"); - // Only assert the ciphertext differs from the plaintext once there is enough of it for - // an accidental match to be negligible rather than a 1-in-256 flake. - if len >= 8 { - assert_ne!(&ct[..], msg, "len {len}: the ciphertext must not be the plaintext"); - } - let mut pt = vec![0u8; D::decrypt_detached_out_len(ct.len())]; - let pt_len = D::decrypt_detached_out(&key, &nonce, aad, &ct, &tag, &mut pt).unwrap(); - pt.truncate(pt_len); - assert_eq!(&pt[..], msg, "one-shot round trip, len {len}"); - - // the std one-shots agree with the _out ones for the same nonce - let (nonce2, ct2, tag2) = E::encrypt_detached(&key, aad, msg).unwrap(); - assert_eq!(ct2.len(), ct_len, "encrypt_detached must return exactly the bytes written"); - let pt2 = D::decrypt_detached(&key, &nonce2, aad, &ct2, &tag2).unwrap(); - assert_eq!(pt2, msg, "std round trip, len {len}"); - let pt3 = D::decrypt_detached(&key, &nonce, aad, &ct, &tag).unwrap(); - assert_eq!(pt3, msg, "decrypt_detached must agree with decrypt_detached_out"); - - // the inline `ciphertext || tag` layout with AAD: `encrypt_with_aad_out` must write exactly - // the detached ciphertext with the tag appended -- the same bytes under the same - // nonce -- and both the one-shot and the streaming finalizer must round trip it. - let mut detached = vec![0u8; E::encrypt_detached_out_len(len)]; - let (pinned_nonce, detached_len, detached_tag) = E::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - aad, - msg, - &mut detached, - ) - .unwrap(); - detached.truncate(detached_len); - detached.extend_from_slice(&detached_tag); - - let mut inline = vec![0u8; E::encrypt_out_len(len)]; - let (inline_nonce, inline_len) = - E::encrypt_with_aad_out(&key, aad, msg, &mut inline).unwrap(); - assert_eq!( - inline_len, - E::encrypt_detached_out_len(len) + TAG_LEN, - "encrypt_with_aad_out must write the ciphertext plus the tag, len {len}" - ); - let mut pt4 = vec![0u8; D::decrypt_out_len(inline_len)]; - let pt4_len = - D::decrypt_with_aad_out(&key, &inline_nonce, aad, &inline[..inline_len], &mut pt4) - .unwrap(); - assert_eq!(&pt4[..pt4_len], msg, "tagged one-shot round trip, len {len}"); - - // ...and so must the RNG-driven and allocating inline-with-AAD one-shots. The roomy - // buffer is deliberate: see the `encrypt_detached_rng_out` probe below. - let mut inline_rng = vec![0u8; E::encrypt_out_len(len) + 3]; - let (rng_nonce, rng_len) = E::encrypt_with_aad_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - aad, - msg, - &mut inline_rng, - ) - .unwrap(); - assert_eq!(rng_nonce, pinned_nonce, "the same RNG stream must give the same nonce"); - assert_eq!( - &inline_rng[..rng_len], - &detached[..], - "len {len}: encrypt_with_aad_rng_out must be the detached ciphertext and its tag" - ); - // exactly the length it asks for must be enough too - let mut exact = vec![0u8; E::encrypt_out_len(len)]; - let (_, exact_len) = E::encrypt_with_aad_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - aad, - msg, - &mut exact, - ) - .unwrap(); - assert_eq!(&exact[..exact_len], &detached[..], "len {len}: exact-size buffer"); - - // the supplied-nonce forms: the nonce the pinned RNG produced, supplied directly, - // must give the same bytes from the one-shots, the allocating one-shots and the - // streaming constructor alike - let mut by_nonce = vec![0u8; E::encrypt_detached_out_len(len)]; - let (n_len, n_tag) = - E::encrypt_detached_nonce_out(&key, &pinned_nonce, aad, msg, &mut by_nonce) - .unwrap(); - by_nonce.truncate(n_len); - by_nonce.extend_from_slice(&n_tag); - assert_eq!(by_nonce, detached, "len {len}: encrypt_detached_nonce_out under the nonce"); - let mut by_nonce = vec![0u8; E::encrypt_out_len(len)]; - let n_len = E::encrypt_with_aad_nonce_out(&key, &pinned_nonce, aad, msg, &mut by_nonce) - .unwrap(); - assert_eq!(&by_nonce[..n_len], &detached[..], "len {len}: encrypt_with_aad_nonce_out"); - let mut short = vec![0u8; E::encrypt_out_len(len) - 1]; - match E::encrypt_with_aad_nonce_out(&key, &pinned_nonce, aad, msg, &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { - assert_eq!(n, E::encrypt_out_len(len)) - } - other => panic!("encrypt_with_aad_nonce_out into a short buffer: {other:?}"), - } - let (mut alloc_ct, alloc_tag) = - E::encrypt_detached_nonce(&key, &pinned_nonce, aad, msg).unwrap(); - alloc_ct.extend_from_slice(&alloc_tag); - assert_eq!(alloc_ct, detached, "len {len}: encrypt_detached_nonce"); - assert_eq!( - E::encrypt_with_aad_nonce(&key, &pinned_nonce, aad, msg).unwrap(), - detached, - "len {len}: encrypt_with_aad_nonce" - ); - let mut enc_n = E::do_encrypt_init_nonce(&key, &pinned_nonce).unwrap(); - enc_n.do_update_aad(aad).unwrap(); - let mut streamed = vec![0u8; enc_n.do_encrypt_out_len(len)]; - let w = enc_n.do_encrypt_out(msg, &mut streamed).unwrap(); - streamed.truncate(w); - let (last_n, last_n_len) = enc_n.do_encrypt_final().unwrap(); - streamed.extend_from_slice(&last_n[..last_n_len]); - assert_eq!(streamed, detached, "len {len}: do_encrypt_init_nonce streaming"); - let mut short = vec![0u8; E::encrypt_out_len(len) - 1]; - match E::encrypt_with_aad_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - aad, - msg, - &mut short, - ) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { - assert_eq!(n, E::encrypt_out_len(len)) - } - other => panic!("encrypt_with_aad_rng_out into a short buffer: {other:?}"), - } - let (alloc_nonce, alloc_ct) = E::encrypt_with_aad(&key, aad, msg).unwrap(); - assert_eq!( - alloc_ct.len(), - inline_len, - "encrypt_with_aad must return the bytes written" - ); - let alloc_pt = D::decrypt_with_aad(&key, &alloc_nonce, aad, &alloc_ct).unwrap(); - assert_eq!(alloc_pt, msg, "allocating inline-with-AAD round trip, len {len}"); - - let (mut enc5, nonce5) = - E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)).unwrap(); - assert_eq!(nonce5, pinned_nonce, "the same RNG stream must give the same nonce"); - enc5.do_update_aad(aad).unwrap(); - let mut inline5 = vec![0u8; enc5.do_encrypt_out_len(len)]; - let written5 = enc5.do_encrypt_out(msg, &mut inline5).unwrap(); - inline5.truncate(written5); - let (last5, last5_len) = enc5.do_encrypt_final().unwrap(); - inline5.extend_from_slice(&last5[..last5_len]); - assert_eq!( - inline5.len(), - inline_len, - "tagged streaming must write as much as the one-shot" - ); - assert_eq!( - inline5, detached, - "len {len}: the inline layout must be the detached ciphertext followed by its tag" - ); - let mut dec5 = D::do_decrypt_init(&key, &nonce5).unwrap(); - dec5.do_update_aad(aad).unwrap(); - let mut pt5 = vec![0u8; dec5.do_decrypt_out_len(inline5.len())]; - let got5 = dec5.do_decrypt_out(&inline5, &mut pt5).unwrap(); - pt5.truncate(got5); - let (last, data_len) = dec5.do_decrypt_final().unwrap(); - pt5.extend_from_slice(&last[..data_len]); - assert_eq!(pt5, msg, "tagged streaming round trip, len {len}"); - - // a stream that ends before a whole tag has been seen is not a short buffer, it - // is a failed decryption - if TAG_LEN > 0 { - let mut dec6 = D::do_decrypt_init(&key, &nonce5).unwrap(); - dec6.do_update_aad(aad).unwrap(); - let short = &inline5[..TAG_LEN - 1]; - let mut scratch = vec![0u8; dec6.do_decrypt_out_len(short.len())]; - dec6.do_decrypt_out(short, &mut scratch).unwrap(); - assert!( - matches!(dec6.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), - "a stream shorter than the tag must be DecryptionFailed, len {len}" - ); - } - - // too-short output buffers on the one-shots are refused with the required length, - // before any work is done - let need = E::encrypt_detached_out_len(len); - if need > 0 { - let mut short = vec![0u8; need - 1]; - match E::encrypt_detached_out(&key, aad, msg, &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { - assert_eq!(n, need) - } - other => panic!("encrypt_detached_out into a short buffer: {other:?}"), - } - let mut short = vec![0u8; need - 1]; - match E::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::::new([0xA5u8; NONCE_LEN]), - aad, - msg, - &mut short, - ) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { - assert_eq!(n, need) - } - other => panic!("encrypt_detached_rng_out into a short buffer: {other:?}"), - } - // ...and one with room to spare must be accepted: without this the guard can be - // flipped to `>` and every short-buffer probe still "passes", because the error - // then comes from `do_update_out` behind it with the same variant and length. - let mut roomy = vec![0u8; need + 3]; - let (_, n, _) = E::encrypt_detached_out(&key, aad, msg, &mut roomy).unwrap(); - assert_eq!(n, need, "encrypt_detached_out into a roomy buffer"); - let mut roomy = vec![0u8; need + 3]; - let (_, n, _) = E::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::::new([0xA5u8; NONCE_LEN]), - aad, - msg, - &mut roomy, - ) - .unwrap(); - assert_eq!( - n, need, - "encrypt_detached_rng_out must write exactly encrypt_detached_out_len bytes" - ); - } - let need = E::encrypt_out_len(len); - let mut short = vec![0u8; need - 1]; - match E::encrypt_with_aad_out(&key, aad, msg, &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), - other => panic!("encrypt_with_aad_out into a short buffer: {other:?}"), - } - let need = D::decrypt_detached_out_len(ct.len()); - if need > 0 { - let mut short = vec![0u8; need - 1]; - match D::decrypt_detached_out(&key, &nonce, aad, &ct, &tag, &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { - assert_eq!(n, need) - } - other => panic!("decrypt_detached_out into a short buffer: {other:?}"), - } - } - let need = D::decrypt_out_len(inline_len); - if need > 0 { - let mut short = vec![0u8; need - 1]; - match D::decrypt_with_aad_out(&key, &inline_nonce, aad, &inline, &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { - assert_eq!(n, need) - } - other => panic!("decrypt_with_aad_out into a short buffer: {other:?}"), - } - } - } - - // streaming in every chunking agrees with the one-shot, for both the AAD and the data. - // The pinned RNG is what makes the nonce -- and so the ciphertext -- comparable. - let msg = &DUMMY_SEED[..self.fixed_message_len.unwrap_or(max_len.max(17))]; - let mut ct_ref = vec![0u8; E::encrypt_detached_out_len(msg.len())]; - let (nonce_ref, ct_ref_len, tag_ref) = E::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - aad, - msg, - &mut ct_ref, - ) - .unwrap(); - ct_ref.truncate(ct_ref_len); - - for chunk in [1usize, 2, 3, 7, TAG_LEN.max(1), TAG_LEN + 1, msg.len().max(1)] { - let (mut enc, nonce) = - E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)).unwrap(); - assert_eq!(nonce, nonce_ref, "the same RNG stream must give the same nonce"); - for piece in aad.chunks(chunk) { - enc.do_update_aad(piece).unwrap(); - } - let mut ct = Vec::new(); - for piece in msg.chunks(chunk) { - let expect = enc.do_encrypt_out_len(piece.len()); - let mut buf = vec![0u8; expect]; - let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); - assert_eq!(n, expect, "chunk {chunk}: update_out_len must be exact (encrypt)"); - ct.extend_from_slice(&buf[..n]); - } - let mut final_buf = [0u8; FINAL_LEN]; - let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf).unwrap(); - assert!( - final_len + TAG_LEN <= FINAL_LEN, - "chunk {chunk}: the detached flush must leave FINAL_LEN room for the tag" - ); - ct.extend_from_slice(&final_buf[..final_len]); - assert_eq!(ct, ct_ref, "chunk {chunk}: streaming must give the one-shot ciphertext"); - assert_eq!(tag, tag_ref, "chunk {chunk}: streaming must give the one-shot tag"); - - // ...and the decryptor agrees in every chunking too - let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); - for piece in aad.chunks(chunk) { - dec.do_update_aad(piece).unwrap(); - } - let mut pt = Vec::new(); - for piece in ct.chunks(chunk) { - let expect = dec.do_decrypt_out_len(piece.len()); - let mut buf = vec![0u8; expect]; - let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); - assert_eq!(n, expect, "chunk {chunk}: update_out_len must be exact (decrypt)"); - pt.extend_from_slice(&buf[..n]); - } - let mut final_buf = [0u8; FINAL_LEN]; - let final_len = dec.do_decrypt_final_detached_out(&tag, &mut final_buf).unwrap(); - pt.extend_from_slice(&final_buf[..final_len]); - assert_eq!(pt, msg, "chunk {chunk}: streaming round trip"); - } - - // the array-returning finals agree with the `_out` ones the chunked loop above used - let (mut enc, nonce) = - E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)).unwrap(); - enc.do_update_aad(aad).unwrap(); - let mut ct = vec![0u8; enc.do_encrypt_out_len(msg.len())]; - let n = enc.do_encrypt_out(msg, &mut ct).unwrap(); - ct.truncate(n); - let (last, last_len, tag) = enc.do_encrypt_final_detached().unwrap(); - ct.extend_from_slice(&last[..last_len]); - assert_eq!(ct, ct_ref, "do_encrypt_final_detached must give the one-shot ciphertext"); - assert_eq!(tag, tag_ref, "do_encrypt_final_detached must give the one-shot tag"); - let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); - dec.do_update_aad(aad).unwrap(); - let mut pt = vec![0u8; dec.do_decrypt_out_len(ct.len())]; - let n = dec.do_decrypt_out(&ct, &mut pt).unwrap(); - pt.truncate(n); - let (last, data_len) = dec.do_decrypt_final_detached(&tag).unwrap(); - pt.extend_from_slice(&last[..data_len]); - assert_eq!(pt, msg, "do_decrypt_final_detached must round trip"); - let mut wrong_tag = tag; - wrong_tag[0] ^= 0xFF; - let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); - dec.do_update_aad(aad).unwrap(); - let mut pt = vec![0u8; dec.do_decrypt_out_len(ct.len())]; - dec.do_decrypt_out(&ct, &mut pt).unwrap(); - assert!( - matches!( - dec.do_decrypt_final_detached(&wrong_tag), - Err(SymmetricCipherError::AEADTagCheckFailed) - ), - "do_decrypt_final_detached must check the tag" - ); - - // an empty AAD is a no-op: it must give exactly what absorbing no AAD at all gives - let mut with_empty = vec![0u8; E::encrypt_detached_out_len(msg.len())]; - let (nonce_empty, len_empty, tag_empty) = E::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - b"", - msg, - &mut with_empty, - ) - .unwrap(); - with_empty.truncate(len_empty); - let mut without = vec![0u8; E::encrypt_detached_out_len(msg.len())]; - let (nonce_none, len_none, tag_none) = E::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - &[], - msg, - &mut without, - ) - .unwrap(); - without.truncate(len_none); - assert_eq!(nonce_empty, nonce_none); - assert_eq!(tag_empty, tag_none, "an empty AAD must be a no-op"); - assert_eq!(with_empty, without, "an empty AAD must be a no-op"); - - // ...and no AAD at all is what the inherited `SymmetricCipherEncryptor` one-shot gives - let mut plain = vec![0u8; E::encrypt_out_len(msg.len())]; - let (nonce_plain, len_plain) = - E::encrypt_rng_out(&key, &mut FixedSeedRNG::::new(pinned), msg, &mut plain) - .unwrap(); - assert_eq!(nonce_plain, nonce_none); - assert_eq!(&plain[..len_plain - TAG_LEN], &without[..], "no-AAD inline ciphertext"); - assert_eq!(&plain[len_plain - TAG_LEN..len_plain], &tag_none, "no-AAD inline tag"); - - // a message with no data at all still authenticates its AAD (unless the pair's fixed - // length rules an empty message out) - if self.fixed_message_len.is_none_or(|fixed| fixed == 0) { - let (nonce, _ct_len, tag) = E::encrypt_detached_out(&key, aad, &[], &mut []).unwrap(); - D::decrypt_detached_out(&key, &nonce, aad, &[], &tag, &mut []).unwrap(); - match D::decrypt_detached_out( - &key, - &nonce, - b"different associated data", - &[], - &tag, - &mut [], - ) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - other => panic!("an empty message must still authenticate its AAD, got {other:?}"), - }; - } - - // the AAD phase is over once data has been fed in -- on both sides, and on the decrypting - // side even when all of it is still being held back as a possible tag. (Not for a message - // with no data at all, where there is no data call to end it.) - if !msg.is_empty() { - let (mut enc, nonce) = E::do_encrypt_init(&key).unwrap(); - let mut ct = vec![0u8; enc.do_encrypt_out_len(msg.len())]; - enc.do_encrypt_out(msg, &mut ct).unwrap(); - match enc.do_update_aad(aad) { - Err(SymmetricCipherError::StateError(_)) => { /* good */ } - other => panic!("AAD after data must be refused, got {other:?}"), - }; - // an empty AAD stays a no-op even here, and the refused call must not have disturbed - // the state: the value is still good for the rest of the flow. - enc.do_update_aad(b"").unwrap(); - let mut final_buf = [0u8; FINAL_LEN]; - let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf).unwrap(); - ct.extend_from_slice(&final_buf[..final_len]); - - let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); - let mut pt = vec![0u8; dec.do_decrypt_out_len(1)]; - let mut got = dec.do_decrypt_out(&ct[..1], &mut pt).unwrap(); - pt.truncate(got); - match dec.do_update_aad(aad) { - Err(SymmetricCipherError::StateError(_)) => { /* good */ } - other => panic!("AAD after data must be refused, got {other:?}"), - }; - dec.do_update_aad(b"").unwrap(); - let mut rest = vec![0u8; dec.do_decrypt_out_len(ct.len() - 1)]; - got = dec.do_decrypt_out(&ct[1..], &mut rest).unwrap(); - pt.extend_from_slice(&rest[..got]); - let mut final_buf = [0u8; FINAL_LEN]; - let final_len = dec.do_decrypt_final_detached_out(&tag, &mut final_buf).unwrap(); - pt.extend_from_slice(&final_buf[..final_len]); - assert_eq!(&pt[..], msg, "a refused do_update_aad must not disturb the state"); - } - - // tampering: every one of these must fail the tag check, and the one-shots must leave no - // plaintext behind when they do. Their buffers are pre-filled with a non-zero byte, so one - // the decryptor never wrote is not mistaken for one it zeroized. A message long enough to - // have a byte 3 to flip, unless the pair's fixed length says otherwise. - let msg = &DUMMY_SEED[..self.fixed_message_len.unwrap_or(max_len.max(17))]; - let mut ct = vec![0u8; E::encrypt_detached_out_len(msg.len())]; - let (nonce, ct_len, tag) = E::encrypt_detached_out(&key, aad, msg, &mut ct).unwrap(); - ct.truncate(ct_len); - - if ct.len() > 3 { - let mut tampered = ct.clone(); - tampered[3] ^= 0xFF; - let mut buf = vec![0xEEu8; D::decrypt_detached_out_len(tampered.len())]; - match D::decrypt_detached_out(&key, &nonce, aad, &tampered, &tag, &mut buf) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - other => panic!("a modified ciphertext must fail the tag check, got {other:?}"), - }; - assert!( - buf.iter().all(|&b| b == 0), - "the one-shot decrypt must zeroize the buffer when the tag check fails" - ); - } - - let mut tampered_inline = ct.clone(); - tampered_inline.extend_from_slice(&tag); - tampered_inline[3] ^= 0xFF; - for with_aad in [false, true] { - let mut buf = vec![0xEEu8; D::decrypt_out_len(tampered_inline.len())]; - let result = if with_aad { - D::decrypt_with_aad_out(&key, &nonce, aad, &tampered_inline, &mut buf) - } else { - D::decrypt_out(&key, &nonce, &tampered_inline, &mut buf) - }; - // Without the AAD the tag was never going to verify; either way what matters is the - // failure and the zeroized buffer. - match result { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - other => panic!("a modified inline ciphertext must fail, got {other:?}"), - }; - assert!( - buf.iter().all(|&b| b == 0), - "the inline one-shot (aad {with_aad}) must zeroize the buffer on a failed check" - ); - } - match D::decrypt_with_aad(&key, &nonce, aad, &tampered_inline) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - other => panic!("decrypt_with_aad of a modified ciphertext must fail, got {other:?}"), - }; - - let mut wrong_tag = tag; - wrong_tag[0] ^= 0xFF; - let mut buf = vec![0xEEu8; D::decrypt_detached_out_len(ct.len())]; - match D::decrypt_detached_out(&key, &nonce, aad, &ct, &wrong_tag, &mut buf) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - other => panic!("a modified tag must fail the tag check, got {other:?}"), - }; - assert!(buf.iter().all(|&b| b == 0), "a modified tag must leave the buffer zeroized"); - - let mut buf = vec![0xEEu8; D::decrypt_detached_out_len(ct.len())]; - match D::decrypt_detached_out( - &key, - &nonce, - b"not the right associated data", - &ct, - &tag, - &mut buf, - ) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - other => panic!("a modified AAD must fail the tag check, got {other:?}"), - }; - assert!(buf.iter().all(|&b| b == 0), "a modified AAD must leave the buffer zeroized"); - - if NONCE_LEN > 0 { - let mut wrong_nonce = nonce; - wrong_nonce[0] ^= 0xFF; - let mut buf = vec![0xEEu8; D::decrypt_detached_out_len(ct.len())]; - match D::decrypt_detached_out(&key, &wrong_nonce, aad, &ct, &tag, &mut buf) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - other => panic!("a modified nonce must fail the tag check, got {other:?}"), - }; - assert!(buf.iter().all(|&b| b == 0), "a modified nonce must leave the buffer zeroized"); - - // two encryptions under the same key must not reuse a nonce - let (_enc1, nonce1) = E::do_encrypt_init(&key).unwrap(); - let (_enc2, nonce2) = E::do_encrypt_init(&key).unwrap(); - assert_ne!(nonce1, nonce2); - } - - // Output-buffer contract for the AEAD `_out` methods, as the symmetric suite above checks it - // for the inherited ones: a buffer larger than needed is accepted, the returned count is - // what that call wrote, and every byte past it is zeroed, whatever the buffer held on the - // way in. Each buffer is pre-filled with a non-zero sentinel, so a byte left as the caller - // had it shows up. The detached finals' buffers are `[u8; FINAL_LEN]` by type and so - // cannot be oversized; for them only the zeroed tail is checked. - const SENTINEL: u8 = 0xA5; - const EXTRA: usize = 7; - let assert_tail_zeroed = |buf: &[u8], n: usize, what: &str| { - assert!( - n <= buf.len(), - "{what}: claims {n} bytes written to a {}-byte buffer", - buf.len() - ); - assert!( - buf[n..].iter().all(|&b| b == 0), - "{what}: the bytes past the {n} written must be zeroed" - ); - }; - let len = self.fixed_message_len.unwrap_or(max_len); - let msg = &DUMMY_SEED[..len]; - - // the detached one-shots - let need = E::encrypt_detached_out_len(len); - let mut ct = vec![SENTINEL; need + EXTRA]; - let (nonce, n, tag) = E::encrypt_detached_out(&key, aad, msg, &mut ct).unwrap(); - assert_eq!(n, need, "encrypt_detached_out into an oversized buffer"); - assert_tail_zeroed(&ct, n, "encrypt_detached_out"); - ct.truncate(n); - let mut buf = vec![SENTINEL; need + EXTRA]; - let (_, n, _) = E::encrypt_detached_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - aad, - msg, - &mut buf, - ) - .unwrap(); - assert_eq!(n, need, "encrypt_detached_rng_out into an oversized buffer"); - assert_tail_zeroed(&buf, n, "encrypt_detached_rng_out"); - let mut pt = vec![SENTINEL; D::decrypt_detached_out_len(ct.len()) + EXTRA]; - let n = D::decrypt_detached_out(&key, &nonce, aad, &ct, &tag, &mut pt).unwrap(); - assert_eq!(&pt[..n], msg, "decrypt_detached_out into an oversized buffer"); - assert_tail_zeroed(&pt, n, "decrypt_detached_out"); - - // the inline one-shots with associated data - let need = E::encrypt_out_len(len); - let mut sealed = vec![SENTINEL; need + EXTRA]; - let (nonce, n) = E::encrypt_with_aad_out(&key, aad, msg, &mut sealed).unwrap(); - assert_eq!(n, need, "encrypt_with_aad_out into an oversized buffer"); - assert_tail_zeroed(&sealed, n, "encrypt_with_aad_out"); - sealed.truncate(n); - let mut buf = vec![SENTINEL; need + EXTRA]; - let (_, n) = E::encrypt_with_aad_rng_out( - &key, - &mut FixedSeedRNG::::new(pinned), - aad, - msg, - &mut buf, - ) - .unwrap(); - assert_eq!(n, need, "encrypt_with_aad_rng_out into an oversized buffer"); - assert_tail_zeroed(&buf, n, "encrypt_with_aad_rng_out"); - let mut pt = vec![SENTINEL; D::decrypt_out_len(sealed.len()) + EXTRA]; - let n = D::decrypt_with_aad_out(&key, &nonce, aad, &sealed, &mut pt).unwrap(); - assert_eq!(&pt[..n], msg, "decrypt_with_aad_out into an oversized buffer"); - assert_tail_zeroed(&pt, n, "decrypt_with_aad_out"); - - // the detached finals: each reports only what it wrote itself, so with what the update - // released it adds up to exactly the message - let (mut enc, nonce) = E::do_encrypt_init(&key).unwrap(); - enc.do_update_aad(aad).unwrap(); - let mut ct = vec![0u8; enc.do_encrypt_out_len(len)]; - let written = enc.do_encrypt_out(msg, &mut ct).unwrap(); - ct.truncate(written); - let mut last = [SENTINEL; FINAL_LEN]; - let (last_len, tag) = enc.do_encrypt_final_detached_out(&mut last).unwrap(); - assert_tail_zeroed(&last, last_len, "do_encrypt_final_detached_out"); - ct.extend_from_slice(&last[..last_len]); - assert_eq!( - ct.len(), - E::encrypt_detached_out_len(len), - "do_encrypt_out and do_encrypt_final_detached_out must report only their own bytes" - ); - let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); - dec.do_update_aad(aad).unwrap(); - let mut rec = vec![0u8; dec.do_decrypt_out_len(ct.len())]; - let released = dec.do_decrypt_out(&ct, &mut rec).unwrap(); - rec.truncate(released); - let mut last = [SENTINEL; FINAL_LEN]; - let data_len = dec.do_decrypt_final_detached_out(&tag, &mut last).unwrap(); - assert_tail_zeroed(&last, data_len, "do_decrypt_final_detached_out"); - rec.extend_from_slice(&last[..data_len]); - assert_eq!( - rec, msg, - "do_decrypt_out and do_decrypt_final_detached_out must report only their own bytes" - ); - - // The key-type and security-strength checks on `do_encrypt_init` / `do_decrypt_init` are - // covered by the `TestFrameworkSymmetricCipher` suite run above. - } - /// The [`AeadEncryptionKey`] / [`AeadDecryptionKey`] form of - /// [`test_encryptor_decryptor`](Self::test_encryptor_decryptor): the same AAD and tag - /// contract reached through a key object, with `rng` supplying every nonce draw that is not + /// The AEAD contract over an [`AeadEncryptionKey`] / [`AeadDecryptionKey`] pair: the AAD and + /// tag contract reached through a key object, with `rng` supplying every nonce draw that is not /// pinned. The key-policy checks are `TestFrameworkFromKeyMaterial`'s, and the no-AAD /// symmetric contract is reached here as the AEAD one with an empty AAD. /// @@ -792,10 +64,22 @@ impl TestFrameworkAEADCipher { FINAL_LEN >= TAG_LEN, "FINAL_LEN must have room for the inline tag the decryptor holds back" ); - let encrypt_out_len = |len: usize| K::Encryptor::encrypt_out_len(len); - let encrypt_detached_out_len = |len: usize| K::Encryptor::encrypt_detached_out_len(len); - let decrypt_out_len = |len: usize| K::Decryptor::decrypt_out_len(len); - let decrypt_detached_out_len = |len: usize| K::Decryptor::decrypt_detached_out_len(len); + let encrypt_out_len = |len: usize| { + as SymmetricCipherEncryptor>::encrypt_out_len( + len, + ) + }; + let encrypt_detached_out_len = |len: usize| { + as AEADCipherEncryptor>::encrypt_detached_out_len(len) + }; + let decrypt_out_len = |len: usize| { + as SymmetricCipherDecryptor>::decrypt_out_len( + len, + ) + }; + let decrypt_detached_out_len = |len: usize| { + as AEADCipherDecryptor>::decrypt_detached_out_len(len) + }; let aad: &[u8] = b"some associated data"; let pinned = [0xA5u8; NONCE_LEN]; let fixed_rng = || FixedSeedRNG::::new(pinned); @@ -836,7 +120,7 @@ impl TestFrameworkAEADCipher { if len >= 8 { assert_ne!(&ct[..], msg, "len {len}: the ciphertext must not be the plaintext"); } - let mut pt = vec![0u8; decrypt_detached_out_len(ct.len())]; + let mut pt = vec![0xEEu8; decrypt_detached_out_len(ct.len())]; let pt_len = key.decrypt_detached_out(&nonce, aad, &ct, &tag, &mut pt).unwrap(); pt.truncate(pt_len); assert_eq!(&pt[..], msg, "one-shot round trip, len {len}"); @@ -867,7 +151,7 @@ impl TestFrameworkAEADCipher { encrypt_detached_out_len(len) + TAG_LEN, "encrypt_with_aad_out must write the ciphertext plus the tag, len {len}" ); - let mut pt4 = vec![0u8; decrypt_out_len(inline_len)]; + let mut pt4 = vec![0xEEu8; decrypt_out_len(inline_len)]; let pt4_len = key .decrypt_with_aad_out(&inline_nonce, aad, &inline[..inline_len], &mut pt4) .unwrap(); @@ -1163,7 +447,9 @@ impl TestFrameworkAEADCipher { } // tampering: every one of these must fail the tag check, and the one-shots must leave no - // plaintext behind when they do. + // plaintext behind when they do. Their buffers are pre-filled with a non-zero byte, so one + // the decryptor never wrote is not mistaken for one it zeroized. A message long enough to + // have a byte 3 to flip, unless the pair's fixed length says otherwise. let msg = &DUMMY_SEED[..self.fixed_message_len.unwrap_or(max_len.max(17))]; let mut ct = vec![0u8; encrypt_detached_out_len(msg.len())]; let (ct_len, nonce, tag) = key.encrypt_detached_out(rng, aad, msg, &mut ct).unwrap(); @@ -1172,7 +458,7 @@ impl TestFrameworkAEADCipher { if ct.len() > 3 { let mut tampered = ct.clone(); tampered[3] ^= 0xFF; - let mut buf = vec![0u8; decrypt_detached_out_len(tampered.len())]; + let mut buf = vec![0xEEu8; decrypt_detached_out_len(tampered.len())]; match key.decrypt_detached_out(&nonce, aad, &tampered, &tag, &mut buf) { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } other => panic!("a modified ciphertext must fail the tag check, got {other:?}"), @@ -1196,7 +482,7 @@ impl TestFrameworkAEADCipher { tampered_inline.extend_from_slice(&tag); tampered_inline[3] ^= 0xFF; for with_aad in [false, true] { - let mut buf = vec![0u8; decrypt_out_len(tampered_inline.len())]; + let mut buf = vec![0xEEu8; decrypt_out_len(tampered_inline.len())]; let aad_used: &[u8] = if with_aad { aad } else { &[] }; // Without the AAD the tag was never going to verify; either way what matters is the // failure and the zeroized buffer. @@ -1216,13 +502,14 @@ impl TestFrameworkAEADCipher { let mut wrong_tag = tag; wrong_tag[0] ^= 0xFF; - let mut buf = vec![0u8; decrypt_detached_out_len(ct.len())]; + let mut buf = vec![0xEEu8; decrypt_detached_out_len(ct.len())]; match key.decrypt_detached_out(&nonce, aad, &ct, &wrong_tag, &mut buf) { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } other => panic!("a modified tag must fail the tag check, got {other:?}"), }; + assert!(buf.iter().all(|&b| b == 0), "a modified tag must leave the buffer zeroized"); - let mut buf = vec![0u8; decrypt_detached_out_len(ct.len())]; + let mut buf = vec![0xEEu8; decrypt_detached_out_len(ct.len())]; match key.decrypt_detached_out( &nonce, b"not the right associated data", @@ -1233,15 +520,17 @@ impl TestFrameworkAEADCipher { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } other => panic!("a modified AAD must fail the tag check, got {other:?}"), }; + assert!(buf.iter().all(|&b| b == 0), "a modified AAD must leave the buffer zeroized"); if NONCE_LEN > 0 { let mut wrong_nonce = nonce; wrong_nonce[0] ^= 0xFF; - let mut buf = vec![0u8; decrypt_detached_out_len(ct.len())]; + let mut buf = vec![0xEEu8; decrypt_detached_out_len(ct.len())]; match key.decrypt_detached_out(&wrong_nonce, aad, &ct, &tag, &mut buf) { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } other => panic!("a modified nonce must fail the tag check, got {other:?}"), }; + assert!(buf.iter().all(|&b| b == 0), "a modified nonce must leave the buffer zeroized"); // two encryptions under the same key must not reuse a nonce let (_enc1, nonce1) = key.create_encryptor(rng).unwrap(); @@ -1340,260 +629,12 @@ impl TestFrameworkAEADTaggedLayout { Self {} } - /// Exercises the inline-layout contract for one encryptor/decryptor pair. - /// - /// Checks, in order: - /// * at every message length from 0 to `4 * TAG_LEN + 5`: the one-shot pair round-trips, the - /// detached one-shot is the same ciphertext with the tag split off, and for every chunking - /// the streaming pair agrees with the one-shot byte for byte -- with the decryptor, not the - /// caller, holding back the possible tag, in both the inline and the detached finalization; - /// * a tampered inline stream fails at finalization on both entry points and the one-shots - /// zeroize their buffer, and an input shorter than the tag is `DecryptionFailed` rather - /// than a panic on the short slice; - /// * every inline one-shot refuses an output buffer one byte short, naming the length it - /// needs, and accepts one of exactly that length. - pub fn test< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const FINAL_LEN: usize, - E: AEADCipherEncryptor, - D: AEADCipherDecryptor, - >( - &self, - ) { - let key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - Self::round_trip_at_every_length_and_chunking::( - &key, - ); - Self::tampering_and_short_input_are_rejected::( - &key, - ); - Self::undersized_buffers_are_rejected::(&key); - } - /// The pinned RNG every encryption draws its nonce from, so that all paths use one nonce. fn rng() -> FixedSeedRNG { FixedSeedRNG::::new(core::array::from_fn(|i| 0xA5 ^ (i as u8))) } - /// Encrypts `msg` into the inline layout with the one-shot, under the pinned nonce, and - /// returns it with that nonce. - fn tagged_ct< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const FINAL_LEN: usize, - E: AEADCipherEncryptor, - >( - key: &KeyMaterial, - msg: &[u8], - ) -> (Vec, [u8; NONCE_LEN]) { - let mut ct = vec![0u8; E::encrypt_out_len(msg.len())]; - let (nonce, written) = - E::encrypt_with_aad_rng_out(key, &mut Self::rng::(), AAD, msg, &mut ct) - .unwrap(); - assert_eq!(written, msg.len() + TAG_LEN, "inline layout is ciphertext || tag"); - ct.truncate(written); - (ct, nonce) - } - - fn round_trip_at_every_length_and_chunking< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const FINAL_LEN: usize, - E: AEADCipherEncryptor, - D: AEADCipherDecryptor, - >( - key: &KeyMaterial, - ) { - for len in 0..=(4 * TAG_LEN + 5) { - let msg = &DUMMY_SEED[..len]; - let (ct, nonce) = - Self::tagged_ct::(key, msg); - - let mut pt = vec![0u8; D::decrypt_out_len(ct.len())]; - let n = D::decrypt_with_aad_out(key, &nonce, AAD, &ct, &mut pt).unwrap(); - assert_eq!(&pt[..n], msg, "len {len}: one-shot round trip"); - - // The detached layout is the same ciphertext with the tag split off. - let mut detached = vec![0u8; E::encrypt_detached_out_len(len)]; - let (d_nonce, d_len, d_tag) = E::encrypt_detached_rng_out( - key, - &mut Self::rng::(), - AAD, - msg, - &mut detached, - ) - .unwrap(); - assert_eq!(d_nonce, nonce, "len {len}: the pinned RNG must give the same nonce"); - assert_eq!(&detached[..d_len], &ct[..len], "len {len}: detached ciphertext"); - assert_eq!(&d_tag[..], &ct[len..], "len {len}: detached tag"); - - for chunk in [1usize, 2, 3, TAG_LEN.max(1), len.max(1)] { - // Encrypt in chunks, finishing with the tag appended by the streaming finalizer. - let (mut enc, stream_nonce) = - E::do_encrypt_init_rng(key, &mut Self::rng::()).unwrap(); - assert_eq!(stream_nonce, nonce, "len {len}: streaming init draws the same nonce"); - enc.do_update_aad(AAD).unwrap(); - let mut stream_ct = vec![0u8; len + FINAL_LEN]; - let mut written = 0; - for piece in msg.chunks(chunk) { - written += enc.do_encrypt_out(piece, &mut stream_ct[written..]).unwrap(); - } - let mut last = [0u8; FINAL_LEN]; - let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); - stream_ct[written..written + last_len].copy_from_slice(&last[..last_len]); - written += last_len; - stream_ct.truncate(written); - assert_eq!( - stream_ct, ct, - "len {len}, chunk {chunk}: streaming must match the one-shot" - ); - - // Decrypt in chunks, tag and all: the decryptor holds the tag back itself, so - // nothing past the plaintext is ever released, and the final call releases - // whatever plaintext it was still holding and nothing more. - let mut dec = D::do_decrypt_init(key, &stream_nonce).unwrap(); - dec.do_update_aad(AAD).unwrap(); - let mut out = vec![0u8; stream_ct.len() + FINAL_LEN]; - let mut written = 0; - for piece in stream_ct.chunks(chunk) { - written += dec.do_decrypt_out(piece, &mut out[written..]).unwrap(); - } - assert!(written <= len, "len {len}, chunk {chunk}: the tag must be held back"); - let (last, data_len) = dec.do_decrypt_final().unwrap(); - assert_eq!( - written + data_len, - len, - "len {len}, chunk {chunk}: the final call releases exactly the rest" - ); - out[written..written + data_len].copy_from_slice(&last[..data_len]); - out.truncate(written + data_len); - assert_eq!(out, msg, "len {len}, chunk {chunk}: streaming round trip"); - - // The same held-back bytes are ciphertext if the tag is detached. - let mut dec = D::do_decrypt_init(key, &stream_nonce).unwrap(); - dec.do_update_aad(AAD).unwrap(); - let mut out = vec![0u8; len + FINAL_LEN]; - let mut written = 0; - for piece in stream_ct[..len].chunks(chunk) { - written += dec.do_decrypt_out(piece, &mut out[written..]).unwrap(); - } - let mut last = [0u8; FINAL_LEN]; - let last_len = dec.do_decrypt_final_detached_out(&d_tag, &mut last).unwrap(); - assert_eq!( - written + last_len, - len, - "len {len}, chunk {chunk}: detached final flushes the rest" - ); - out[written..written + last_len].copy_from_slice(&last[..last_len]); - out.truncate(written + last_len); - assert_eq!(out, msg, "len {len}, chunk {chunk}: detached streaming round trip"); - } - } - } - - fn tampering_and_short_input_are_rejected< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const FINAL_LEN: usize, - E: AEADCipherEncryptor, - D: AEADCipherDecryptor, - >( - key: &KeyMaterial, - ) { - let msg = &DUMMY_SEED[..10]; - let (ct, nonce) = Self::tagged_ct::(key, msg); - - let mut tampered = ct.clone(); - tampered[0] ^= 0xFF; - let mut pt = vec![0xEEu8; tampered.len()]; - assert!(matches!( - D::decrypt_with_aad_out(key, &nonce, AAD, &tampered, &mut pt), - Err(SymmetricCipherError::AEADTagCheckFailed) - )); - assert_eq!(pt, vec![0u8; tampered.len()], "the one-shot zeroizes on a failed tag check"); - - let mut dec = D::do_decrypt_init(key, &nonce).unwrap(); - dec.do_update_aad(AAD).unwrap(); - dec.do_decrypt_out(&tampered, &mut pt).unwrap(); - assert!(matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::AEADTagCheckFailed))); - - // A wrong detached tag fails, and `decrypt_detached_out` zeroizes what it wrote. - let mut wrong_tag = [0u8; TAG_LEN]; - wrong_tag.copy_from_slice(&ct[msg.len()..]); - wrong_tag[0] ^= 0xFF; - let mut pt = vec![0xEEu8; msg.len()]; - assert!(matches!( - D::decrypt_detached_out(key, &nonce, AAD, &ct[..msg.len()], &wrong_tag, &mut pt), - Err(SymmetricCipherError::AEADTagCheckFailed) - )); - assert_eq!(pt, vec![0u8; msg.len()], "the detached one-shot zeroizes on a failed check"); - - for short_len in 0..TAG_LEN { - let mut pt = vec![0u8; TAG_LEN]; - assert!( - matches!( - D::decrypt_with_aad_out(key, &nonce, AAD, &ct[..short_len], &mut pt), - Err(SymmetricCipherError::DecryptionFailed) - ), - "{short_len} bytes cannot carry a {TAG_LEN}-byte tag (one-shot)" - ); - let mut dec = D::do_decrypt_init(key, &nonce).unwrap(); - assert_eq!(dec.do_decrypt_out(&ct[..short_len], &mut pt).unwrap(), 0); - assert!( - matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), - "{short_len} bytes cannot carry a {TAG_LEN}-byte tag (streaming)" - ); - } - } - - fn undersized_buffers_are_rejected< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const FINAL_LEN: usize, - E: AEADCipherEncryptor, - D: AEADCipherDecryptor, - >( - key: &KeyMaterial, - ) { - let msg = &DUMMY_SEED[..8]; - let (ct, nonce) = Self::tagged_ct::(key, msg); - - let needed = E::encrypt_out_len(msg.len()); - assert_eq!(needed, msg.len() + TAG_LEN); - let mut short = vec![0u8; needed - 1]; - match E::encrypt_with_aad_rng_out(key, &mut Self::rng::(), AAD, msg, &mut short) - { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, needed), - other => panic!("encrypt_with_aad_out into a short buffer: {other:?}"), - } - - let needed = D::decrypt_out_len(ct.len()); - assert_eq!(needed, msg.len()); - let mut short = vec![0u8; needed - 1]; - match D::decrypt_with_aad_out(key, &nonce, AAD, &ct, &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, needed), - other => panic!("decrypt_with_aad_out into a short buffer: {other:?}"), - } - - // A buffer of exactly the length it asks for must be accepted. Without this the - // `plaintext.len() < needed` guard can be weakened to `<=` or `==` without any test - // noticing: a too-short buffer is caught either way, by the guard or by `do_update_out` - // behind it, and both report the same error with the same length. - let mut exact = vec![0u8; needed]; - let n = D::decrypt_with_aad_out(key, &nonce, AAD, &ct, &mut exact).unwrap(); - assert_eq!(&exact[..n], msg, "a buffer of exactly `needed` bytes must be enough"); - } - /// The key-object form of [`test`](Self::test), over an [`AeadEncryptionKey`] / + /// The two tag layouts' contract over an [`AeadEncryptionKey`] / /// [`AeadDecryptionKey`] pair; every nonce is drawn from the pinned RNG. pub fn test_key< const KEY_LEN: usize, @@ -1606,9 +647,19 @@ impl TestFrameworkAEADTaggedLayout { &self, key: &K, ) { - let encrypt_out_len = |len: usize| K::Encryptor::encrypt_out_len(len); - let encrypt_detached_out_len = |len: usize| K::Encryptor::encrypt_detached_out_len(len); - let decrypt_out_len = |len: usize| K::Decryptor::decrypt_out_len(len); + let encrypt_out_len = |len: usize| { + as SymmetricCipherEncryptor>::encrypt_out_len( + len, + ) + }; + let encrypt_detached_out_len = |len: usize| { + as AEADCipherEncryptor>::encrypt_detached_out_len(len) + }; + let decrypt_out_len = |len: usize| { + as SymmetricCipherDecryptor>::decrypt_out_len( + len, + ) + }; // Encrypts `msg` into the inline layout with the one-shot, under the pinned nonce. let tagged_ct = |msg: &[u8]| -> (Vec, [u8; NONCE_LEN]) { let mut ct = vec![0u8; encrypt_out_len(msg.len())]; @@ -1624,7 +675,7 @@ impl TestFrameworkAEADTaggedLayout { let msg = &DUMMY_SEED[..len]; let (ct, nonce) = tagged_ct(msg); - let mut pt = vec![0u8; decrypt_out_len(ct.len())]; + let mut pt = vec![0xEEu8; decrypt_out_len(ct.len())]; let n = key.decrypt_with_aad_out(&nonce, AAD, &ct, &mut pt).unwrap(); assert_eq!(&pt[..n], msg, "len {len}: one-shot round trip"); @@ -1701,7 +752,7 @@ impl TestFrameworkAEADTaggedLayout { let (ct, nonce) = tagged_ct(msg); let mut tampered = ct.clone(); tampered[0] ^= 0xFF; - let mut pt = vec![0u8; tampered.len()]; + let mut pt = vec![0xEEu8; tampered.len()]; assert!(matches!( key.decrypt_with_aad_out(&nonce, AAD, &tampered, &mut pt), Err(SymmetricCipherError::AEADTagCheckFailed) @@ -1715,7 +766,7 @@ impl TestFrameworkAEADTaggedLayout { let mut wrong_tag = [0u8; TAG_LEN]; wrong_tag.copy_from_slice(&ct[msg.len()..]); wrong_tag[0] ^= 0xFF; - let mut pt = vec![0u8; msg.len()]; + let mut pt = vec![0xEEu8; msg.len()]; assert!(matches!( key.decrypt_detached_out(&nonce, AAD, &ct[..msg.len()], &wrong_tag, &mut pt), Err(SymmetricCipherError::AEADTagCheckFailed) diff --git a/crypto/core-test-framework/src/block_cipher.rs b/crypto/core-test-framework/src/block_cipher.rs index b1e3b4765..814ef530c 100644 --- a/crypto/core-test-framework/src/block_cipher.rs +++ b/crypto/core-test-framework/src/block_cipher.rs @@ -3,10 +3,7 @@ use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey}; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{BlockDecryptionKey, BlockEncryptionKey}; use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, RNG}; @@ -21,193 +18,7 @@ impl TestFrameworkBlockCipher { Self {} } - /// - pub fn test< - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, - E: BlockCipherEncryptor, - D: BlockCipherDecryptor, - >( - &self, - ) { - let key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - - // multi-block (two at a time) 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(); - - for msg_pair in DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0.iter() { - // encrypt together, decrypt together - let mut buf = *msg_pair; - encryptor.do_encrypt_blocks_inplace(&mut buf).unwrap(); - decryptor.do_decrypt_blocks_inplace(&mut buf).unwrap(); - assert_eq!(msg_pair, &buf); - - // encrypt together, decrypt one at a time - let mut buf = *msg_pair; - encryptor.do_encrypt_blocks_inplace(&mut buf).unwrap(); - for (msg_chunk, block) in msg_pair.iter().zip(buf.iter_mut()) { - decryptor.do_decrypt_blocks_inplace(block.as_chunks_mut().0).unwrap(); - assert_eq!(msg_chunk, block); - } - - // encrypt one at a time, decrypt together - let mut buf = *msg_pair; - for block in buf.iter_mut() { - encryptor.do_encrypt_blocks_inplace(block.as_chunks_mut().0).unwrap(); - } - decryptor.do_decrypt_blocks_inplace(&mut buf).unwrap(); - assert_eq!(msg_pair, &buf); - } - - // 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 mut buf = *one_block; - let (n, iv) = E::encrypt_inplace(&key, &mut buf).unwrap(); - assert_eq!(n, BLOCK_LEN, "encrypt must report the number of bytes written"); - let ct = buf; - let n = D::decrypt_inplace(&key, &iv, &mut buf).unwrap(); - assert_eq!(n, BLOCK_LEN, "decrypt must report the number of bytes written"); - 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(); - let mut buf = ct; - streamed.do_decrypt_blocks_inplace(buf.as_chunks_mut().0).unwrap(); - assert_eq!(buf, *one_block); - - // The one-shots take a runtime length, so they must refuse data that is not a whole number - // of blocks, before touching it. - for len in [1, BLOCK_LEN - 1, BLOCK_LEN + 1, 2 * BLOCK_LEN + 1] { - let mut bad = DUMMY_SEED[..len].to_vec(); - assert!( - matches!( - E::encrypt_inplace(&key, &mut bad), - Err(SymmetricCipherError::DataNotBlockAligned) - ), - "len {len}: an unaligned plaintext must be refused" - ); - // The RNG one-shot is only driven for a cipher with init data; see below. - if INIT_DATA_LEN > 0 { - assert!( - matches!( - E::encrypt_rng_inplace( - &key, - &mut FixedSeedRNG::::new([0xA5u8; INIT_DATA_LEN]), - &mut bad - ), - Err(SymmetricCipherError::DataNotBlockAligned) - ), - "len {len}: an unaligned plaintext must be refused by the RNG one-shot" - ); - } - assert!( - matches!( - D::decrypt_inplace(&key, &iv, &mut bad), - Err(SymmetricCipherError::DataNotBlockAligned) - ), - "len {len}: an unaligned ciphertext must be refused" - ); - assert_eq!(bad, DUMMY_SEED[..len], "len {len}: refused data must be untouched"); - } - - // The RNG-taking constructor is only exercised for a cipher that has init data to - // generate. Its contract requires an implementation with `INIT_DATA_LEN == 0` (ECB) to - // panic instead, so driving it here would fail that implementor for conforming. - if INIT_DATA_LEN > 0 { - // 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_blocks_inplace(expected.as_chunks_mut().0).unwrap(); - let mut buf = *one_block; - let (n, iv) = E::encrypt_rng_inplace( - &key, - &mut FixedSeedRNG::::new(pinned), - &mut buf, - ) - .unwrap(); - assert_eq!(n, BLOCK_LEN, "encrypt_rng must report the number of bytes written"); - assert_eq!(iv, iv_streamed); - assert_eq!(buf, expected); - } - - // test that the iv is random (ie not the same on two runs). A mode with no init data at all - // (ECB, INIT_DATA_LEN == 0) has nothing to compare: two empty arrays are always equal. - if INIT_DATA_LEN > 0 { - let (_encryptor, iv1) = E::do_encrypt_init(&key).unwrap(); - let (_encryptor, iv2) = E::do_encrypt_init(&key).unwrap(); - assert_ne!(iv1, iv2); - } - - // error case: KeyMaterial of wrong type - let mac_key = - KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) - .unwrap(); - match E::do_encrypt_init(&mac_key) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Unexpected error"), - }; - - // error case: security strengths too weak and too strong - let mut key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - let security_strengths = [ - SecurityStrength::None, - SecurityStrength::_112bit, - SecurityStrength::_128bit, - SecurityStrength::_192bit, - SecurityStrength::_256bit, - ]; - let mut strengths_tested = 0; - for ss in security_strengths.iter() { - // `set_security_strength` enforces its key-length guard even inside a - // do_hazardous_operations() closure -- a KEY_LEN-byte key cannot be tagged at a - // strength above `from_bytes(KEY_LEN)` -- so skip the strengths this key cannot carry - // rather than unwrapping an error. (A 16-byte key can reach 128-bit and no higher.) - // Do NOT "fix" this by relaxing that guard in `KeyMaterial`: core's - // `test_hazardous_ops_error_handling` requires it to stay enforced. - if ss > &SecurityStrength::from_bytes(KEY_LEN) { - continue; - } - // Inside a do_hazardous_operations() closure set_security_strength() raises the - // strength without complaining; any error here is a framework bug, hence unwrap(). - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); - strengths_tested += 1; - - match E::do_encrypt_init(&key) { - Ok(_) => { - if ss >= &E::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should have been a strong enough key"); - } - } - Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &E::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should not have accepted a key weaker than algorithm"); - } - } - _ => panic!("Unexpected error"), - }; - } - assert!(strengths_tested > 0, "strength sweep must not be vacuous"); - } - /// The [`BlockEncryptionKey`] / [`BlockDecryptionKey`] form of [`test`](Self::test), with + /// The block-cipher contract over a [`BlockEncryptionKey`] / [`BlockDecryptionKey`] pair, with /// `rng` supplying every init data draw. The key-policy checks are /// `TestFrameworkFromKeyMaterial`'s. pub fn test_key< @@ -283,7 +94,7 @@ impl TestFrameworkBlockCipher { assert_ne!(iv1, iv2); } - /// The [`EcbEncryptionKey`] / [`EcbDecryptionKey`] form of [`test`](Self::test): no init + /// The same contract over an [`EcbEncryptionKey`] / [`EcbDecryptionKey`] pair: no init /// data, so nothing is drawn and nothing can differ between two runs. pub fn test_ecb_key< const KEY_LEN: usize, @@ -344,11 +155,10 @@ impl TestFrameworkBlockCipher { /// One block at a time, through the flat streaming methods (LEN = BLOCK_LEN), in place. fn round_trip_blocks< - const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, - E: BlockCipherEncryptor, - D: BlockCipherDecryptor, + E: BlockCipherEncryptor, + D: BlockCipherDecryptor, >( encryptor: &mut E, decryptor: &mut D, @@ -365,11 +175,10 @@ impl TestFrameworkBlockCipher { /// together must decrypt both together and one at a time, and blocks encrypted one at a time /// must decrypt together. fn round_trip_pairs< - const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, - E: BlockCipherEncryptor, - D: BlockCipherDecryptor, + E: BlockCipherEncryptor, + D: BlockCipherDecryptor, >( encryptor: &mut E, decryptor: &mut D, @@ -383,13 +192,13 @@ impl TestFrameworkBlockCipher { let mut buf = *msg_pair; encryptor.do_encrypt_blocks_inplace(&mut buf).unwrap(); for (msg_chunk, block) in msg_pair.iter().zip(buf.iter_mut()) { - decryptor.do_decrypt_blocks_inplace(core::slice::from_mut(block)).unwrap(); + decryptor.do_decrypt_blocks_inplace(block.as_chunks_mut().0).unwrap(); assert_eq!(msg_chunk, block); } let mut buf = *msg_pair; for block in buf.iter_mut() { - encryptor.do_encrypt_blocks_inplace(core::slice::from_mut(block)).unwrap(); + encryptor.do_encrypt_blocks_inplace(block.as_chunks_mut().0).unwrap(); } decryptor.do_decrypt_blocks_inplace(&mut buf).unwrap(); assert_eq!(msg_pair, &buf); diff --git a/crypto/core-test-framework/src/mac.rs b/crypto/core-test-framework/src/mac.rs index da1ba6b90..7067bf9e2 100644 --- a/crypto/core-test-framework/src/mac.rs +++ b/crypto/core-test-framework/src/mac.rs @@ -1,10 +1,5 @@ -//! Generic behaviour tests for anything that implements [`MAC`]. +//! Generic behaviour tests for any MAC key object, through the [`MAC`] workers it hands out. -use crate::DUMMY_SEED; -use bouncycastle_core::errors::{KeyMaterialError, MACError}; -use bouncycastle_core::hazmat::do_hazardous_operations; -use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; -use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::MAC; @@ -19,142 +14,9 @@ impl TestFrameworkMAC { Self {} } - /// Test all the members of trait Hash against the given input-output pair. - /// This gives good baseline test coverage, but is not exhaustive. - pub fn test_mac( - &self, - key: &impl KeyMaterialTrait, - input: &[u8], - expected_output: &[u8], - ) { - // Test ::mac() - let out = M::new_allow_weak_key(key).unwrap().mac(input); - assert_eq!(out, expected_output); - - // Test ::mac_out - let mut out = vec![0u8; expected_output.len()]; - let bytes_written = M::new_allow_weak_key(key).unwrap().mac_out(input, &mut out).unwrap(); - assert_eq!(bytes_written, expected_output.len()); - assert_eq!(out, expected_output); - - // Test an output buffer that's too small (should truncate) - let mut out = vec![0u8; expected_output.len() - 2]; - let bytes_written = M::new_allow_weak_key(key).unwrap().mac_out(input, &mut out).unwrap(); - assert_eq!(bytes_written, expected_output.len() - 2); - assert_eq!(out, expected_output[..expected_output.len() - 2]); - - // Test an output buffer that's too big (expect the first L bytes to get filled) - let mut out = vec![0u8; 2 * expected_output.len()]; - let bytes_written = M::new_allow_weak_key(key).unwrap().mac_out(input, &mut out).unwrap(); - assert_eq!(bytes_written, expected_output.len()); - assert_eq!(&out[..expected_output.len()], expected_output); - assert_eq!(&out[expected_output.len()..], vec![0u8; expected_output.len()]); - - // Test ::verify() - assert!(M::new_allow_weak_key(key).unwrap().verify(input, expected_output)); - - // Test .new(), .do_update(), .do_mac_final() - // At the same time, test .output_len() - let mut mac = M::new_allow_weak_key(key).unwrap(); - let output_len = mac.output_len(); - mac.do_update(input); - let out = mac.do_final(); - assert_eq!(out, expected_output); - - // Test .output_len() - assert_eq!(output_len, out.len()); - - // Test .init(), .do_update(), .do_mac_final_out() - let mut mac = M::new_allow_weak_key(key).unwrap(); - mac.do_update(input); - let mut out = vec![0u8; mac.output_len()]; - let out_len = mac.do_final_out(&mut *out).unwrap(); - assert_eq!(out, expected_output); - assert_eq!(out_len, out.len()); - - // Test .init(), .do_update(), .do_verify_final_out() - let mut mac = M::new_allow_weak_key(key).unwrap(); - mac.do_update(input); - mac.do_verify_final(expected_output); - - // entropy of input key - - // MACs of all security strengths should throw an error on a no-security (and non-zero) key. - - let mut key_none = - KeyMaterial512::from_bytes_as_type(&DUMMY_SEED[0..64], KeyType::MACKey).unwrap(); - key_none.set_security_strength(SecurityStrength::None).unwrap(); - - match M::new(&key_none) { - Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) => { /* fine */ } - _ => panic!( - "This should have thrown a KeyMaterialError::SecurityStrength error but it didn't" - ), - } - - // A MAC keyed by a block cipher (CMAC) accepts exactly one key length, so the weak key - // takes the length of the caller's key whenever that fits in the buffer, raised if need be - // to the shortest length `KeyMaterial` lets carry the lowered strength; the per-strength - // defaults apply only to a key that does not fit (empty, or longer than 64 bytes). - let weak_key_len = |default_len: usize, min_len: usize| match key.key_len() { - 1..=64 => key.key_len().max(min_len), - _ => default_len, - }; - let mut low_security_key = - KeyMaterial512::from_bytes_as_type(&DUMMY_SEED[..64], KeyType::MACKey).unwrap(); - do_hazardous_operations(&mut low_security_key, |low_security_key| { - match M::new_allow_weak_key(key).unwrap().max_security_strength() { - SecurityStrength::None => { - low_security_key.set_key_len(weak_key_len(13, 0)).unwrap(); // truncates should be infallible - low_security_key.set_security_strength(SecurityStrength::None).unwrap(); - } - SecurityStrength::_112bit => { - low_security_key.set_key_len(weak_key_len(28, 0)).unwrap(); // truncate should be infallible - low_security_key.set_security_strength(SecurityStrength::None).unwrap(); - } - SecurityStrength::_128bit => { - low_security_key.set_key_len(weak_key_len(32, 14)).unwrap(); // truncate should be infallible - low_security_key.set_security_strength(SecurityStrength::_112bit).unwrap(); - } - SecurityStrength::_192bit => { - low_security_key.set_key_len(weak_key_len(48, 16)).unwrap(); // truncate should be infallible - low_security_key.set_security_strength(SecurityStrength::_128bit).unwrap(); - } - SecurityStrength::_256bit => { - low_security_key.set_key_len(weak_key_len(64, 24)).unwrap(); // truncate should be infallible - low_security_key.set_security_strength(SecurityStrength::_192bit).unwrap(); - } - // `SecurityStrength` is `#[non_exhaustive]`, so this arm is required. - _ => panic!( - "unhandled SecurityStrength variant -- add a case for it in the MAC test framework" - ), - }; - Ok(()) - }) - .unwrap(); - - // init - assert!( - low_security_key.security_strength() - < M::new_allow_weak_key(key).unwrap().max_security_strength() - ); - // complains at first - match M::new(&low_security_key) { - Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) => { /* fine */ } - _ => { - panic!( - "This should have thrown a KeyMaterialError::SecurityStrength error but it didn't" - ) - } - } - // but fine if you do it with .allow_weak_keys() - let mut hmac = M::new_allow_weak_key(&low_security_key).unwrap(); - hmac.do_update(b"Hi There"); - hmac.do_final(); - } - /// The key-object form of [`test_mac`](Self::test_mac): the workers a - /// [`MacGenerationKey`] / [`MacVerificationKey`] hands out, against the given input-output - /// pair. The key-policy checks are `TestFrameworkFromAnyKeyMaterial`'s. + /// The one-shots and the workers a [`MacGenerationKey`] / [`MacVerificationKey`] hands out, + /// against the given input-output pair. This gives good baseline test coverage, but is not + /// exhaustive; the key-policy checks are `TestFrameworkFromAnyKeyMaterial`'s. pub fn test_mac_key( &self, key: &K, diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 36be68fbb..335c49d76 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -4,9 +4,6 @@ use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::do_hazardous_operations; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; use bouncycastle_core::traits::{ RNG, StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, @@ -15,13 +12,13 @@ use bouncycastle_core::traits::{ /// Instance of the test framework. pub struct TestFrameworkSymmetricCipher { - /// For [`test_encryptor_decryptor`](Self::test_encryptor_decryptor): the plaintext length + /// For [`test_key`](Self::test_key): the plaintext length /// granularity the pair accepts. 1 (the default) means every length round-trips. A larger value /// -- the block length, for a `PaddedBlockCipherEncryptor` over `NoPadding` -- means only /// multiples of it round-trip, and every other length must be *rejected* by `do_encrypt_final` /// / `encrypt_out` with `DataNotBlockAligned`, which the test then asserts instead. pub required_alignment: usize, - /// For [`test_encryptor_decryptor`](Self::test_encryptor_decryptor): the one message length + /// For [`test_key`](Self::test_key): the one message length /// the pair's streaming methods accept, if they accept only one. `None` (the default) means /// any length. A cipher whose payload length is fixed by its type -- CCM, whose `B0` block /// encodes the payload length, through its `DATA_LEN` parameter -- sets it here: the @@ -37,461 +34,8 @@ impl TestFrameworkSymmetricCipher { Self { required_alignment: 1, fixed_message_len: None } } - /// Exercises the [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] contract for a - /// paired implementor. - /// - /// Checks, in order: - /// * the one-shot `encrypt_out` / `decrypt_out` round-trip for every plaintext length from - /// 0 to a few times `FINAL_LEN`, writing exactly `encrypt_out_len` bytes and at most - /// `decrypt_out_len`; - /// * the `std` one-shots agree with the `_out` ones; - /// * streaming in every chunking agrees with the one-shot, `update_out_len` is exact on every - /// call, and `do_encrypt_final_out` / `do_decrypt_final_out` agree with `do_encrypt_final` / - /// `do_decrypt_final`; - /// * a driven RNG reproduces its init data, and the same key and init data give the same - /// ciphertext through `do_encrypt_init_rng` and `encrypt_rng_out`; - /// * a corrupted ciphertext either fails to decrypt or decrypts to something else; - /// * an output buffer that is too short is refused, naming the required length, before any - /// work is done; - /// * every method that writes into a caller's buffer accepts one larger than needed, returns - /// the number of bytes that call wrote (not a running total over the stream), and zeroes - /// every byte of the buffer past that count; - /// * a key of the wrong [`KeyType`] is rejected, and the security-strength policy matches - /// [`Algorithm::MAX_SECURITY_STRENGTH`]. - /// - /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH - pub fn test_encryptor_decryptor< - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const FINAL_LEN: usize, - E: SymmetricCipherEncryptor, - D: SymmetricCipherDecryptor, - >( - &self, - ) { - let key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - // Enough plaintext lengths to cross several final-chunk boundaries (a block, for padding). - let align = self.required_alignment.max(1); - let max_len = (3 * FINAL_LEN.max(1) + 5).next_multiple_of(align); - if let Some(fixed) = self.fixed_message_len { - assert!(fixed <= DUMMY_SEED.len(), "fixed_message_len must fit the seed buffer"); - } - - // one-shot round trip, every (accepted) length; every other length must be refused - for len in 0..=max_len { - let msg = &DUMMY_SEED[..len]; - if !len.is_multiple_of(align) { - let mut ct = vec![0u8; E::encrypt_out_len(len) + FINAL_LEN]; - match E::encrypt_out(&key, msg, &mut ct) { - Err(SymmetricCipherError::DataNotBlockAligned) => {} - other => panic!("len {len} is not aligned and must be refused, got {other:?}"), - } - let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); - let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; - enc.do_encrypt_out(msg, &mut buf).unwrap(); - assert!( - matches!( - enc.do_encrypt_final(), - Err(SymmetricCipherError::DataNotBlockAligned) - ), - "len {len}: streaming do_encrypt_final must refuse an unaligned message" - ); - continue; - } - // a fixed-length pair's streaming methods take only that length; its one-shots take - // any length and must round trip it - if let Some(fixed) = self.fixed_message_len - && fixed != len - { - let mut ct = vec![0u8; E::encrypt_out_len(len)]; - let (init, n) = E::encrypt_out(&key, msg, &mut ct).unwrap_or_else(|e| { - panic!("fixed length: a {len}-byte one-shot must be accepted: {e:?}") - }); - let mut pt = vec![0u8; D::decrypt_out_len(n)]; - let m = D::decrypt_out(&key, &init, &ct[..n], &mut pt).unwrap(); - assert_eq!(&pt[..m], msg, "fixed length: {len}-byte one-shot round trip"); - let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); - let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; - assert!( - enc.do_encrypt_out(msg, &mut buf).is_err() || enc.do_encrypt_final().is_err(), - "fixed length: a {len}-byte stream must be refused" - ); - continue; - } - let mut ct = vec![0u8; E::encrypt_out_len(len)]; - let (init_data, ct_len) = E::encrypt_out(&key, msg, &mut ct).unwrap(); - assert_eq!(ct_len, ct.len(), "encrypt_out must write exactly encrypt_out_len bytes"); - - let mut pt = vec![0u8; D::decrypt_out_len(ct_len)]; - let pt_len = D::decrypt_out(&key, &init_data, &ct[..ct_len], &mut pt).unwrap(); - assert!(pt_len <= pt.len(), "decrypt_out_len must bound the plaintext"); - assert_eq!(&pt[..pt_len], msg, "one-shot round trip, len {len}"); - - // the std one-shots agree with the _out ones for the same init data - let (init_data2, ct2) = E::encrypt(&key, msg).unwrap(); - assert_eq!(ct2.len(), ct_len, "encrypt must return exactly the bytes written"); - let pt2 = D::decrypt(&key, &init_data2, &ct2).unwrap(); - assert_eq!(pt2, msg, "std round trip, len {len}"); - let pt3 = D::decrypt(&key, &init_data, &ct[..ct_len]).unwrap(); - assert_eq!(pt3, msg, "decrypt must agree with decrypt_out"); - } - - // streaming in every chunking agrees with the one-shot - let len = self.fixed_message_len.unwrap_or(max_len); - let msg = &DUMMY_SEED[..len]; - let chunkings: [usize; 8] = - [1, 2, 3, 7, FINAL_LEN.max(1), FINAL_LEN + 1, 2 * FINAL_LEN + 3, len.max(1)]; - for chunk in chunkings { - // encrypt in chunks, checking update_out_len is exact each time - let (mut enc, init_data) = E::do_encrypt_init(&key).unwrap(); - let mut ct = Vec::new(); - for piece in msg.chunks(chunk) { - let expect = enc.do_encrypt_out_len(piece.len()); - let mut buf = vec![0u8; expect]; - let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); - assert_eq!(n, expect, "update_out_len must be exact (encrypt, chunk {chunk})"); - ct.extend_from_slice(&buf[..n]); - } - let mut last = [0u8; FINAL_LEN]; - let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); - assert!( - last_len <= FINAL_LEN, - "do_encrypt_final_out must not claim more than FINAL_LEN bytes" - ); - ct.extend_from_slice(&last[..last_len]); - assert_eq!( - ct.len(), - E::encrypt_out_len(len), - "streaming total must match encrypt_out_len" - ); - - // one-shot decrypt of the streamed ciphertext - let mut pt = vec![0u8; D::decrypt_out_len(ct.len())]; - let m = D::decrypt_out(&key, &init_data, &ct, &mut pt).unwrap(); - assert_eq!( - &pt[..m], - msg, - "streamed ciphertext must decrypt in one shot (chunk {chunk})" - ); - - // decrypt in the same chunks, via do_decrypt_final and via do_decrypt_final_out - for use_out in [false, true] { - let mut dec = D::do_decrypt_init(&key, &init_data).unwrap(); - let mut rec = Vec::new(); - for piece in ct.chunks(chunk) { - let expect = dec.do_decrypt_out_len(piece.len()); - let mut buf = vec![0u8; expect]; - let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); - assert_eq!(n, expect, "update_out_len must be exact (decrypt, chunk {chunk})"); - rec.extend_from_slice(&buf[..n]); - } - let (block, data_len) = if use_out { - let mut block = [0u8; FINAL_LEN]; - let data_len = dec.do_decrypt_final_out(&mut block).unwrap(); - (block, data_len) - } else { - dec.do_decrypt_final().unwrap() - }; - rec.extend_from_slice(&block[..data_len]); - assert_eq!( - rec, msg, - "streamed round trip (chunk {chunk}, do_decrypt_final_out {use_out})" - ); - } - } - - // a fixed message length is enforced on both sides: one byte more is refused at the - // update, consuming nothing, and one byte fewer is refused at the final. Which variant - // each refusal carries is the implementor's to pin; here only that it refuses. - if let Some(fixed) = self.fixed_message_len { - let (mut enc, init_data) = E::do_encrypt_init(&key).unwrap(); - let mut ct = vec![0u8; enc.do_encrypt_out_len(fixed)]; - let n = enc.do_encrypt_out(msg, &mut ct).unwrap(); - ct.truncate(n); - let mut more = vec![0u8; enc.do_encrypt_out_len(1) + 1]; - assert!( - enc.do_encrypt_out(&DUMMY_SEED[..1], &mut more).is_err(), - "fixed length: one byte more must be refused at the update" - ); - // ...and the refusal consumed nothing: the final still completes the message. - let (last, last_len) = enc.do_encrypt_final().unwrap(); - ct.extend_from_slice(&last[..last_len]); - let mut pt = vec![0u8; D::decrypt_out_len(ct.len())]; - let m = D::decrypt_out(&key, &init_data, &ct, &mut pt).unwrap(); - assert_eq!(&pt[..m], msg, "fixed length: a refused update must not disturb the state"); - - if fixed > 0 { - let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); - let mut buf = vec![0u8; enc.do_encrypt_out_len(fixed - 1)]; - enc.do_encrypt_out(&msg[..fixed - 1], &mut buf).unwrap(); - assert!( - enc.do_encrypt_final().is_err(), - "fixed length: one byte fewer must be refused at the final (encrypt)" - ); - let mut dec = D::do_decrypt_init(&key, &init_data).unwrap(); - let mut buf = vec![0u8; dec.do_decrypt_out_len(fixed - 1)]; - dec.do_decrypt_out(&ct[..fixed - 1], &mut buf).unwrap(); - assert!( - dec.do_decrypt_final().is_err(), - "fixed length: one byte fewer must be refused at the final (decrypt)" - ); - } - let mut dec = D::do_decrypt_init(&key, &init_data).unwrap(); - let mut rec = vec![0u8; dec.do_decrypt_out_len(ct.len())]; - let n = dec.do_decrypt_out(&ct, &mut rec).unwrap(); - rec.truncate(n); - let mut more = vec![0u8; dec.do_decrypt_out_len(1) + 1]; - assert!( - dec.do_decrypt_out(&DUMMY_SEED[..1], &mut more).is_err(), - "fixed length: one byte more must be refused at the update (decrypt)" - ); - let (last, data_len) = dec.do_decrypt_final().unwrap(); - rec.extend_from_slice(&last[..data_len]); - assert_eq!( - rec, msg, - "fixed length: a refused update must not disturb the state (decrypt)" - ); - } - - // The RNG-taking constructor is only exercised for a cipher that has init data to - // generate. Its contract requires an implementation with `INIT_DATA_LEN == 0` (ECB) to - // panic instead, so driving it here would fail that implementor for conforming. - if INIT_DATA_LEN > 0 { - // a driven RNG reproduces its init data, and determines the ciphertext - let seed: [u8; INIT_DATA_LEN] = core::array::from_fn(|i| DUMMY_SEED[100 + i]); - let (mut enc, init_data) = - E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(seed)) - .unwrap(); - assert_eq!(init_data, seed, "a fixed RNG must yield its stream as the init data"); - let mut streamed = vec![0u8; enc.do_encrypt_out_len(len)]; - let n = enc.do_encrypt_out(msg, &mut streamed).unwrap(); - streamed.truncate(n); - let (last, last_len) = enc.do_encrypt_final().unwrap(); - streamed.extend_from_slice(&last[..last_len]); - let mut one_shot = vec![0u8; E::encrypt_out_len(len)]; - let (init_data2, n2) = E::encrypt_rng_out( - &key, - &mut FixedSeedRNG::::new(seed), - msg, - &mut one_shot, - ) - .unwrap(); - assert_eq!(init_data2, seed); - assert_eq!( - &one_shot[..n2], - &streamed[..], - "same key and init data must give the same ciphertext" - ); - } - - // corrupting the ciphertext does not give back the plaintext (or fails to decrypt) - let mut ct = vec![0u8; E::encrypt_out_len(len)]; - let (init_data, ct_len) = E::encrypt_out(&key, msg, &mut ct).unwrap(); - assert!(ct_len > 0, "the test message is non-empty, so its ciphertext must be"); - for flip in [0usize, ct_len / 2, ct_len - 1] { - let mut bad = ct[..ct_len].to_vec(); - bad[flip] ^= 0x80; - let mut pt = vec![0u8; D::decrypt_out_len(ct_len)]; - match D::decrypt_out(&key, &init_data, &bad, &mut pt) { - Ok(m) => { - assert_ne!(&pt[..m], msg, "corrupted byte {flip} decrypted to the plaintext") - } - Err(SymmetricCipherError::DecryptionFailed) - | Err(SymmetricCipherError::PaddingError(_)) - | Err(SymmetricCipherError::AEADTagCheckFailed) => { /* also fine */ } - Err(e) => panic!("unexpected error for corrupted byte {flip}: {e:?}"), - } - } - - // too-short output buffers are refused with the required length, before any work is done - let need = E::encrypt_out_len(len); - let mut short = vec![0u8; need - 1]; - match E::encrypt_out(&key, msg, &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), - other => panic!("encrypt_out into a short buffer: {other:?}"), - } - let need = D::decrypt_out_len(ct_len); - if need > 0 { - let mut short = vec![0u8; need - 1]; - match D::decrypt_out(&key, &init_data, &ct[..ct_len], &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), - other => panic!("decrypt_out into a short buffer: {other:?}"), - } - } - // ...and ones with room to spare are accepted: without this each `<` guard can be flipped - // to `>` and the short-buffer probes above still "pass". - let mut roomy = vec![0u8; E::encrypt_out_len(len) + 3]; - let (_, n) = E::encrypt_out(&key, msg, &mut roomy).unwrap(); - assert_eq!(n, E::encrypt_out_len(len), "encrypt_out into a roomy buffer"); - let mut roomy = vec![0u8; need + 3]; - let n = D::decrypt_out(&key, &init_data, &ct[..ct_len], &mut roomy).unwrap(); - assert_eq!(&roomy[..n], msg, "decrypt_out into a roomy buffer"); - let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); - let need = enc.do_encrypt_out_len(len); - if need > 0 { - let mut short = vec![0u8; need - 1]; - match enc.do_encrypt_out(msg, &mut short) { - Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), - other => panic!("do_update_out into a short buffer: {other:?}"), - } - } - - // Output-buffer contract, for every method that writes into a caller's buffer: a buffer - // larger than needed is accepted; the returned count is the number of bytes *that call* - // wrote, not a running total over the stream; and every byte past it is zeroed, whatever - // the buffer held on the way in. Each buffer is pre-filled with a non-zero sentinel, so a - // byte left as the caller had it shows up. The `_final_out` buffers are `[u8; FINAL_LEN]` - // by type and so cannot be oversized; for them only the zeroed tail is checked. - const SENTINEL: u8 = 0xA5; - const EXTRA: usize = 7; - let assert_tail_zeroed = |buf: &[u8], n: usize, what: &str| { - assert!( - n <= buf.len(), - "{what}: claims {n} bytes written to a {}-byte buffer", - buf.len() - ); - assert!( - buf[n..].iter().all(|&b| b == 0), - "{what}: the bytes past the {n} written must be zeroed" - ); - }; - - // the one-shots - let need = E::encrypt_out_len(len); - let mut ct = vec![SENTINEL; need + EXTRA]; - let (init_data, ct_len) = E::encrypt_out(&key, msg, &mut ct).unwrap(); - assert_eq!(ct_len, need, "encrypt_out into an oversized buffer must write encrypt_out_len"); - assert_tail_zeroed(&ct, ct_len, "encrypt_out"); - ct.truncate(ct_len); - if INIT_DATA_LEN > 0 { - let seed: [u8; INIT_DATA_LEN] = core::array::from_fn(|i| DUMMY_SEED[100 + i]); - let mut buf = vec![SENTINEL; need + EXTRA]; - let (_, n) = E::encrypt_rng_out( - &key, - &mut FixedSeedRNG::::new(seed), - msg, - &mut buf, - ) - .unwrap(); - assert_eq!( - n, need, - "encrypt_rng_out into an oversized buffer must write encrypt_out_len" - ); - assert_tail_zeroed(&buf, n, "encrypt_rng_out"); - } - let mut pt = vec![SENTINEL; D::decrypt_out_len(ct_len) + EXTRA]; - let n = D::decrypt_out(&key, &init_data, &ct, &mut pt).unwrap(); - assert_eq!(&pt[..n], msg, "decrypt_out into an oversized buffer"); - assert_tail_zeroed(&pt, n, "decrypt_out"); - - // Streaming, in pieces of 1 byte, FINAL_LEN + 1 bytes and the rest: a cipher that holds - // data back then releases nothing on some calls after earlier calls released data, which is - // where a running total and a per-call count differ. Each call must report its own - // `do_*_out_len`, and the per-call counts must add up to exactly the whole stream's - // length, which a running total would overshoot. - let cuts = |total: usize| [0, 1.min(total), (FINAL_LEN + 2).min(total), total]; - let c = cuts(len); - let (mut enc, init_data) = E::do_encrypt_init(&key).unwrap(); - let mut streamed = Vec::new(); - for w in c.windows(2) { - let piece = &msg[w[0]..w[1]]; - let expect = enc.do_encrypt_out_len(piece.len()); - let mut buf = vec![SENTINEL; expect + EXTRA]; - let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); - assert_eq!(n, expect, "do_encrypt_out must report the bytes this call wrote"); - assert_tail_zeroed(&buf, n, "do_encrypt_out"); - streamed.extend_from_slice(&buf[..n]); - } - let mut last = [SENTINEL; FINAL_LEN]; - let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); - assert_tail_zeroed(&last, last_len, "do_encrypt_final_out"); - streamed.extend_from_slice(&last[..last_len]); - assert_eq!( - streamed.len(), - E::encrypt_out_len(len), - "the per-call counts must sum to encrypt_out_len, not overshoot it as running totals" - ); - - let c = cuts(streamed.len()); - let mut dec = D::do_decrypt_init(&key, &init_data).unwrap(); - let mut rec = Vec::new(); - for w in c.windows(2) { - let piece = &streamed[w[0]..w[1]]; - let expect = dec.do_decrypt_out_len(piece.len()); - let mut buf = vec![SENTINEL; expect + EXTRA]; - let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); - assert_eq!(n, expect, "do_decrypt_out must report the bytes this call wrote"); - assert_tail_zeroed(&buf, n, "do_decrypt_out"); - rec.extend_from_slice(&buf[..n]); - } - let mut last = [SENTINEL; FINAL_LEN]; - let data_len = dec.do_decrypt_final_out(&mut last).unwrap(); - assert_tail_zeroed(&last, data_len, "do_decrypt_final_out"); - rec.extend_from_slice(&last[..data_len]); - assert_eq!( - rec, msg, - "the per-call counts must sum to the plaintext, not overshoot it as running totals" - ); - - // error case: KeyMaterial of the wrong type - let mac_key = - KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) - .unwrap(); - match E::do_encrypt_init(&mac_key) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("A key that is not a SymmetricCipherKey should have been rejected"), - }; - match D::do_decrypt_init(&mac_key, &init_data) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("A key that is not a SymmetricCipherKey should have been rejected"), - }; - - // error case: security strengths too weak, and strong enough - let mut key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - let security_strengths = [ - SecurityStrength::None, - SecurityStrength::_112bit, - SecurityStrength::_128bit, - SecurityStrength::_192bit, - SecurityStrength::_256bit, - ]; - for ss in security_strengths.iter() { - // Skip the strengths a KEY_LEN-byte key cannot carry; see `TestFrameworkElectronicCodeBook`. - if ss > &SecurityStrength::from_bytes(KEY_LEN) { - continue; - } - do_hazardous_operations(&mut key, |key| key.set_security_strength(*ss)).unwrap(); - - match E::do_encrypt_init(&key) { - Ok(_) => assert!( - ss >= &E::MAX_SECURITY_STRENGTH, - "should have required a key at least as strong as the algorithm" - ), - Err(SymmetricCipherError::KeyMaterialError(_)) => assert!( - ss < &E::MAX_SECURITY_STRENGTH, - "should not have rejected a key strong enough for the algorithm" - ), - _ => panic!("Unexpected error"), - }; - match D::do_decrypt_init(&key, &init_data) { - Ok(_) => assert!(ss >= &D::MAX_SECURITY_STRENGTH), - Err(SymmetricCipherError::KeyMaterialError(_)) => { - assert!(ss < &D::MAX_SECURITY_STRENGTH) - } - _ => panic!("Unexpected error"), - }; - } - } /// The [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] form of - /// [`test_encryptor_decryptor`](Self::test_encryptor_decryptor): the same contract reached + /// the symmetric-cipher contract reached /// through a key object, with `rng` supplying every init data draw. The key-policy checks /// are not here, since the key is already made; see `TestFrameworkFromKeyMaterial`. pub fn test_key< @@ -511,8 +55,12 @@ impl TestFrameworkSymmetricCipher { if let Some(fixed) = self.fixed_message_len { assert!(fixed <= DUMMY_SEED.len(), "fixed_message_len must fit the seed buffer"); } - let encrypt_out_len = |len: usize| K::Encryptor::encrypt_out_len(len); - let decrypt_out_len = |len: usize| K::Decryptor::decrypt_out_len(len); + let encrypt_out_len = |len: usize| { + as SymmetricCipherEncryptor>::encrypt_out_len(len) + }; + let decrypt_out_len = |len: usize| { + as SymmetricCipherDecryptor>::decrypt_out_len(len) + }; // one-shot round trip, every (accepted) length; every other length must be refused for len in 0..=max_len { @@ -520,14 +68,17 @@ impl TestFrameworkSymmetricCipher { if !len.is_multiple_of(align) { let mut ct = vec![0u8; encrypt_out_len(len) + FINAL_LEN]; match key.encrypt_out(rng, msg, &mut ct) { - Err(SymmetricCipherError::PaddingError(_)) => {} + Err(SymmetricCipherError::DataNotBlockAligned) => {} other => panic!("len {len} is not aligned and must be refused, got {other:?}"), } let (mut enc, _) = key.create_encryptor(rng).unwrap(); let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; enc.do_encrypt_out(msg, &mut buf).unwrap(); assert!( - matches!(enc.do_encrypt_final(), Err(SymmetricCipherError::PaddingError(_))), + matches!( + enc.do_encrypt_final(), + Err(SymmetricCipherError::DataNotBlockAligned) + ), "len {len}: streaming do_encrypt_final must refuse an unaligned message" ); continue; @@ -846,206 +397,22 @@ impl TestFrameworkStreamCipher { Self {} } - /// Test the contract of a [`StreamCipherEncryptor`] / [`StreamCipherDecryptor`] pair: every - /// chunking of the streaming API agrees with the one-shot and round-trips through the other - /// direction, the RNG-taking constructors reproduce their init data, and the key-type and - /// security-strength policy is enforced. This gives good baseline test coverage, but is not - /// exhaustive; algorithm-specific test vectors belong in the implementing crate. - pub fn test< - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - E: StreamCipherEncryptor, - D: StreamCipherDecryptor, - >( - &self, - ) { - let key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - - // one-shot, in place: must round-trip, and report every byte as written. - let mut buf = *DUMMY_SEED; - let (n, iv) = E::encrypt_inplace(&key, &mut buf).unwrap(); - assert_eq!(n, buf.len(), "encrypt must report the number of bytes written"); - let reference_ct = buf; - assert_ne!(&reference_ct[..], &DUMMY_SEED[..], "encryption must change the data"); - let n = D::decrypt_inplace(&key, &iv, &mut buf).unwrap(); - assert_eq!(n, buf.len(), "decrypt must report the number of bytes written"); - assert_eq!(&buf[..], &DUMMY_SEED[..]); - - // the streaming API under the same init data must give the one-shot's answer whatever - // the chunking, including chunks that are not a multiple of any internal keystream block - // and empty chunks; and encrypting in one chunking must decrypt in any other. - let chunkings: &[usize] = &[1, 3, 7, 16, 63, 64, 65, 250, DUMMY_SEED.len()]; - for &enc_chunk in chunkings { - let mut buf = *DUMMY_SEED; - let (mut encryptor, iv2) = E::do_encrypt_init(&key).unwrap(); - // stream through the encryptor, with an empty chunk thrown in at the start and end - encryptor.do_encrypt_inplace(&mut []).unwrap(); - for chunk in buf.chunks_mut(enc_chunk) { - encryptor.do_encrypt_inplace(chunk).unwrap(); - } - encryptor.do_encrypt_inplace(&mut []).unwrap(); - let ct = buf; - - for &dec_chunk in chunkings { - let mut buf = ct; - let mut decryptor = D::do_decrypt_init(&key, &iv2).unwrap(); - decryptor.do_decrypt_inplace(&mut []).unwrap(); - for chunk in buf.chunks_mut(dec_chunk) { - decryptor.do_decrypt_inplace(chunk).unwrap(); - } - decryptor.do_decrypt_inplace(&mut []).unwrap(); - assert_eq!( - &buf[..], - &DUMMY_SEED[..], - "enc chunk {enc_chunk}, dec chunk {dec_chunk}" - ); - } - - // and the one-shot decrypt agrees with every streaming encryption - let mut buf = ct; - D::decrypt_inplace(&key, &iv2, &mut buf).unwrap(); - assert_eq!(&buf[..], &DUMMY_SEED[..]); - } - - // the streaming decryptor must agree with the one-shot encryptor under its init data - let mut buf = reference_ct; - let mut streamed = D::do_decrypt_init(&key, &iv).unwrap(); - for chunk in buf.chunks_mut(5) { - streamed.do_decrypt_inplace(chunk).unwrap(); - } - assert_eq!(&buf[..], &DUMMY_SEED[..]); - - // The RNG-taking constructor is only exercised for a cipher that has init data to - // generate. Its contract requires an implementation with `INIT_DATA_LEN == 0` (ECB) to - // panic instead, so driving it here would fail that implementor for conforming. - if INIT_DATA_LEN > 0 { - // the RNG-taking one-shot must give the streaming API's answer for the same RNG stream, - // and the same init data. - let pinned = [0xA5u8; INIT_DATA_LEN]; - let mut expected = *DUMMY_SEED; - let (mut streamed, iv_streamed) = - E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)) - .unwrap(); - streamed.do_encrypt_inplace(&mut expected).unwrap(); - let mut buf = *DUMMY_SEED; - let (n, iv) = E::encrypt_rng_inplace( - &key, - &mut FixedSeedRNG::::new(pinned), - &mut buf, - ) - .unwrap(); - assert_eq!(n, buf.len(), "encrypt_rng must report the number of bytes written"); - assert_eq!(iv, iv_streamed); - assert_eq!(&buf[..], &expected[..]); - // ...and a driven RNG determines the ciphertext: the same RNG stream again gives the same - // init data and ciphertext, so the ciphertext is a function of (key, init data) alone. - let mut buf2 = *DUMMY_SEED; - let (_, iv_again) = E::encrypt_rng_inplace( - &key, - &mut FixedSeedRNG::::new(pinned), - &mut buf2, - ) - .unwrap(); - assert_eq!(iv, iv_again); - assert_eq!(&buf[..], &buf2[..]); - } - - // test that the init data is random (ie not the same on two runs). A cipher with no init - // data at all (INIT_DATA_LEN == 0) has nothing to compare: two empty arrays are always equal. - if INIT_DATA_LEN > 0 { - let (_encryptor, iv1) = E::do_encrypt_init(&key).unwrap(); - let (_encryptor, iv2) = E::do_encrypt_init(&key).unwrap(); - assert_ne!(iv1, iv2); - // and different init data under the same key gives different ciphertext - let mut a = *DUMMY_SEED; - let mut b = *DUMMY_SEED; - let (_, iv_a) = E::encrypt_inplace(&key, &mut a).unwrap(); - let (_, iv_b) = E::encrypt_inplace(&key, &mut b).unwrap(); - assert_ne!(iv_a, iv_b); - assert_ne!(&a[..], &b[..]); - } - - // error case: KeyMaterial of wrong type, for both directions - let mac_key = - KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) - .unwrap(); - match E::do_encrypt_init(&mac_key) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Unexpected error"), - }; - match D::do_decrypt_init(&mac_key, &[0u8; INIT_DATA_LEN]) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Unexpected error"), - }; - - // error case: security strengths too weak and too strong - let mut key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - let security_strengths = [ - SecurityStrength::None, - SecurityStrength::_112bit, - SecurityStrength::_128bit, - SecurityStrength::_192bit, - SecurityStrength::_256bit, - ]; - for ss in security_strengths.iter() { - // `set_security_strength` enforces its key-length guard even inside a - // do_hazardous_operations() closure -- a KEY_LEN-byte key cannot be tagged at a - // strength above `from_bytes(KEY_LEN)` -- so skip the strengths this key cannot carry - // rather than unwrapping an error. (A 16-byte key can reach 128-bit and no higher.) - // Do NOT "fix" this by relaxing that guard in `KeyMaterial`: core's - // `test_hazardous_ops_error_handling` requires it to stay enforced. - if ss > &SecurityStrength::from_bytes(KEY_LEN) { - continue; - } - - // Tag the key at an arbitrary strength for the purpose of this test. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); - - let check = |r: Result<(), SymmetricCipherError>, max: &SecurityStrength| match r { - Ok(_) => { - if ss >= max { /* good */ - } else { - panic!("Should have been a strong enough key"); - } - } - Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < max { /* good */ - } else { - panic!("Should not have accepted a key weaker than algorithm"); - } - } - _ => panic!("Unexpected error"), - }; - check(E::do_encrypt_init(&key).map(|_| ()), &E::MAX_SECURITY_STRENGTH); - check( - D::do_decrypt_init(&key, &[0u8; INIT_DATA_LEN]).map(|_| ()), - &D::MAX_SECURITY_STRENGTH, - ); - } - } - /// The key-object form of [`test`](Self::test): a [`SymmetricEncryptionKey`] / + /// The stream-cipher contract over a [`SymmetricEncryptionKey`] / /// [`SymmetricDecryptionKey`] pair whose workers are stream ciphers, with `rng` supplying /// every init data draw. The key-policy checks are `TestFrameworkFromKeyMaterial`'s. pub fn test_key< + 'k, const KEY_LEN: usize, const INIT_DATA_LEN: usize, K: SymmetricEncryptionKey + SymmetricDecryptionKey, >( &self, - key: &K, + key: &'k K, rng: &mut dyn RNG, ) where - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, { // one-shot, in place: must round-trip, and report every byte as written. let mut buf = *DUMMY_SEED; @@ -1100,13 +467,19 @@ impl TestFrameworkStreamCipher { assert_eq!(&buf[..], &DUMMY_SEED[..]); // the separate-output methods of the symmetric traits agree with the in-place ones - let mut out = vec![0u8; K::Encryptor::encrypt_out_len(DUMMY_SEED.len())]; + let out_len = + as SymmetricCipherEncryptor>::encrypt_out_len( + DUMMY_SEED.len(), + ); + let mut out = vec![0u8; out_len]; let (n, iv3) = key.encrypt_out(rng, DUMMY_SEED, &mut out).unwrap(); assert_eq!(n, DUMMY_SEED.len(), "a stream cipher never changes the length"); let mut back = out.clone(); key.decrypt_inplace(&iv3, &mut back).unwrap(); assert_eq!(&back[..], &DUMMY_SEED[..]); - let mut pt = vec![0u8; K::Decryptor::decrypt_out_len(n)]; + let pt_len = + as SymmetricCipherDecryptor>::decrypt_out_len(n); + let mut pt = vec![0u8; pt_len]; let m = key.decrypt_out(&iv3, &out[..n], &mut pt).unwrap(); assert_eq!(&pt[..m], &DUMMY_SEED[..]); diff --git a/crypto/core/src/hazmat/ecb_key.rs b/crypto/core/src/hazmat/ecb_key.rs index 4812f0689..3a46309f9 100644 --- a/crypto/core/src/hazmat/ecb_key.rs +++ b/crypto/core/src/hazmat/ecb_key.rs @@ -13,7 +13,7 @@ use crate::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; /// uses. pub trait EcbEncryptionKey { /// The streaming encryptor this key produces. - type Encryptor: BlockCipherEncryptor; + type Encryptor: BlockCipherEncryptor<0, BLOCK_LEN>; /// Begins an encryption. fn create_encryptor(&self) -> Self::Encryptor; @@ -34,7 +34,7 @@ pub trait EcbEncryptionKey { /// A key that can begin an ECB decryption; see [`EcbEncryptionKey`]. pub trait EcbDecryptionKey { /// The streaming decryptor this key produces. - type Decryptor: BlockCipherDecryptor; + type Decryptor: BlockCipherDecryptor<0, BLOCK_LEN>; /// Begins a decryption. fn create_decryptor(&self) -> Self::Decryptor; diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index 1c299468b..153bc9ae9 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -17,10 +17,9 @@ use crate::errors::SymmetricCipherError; use crate::key_material::KeyType; -use crate::symmetric_key::{AeadDecryptWorker, AeadEncryptWorker}; use crate::traits::{ - BlockCipherDecryptor, BlockCipherEncryptor, MAC, RNG, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, + AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, MAC, RNG, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; /// A mode's per-key state over the permutation `P`. @@ -56,7 +55,7 @@ pub trait AeadEncryptMode< >: Mode

{ /// The streaming encryptor this mode produces over `P`. - type Encryptor: AeadEncryptWorker; + type Encryptor: AEADCipherEncryptor; /// Begins an encryption from `state` under a nonce drawn from `rng`. /// @@ -116,7 +115,7 @@ pub trait AeadDecryptMode< >: Mode

{ /// The streaming decryptor this mode produces over `P`. - type Decryptor: AeadDecryptWorker; + type Decryptor: AEADCipherDecryptor; /// Begins the decryption of a message encrypted under `nonce` from `state`. Infallible: the /// state is already keyed and the nonce length is in the type. @@ -153,7 +152,7 @@ pub trait AeadPacketEncryptMode< >: Mode

{ /// The streaming encryptor this mode produces over `P`. - type Encryptor: AeadEncryptWorker; + type Encryptor: AEADCipherEncryptor; /// Begins an encryption of `payload_len` bytes under `aad_len` bytes of AAD, from `state` /// under a nonce drawn from `rng`. @@ -225,7 +224,7 @@ pub trait AeadPacketDecryptMode< >: Mode

{ /// The streaming decryptor this mode produces over `P`. - type Decryptor: AeadDecryptWorker; + type Decryptor: AEADCipherDecryptor; /// Begins the decryption of `payload_len` bytes of ciphertext under `aad_len` bytes of AAD, /// encrypted under `nonce`, from `state`. @@ -268,7 +267,7 @@ pub trait SymmetricEncryptMode< >: Mode

{ /// The streaming encryptor this mode produces over `P`. - type Encryptor: SymmetricCipherEncryptor; + type Encryptor: SymmetricCipherEncryptor; /// Begins an encryption from `state` under init data drawn from `rng`. /// @@ -289,7 +288,7 @@ pub trait SymmetricDecryptMode< >: Mode

{ /// The streaming decryptor this mode produces over `P`. - type Decryptor: SymmetricCipherDecryptor; + type Decryptor: SymmetricCipherDecryptor; /// Begins the decryption of a message encrypted under `init_data` from `state`. Infallible: /// the state is already keyed and the init data length is in the type. @@ -306,7 +305,7 @@ pub trait BlockEncryptMode< >: Mode

{ /// The streaming encryptor this mode produces over `P`. - type Encryptor: BlockCipherEncryptor; + type Encryptor: BlockCipherEncryptor; /// Begins an encryption from `state` under init data drawn from `rng`. /// @@ -327,7 +326,7 @@ pub trait BlockDecryptMode< >: Mode

{ /// The streaming decryptor this mode produces over `P`. - type Decryptor: BlockCipherDecryptor; + type Decryptor: BlockCipherDecryptor; /// Begins the decryption of a message encrypted under `init_data` from `state`. Infallible. fn decrypt_init(state: &Self::KeyState, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; @@ -338,7 +337,7 @@ pub trait BlockDecryptMode< /// `INIT_DATA_LEN = 0`, so that nothing is handed an RNG it has no use for. pub trait EcbEncryptMode: Mode

{ /// The streaming encryptor this mode produces over `P`. - type Encryptor: BlockCipherEncryptor; + type Encryptor: BlockCipherEncryptor<0, BLOCK_LEN>; /// Begins an encryption from `state`. fn encrypt_init(state: &Self::KeyState) -> Self::Encryptor; @@ -347,7 +346,7 @@ pub trait EcbEncryptMode: Mode< /// The decryption side of ECB over `P`; see [`EcbEncryptMode`]. pub trait EcbDecryptMode: Mode

{ /// The streaming decryptor this mode produces over `P`. - type Decryptor: BlockCipherDecryptor; + type Decryptor: BlockCipherDecryptor<0, BLOCK_LEN>; /// Begins a decryption from `state`. fn decrypt_init(state: &Self::KeyState) -> Self::Decryptor; diff --git a/crypto/core/src/provided.rs b/crypto/core/src/provided.rs index 8a6f56e97..d2f81961b 100644 --- a/crypto/core/src/provided.rs +++ b/crypto/core/src/provided.rs @@ -12,8 +12,9 @@ //! and no second buffer the size of the message. use crate::errors::SymmetricCipherError; -use crate::symmetric_key::{AeadDecryptWorker, AeadEncryptWorker}; -use crate::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use crate::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; /// Bytes copied out per step. Four 16-byte blocks keeps a block-cipher mode on its wide path. const CHUNK: usize = 64; @@ -37,7 +38,7 @@ pub(crate) fn encrypt_inline< ciphertext: &mut [u8], ) -> Result where - E: AeadEncryptWorker, + E: AEADCipherEncryptor, { ciphertext.fill(0); let needed = E::encrypt_out_len(plaintext.len()); @@ -69,7 +70,7 @@ pub(crate) fn encrypt_detached< ciphertext: &mut [u8], ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> where - E: AeadEncryptWorker, + E: AEADCipherEncryptor, { ciphertext.fill(0); let needed = E::encrypt_detached_out_len(plaintext.len()); @@ -101,7 +102,7 @@ pub(crate) fn encrypt_detached_in_place< buf: &mut [u8], ) -> Result<[u8; TAG_LEN], SymmetricCipherError> where - E: AeadEncryptWorker, + E: AEADCipherEncryptor, { enc.do_update_aad(aad)?; let mut tmp = [0u8; CHUNK]; @@ -139,7 +140,7 @@ pub(crate) fn decrypt_inline< plaintext: &mut [u8], ) -> Result where - D: AeadDecryptWorker, + D: AEADCipherDecryptor, { plaintext.fill(0); let needed = D::decrypt_out_len(ciphertext.len()); @@ -181,7 +182,7 @@ pub(crate) fn decrypt_detached< plaintext: &mut [u8], ) -> Result where - D: AeadDecryptWorker, + D: AEADCipherDecryptor, { plaintext.fill(0); let needed = D::decrypt_detached_out_len(ciphertext.len()); @@ -221,7 +222,7 @@ pub(crate) fn decrypt_detached_in_place< tag: &[u8; TAG_LEN], ) -> Result<(), SymmetricCipherError> where - D: AeadDecryptWorker, + D: AEADCipherDecryptor, { let result = (|| { dec.do_update_aad(aad)?; @@ -263,18 +264,13 @@ pub(crate) fn inline_payload_len( /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, or any error of /// the worker's streaming methods. -pub(crate) fn symmetric_encrypt_out< - E, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const FINAL_LEN: usize, ->( +pub(crate) fn symmetric_encrypt_out( mut enc: E, plaintext: &[u8], ciphertext: &mut [u8], ) -> Result where - E: SymmetricCipherEncryptor, + E: SymmetricCipherEncryptor, { ciphertext.fill(0); let needed = E::encrypt_out_len(plaintext.len()); @@ -296,18 +292,13 @@ where /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, or any error of /// the worker's streaming methods. -pub(crate) fn symmetric_decrypt_out< - D, - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const FINAL_LEN: usize, ->( +pub(crate) fn symmetric_decrypt_out( mut dec: D, ciphertext: &[u8], plaintext: &mut [u8], ) -> Result where - D: SymmetricCipherDecryptor, + D: SymmetricCipherDecryptor, { plaintext.fill(0); let needed = D::decrypt_out_len(ciphertext.len()); diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index 879a13510..668d46f70 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -19,8 +19,7 @@ //! [`crate::hazmat`], and [`MacGenerationKey`] / [`MacVerificationKey`]. A key type //! implements the sides it is allowed to use, so a decrypt-only or verify-only key refuses the //! other side at compile time. The one-shots are provided methods over `create_*` and the -//! worker's streaming methods, which [`AeadEncryptWorker`] / [`AeadDecryptWorker`] describe -//! for the AEAD pair and the cipher traits in [`crate::traits`] for the rest. +//! worker's streaming methods, the cipher traits in [`crate::traits`]. //! //! Which role a cipher key carries is decided by its mode type through the mode traits in //! [`crate::hazmat`] ([`Mode`], [`AeadEncryptMode`], [`AeadDecryptMode`] and their packet, @@ -49,8 +48,9 @@ use crate::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; #[cfg(feature = "std")] use crate::traits::AEADEncryptedTuple; use crate::traits::{ - BlockCipherDecryptor, BlockCipherEncryptor, MAC, RNG, StreamCipherDecryptor, - StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, + AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, MAC, RNG, + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, }; /// Construction of a key object from exactly `KEY_LEN` bytes of key material. @@ -114,112 +114,10 @@ pub trait FromAnyKeyMaterial: Sized { fn generate(rng: &mut dyn RNG) -> Result; } -// The lengths the cipher traits carry are const generics on the role traits rather than associated -// constants, because stable Rust can neither pass an associated constant as a const-generic -// argument (the worker bound) nor size an array from one (the returned nonce). `KEY_LEN` is on -// the role traits only while the cipher traits still carry it; it goes when their constructors do. - -/// An AEAD encryptor that already exists: the streaming methods of the cipher traits without -/// their constructors, which is all a key's worker needs. A worker produced by a key implements -/// this; the cipher traits' own types implement it by forwarding. -pub trait AeadEncryptWorker: - Sized -{ - /// Absorbs associated data, which is authenticated but not encrypted, before any payload. - /// - /// # Errors - /// [`SymmetricCipherError::StateError`] if non-empty after the payload has begun, or the - /// worker's own refusal of more than it declared. - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError>; - - /// The most ciphertext bytes the next `do_encrypt_out` of `input_len` bytes can release. - fn do_encrypt_out_len(&self, input_len: usize) -> usize; - - /// Encrypts `plaintext` into `ciphertext`, returning the bytes written. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is shorter than - /// [`do_encrypt_out_len`](Self::do_encrypt_out_len) says, or the worker's own refusals. - fn do_encrypt_out( - &mut self, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result; - - /// Finishes with the tag inline: whatever was held back, then the tag, and how many bytes - /// of the array are valid. - /// - /// # Errors - /// The worker's own refusal of a message it was not given all of. - fn do_encrypt_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError>; - - /// Finishes with the tag detached: whatever was held back goes into `ciphertext`, and the - /// bytes written come back with the tag. - /// - /// # Errors - /// As [`do_encrypt_final`](Self::do_encrypt_final). - fn do_encrypt_final_detached_out( - self, - ciphertext: &mut [u8; FINAL_LEN], - ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError>; - - /// The ciphertext length, tag inline, for `plaintext_len` bytes. - fn encrypt_out_len(plaintext_len: usize) -> usize; - - /// The ciphertext length, tag detached, for `plaintext_len` bytes. - fn encrypt_detached_out_len(plaintext_len: usize) -> usize; -} - -/// An AEAD decryptor that already exists; see [`AeadEncryptWorker`]. -pub trait AeadDecryptWorker: - Sized -{ - /// Absorbs associated data before any ciphertext. - /// - /// # Errors - /// As [`AeadEncryptWorker::do_update_aad`]. - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError>; - - /// The most plaintext bytes the next `do_decrypt_out` of `input_len` bytes can release. - fn do_decrypt_out_len(&self, input_len: usize) -> usize; - - /// Decrypts `ciphertext` into `plaintext`, returning the bytes written. What is written is - /// not yet authenticated: only the final says whether it may be trusted. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is shorter than - /// [`do_decrypt_out_len`](Self::do_decrypt_out_len) says, or the worker's own refusals. - fn do_decrypt_out( - &mut self, - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result; - - /// Finishes with the tag taken from the end of the ciphertext: whatever was held back, and - /// how many bytes of the array are valid. - /// - /// # Errors - /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify, and - /// [`SymmetricCipherError::DecryptionFailed`] for a malformed ciphertext. - fn do_decrypt_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError>; - - /// Finishes against a detached `tag`: whatever was held back goes into `plaintext`, and the - /// bytes written come back. - /// - /// # Errors - /// As [`do_decrypt_final`](Self::do_decrypt_final). - fn do_decrypt_final_detached_out( - self, - tag: &[u8; TAG_LEN], - plaintext: &mut [u8; FINAL_LEN], - ) -> Result; - - /// The plaintext length for `ciphertext_len` bytes with the tag inline. - fn decrypt_out_len(ciphertext_len: usize) -> usize; - - /// The plaintext length for `ciphertext_len` bytes with the tag detached. - fn decrypt_detached_out_len(ciphertext_len: usize) -> usize; -} +// The lengths are const generics on the role traits rather than associated constants, because +// stable Rust can neither pass an associated constant as a const-generic argument (the worker +// bound) nor size an array from one (the returned nonce). `KEY_LEN` is here, and not on the +// cipher traits, because it is the key's: the worker never sees the material. /// A key that can begin an authenticated encryption: the encryption half of an AEAD key. /// @@ -241,7 +139,7 @@ pub trait AeadEncryptionKey< > { /// The streaming encryptor this key produces. - type Encryptor: AeadEncryptWorker; + type Encryptor: AEADCipherEncryptor; /// Begins an encryption under a nonce drawn from `rng`. The nonce is returned alongside the /// encryptor and must reach the recipient, who passes it to @@ -453,7 +351,7 @@ pub trait AeadDecryptionKey< > { /// The streaming decryptor this key produces. - type Decryptor: AeadDecryptWorker; + type Decryptor: AEADCipherDecryptor; /// Begins the decryption of a message whose encryptor returned `nonce`. fn create_decryptor(&self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor; @@ -566,7 +464,7 @@ pub trait AeadPacketEncryptionKey< > { /// The streaming encryptor this key produces. - type Encryptor: AeadEncryptWorker; + type Encryptor: AEADCipherEncryptor; /// Begins an encryption of `payload_len` bytes under `aad_len` bytes of AAD and a nonce /// drawn from `rng`, which is returned. The worker then takes exactly those amounts. @@ -770,7 +668,7 @@ pub trait AeadPacketDecryptionKey< > { /// The streaming decryptor this key produces. - type Decryptor: AeadDecryptWorker; + type Decryptor: AEADCipherDecryptor; /// Begins the decryption of `payload_len` bytes of ciphertext under `aad_len` bytes of AAD, /// encrypted under `nonce`. @@ -885,7 +783,7 @@ pub trait SymmetricEncryptionKey< > { /// The streaming encryptor this key produces. - type Encryptor: SymmetricCipherEncryptor; + type Encryptor: SymmetricCipherEncryptor; /// Begins an encryption under init data drawn from `rng`, which is returned alongside the /// encryptor and must reach the recipient, who passes it to @@ -945,7 +843,7 @@ pub trait SymmetricEncryptionKey< data: &mut [u8], ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> where - Self::Encryptor: StreamCipherEncryptor, + Self::Encryptor: StreamCipherEncryptor, { let (mut enc, init_data) = self.create_encryptor(rng)?; let written = StreamCipherEncryptor::do_encrypt_inplace(&mut enc, data)?; @@ -963,7 +861,7 @@ pub trait SymmetricDecryptionKey< > { /// The streaming decryptor this key produces. - type Decryptor: SymmetricCipherDecryptor; + type Decryptor: SymmetricCipherDecryptor; /// Begins the decryption of a message whose encryptor returned `init_data`. fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; @@ -1016,7 +914,7 @@ pub trait SymmetricDecryptionKey< data: &mut [u8], ) -> Result where - Self::Decryptor: StreamCipherDecryptor, + Self::Decryptor: StreamCipherDecryptor, { let mut dec = self.create_decryptor(init_data); StreamCipherDecryptor::do_decrypt_inplace(&mut dec, data) @@ -1034,7 +932,7 @@ pub trait BlockEncryptionKey< > { /// The streaming encryptor this key produces. - type Encryptor: BlockCipherEncryptor; + type Encryptor: BlockCipherEncryptor; /// Begins an encryption under init data drawn from `rng`, which is returned alongside the /// encryptor and must reach the recipient, who passes it to @@ -1076,7 +974,7 @@ pub trait BlockDecryptionKey< > { /// The streaming decryptor this key produces. - type Decryptor: BlockCipherDecryptor; + type Decryptor: BlockCipherDecryptor; /// Begins the decryption of a message whose encryptor returned `init_data`. fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 4a68f9a45..2e7a9298d 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -15,7 +15,7 @@ use crate::key_material::KeyMaterial; use crate::key_material::KeyType; // end of imports needed for docs -/// What the allocating one-shot [`AEADCipherEncryptor::encrypt_detached`] hands back: +/// What an AEAD key's allocating one-shot with a detached tag hands back: /// `(nonce, ciphertext, tag)` #[cfg(feature = "std")] pub type AEADEncryptedTuple = @@ -44,16 +44,10 @@ pub type AEADEncryptedTuple = /// A streaming caller must therefore treat everything `do_update_out` produces as untrusted until /// the final call succeeds, and scrub it if it does not. /// -/// The one-shots -- [`decrypt_detached_out`](Self::decrypt_detached_out), -/// [`decrypt_with_aad_out`](Self::decrypt_with_aad_out) and [`SymmetricCipherDecryptor::decrypt_out`] -- have -/// no such caveat: each owns the whole message, so it zeroizes the buffer itself before returning -/// the error. -pub trait AEADCipherDecryptor< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const FINAL_LEN: usize, ->: SymmetricCipherDecryptor +/// The key's one-shots have no such caveat: each owns the whole message, so it zeroizes the +/// buffer itself before returning the error. +pub trait AEADCipherDecryptor: + SymmetricCipherDecryptor { /// Absorbs additional authenticated data; see [`AEADCipherEncryptor::do_update_aad`] for the /// rules, which are the same on both sides. The concatenation of what a decryptor absorbs must @@ -101,141 +95,13 @@ pub trait AEADCipherDecryptor< } /// An upper bound on the plaintext recovered from `ciphertext_len` bytes of ciphertext with - /// the tag detached, i.e. the buffer [`decrypt_detached_out`](Self::decrypt_detached_out) + /// the tag detached, i.e. the buffer the key's `decrypt_detached_out` /// requires. The default returns `ciphertext_len` itself, which is exact for every conformant /// AEAD: unlike a padding scheme, an AEAD never expands or shrinks the data it is given, only /// adds the separate `tag`. fn decrypt_detached_out_len(ciphertext_len: usize) -> usize { ciphertext_len } - - /// One-shot with the tag detached: decrypts `ciphertext` into `plaintext`, which needs - /// [`decrypt_detached_out_len`](Self::decrypt_detached_out_len) bytes, under `nonce` - /// and `aad`, and checks `tag`. Returns the number of plaintext bytes written. The entire - /// output buffer is zeroized before the plaintext is written, so any bytes past that count - /// will be 0. - /// - /// Unlike the streaming methods this releases nothing unauthenticated: on failure `plaintext` - /// is zeroized before the error is returned, so a caller who ignores the `Result` is left with - /// zeros rather than attacker-chosen plaintext. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked - /// before any work is done; otherwise whatever the streaming methods return, including - /// [`do_decrypt_final_detached_out`](Self::do_decrypt_final_detached_out)'s. - fn decrypt_detached_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - let needed = Self::decrypt_detached_out_len(ciphertext.len()); - if plaintext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let mut dec = Self::do_decrypt_init(key, nonce)?; - dec.do_update_aad(aad)?; - let written = dec.do_decrypt_out(ciphertext, plaintext)?; - let mut final_buf = [0u8; FINAL_LEN]; - match dec.do_decrypt_final_detached_out(tag, &mut final_buf) { - Ok(final_len) => { - // Everything held back comes out of `do_decrypt_final_detached_out`, so `written - // + final_len` is the ciphertext length, which `decrypt_detached_out_len` bounds. - plaintext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); - Ok(written + final_len) - } - Err(e) => { - // As in the trait docs: what `do_update_out` already released is unauthenticated, - // and this one-shot owns the whole message, so it does not leave that in the - // caller's hands. A plain `fill` rather than a volatile write because `core` is - // `#![forbid(unsafe_code)]`; the store is to the caller's own buffer, which the - // caller may read after this returns, so it is not a dead store the optimizer is - // entitled to drop. - plaintext[..written].fill(0); - Err(e) - } - } - } - - /// One-shot over the inline `ciphertext || tag` layout with associated data: the trailing - /// `TAG_LEN` bytes of `ciphertext` are the tag. This is [`SymmetricCipherDecryptor::decrypt_out`] - /// with an `aad`, and needs the same - /// [`decrypt_out_len`](SymmetricCipherDecryptor::decrypt_out_len) bytes of - /// `plaintext`. Returns the number of plaintext bytes written. The entire output buffer is - /// zeroized before the plaintext is written, so any bytes past that count will be 0. As with - /// every AEAD one-shot, `plaintext` is zeroized when the tag does not verify. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked - /// before any work is done; [`SymmetricCipherError::DecryptionFailed`] if `ciphertext` is - /// shorter than the tag it is supposed to end with; - /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. - fn decrypt_with_aad_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - let needed = Self::decrypt_out_len(ciphertext.len()); - if plaintext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let mut dec = Self::do_decrypt_init(key, nonce)?; - dec.do_update_aad(aad)?; - let written = dec.do_decrypt_out(ciphertext, plaintext)?; - match dec.do_decrypt_final() { - Ok((last, data_len)) => { - // `decrypt_out_len` bounds `written + data_len`, so this fits in - // `plaintext[..needed]`. - plaintext[written..written + data_len].copy_from_slice(&last[..data_len]); - Ok(written + data_len) - } - Err(e) => { - // As in `decrypt_detached_out`. - plaintext[..written].fill(0); - Err(e) - } - } - } - - #[cfg(feature = "std")] - /// One-shot, allocating, with the tag detached: as - /// [`decrypt_detached_out`](Self::decrypt_detached_out), returning the plaintext as a - /// `Vec` of exactly the recovered length. Only available with the `std` feature. - fn decrypt_detached( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - ) -> Result, SymmetricCipherError> { - let mut plaintext = vec![0u8; Self::decrypt_detached_out_len(ciphertext.len())]; - let written = Self::decrypt_detached_out(key, nonce, aad, ciphertext, tag, &mut plaintext)?; - plaintext.truncate(written); - Ok(plaintext) - } - - #[cfg(feature = "std")] - /// One-shot, allocating, over the inline `ciphertext || tag` layout with associated data: as - /// [`decrypt_with_aad_out`](Self::decrypt_with_aad_out), returning the plaintext as a - /// `Vec` of exactly the recovered length. This is [`SymmetricCipherDecryptor::decrypt`] - /// with an `aad`. Only available with the `std` feature. - fn decrypt_with_aad( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - ciphertext: &[u8], - ) -> Result, SymmetricCipherError> { - let mut plaintext = vec![0u8; Self::decrypt_out_len(ciphertext.len())]; - let written = Self::decrypt_with_aad_out(key, nonce, aad, ciphertext, &mut plaintext)?; - plaintext.truncate(written); - Ok(plaintext) - } } /// The encryption half of an AEAD cipher's streaming API. This extends @@ -261,7 +127,7 @@ pub trait AEADCipherDecryptor< /// be protected against tampering. Every AEAD construction absorbs that AAD *before* the plaintext. /// This leads to a stateful API flow: /// -/// * [`do_encrypt_init`](SymmetricCipherEncryptor::do_encrypt_init) constructs the instance. +/// * The key's `create_encryptor` constructs the instance under a nonce. /// * [`do_update_aad`](Self::do_update_aad) may be called any number of times, including zero if /// there is no AAD. /// * The first [`do_encrypt_out`](SymmetricCipherEncryptor::do_encrypt_out) switches to encrypting, @@ -270,14 +136,14 @@ pub trait AEADCipherDecryptor< /// /// (An empty `aad` slice is a no-op and is accepted at any point.) /// -/// # The nonce is generated by default +/// # The nonce is drawn by default /// -/// [`do_encrypt_init`](SymmetricCipherEncryptor::do_encrypt_init) and the one-shots provided over -/// it draw the nonce themselves and return it for transmission alongside the ciphertext, so a -/// caller cannot repeat one by mistake. A protocol that manages its own nonces -- a counter, or -/// the construction its specification prescribes -- uses -/// [`do_encrypt_init_nonce`](Self::do_encrypt_init_nonce) and the `_nonce` one-shots instead, -/// and then carries the obligation itself: **a nonce must never repeat under one key.** For every +/// The key's `create_encryptor` and the one-shots provided over it draw the nonce from the RNG +/// they are given and return it for transmission alongside the ciphertext, so a caller cannot +/// repeat one by mistake. A protocol that manages its own nonces -- a counter, or the +/// construction its specification prescribes -- uses the key's `create_encryptor_nonce` and the +/// `_nonce` one-shots instead, and then carries the obligation itself: **a nonce must never +/// repeat under one key.** For every /// AEAD here a repeat exposes the XOR of the two plaintexts, and for GCM the authentication key /// as well. /// @@ -293,12 +159,8 @@ pub trait AEADCipherDecryptor< /// to see how much output will be produced for the given amount of input. Doing it this way has /// the advantage of being able to correctly size the output buffer for a subsequent /// [`do_encrypt_out`](SymmetricCipherEncryptor::do_encrypt_out) call. -pub trait AEADCipherEncryptor< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - const FINAL_LEN: usize, ->: SymmetricCipherEncryptor +pub trait AEADCipherEncryptor: + SymmetricCipherEncryptor { /// Absorbs `aad`: data that is authenticated by the tag but not encrypted. May be called /// repeatedly before the first [`SymmetricCipherEncryptor::do_encrypt_out`]; a sequence of calls @@ -341,258 +203,13 @@ pub trait AEADCipherEncryptor< } /// The exact ciphertext length for a `plaintext_len`-byte plaintext with the tag detached, i.e. - /// the buffer [`encrypt_detached_out`](Self::encrypt_detached_out) requires and the number of + /// the buffer the key's `encrypt_detached_out` requires and the number of /// bytes it writes (the tag is returned separately, not counted here). The default returns /// `plaintext_len` itself, which holds for every conformant AEAD: unlike a padding scheme, an /// AEAD never expands or shrinks the data it is given. fn encrypt_detached_out_len(plaintext_len: usize) -> usize { plaintext_len } - - /// One-shot with the tag detached: encrypts `plaintext` into `ciphertext`, which needs - /// [`encrypt_detached_out_len`](Self::encrypt_detached_out_len) bytes, authenticating `aad` - /// along with it under a fresh nonce. Returns the generated nonce, the number of bytes - /// written, and the tag. The entire output buffer is zeroized before the ciphertext is - /// written, so any bytes past that count will be 0. - /// - /// Provided as `do_encrypt_init`, one `do_update_aad`, one `do_update_out` and - /// `do_encrypt_final_detached_out`. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked - /// before any work is done; otherwise whatever the streaming methods return. - fn encrypt_detached_out( - key: &KeyMaterial, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { - ciphertext.fill(0); - let needed = Self::encrypt_detached_out_len(plaintext.len()); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let (mut enc, nonce) = Self::do_encrypt_init(key)?; - enc.do_update_aad(aad)?; - let written = enc.do_encrypt_out(plaintext, ciphertext)?; - let mut final_buf = [0u8; FINAL_LEN]; - let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf)?; - // Implementors that hold plaintext back must override `encrypt_detached_out_len` if - // `written + final_len` can exceed the plaintext length, so this fits in - // `ciphertext[..needed]`. - ciphertext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); - Ok((nonce, written + final_len, tag)) - } - - #[cfg(feature = "std")] - /// One-shot, allocating, with the tag detached: as - /// [`encrypt_detached_out`](Self::encrypt_detached_out), returning the ciphertext as a - /// `Vec`. Only available with the `std` feature. - fn encrypt_detached( - key: &KeyMaterial, - aad: &[u8], - plaintext: &[u8], - ) -> Result, SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::encrypt_detached_out_len(plaintext.len())]; - let (nonce, written, tag) = - Self::encrypt_detached_out(key, aad, plaintext, &mut ciphertext)?; - ciphertext.truncate(written); - Ok((nonce, ciphertext, tag)) - } - - /// As [`encrypt_detached_out`](Self::encrypt_detached_out), but sources randomness from the - /// provided RNG. - fn encrypt_detached_rng_out( - key: &KeyMaterial, - rng: &mut dyn RNG, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { - ciphertext.fill(0); - let needed = Self::encrypt_detached_out_len(plaintext.len()); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let (mut enc, nonce) = Self::do_encrypt_init_rng(key, rng)?; - enc.do_update_aad(aad)?; - let written = enc.do_encrypt_out(plaintext, ciphertext)?; - let mut final_buf = [0u8; FINAL_LEN]; - let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf)?; - // As in `encrypt_detached_out`. - ciphertext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); - Ok((nonce, written + final_len, tag)) - } - - /// One-shot into the inline `ciphertext || tag` layout with associated data: this is - /// [`SymmetricCipherEncryptor::encrypt_out`] with an `aad`, and needs the same - /// [`encrypt_out_len`](SymmetricCipherEncryptor::encrypt_out_len) bytes of `ciphertext`. - /// Returns the generated nonce and the total number of bytes written, tag included. The - /// entire output buffer is zeroized before the ciphertext is written, so any bytes past that - /// count will be 0. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked - /// before any work is done; otherwise whatever the streaming methods return. - fn encrypt_with_aad_out( - key: &KeyMaterial, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { - ciphertext.fill(0); - let needed = Self::encrypt_out_len(plaintext.len()); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let (mut enc, nonce) = Self::do_encrypt_init(key)?; - enc.do_update_aad(aad)?; - let written = enc.do_encrypt_out(plaintext, ciphertext)?; - let (last, last_len) = enc.do_encrypt_final()?; - // `encrypt_out_len` is exactly `written + last_len`, so this fits in `ciphertext[..needed]`. - ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); - Ok((nonce, written + last_len)) - } - - #[cfg(feature = "std")] - /// One-shot, allocating, into the inline `ciphertext || tag` layout with associated data: as - /// [`encrypt_with_aad_out`](Self::encrypt_with_aad_out), returning the ciphertext, tag - /// included, as a `Vec`. This is [`SymmetricCipherEncryptor::encrypt`] with an `aad`. Only - /// available with the `std` feature. - fn encrypt_with_aad( - key: &KeyMaterial, - aad: &[u8], - plaintext: &[u8], - ) -> Result<([u8; NONCE_LEN], Vec), SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::encrypt_out_len(plaintext.len())]; - let (nonce, written) = Self::encrypt_with_aad_out(key, aad, plaintext, &mut ciphertext)?; - ciphertext.truncate(written); - Ok((nonce, ciphertext)) - } - - /// As [`encrypt_with_aad_out`](Self::encrypt_with_aad_out), but sources randomness from the - /// provided RNG: [`SymmetricCipherEncryptor::encrypt_rng_out`] with an `aad`. - fn encrypt_with_aad_rng_out( - key: &KeyMaterial, - rng: &mut dyn RNG, - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { - ciphertext.fill(0); - let needed = Self::encrypt_out_len(plaintext.len()); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let (mut enc, nonce) = Self::do_encrypt_init_rng(key, rng)?; - enc.do_update_aad(aad)?; - let written = enc.do_encrypt_out(plaintext, ciphertext)?; - let (last, last_len) = enc.do_encrypt_final()?; - // As in `encrypt_with_aad_out`. - ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); - Ok((nonce, written + last_len)) - } - - /// Begins a streaming encryption under a nonce the caller supplies, for a protocol whose - /// nonce is a counter or another value it manages itself; see the trait docs. The caller - /// takes on the obligation the generating constructors discharge for it: the nonce must never - /// repeat under this key. - /// - /// # Errors - /// As [`do_encrypt_init`](SymmetricCipherEncryptor::do_encrypt_init). - fn do_encrypt_init_nonce( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - ) -> Result; - - /// As [`encrypt_detached_out`](Self::encrypt_detached_out), under a nonce the caller supplies - /// (see [`do_encrypt_init_nonce`](Self::do_encrypt_init_nonce)). Returns the number of bytes - /// written and the tag; the nonce is the caller's, so it is not returned. - /// - /// # Errors - /// As [`encrypt_detached_out`](Self::encrypt_detached_out). - fn encrypt_detached_nonce_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { - ciphertext.fill(0); - let needed = Self::encrypt_detached_out_len(plaintext.len()); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let mut enc = Self::do_encrypt_init_nonce(key, nonce)?; - enc.do_update_aad(aad)?; - let written = enc.do_encrypt_out(plaintext, ciphertext)?; - let mut final_buf = [0u8; FINAL_LEN]; - let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf)?; - // As in `encrypt_detached_out`. - ciphertext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); - Ok((written + final_len, tag)) - } - - #[cfg(feature = "std")] - /// One-shot, allocating, with the tag detached, under a nonce the caller supplies: as - /// [`encrypt_detached_nonce_out`](Self::encrypt_detached_nonce_out), returning the ciphertext - /// as a `Vec` with the tag. Only available with the `std` feature. - fn encrypt_detached_nonce( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - plaintext: &[u8], - ) -> Result<(Vec, [u8; TAG_LEN]), SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::encrypt_detached_out_len(plaintext.len())]; - let (written, tag) = - Self::encrypt_detached_nonce_out(key, nonce, aad, plaintext, &mut ciphertext)?; - ciphertext.truncate(written); - Ok((ciphertext, tag)) - } - - /// As [`encrypt_with_aad_out`](Self::encrypt_with_aad_out), under a nonce the caller supplies - /// (see [`do_encrypt_init_nonce`](Self::do_encrypt_init_nonce)). Returns the number of bytes - /// written, tag included. - /// - /// # Errors - /// As [`encrypt_with_aad_out`](Self::encrypt_with_aad_out). - fn encrypt_with_aad_nonce_out( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result { - ciphertext.fill(0); - let needed = Self::encrypt_out_len(plaintext.len()); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let mut enc = Self::do_encrypt_init_nonce(key, nonce)?; - enc.do_update_aad(aad)?; - let written = enc.do_encrypt_out(plaintext, ciphertext)?; - let (last, last_len) = enc.do_encrypt_final()?; - // As in `encrypt_with_aad_out`. - ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); - Ok(written + last_len) - } - - #[cfg(feature = "std")] - /// One-shot, allocating, into the inline `ciphertext || tag` layout under a nonce the caller - /// supplies: as [`encrypt_with_aad_nonce_out`](Self::encrypt_with_aad_nonce_out), returning - /// the ciphertext, tag included, as a `Vec`. Only available with the `std` feature. - fn encrypt_with_aad_nonce( - key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - plaintext: &[u8], - ) -> Result, SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::encrypt_out_len(plaintext.len())]; - let written = - Self::encrypt_with_aad_nonce_out(key, nonce, aad, plaintext, &mut ciphertext)?; - ciphertext.truncate(written); - Ok(ciphertext) - } } /// Metadata about a cryptographic algorithm. @@ -615,17 +232,9 @@ pub trait AlgorithmOID { /// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`], whose /// notes on in-place operation, 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 +pub trait BlockCipherDecryptor: + 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 consecutive whole blocks in place. See /// [`BlockCipherEncryptor::do_encrypt_blocks_inplace`]. Returns the number of bytes written, which is /// always `blocks.len() * BLOCK_LEN` since a block cipher mode never changes the length of its @@ -635,23 +244,6 @@ pub trait BlockCipherDecryptor< &mut self, blocks: &mut [[u8; BLOCK_LEN]], ) -> Result; - - /// One-shot: decrypts `data` in place from the given init data. Returns the number of bytes - /// written; see [`Self::do_decrypt_blocks_inplace`]. - /// - /// # Errors - /// [`SymmetricCipherError::DataNotBlockAligned`] if `data` is not a whole number of blocks, - /// checked before any work is done, so `data` is left untouched. - fn decrypt_inplace( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - data: &mut [u8], - ) -> Result { - let (blocks, []) = data.as_chunks_mut::() else { - return Err(SymmetricCipherError::DataNotBlockAligned); - }; - Self::do_decrypt_init(key, init_data)?.do_decrypt_blocks_inplace(blocks) - } } /// The encryption half of a block cipher's API. @@ -694,31 +286,9 @@ pub trait BlockCipherDecryptor< /// use, so a mode like CBC never returns `Err` from the streaming methods. 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, - const BLOCK_LEN: usize, ->: Algorithm + Sized +pub trait BlockCipherEncryptor: + 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. - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// As [`BlockCipherEncryptor::do_encrypt_init`], but sources randomness from the provided RNG. - /// - /// # Panics - /// An implementation that generates no init data -- `INIT_DATA_LEN == 0`, as in ECB -- must - /// panic here rather than ignore `rng` and succeed. There is no randomness for it to consume, - /// so a caller reaching for this constructor has mistaken the cipher for a randomized one, and - /// quietly returning a deterministic encryptor would leave that mistake undetected. This is a - /// programmer error, not bad input, so it is a panic rather than a - /// [`SymmetricCipherError`]. Implementations with `INIT_DATA_LEN > 0` must draw their init - /// data from `rng` and must not panic. - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; /// The implementor hook: encrypts consecutive whole blocks in place. A sequence of calls is /// equivalent to one call over the concatenation. /// @@ -735,43 +305,6 @@ pub trait BlockCipherEncryptor< &mut self, blocks: &mut [[u8; BLOCK_LEN]], ) -> Result; - - /// One-shot: encrypts `data` in place under a fresh init, and returns the number of bytes - /// written (see [`Self::do_encrypt_blocks_inplace`]) alongside the generated init data. - /// - /// # Errors - /// [`SymmetricCipherError::DataNotBlockAligned`] if `data` is not a whole number of blocks, - /// checked before any work is done, so `data` is left untouched. - fn encrypt_inplace( - key: &KeyMaterial, - data: &mut [u8], - ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { - let (blocks, []) = data.as_chunks_mut::() else { - return Err(SymmetricCipherError::DataNotBlockAligned); - }; - let (mut enc, init_data) = Self::do_encrypt_init(key)?; - let written = enc.do_encrypt_blocks_inplace(blocks)?; - Ok((written, init_data)) - } - /// As [`BlockCipherEncryptor::encrypt_inplace`], but sources randomness from the provided RNG. - /// A misaligned `data` is refused before `rng` is drawn from. - /// - /// # Panics - /// Provided over [`do_encrypt_init_rng`](Self::do_encrypt_init_rng), so it panics in exactly - /// the cases that does: an implementation with `INIT_DATA_LEN == 0`, which has no randomness - /// to consume. See that method for why. - fn encrypt_rng_inplace( - key: &KeyMaterial, - rng: &mut dyn RNG, - data: &mut [u8], - ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { - let (blocks, []) = data.as_chunks_mut::() else { - return Err(SymmetricCipherError::DataNotBlockAligned); - }; - let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; - let written = enc.do_encrypt_blocks_inplace(blocks)?; - Ok((written, init_data)) - } } /// A hash function is a cryptographic primitive that takes an input of any length and produces a fixed-size output. @@ -1091,8 +624,8 @@ pub trait KEMPublicKey: /// This trait provides one-shot functions [`MAC::mac`], [`MAC::mac_out`], and [`MAC::verify`]. /// It also provides streaming functions [`MAC::do_update`], [`MAC::do_final`], [`MAC::do_final_out`], /// and [`MAC::do_verify_final`]. -/// The workflow is that a MAC object is initialized with a key with [`MAC::new`] -- or [`MAC::new_allow_weak_key`] if you -/// need to disable the library's safety mechanism to prevent the use of weak keys -- then data is +/// The workflow is that a MAC object is obtained from a key object (`HmacKey`, `KMAC128Key`, +/// `AES128Key` and the like, through `create_generator` / `create_verifier`), then data is /// processed into one or more calls to [`MAC::do_update`], /// after that the object can either create a MAC with [`MAC::do_final`] or [`MAC::do_final_out`] (which are final functions, and so consume the object), /// or the object can be used to verify a MAC. @@ -1102,37 +635,11 @@ pub trait KEMPublicKey: /// use constant-time comparison to avoid cryptographic timing attacks whereby an attacker could learn /// the bytes of the MAC value under some conditions. Therefore, it is highly recommended to use the provided verification functions. /// -/// Note that the MAC key is not represented in this trait because it is provided to the MAC algorithm -/// as part of its new functions. +/// Note that the MAC key is not represented in this trait because it belongs to the key object +/// that makes the worker. /// /// MACs do not implement Default because they do not have a sensible no-args constructor. pub trait MAC: Sized { - /// Create a new MAC instance with the given key. - /// - /// This is a common constructor whether creating or verifying a MAC value. - /// - /// Key / Salt is optional, which is indicated by providing an uninitialized KeyMaterial object of length zero, - /// the capacity is irrelevant, so KeyMateriol256::new() or KeyMaterial_internal::<0>::new() would both count as an absent salt. - /// - /// # Note about the security strength of the provided key: - /// If you initialize the MAC with a key that is tagged at a lower [`SecurityStrength`] than the - /// underlying hash function then [`MAC::new`] will fail with the following error: - /// ```text - /// MACError::KeyMaterialError(KeyMaterialError::SecurityStrength("HMAC::init(): provided key has a lower security strength than the instantiated HMAC") - /// ``` - /// There are situations in which it is completely reasonable and secure to provide low-entropy - /// (and sometimes all-zero) keys / salts; for these cases we have provided [`MAC::new_allow_weak_key`]. - fn new(key: &impl KeyMaterialTrait) -> Result; - - /// Create a new HMAC instance with the given key. - /// - /// This constructor completely ignores the [`SecurityStrength`] tag on the input key and will "just work". - /// This should be used if you really do need to use a weak key, such as an all-zero salt, - /// but use of this constructor is discouraged and you should really be asking yourself why you need it; - /// in most cases it indicates that your key is not long enough to support the security level of this - /// HMAC instance, or the key was derived using algorithms at a lower security level, etc. - fn new_allow_weak_key(key: &impl KeyMaterialTrait) -> Result; - /// The size of the output in bytes. fn output_len(&self) -> usize; @@ -1479,8 +986,8 @@ pub trait Signer, const SK_LEN: usize, const SIG /// The decryption half of a stream cipher's streaming API; see [`StreamCipherEncryptor`], whose /// notes on in-place operation, arbitrary lengths and the `Result` all apply here too. -pub trait StreamCipherDecryptor: - SymmetricCipherDecryptor +pub trait StreamCipherDecryptor: + SymmetricCipherDecryptor { /// Streaming: decrypts `data`, of any length, in place. A sequence of calls is equivalent to /// one call over the concatenation, whatever the chunking, exactly as for @@ -1488,16 +995,6 @@ pub trait StreamCipherDecryptor Result; - - /// One-shot: decrypts `data` in place from the given init data. Returns the number of bytes - /// written; see [`Self::do_decrypt_inplace`]. - fn decrypt_inplace( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - data: &mut [u8], - ) -> Result { - Self::do_decrypt_init(key, init_data)?.do_decrypt_inplace(data) - } } /// The encryption half of a stream cipher's streaming API: the in-place view of a @@ -1525,8 +1022,8 @@ pub trait StreamCipherDecryptor: - SymmetricCipherEncryptor +pub trait StreamCipherEncryptor: + SymmetricCipherEncryptor { /// Streaming: encrypts `data`, of any length, in place; on top of the generic APIs offered by /// [`SymmetricCipherEncryptor`], a stream cipher can offer `_inplace()` versions since a stream @@ -1545,39 +1042,6 @@ pub trait StreamCipherEncryptor Result; - - /// One-shot: encrypts `data` in place under a fresh init, and returns the number of bytes - /// written (see [`Self::do_encrypt_inplace`]) alongside the generated init data. - /// - /// # Errors - /// Whatever [`SymmetricCipherEncryptor::do_encrypt_init`] or [`Self::do_encrypt_inplace`] returns. - fn encrypt_inplace( - key: &KeyMaterial, - data: &mut [u8], - ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { - let (mut enc, init_data) = Self::do_encrypt_init(key)?; - let written = StreamCipherEncryptor::do_encrypt_inplace(&mut enc, data)?; - Ok((written, init_data)) - } - /// As [`StreamCipherEncryptor::encrypt_inplace`], but sources randomness from the provided - /// RNG. - /// - /// # Panics - /// Provided over [`SymmetricCipherEncryptor::do_encrypt_init_rng`], so it panics in exactly - /// the cases that does: an implementation with `INIT_DATA_LEN == 0`, which has no randomness - /// to consume. See that method for why. - /// - /// # Errors - /// Whatever [`SymmetricCipherEncryptor::do_encrypt_init_rng`] or [`Self::do_encrypt_inplace`] returns. - fn encrypt_rng_inplace( - key: &KeyMaterial, - rng: &mut dyn RNG, - data: &mut [u8], - ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { - let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; - let written = StreamCipherEncryptor::do_encrypt_inplace(&mut enc, data)?; - Ok((written, init_data)) - } } /// Allows a stateful object to suspend its operation by serializing its state into a byte array @@ -1619,8 +1083,10 @@ pub trait Suspendable: Sized { /// 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. +pub trait SuspendableKeyed<'k, const SERIALIZED_STATE_LEN: usize>: Sized { + /// The type of key that must be re-supplied to resume this object. A worker that borrows its + /// key for `'k` names the key's type (the mode's key state, for a cipher mode) and is resumed + /// over the reference it is given; one that owns a copy of its key ignores the lifetime. type Key: ?Sized; /// Suspend operation by serializing out the state of the object. @@ -1639,7 +1105,7 @@ pub trait SuspendableKeyed: Sized { /// deserializer should reject serialized states from that version or older. fn from_suspended( state: [u8; SERIALIZED_STATE_LEN], - key: &Self::Key, + key: &'k Self::Key, ) -> Result; } @@ -1654,29 +1120,13 @@ pub trait SuspendableKeyed: Sized { /// [`do_decrypt_final`](Self::do_decrypt_final) returns the buffer *and* how much of it is data, /// and the one-shot length helper is an upper bound rather than an exact count. /// -/// The one-shot [`decrypt_out`](Self::decrypt_out) is provided over the streaming methods, as is -/// the allocating [`decrypt`](Self::decrypt) behind the `std` feature. An implementor writes only -/// [`do_decrypt_init`](Self::do_decrypt_init), [`update_out_len`](Self::do_decrypt_out_len), -/// [`do_update_out`](Self::do_decrypt_out), [`do_decrypt_final`](Self::do_decrypt_final) and -/// [`decrypt_out_len`](Self::decrypt_out_len). -pub trait SymmetricCipherDecryptor< - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const FINAL_LEN: usize, ->: Algorithm + Sized +/// The one-shots are the key's, provided over these streaming methods. An implementor writes +/// only [`update_out_len`](Self::do_decrypt_out_len), [`do_update_out`](Self::do_decrypt_out), +/// [`do_decrypt_final`](Self::do_decrypt_final) and [`decrypt_out_len`](Self::decrypt_out_len); +/// the constructor is the key's, and never part of the trait. +pub trait SymmetricCipherDecryptor: + Algorithm + Sized { - /// Begins a streaming decryption from the init data returned by - /// [`SymmetricCipherEncryptor::do_encrypt_init`]. - /// - /// # 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 do_decrypt_init( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ) -> Result; - /// The exact number of bytes the next [`do_update_out`](Self::do_decrypt_out) will write if /// given `input_len` more bytes of ciphertext, so a caller can size the `plaintext` buffer for /// that call before making it. @@ -1769,70 +1219,13 @@ pub trait SymmetricCipherDecryptor< } /// An upper bound on the plaintext recovered from `ciphertext_len` bytes of ciphertext, i.e. - /// the buffer [`decrypt_out`](Self::decrypt_out) requires. Exact for ciphers with no padding; + /// the buffer the key's `decrypt_out` requires. Exact for ciphers with no padding; /// for a padding scheme the exact length is only known after decryption. fn decrypt_out_len(ciphertext_len: usize) -> usize; - - /// One-shot: decrypts `ciphertext` into `plaintext`, which needs - /// [`decrypt_out_len`](Self::decrypt_out_len) bytes. Returns the number of plaintext - /// bytes written. The entire output buffer is zeroized before the plaintext is written, so any - /// bytes past that count will be 0. - /// - /// Provided as `do_decrypt_init`, one `do_update_out` and `do_decrypt_final`. If - /// `do_decrypt_final` fails -- a bad tag, bad padding -- the plaintext already written is - /// zeroized before the error is returned, so a caller who ignores the `Result` is not left - /// holding unauthenticated data. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked - /// before any work is done; otherwise whatever the streaming methods return. - fn decrypt_out( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result { - plaintext.fill(0); - let needed = Self::decrypt_out_len(ciphertext.len()); - if plaintext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let mut dec = Self::do_decrypt_init(key, init_data)?; - let written = dec.do_decrypt_out(ciphertext, plaintext)?; - match dec.do_decrypt_final() { - Ok((last, data_len)) => { - // `decrypt_out_len` bounds `written + data_len`, so this fits in - // `plaintext[..needed]`. - plaintext[written..written + data_len].copy_from_slice(&last[..data_len]); - Ok(written + data_len) - } - Err(e) => { - // An AEAD reaches this one-shot through its `SymmetricCipherDecryptor` side, and - // what `do_update_out` released is unauthenticated; see - // `AEADCipherDecryptor::decrypt_detached_out` for why a plain `fill` is enough. - plaintext[..written].fill(0); - Err(e) - } - } - } - - /// One-shot, allocating: as [`decrypt_out`](Self::decrypt_out), returning the plaintext as a - /// `Vec` of exactly the recovered length. Only available with the `std` feature. - #[cfg(feature = "std")] - fn decrypt( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ciphertext: &[u8], - ) -> Result, SymmetricCipherError> { - let mut plaintext = vec![0u8; Self::decrypt_out_len(ciphertext.len())]; - let written = Self::decrypt_out(key, init_data, ciphertext, &mut plaintext)?; - plaintext.truncate(written); - Ok(plaintext) - } } /// The encryption half of a symmetric cipher's arbitrary-length API: streaming `do_update_out` / -/// `do_encrypt_final`, plus one-shots provided over them. +/// `do_encrypt_final`. The one-shots, and the constructor, are the key's. /// /// This is the layer a caller with *data* uses, as opposed to the block-aligned /// [`BlockCipherEncryptor`] a mode implements. Its shape is that of the padding adapters in @@ -1845,47 +1238,16 @@ pub trait SymmetricCipherDecryptor< /// length is answered exactly, before the fact, by [`update_out_len`](Self::do_encrypt_out_len) and /// [`encrypt_out_len`](Self::encrypt_out_len), so a caller can size buffers without guessing. /// -/// Init data (an IV or nonce) is generated by the constructor and returned, never supplied, for -/// the same reason as in [`BlockCipherEncryptor`]. Everything is `no_std`-friendly except the -/// allocating [`encrypt`](Self::encrypt), which sits behind the `std` feature. -/// -/// The one-shots [`encrypt_out`](Self::encrypt_out) and [`encrypt_rng_out`](Self::encrypt_rng_out) -/// are provided over the streaming methods. An implementor writes only the two `_init` -/// constructors, [`update_out_len`](Self::do_encrypt_out_len), [`do_update_out`](Self::do_encrypt_out), -/// [`do_encrypt_final`](Self::do_encrypt_final) and [`encrypt_out_len`](Self::encrypt_out_len). -pub trait SymmetricCipherEncryptor< - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const FINAL_LEN: usize, ->: Algorithm + Sized +/// Init data (an IV or nonce) is drawn by the key's `create_encryptor` and returned, never +/// supplied, for the same reason as in [`BlockCipherEncryptor`]. Everything is `no_std`-friendly +/// except the allocating [`do_encrypt`](Self::do_encrypt), which sits behind the `std` feature. +/// +/// An implementor writes only [`update_out_len`](Self::do_encrypt_out_len), +/// [`do_update_out`](Self::do_encrypt_out), [`do_encrypt_final`](Self::do_encrypt_final) and +/// [`encrypt_out_len`](Self::encrypt_out_len). +pub trait SymmetricCipherEncryptor: + Algorithm + Sized { - /// Begins a streaming encryption, returning the encryptor and the generated init data (IV or - /// nonce), which the recipient needs for [`SymmetricCipherDecryptor::do_decrypt_init`]. Sources - /// randomness from the library's default OS-backed RNG. - /// - /// # 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 do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - - /// As [`do_encrypt_init`](Self::do_encrypt_init), but sources randomness from the provided RNG. - /// - /// # Panics - /// An implementation that generates no init data -- `INIT_DATA_LEN == 0`, as in ECB -- must - /// panic here rather than ignore `rng` and succeed. There is no randomness for it to consume, - /// so a caller reaching for this constructor has mistaken the cipher for a randomized one, and - /// quietly returning a deterministic encryptor would leave that mistake undetected. This is a - /// programmer error, not bad input, so it is a panic rather than a - /// [`SymmetricCipherError`]. Implementations with `INIT_DATA_LEN > 0` must draw their init - /// data from `rng` and must not panic. - fn do_encrypt_init_rng( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// The exact number of bytes the next [`do_update_out`](Self::do_encrypt_out) will write if /// given `input_len` more bytes of plaintext, so a caller can size the `ciphertext` buffer for /// that call before making it. @@ -1971,78 +1333,9 @@ pub trait SymmetricCipherEncryptor< } /// The exact ciphertext length for a `plaintext_len`-byte plaintext that the cipher accepts, - /// i.e. the buffer [`encrypt_out`](Self::encrypt_out) requires and the number of bytes it - /// writes. (A length the cipher rejects -- unaligned data under a scheme that adds no padding -- + /// i.e. the buffer the key's `encrypt_out` requires and the number of bytes it writes. (A length the cipher rejects -- unaligned data under a scheme that adds no padding -- /// fails in [`do_encrypt_final`](Self::do_encrypt_final) instead.) fn encrypt_out_len(plaintext_len: usize) -> usize; - - /// One-shot: encrypts `plaintext` into `ciphertext`, which needs - /// [`encrypt_out_len`](Self::encrypt_out_len) bytes. Returns the generated init data and the - /// number of bytes written. The entire output buffer is zeroized before the ciphertext is - /// written, so any bytes past that count will be 0. - /// - /// Provided as `do_encrypt_init`, one `do_update_out` and `do_encrypt_final`. - /// - /// # Errors - /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked - /// before any work is done; otherwise whatever the streaming methods return. - fn encrypt_out( - key: &KeyMaterial, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { - ciphertext.fill(0); - let needed = Self::encrypt_out_len(plaintext.len()); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let (mut enc, init_data) = Self::do_encrypt_init(key)?; - let written = enc.do_encrypt_out(plaintext, ciphertext)?; - let (last, last_len) = enc.do_encrypt_final()?; - // `encrypt_out_len` is exactly `written + last_len`, so this fits in `ciphertext[..needed]`. - // .copy_from_slice is a bit of a code smell for a function meant to work in-place, - // but `ciphertext` is not required to be block-aligned, so it may not be large enough - // to hand to `do_encrypt_final_out()`. - ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); - Ok((init_data, written + last_len)) - } - - /// As [`encrypt_out`](Self::encrypt_out), but sources randomness from the provided RNG. - /// - /// # Panics - /// Provided over [`do_encrypt_init_rng`](Self::do_encrypt_init_rng), so it panics in exactly - /// the cases that does: an implementation with `INIT_DATA_LEN == 0`, which has no randomness - /// to consume. See that method for why. - fn encrypt_rng_out( - key: &KeyMaterial, - rng: &mut dyn RNG, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { - ciphertext.fill(0); - let needed = Self::encrypt_out_len(plaintext.len()); - if ciphertext.len() < needed { - return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); - } - let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; - let written = enc.do_encrypt_out(plaintext, ciphertext)?; - let (last, last_len) = enc.do_encrypt_final()?; - ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); - Ok((init_data, written + last_len)) - } - - #[cfg(feature = "std")] - /// One-shot, allocating: as [`encrypt_out`](Self::encrypt_out), returning the ciphertext as a - /// `Vec`. Only available with the `std` feature. - fn encrypt( - key: &KeyMaterial, - plaintext: &[u8], - ) -> Result<([u8; INIT_DATA_LEN], Vec), SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::encrypt_out_len(plaintext.len())]; - let (init_data, written) = Self::encrypt_out(key, plaintext, &mut ciphertext)?; - ciphertext.truncate(written); - Ok((init_data, ciphertext)) - } } /// The squeezing phase of an [`XOF`]: a value that produces output and can no longer take input, diff --git a/crypto/core/tests/aead_buffering_toy_tests.rs b/crypto/core/tests/aead_buffering_toy_tests.rs index 9180a2612..27aa50e2f 100644 --- a/crypto/core/tests/aead_buffering_toy_tests.rs +++ b/crypto/core/tests/aead_buffering_toy_tests.rs @@ -15,8 +15,8 @@ /// Pins that a *genuinely buffering* `AEADCipherEncryptor` / `AEADCipherDecryptor` pair's /// `update_out_len` is honoured through every chunking, against a toy built to hold back up to /// three bytes at a time before releasing them -- more than the tag the decryptor has to hold -/// back anyway -- the property `TestFrameworkAEADCipher::test_encryptor_decryptor` cannot pin on its own, since -/// a caller-supplied `E`/`D` might hold back nothing but the tag (Ascon-AEAD128 holds back +/// back anyway -- the property `TestFrameworkAEADCipher::test_key` cannot pin on its own, since +/// a key's workers might hold back nothing but the tag (Ascon-AEAD128 holds back /// nothing else). Modelled on the toy permutations `crypto/cipher/tests/modes/common/mod.rs` uses for /// the equivalent block-cipher property. /// @@ -27,15 +27,13 @@ #[test] fn a_buffering_pair_is_handled_by_every_default_method() { use bouncycastle_core::errors::SymmetricCipherError; - use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SymmetricCipherDecryptor, + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; const HOLD_BACK: usize = 3; - const KEY_LEN: usize = 4; const NONCE_LEN: usize = 4; const TAG_LEN: usize = 1; // What either side's final call can produce: the encryptor's held-back bytes plus the tag @@ -113,18 +111,7 @@ fn a_buffering_pair_is_handled_by_every_default_method() { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; } - impl SymmetricCipherEncryptor for Enc { - fn do_encrypt_init( - _key: &KeyMaterial, - ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { - Ok((Self(Buffered::new(HOLD_BACK)), [0u8; NONCE_LEN])) - } - fn do_encrypt_init_rng( - key: &KeyMaterial, - _rng: &mut dyn RNG, - ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { - Self::do_encrypt_init(key) - } + impl SymmetricCipherEncryptor for Enc { fn do_encrypt_out_len(&self, input_len: usize) -> usize { self.0.update_out_len(input_len) } @@ -147,16 +134,10 @@ fn a_buffering_pair_is_handled_by_every_default_method() { } } - impl AEADCipherEncryptor for Enc { + impl AEADCipherEncryptor for Enc { fn do_update_aad(&mut self, _aad: &[u8]) -> Result<(), SymmetricCipherError> { Ok(()) } - fn do_encrypt_init_nonce( - key: &KeyMaterial, - _nonce: &[u8; NONCE_LEN], - ) -> Result { - Ok(Self::do_encrypt_init(key)?.0) - } fn do_encrypt_final_detached_out( mut self, ciphertext: &mut [u8; FINAL_LEN], @@ -168,13 +149,7 @@ fn a_buffering_pair_is_handled_by_every_default_method() { } } - impl SymmetricCipherDecryptor for Dec { - fn do_decrypt_init( - _key: &KeyMaterial, - _nonce: &[u8; NONCE_LEN], - ) -> Result { - Ok(Self(Buffered::new(FINAL_LEN))) - } + impl SymmetricCipherDecryptor for Dec { fn do_decrypt_out_len(&self, input_len: usize) -> usize { self.0.update_out_len(input_len) } @@ -203,7 +178,7 @@ fn a_buffering_pair_is_handled_by_every_default_method() { } } - impl AEADCipherDecryptor for Dec { + impl AEADCipherDecryptor for Dec { fn do_update_aad(&mut self, _aad: &[u8]) -> Result<(), SymmetricCipherError> { Ok(()) } @@ -222,21 +197,30 @@ fn a_buffering_pair_is_handled_by_every_default_method() { } } - // The bytes every key and message is cut from: `0x00, 0x01, ...`, long enough for the longest + // The toy takes no key: these stand in for a key's `create_encryptor` / `create_decryptor`. + let fresh_enc = || Enc(Buffered::new(HOLD_BACK)); + let fresh_dec = || Dec(Buffered::new(FINAL_LEN)); + + // The bytes every message is cut from: `0x00, 0x01, ...`, long enough for the longest // message below. let seed: [u8; 64] = core::array::from_fn(|i| i as u8); - let key = - KeyMaterial::::from_bytes_as_type(&seed[..KEY_LEN], KeyType::SymmetricCipherKey) - .unwrap(); for len in 0..=(3 * FINAL_LEN + 5) { let msg = &seed[..len]; + // The detached one-shot, streamed by hand: everything in one update, then the final. let mut ct = vec![0u8; len]; - let (nonce, ct_len, tag) = Enc::encrypt_detached_out(&key, b"", msg, &mut ct).unwrap(); + let (ct_len, tag) = { + let mut enc = fresh_enc(); + let written = enc.do_encrypt_out(msg, &mut ct).unwrap(); + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf).unwrap(); + ct[written..written + final_len].copy_from_slice(&final_buf[..final_len]); + (written + final_len, tag) + }; assert_eq!(ct_len, len, "the toy never expands the data, only the finalizer flushes"); for chunk in [1usize, 2, 3, HOLD_BACK, FINAL_LEN, FINAL_LEN + 1, len.max(1)] { - let (mut enc, _) = Enc::do_encrypt_init(&key).unwrap(); + let mut enc = fresh_enc(); let mut chunked = Vec::new(); for piece in msg.chunks(chunk) { let expect = enc.do_encrypt_out_len(piece.len()); @@ -257,7 +241,7 @@ fn a_buffering_pair_is_handled_by_every_default_method() { // detached: the decryptor releases what it held back as a possible tag in // `do_decrypt_final_detached_out`, alongside what it held back of its own accord - let mut dec = Dec::do_decrypt_init(&key, &nonce).unwrap(); + let mut dec = fresh_dec(); let mut pt = Vec::new(); for piece in ct.chunks(chunk) { let expect = dec.do_decrypt_out_len(piece.len()); @@ -274,7 +258,7 @@ fn a_buffering_pair_is_handled_by_every_default_method() { // inline: the same stream with the tag on the end, chunked the same way let mut inline = ct.clone(); inline.extend_from_slice(&tag); - let mut dec = Dec::do_decrypt_init(&key, &nonce).unwrap(); + let mut dec = fresh_dec(); let mut pt = Vec::new(); for piece in inline.chunks(chunk) { let expect = dec.do_decrypt_out_len(piece.len()); @@ -291,7 +275,7 @@ fn a_buffering_pair_is_handled_by_every_default_method() { // The inline `ciphertext || tag` layout, which is where a buffering cipher makes // `do_encrypt_final` do two things at once: flush the held-back bytes and then append the // tag after them. - let (mut enc, nonce) = Enc::do_encrypt_init(&key).unwrap(); + let mut enc = fresh_enc(); let mut inline = vec![0u8; enc.do_encrypt_out_len(len)]; let written = enc.do_encrypt_out(msg, &mut inline).unwrap(); assert!(written < len || len == 0, "len {len}: the toy must be holding something back"); @@ -303,47 +287,12 @@ fn a_buffering_pair_is_handled_by_every_default_method() { "len {len}: inline layout is the message plus a tag" ); - let mut one = vec![0u8; Enc::encrypt_out_len(len)]; - let (one_nonce, one_len) = Enc::encrypt_with_aad_out(&key, b"", msg, &mut one).unwrap(); - assert_eq!(&one[..one_len], &inline[..], "len {len}: one-shot must agree"); - assert_eq!(one_nonce, nonce); - // Exactly the buffer it asks for: that is what makes the `+ data_len` arithmetic in - // the one-shot observable, since with a generous buffer any arithmetic there would do. - let mut back = vec![0u8; Dec::decrypt_out_len(one_len)]; - let back_len = - Dec::decrypt_with_aad_out(&key, &one_nonce, b"", &one[..one_len], &mut back).unwrap(); - assert_eq!(&back[..back_len], msg, "len {len}: inline one-shot round trip"); - - // Every other one-shot over the toy too: its final calls flush real data, which is - // what makes the `written + final_len` arithmetic in each of them observable. - let mut ct_rng = vec![0u8; len]; - let (_, n_rng, tag_rng) = Enc::encrypt_detached_rng_out( - &key, - &mut bouncycastle_rng::DefaultRNG::default(), - b"", - msg, - &mut ct_rng, - ) - .unwrap(); - assert_eq!(&ct_rng[..n_rng], &ct[..], "len {len}: encrypt_detached_rng_out"); - assert_eq!(tag_rng, tag, "len {len}: encrypt_detached_rng_out tag"); - let mut back = vec![0u8; len]; - let back_len = Dec::decrypt_detached_out(&key, &nonce, b"", &ct, &tag, &mut back).unwrap(); - assert_eq!(&back[..back_len], msg, "len {len}: decrypt_detached_out"); - let mut plain = vec![0u8; Enc::encrypt_out_len(len)]; - let (plain_nonce, plain_len) = Enc::encrypt_out(&key, msg, &mut plain).unwrap(); - assert_eq!(&plain[..plain_len], &inline[..], "len {len}: encrypt_out"); - let mut back = vec![0u8; Dec::decrypt_out_len(plain_len)]; - let back_len = - Dec::decrypt_out(&key, &plain_nonce, &plain[..plain_len], &mut back).unwrap(); - assert_eq!(&back[..back_len], msg, "len {len}: decrypt_out"); - // For any length past the hold-back window, at least one prefix of the input must be // held back rather than released immediately -- the property this whole test exists // to pin. (For `len < HOLD_BACK` nothing is ever releasable until `do_encrypt_final`, which // is also correct but does not exercise `do_update_out` returning less than it was given.) if len > HOLD_BACK { - let (mut enc, _) = Enc::do_encrypt_init(&key).unwrap(); + let mut enc = fresh_enc(); let first = &msg[..1]; let mut buf = vec![0u8; enc.do_encrypt_out_len(first.len())]; let n = enc.do_encrypt_out(first, &mut buf).unwrap(); diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index fe0509c2a..3914b759f 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -205,16 +205,6 @@ impl MACFactory { } impl MAC for MACFactory { - /// This is a dummy function, required by the [`MAC`] trait. DO NOT call it, it does not do anything. - fn new(_key: &impl KeyMaterialTrait) -> Result { - unimplemented!() - } - - /// This is a dummy function, required by the [`MAC`] trait. DO NOT call it, it does not do anything. - fn new_allow_weak_key(_key: &impl KeyMaterialTrait) -> Result { - unimplemented!() - } - fn output_len(&self) -> usize { match self { Self::KMAC128(h) => h.output_len(), diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index ec7d6105d..fdd803013 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -42,10 +42,6 @@ //! assert!(key.verify(b"part onepart two", &tag[..n])); // one-shot, same path underneath //! ``` //! -//! [`MAC::new`] and [`MAC::new_allow_weak_key`] on the worker build the same key internally and -//! take one worker from it, so they cost the pad computation per message; a key that will -//! authenticate more than one message is better held as an [`HmacKey`]. -//! //! # Instantiating HMAC over a Hash //! //! HMAC works with any hash: [`HMAC`](HMAC) needs only [`Hash`]. What HMAC cannot @@ -96,14 +92,16 @@ //! These apply to every instantiation; the hash crates' `hmac` modules repeat the ones that matter //! most in day-to-day use. //! -//! * [`HMACParams::HMAC_MAX_SECURITY_STRENGTH`] is a claim that [`MAC::new`] enforces against the -//! key's tagged strength, and that [`HMAC::keygen_from_rng`] enforces against the RNG's. Declaring -//! a strength the underlying hash cannot support does not make the construction stronger, it just -//! makes the check wrong. NIST SP 800-107-r1 Section 5.3.4 gives the ceiling: the effective -//! strength is `min(strength of K, 2C)` for an internal chaining value of `C` bits. -//! * [`MAC::new_allow_weak_key`] deliberately skips the key-strength check. It exists for protocols -//! that call for a weak or all-zero key -- an all-zero HKDF salt, for example -- and should not be -//! used to silence an error from [`MAC::new`]. +//! * [`HMACParams::HMAC_MAX_SECURITY_STRENGTH`] is a claim that +//! [`FromAnyKeyMaterial::from_key_material`] enforces against the key's tagged strength, and +//! that [`HMAC::keygen_from_rng`] enforces against the RNG's. Declaring a strength the +//! underlying hash cannot support does not make the construction stronger, it just makes the +//! check wrong. NIST SP 800-107-r1 Section 5.3.4 gives the ceiling: the effective strength is +//! `min(strength of K, 2C)` for an internal chaining value of `C` bits. +//! * [`FromAnyKeyMaterial::from_key_material_allow_weak`] deliberately skips the key-strength +//! check. It exists for protocols that call for a weak or all-zero key -- an all-zero HKDF salt, +//! for example -- and should not be used to silence an error from +//! [`FromAnyKeyMaterial::from_key_material`]. //! * Verification via [`MAC::verify`] / [`MAC::do_verify_final`] uses a constant-time comparison. //! Recomputing the MAC and comparing it with `==` leaks how many leading bytes matched. //! * [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is the shortest truncation this crate will produce, per @@ -335,8 +333,7 @@ impl MacVerificationKey for HmacKey { /*** Worker ***/ /// The HMAC worker for one message: the inner hash, absorbing the message, and the outer pad state -/// it is finished through. Obtained from an [`HmacKey`], or built with its own key for one message -/// by [`MAC::new`]. +/// it is finished through. Obtained from an [`HmacKey`]. #[derive(Clone)] pub struct HMAC { /// RFC 2104 Section 2 steps (3)-(4): `H(K XOR ipad, text)` in progress. @@ -393,14 +390,6 @@ impl HMAC { } impl MAC for HMAC { - fn new(key: &impl KeyMaterialTrait) -> Result { - Ok(HmacKey::::from_key_material_checked(key, false)?.create_generator()) - } - - fn new_allow_weak_key(key: &impl KeyMaterialTrait) -> Result { - Ok(HmacKey::::from_key_material_checked(key, true)?.create_generator()) - } - fn output_len(&self) -> usize { self.hasher.output_len() } diff --git a/crypto/hmac/tests/hmac_key_tests.rs b/crypto/hmac/tests/hmac_key_tests.rs index dcc56d072..394e65f27 100644 --- a/crypto/hmac/tests/hmac_key_tests.rs +++ b/crypto/hmac/tests/hmac_key_tests.rs @@ -1,11 +1,11 @@ //! `HmacKey`: the key object holding RFC 2104 Section 4's two pad states, and the workers it hands -//! out producing exactly what `MAC::new` produces from the same material. +//! out producing exactly what RFC 2104's formula gives from the same material. use bouncycastle_core::errors::{KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; -use bouncycastle_core::traits::{MAC, SuspendableKeyed}; +use bouncycastle_core::traits::{Hash, MAC, SuspendableKeyed}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hmac::{HMAC, HmacKey}; use bouncycastle_sha2::{SHA256, SHA512, SUSPENDED_SHA256_STATE_LEN}; @@ -18,15 +18,30 @@ fn mac_key(fill: u8) -> KeyMaterial { const MSG: &[u8] = b"The quick brown fox jumps over the lazy dog"; #[test] -fn the_key_and_the_worker_constructor_agree_for_every_key_shape() { +fn the_key_follows_rfc_2104_for_every_key_shape() { // Shorter than the block, exactly the block, and longer than the block (pre-hashed, RFC 2104 - // Section 3), for a 64-byte-block hash and a 136-byte-block one. - fn check(key_bytes: &[u8]) - where - H: bouncycastle_core::traits::Hash + Default, - { + // Section 3), for a 64-byte-block hash and a 136-byte-block one, against RFC 2104 Section 2 + // written out over the bare hash. + fn reference(key_bytes: &[u8], msg: &[u8]) -> Vec { + let block_len = H::default().block_bitlen() / 8; + let mut k = vec![0u8; block_len]; + if key_bytes.len() > block_len { + let hashed = H::default().hash(key_bytes); + k[..hashed.len()].copy_from_slice(&hashed); + } else { + k[..key_bytes.len()].copy_from_slice(key_bytes); + } + let mut inner = H::default(); + inner.do_update(&k.iter().map(|b| b ^ 0x36).collect::>()); + inner.do_update(msg); + let mut outer = H::default(); + outer.do_update(&k.iter().map(|b| b ^ 0x5c).collect::>()); + outer.do_update(&inner.do_final()); + outer.do_final() + } + fn check(key_bytes: &[u8]) { let km = KeyMaterial::<200>::from_bytes_as_type(key_bytes, KeyType::MACKey).unwrap(); - let expected = HMAC::::new_allow_weak_key(&km).unwrap().mac(MSG); + let expected = reference::(key_bytes, MSG); let key = HmacKey::::from_key_material_allow_weak(&km).unwrap(); assert_eq!(key.mac(MSG), expected); let mut generator = key.create_generator(); @@ -57,10 +72,7 @@ fn several_workers_from_one_key_are_independent_and_the_key_is_unchanged() { let na = a.do_final_out(&mut tag_a).unwrap(); assert_eq!((na, nb), (32, 32)); assert_ne!(tag_a, tag_b); - assert_eq!( - tag_a.to_vec(), - HMAC::::new(&mac_key::<32>(0x0b)).unwrap().mac(b"message one") - ); + assert_eq!(tag_a.to_vec(), key.mac(b"message one")); let mut v = key.create_verifier(); v.do_update(b"message one"); @@ -112,10 +124,14 @@ fn construction_applies_the_key_type_and_strength_policy() { )); assert!(HmacKey::::from_key_material_allow_weak(&weak).is_ok()); - // An empty (zeroized) key, HKDF's absent salt, is admitted by the weak form. + // An empty (zeroized) key, HKDF's absent salt, is admitted by the weak form and is the + // all-zero key of RFC 2104 step (1): the zero-length-key vector from `hmac_tests.rs`. let no_salt = KeyMaterial256::new(); let salt_key = HmacKey::::from_key_material_allow_weak(&no_salt).unwrap(); - assert_eq!(salt_key.mac(MSG), HMAC::::new_allow_weak_key(&no_salt).unwrap().mac(MSG)); + assert_eq!( + salt_key.mac(b"Hi There"), + b"\xe4\x84\x11\x26\x27\x15\xc8\x37\x0c\xd5\xe7\xbf\x8e\x82\xbe\xf5\x3b\xd5\x37\x12\xd0\x07\xf3\x42\x93\x51\x84\x3b\x77\xc7\xbb\x9b" + ); } #[test] @@ -147,7 +163,7 @@ fn generate_draws_a_key_of_the_hash_output_length_at_the_rng_strength() { } #[test] -fn a_worker_from_the_key_suspends_and_resumes_like_one_from_the_constructor() { +fn a_worker_from_the_key_suspends_and_resumes() { let km = mac_key::<32>(0x0b); let key = HmacKey::::from_key_material(&km).unwrap(); let mut w = key.create_generator(); diff --git a/crypto/hmac/tests/hmac_tests.rs b/crypto/hmac/tests/hmac_tests.rs index 481b0e42f..d8029e205 100644 --- a/crypto/hmac/tests/hmac_tests.rs +++ b/crypto/hmac/tests/hmac_tests.rs @@ -6,8 +6,12 @@ mod hmac_tests { KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::symmetric_key::{ + FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey, + }; use bouncycastle_core::traits::{Algorithm, Hash, MAC}; use bouncycastle_core_test_framework::DUMMY_SEED; + use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromAnyKeyMaterial; use bouncycastle_core_test_framework::mac::TestFrameworkMAC; use bouncycastle_hex as hex; use bouncycastle_hmac::*; @@ -30,7 +34,9 @@ mod hmac_tests { assert_eq!(zero_length_key.key_len(), 0); assert_eq!(zero_length_key.key_type(), KeyType::MACKey); - let mut mac = HMAC::::new_allow_weak_key(&zero_length_key).unwrap(); + let mut mac = HmacKey::::from_key_material_allow_weak(&zero_length_key) + .unwrap() + .create_generator(); mac.do_update("Hi There".as_bytes()); let output = mac.do_final(); assert_eq!(output, b"\xe4\x84\x11\x26\x27\x15\xc8\x37\x0c\xd5\xe7\xbf\x8e\x82\xbe\xf5\x3b\xd5\x37\x12\xd0\x07\xf3\x42\x93\x51\x84\x3b\x77\xc7\xbb\x9b"); @@ -41,7 +47,7 @@ mod hmac_tests { KeyType::MACKey, ) .unwrap(); - let mut mac = HMAC::::new(&key).unwrap(); + let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); mac.do_update(b"Hi There"); let output = mac.do_final(); assert_eq!(output, b"\x89\x6f\xb1\x12\x8a\xbb\xdf\x19\x68\x32\x10\x7c\xd4\x9d\xf3\x3f\x47\xb4\xb1\x16\x99\x12\xba\x4f\x53\x68\x4b\x22"); @@ -52,7 +58,7 @@ mod hmac_tests { KeyType::MACKey, ) .unwrap(); - let mac = HMAC::::new(&key).unwrap(); + let mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); // mac.do_update(b"").unwrap(); let output = mac.do_final(); assert_eq!( @@ -68,38 +74,33 @@ mod hmac_tests { b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", KeyType::MACKey).unwrap(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA224::new(&key).unwrap(); + let _: HMAC_SHA224 = HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA256::new(&key).unwrap(); + let _: HMAC_SHA256 = HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA384::new(&key).unwrap(); + let _: HMAC_SHA384 = HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA512::new(&key).unwrap(); + let _: HMAC_SHA512 = HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA512_224::new(&key).unwrap(); + let _: HMAC_SHA512_224 = + HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA512_256::new(&key).unwrap(); + let _: HMAC_SHA512_256 = + HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA3_224::new(&key).unwrap(); + let _: HMAC_SHA3_224 = + HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA3_256::new(&key).unwrap(); + let _: HMAC_SHA3_256 = + HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA3_384::new(&key).unwrap(); + let _: HMAC_SHA3_384 = + HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SHA3_512::new(&key).unwrap(); + let _: HMAC_SHA3_512 = + HmacKey::::from_key_material(&key).unwrap().create_generator(); - _ = HMAC::::new(&key).unwrap(); - _ = HMAC_SM3::new(&key).unwrap(); + let _: HMAC_SM3 = HmacKey::::from_key_material(&key).unwrap().create_generator(); } #[test] @@ -111,15 +112,15 @@ mod hmac_tests { .unwrap(); assert_eq!(short_key.security_strength(), SecurityStrength::_112bit); // key is too short, so it is expected to fail - match HMAC::::new(&short_key) { + match HmacKey::::from_key_material(&short_key) { Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) => { /* good */ } _ => panic!( "This should have thrown a KeyMaterialError::SecurityStrength error but it didn't" ), } - // It works after allowing weak keys - HMAC::::new_allow_weak_key(&short_key).unwrap(); + // It works through the weak form + HmacKey::::from_key_material_allow_weak(&short_key).unwrap(); // It works with a long enough key let key = KeyMaterial256::from_bytes_as_type( @@ -127,7 +128,7 @@ mod hmac_tests { KeyType::MACKey, ) .unwrap(); - HMAC::::new(&key).unwrap(); + HmacKey::::from_key_material(&key).unwrap(); } #[test] @@ -139,7 +140,7 @@ mod hmac_tests { ) .unwrap(); assert!( - HMAC_SHA256::new(&key).unwrap().verify( + HmacKey::::from_key_material(&key).unwrap().verify( b"Hi There", &hex::decode("4da0a0bb56f010db147b8f6e5f2dbecf7bb35ff00c8b9da31c9b94cc81815873") .unwrap() @@ -152,7 +153,7 @@ mod hmac_tests { KeyType::MACKey, ) .unwrap(); - HMAC_SHA256::new(&key).unwrap().verify( + HmacKey::::from_key_material(&key).unwrap().verify( b"Hi There", &hex::decode("5cb9475aba606afe8c82c2d1b3e1cfb1c814e8a72ce5a7b4fe43b0a0aac45144") .unwrap(), @@ -167,17 +168,17 @@ mod hmac_tests { // SHA-512 has a 128-byte block, so use a 127-byte key let key = KeyMaterial::<200>::from_bytes_as_type(&[0x0B; 127], KeyType::MACKey).unwrap(); - let mut mac = HMAC_SHA512::new(&key).unwrap(); + let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); mac.do_update(b"Hi There"); let tag = mac.do_final(); - assert!(HMAC_SHA512::new(&key).unwrap().verify(b"Hi There", &tag)); + assert!(HmacKey::::from_key_material(&key).unwrap().verify(b"Hi There", &tag)); // SHA3-224 has the largest block (144 bytes); a 143-byte key exercises the top of the range. let key = KeyMaterial::<200>::from_bytes_as_type(&[0x0B; 143], KeyType::MACKey).unwrap(); - let mut mac = HMAC_SHA3_224::new(&key).unwrap(); + let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); mac.do_update(b"Hi There"); let tag = mac.do_final(); - assert!(HMAC_SHA3_224::new(&key).unwrap().verify(b"Hi There", &tag)); + assert!(HmacKey::::from_key_material(&key).unwrap().verify(b"Hi There", &tag)); } #[test] @@ -185,14 +186,14 @@ mod hmac_tests { // Test: provided key has the correct length, but insufficient tagged security strength // HMAC should still work, but should return an error - // it works with a zero key (as new_allow_weak_key) + // it works with a zero key (through the weak form) // zero-len key let mut zero_key = KeyMaterial256::default(); - HMAC_SHA256::new_allow_weak_key(&zero_key).unwrap(); + HmacKey::::from_key_material_allow_weak(&zero_key).unwrap(); // non-zero len key of all-zero bytes do_hazardous_operations(&mut zero_key, |zero_key| zero_key.set_key_len(32)).unwrap(); - HMAC_SHA256::new_allow_weak_key(&zero_key).unwrap(); + HmacKey::::from_key_material_allow_weak(&zero_key).unwrap(); // Note: zero-len keys that are not Zeroized or MACKey are not allowed @@ -202,7 +203,7 @@ mod hmac_tests { assert_eq!(key.security_strength(), SecurityStrength::_256bit); key.set_security_strength(SecurityStrength::_128bit).unwrap(); // The call should fail, as the key's security strength is set below the required threshold - match HMAC::::new(&key) { + match HmacKey::::from_key_material(&key) { Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) => { /* fine */ } _ => { panic!( @@ -210,18 +211,30 @@ mod hmac_tests { ) } } - // It passes after setting .allow_weak_keys() - let mut hmac = HMAC::::new_allow_weak_key(&key).unwrap(); + // It passes through the weak form + let mut hmac = + HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); hmac.do_update(b"Hi There"); hmac.do_final(); // one-shot APIs still work with a weak key - let out = HMAC::::new_allow_weak_key(&key).unwrap().mac(b"Hi There"); - assert!(HMAC::::new_allow_weak_key(&key).unwrap().verify(b"Hi There", &out)); + let out = HmacKey::::from_key_material_allow_weak(&key).unwrap().mac(b"Hi There"); + assert!( + HmacKey::::from_key_material_allow_weak(&key) + .unwrap() + .verify(b"Hi There", &out) + ); // likewise with pre-allocated buffers let mut out = [0u8; 64]; - HMAC::::new_allow_weak_key(&key).unwrap().mac_out(b"Hi There", &mut out).unwrap(); - assert!(HMAC::::new_allow_weak_key(&key).unwrap().verify(b"Hi There", &out)); + HmacKey::::from_key_material_allow_weak(&key) + .unwrap() + .mac_out(b"Hi There", &mut out) + .unwrap(); + assert!( + HmacKey::::from_key_material_allow_weak(&key) + .unwrap() + .verify(b"Hi There", &out) + ); } #[test] @@ -233,23 +246,27 @@ mod hmac_tests { .unwrap(); // get the known-good output - let out = HMAC::::new(&key).unwrap().mac(b"Hi There"); + let out = HmacKey::::from_key_material(&key).unwrap().mac(b"Hi There"); // test output that's the wrong length, should simply return False - let mut mac = HMAC::::new(&key).unwrap(); + let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); mac.do_update(b"Hi There"); assert!(!mac.do_verify_final(&out[..out.len() - 1])); // test output that's the right length but wrong value -- do_verify - let mut mac = HMAC::::new(&key).unwrap(); + let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); mac.do_update(b"Hi There"); assert!(!mac.do_verify_final(&[0x01_u8; 28])); // test output that's the right length but wrong value -- static verify - assert!(!HMAC_SHA224::new(&key).unwrap().verify(b"Hi There", &[0x01_u8; 28])); + assert!( + !HmacKey::::from_key_material(&key) + .unwrap() + .verify(b"Hi There", &[0x01_u8; 28]) + ); // error case: test that it'll refuse to truncate below MIN_FIPS_DIGEST_LEN - let mut mac = HMAC::::new(&key).unwrap(); + let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); mac.do_update(b"Hi There"); let mut out = vec![0u8; MIN_FIPS_DIGEST_LEN - 1]; match mac.do_final_out(&mut out) { @@ -263,7 +280,7 @@ mod hmac_tests { } // success case: ... but it will truncate to exactly MIN_FIPS_DIGEST_LEN - let mut mac = HMAC::::new(&key).unwrap(); + let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); mac.do_update(b"Hi There"); let mut out = vec![0u8; MIN_FIPS_DIGEST_LEN]; let bytes_written = mac.do_final_out(&mut out).unwrap(); @@ -271,17 +288,17 @@ mod hmac_tests { assert_eq!(&out, b"\x89\x6f\xb1\x12"); // fail case: mac value is correct but truncated - let mac = HMAC_SHA3_224::new(&key).unwrap(); + let mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); let mut mac_val = mac.mac(b"Polly want a cracker?"); - let verifier = HMAC_SHA3_224::new(&key).unwrap(); + let verifier = HmacKey::::from_key_material(&key).unwrap().create_generator(); assert!(verifier.verify(b"Polly want a cracker?", &mac_val)); // truncation of the mac value is considered a fail - let verifier = HMAC_SHA3_224::new(&key).unwrap(); + let verifier = HmacKey::::from_key_material(&key).unwrap().create_generator(); assert!(!verifier.verify(b"Polly want a cracker?", &mac_val[..mac_val.len() - 1])); // .. as is some extra bytes at the end - let verifier = HMAC_SHA3_224::new(&key).unwrap(); + let verifier = HmacKey::::from_key_material(&key).unwrap().create_generator(); mac_val.extend_from_slice(&[0u8; 4]); assert!(!verifier.verify(b"Polly want a cracker?", &mac_val)); } @@ -325,11 +342,12 @@ mod hmac_tests { .unwrap(); let msg = hex::decode("2579f5df03e0fccde2b515944d88dc81ca3b4a20517cdc54170559f0d2f889e2f543eacf8a84b34563d0139351ea9a77399d274c5c6c1b0f488063b7255f9df648667fe800151ef288a68d6c8c24d57abd7e4f70eed149752beae4a9763cebf03c").unwrap(); let expected = hex::decode("6e927067f724d4fedc96b310c5115979e8dde8a4").unwrap(); - let full = HMAC_SHA512_224::new(&key).unwrap().mac(&msg); + let full = HmacKey::::from_key_material(&key).unwrap().mac(&msg); assert_eq!(full.len(), 28); assert_eq!(&full[..20], &expected[..]); // the same vector through the streaming API in uneven chunks - let mut mac = HMAC_SHA512_224::new(&key).unwrap(); + let mut mac = + HmacKey::::from_key_material(&key).unwrap().create_generator(); for chunk in msg.chunks(13) { mac.do_update(chunk); } @@ -343,11 +361,12 @@ mod hmac_tests { .unwrap(); let msg = hex::decode("dedb0cc1c2a9b960d3").unwrap(); let expected = hex::decode("9cf6def15b5ead939e1fda675b52147a01a6ccb6").unwrap(); - let full = HMAC_SHA512_224::new(&key).unwrap().mac(&msg); + let full = HmacKey::::from_key_material(&key).unwrap().mac(&msg); assert_eq!(full.len(), 28); assert_eq!(&full[..20], &expected[..]); // the same vector through the streaming API in uneven chunks - let mut mac = HMAC_SHA512_224::new(&key).unwrap(); + let mut mac = + HmacKey::::from_key_material(&key).unwrap().create_generator(); for chunk in msg.chunks(13) { mac.do_update(chunk); } @@ -369,11 +388,12 @@ mod hmac_tests { .unwrap(); let msg = hex::decode("").unwrap(); let expected = hex::decode("7857d4737760e127f1533185c6ad183ac4e10bd9").unwrap(); - let full = HMAC_SHA512_256::new(&key).unwrap().mac(&msg); + let full = HmacKey::::from_key_material(&key).unwrap().mac(&msg); assert_eq!(full.len(), 32); assert_eq!(&full[..20], &expected[..]); // the same vector through the streaming API in uneven chunks - let mut mac = HMAC_SHA512_256::new(&key).unwrap(); + let mut mac = + HmacKey::::from_key_material(&key).unwrap().create_generator(); for chunk in msg.chunks(13) { mac.do_update(chunk); } @@ -388,11 +408,12 @@ mod hmac_tests { let msg = hex::decode("9268f10c36fd3366012e841260e60227a968f6c8546dee6abc83b3").unwrap(); let expected = hex::decode("3288232187dcf1ea421f5c12bdeb4fd9d0a0a25b").unwrap(); - let full = HMAC_SHA512_256::new(&key).unwrap().mac(&msg); + let full = HmacKey::::from_key_material(&key).unwrap().mac(&msg); assert_eq!(full.len(), 32); assert_eq!(&full[..20], &expected[..]); // the same vector through the streaming API in uneven chunks - let mut mac = HMAC_SHA512_256::new(&key).unwrap(); + let mut mac = + HmacKey::::from_key_material(&key).unwrap().create_generator(); for chunk in msg.chunks(13) { mac.do_update(chunk); } @@ -400,6 +421,14 @@ mod hmac_tests { } } + /// What `TestFrameworkMAC::test_mac` ran per vector: the key built with the weak form (RFC 4231 + /// keys are below the HMAC's strength), its workers against the vector, and the key policy. + fn test_hmac(key: &impl KeyMaterialTrait, input: &[u8], expected: &[u8]) { + let hmac_key = HmacKey::::from_key_material_allow_weak(key).unwrap(); + TestFrameworkMAC::new().test_mac_key(&hmac_key, input, expected); + TestFrameworkFromAnyKeyMaterial::new().test::>(key); + } + #[cfg(test)] mod core_test_framework_rfc4231 { use super::*; @@ -407,7 +436,6 @@ mod hmac_tests { #[test] fn hmac_sha224() { - let test_framework = TestFrameworkMAC::new(); let mut zero_length_key = KeyMaterial256::default(); do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) @@ -416,15 +444,14 @@ mod hmac_tests { assert_eq!(zero_length_key.key_len(), 0); assert_eq!(zero_length_key.key_type(), KeyType::MACKey); - test_framework.test_mac::>( + test_hmac::( &zero_length_key, b"Hello, world", &hex::decode("57454372e6a8780b11150274d7056c6fbcffef902f9c23f24fbbfee9").unwrap(), ); // RFC4231 Test Case 1 - let test_framework = TestFrameworkMAC::new(); - test_framework.test_mac::>( + test_hmac::( &KeyMaterial256::from_bytes_as_type( &hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(), KeyType::MACKey, @@ -435,7 +462,7 @@ mod hmac_tests { ); // RFC4231 Test Case 2 -- Test with a key shorter than the length of the HMAC output. - test_framework.test_mac::>( + test_hmac::( &KeyMaterial256::from_bytes_as_type(b"Jefe", KeyType::MACKey).unwrap(), b"what do ya want for nothing?", &hex::decode("a30e01098bc6dbbf45690f3a7e9e6d0f8bbea2a39e6148008fd05e44").unwrap(), @@ -443,14 +470,14 @@ mod hmac_tests { // RFC4231 Test Case 3 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial256::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial256::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), &hex::decode("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd").unwrap(), &hex::decode("7fb3cb3588c6c1f6ffa9694d7d6ad2649365b0c1f65d69d1ec8333ea").unwrap(), ); // RFC4231 Test Case 4 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial256::from_bytes_as_type(&hex::decode("0102030405060708090a0b0c0d0e0f10111213141516171819").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial256::from_bytes_as_type(&hex::decode("0102030405060708090a0b0c0d0e0f10111213141516171819").unwrap(), KeyType::MACKey).unwrap(), &hex::decode("cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd").unwrap(), &hex::decode("6c11506874013cac6a2abc1bb382627cec6a90d86efc012de7afec5a").unwrap(), ); @@ -462,13 +489,16 @@ mod hmac_tests { ) .unwrap(); let mut out = [0u8; 128 / 8]; - HMAC::::new(&key).unwrap().mac_out(b"Test With Truncation", &mut out).unwrap(); + HmacKey::::from_key_material(&key) + .unwrap() + .mac_out(b"Test With Truncation", &mut out) + .unwrap(); assert_eq!(&Vec::from(out), &hex::decode("0e2aea68a90c8d37c988bcdb9fca6fa8").unwrap()); // RFC4231 Test Case 6 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). let key = KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(); - test_framework.test_mac::>( + test_hmac::( &key, b"Test Using Larger Than Block-Size Key - Hash Key First", &hex::decode("95e9a0db962095adaebe9b2d6f0dbce2d499f112f2d2b7273fa6870e").unwrap(), @@ -476,7 +506,7 @@ mod hmac_tests { // RFC4231 Test Case 7 -- Test with a key and data that is larger than 128 bytes (= block-size // of SHA-384 and SHA-512) - test_framework.test_mac::>(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), b"This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.", &hex::decode("3a854166ac5d9f023f54d517d0b39dbd946770db9c2b95c9f6f565d1").unwrap(), ); @@ -485,7 +515,6 @@ mod hmac_tests { #[test] fn hmac_sha256() { // test with zero-length key - let test_framework = TestFrameworkMAC::new(); let mut zero_length_key = KeyMaterial256::default(); do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) @@ -494,7 +523,7 @@ mod hmac_tests { assert_eq!(zero_length_key.key_len(), 0); assert_eq!(zero_length_key.key_type(), KeyType::MACKey); - test_framework.test_mac::>( + test_hmac::( &zero_length_key, b"Hello, world", &hex::decode("c0fa4c55880318c31c1020e7a2cf830c2c695716387795c7a0eb918ba84e4bf0") @@ -502,8 +531,7 @@ mod hmac_tests { ); // RFC4231 Test Case 1 - let test_framework = TestFrameworkMAC::new(); - test_framework.test_mac::>( + test_hmac::( &KeyMaterial256::from_bytes_as_type( &hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(), KeyType::MACKey, @@ -515,7 +543,7 @@ mod hmac_tests { ); // RFC4231 Test Case 2 -- Test with a key shorter than the length of the HMAC output. - test_framework.test_mac::>( + test_hmac::( &KeyMaterial256::from_bytes_as_type(b"Jefe", KeyType::MACKey).unwrap(), b"what do ya want for nothing?", &hex::decode("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843") @@ -524,14 +552,14 @@ mod hmac_tests { // RFC4231 Test Case 3 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial256::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial256::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), &hex::decode("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd").unwrap(), &hex::decode("773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe").unwrap(), ); // RFC4231 Test Case 4 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial256::from_bytes_as_type(&hex::decode("0102030405060708090a0b0c0d0e0f10111213141516171819").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial256::from_bytes_as_type(&hex::decode("0102030405060708090a0b0c0d0e0f10111213141516171819").unwrap(), KeyType::MACKey).unwrap(), &hex::decode("cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd").unwrap(), &hex::decode("82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b").unwrap(), ); @@ -543,19 +571,22 @@ mod hmac_tests { ) .unwrap(); let mut out = [0u8; 128 / 8]; - HMAC::::new(&key).unwrap().mac_out(b"Test With Truncation", &mut out).unwrap(); + HmacKey::::from_key_material(&key) + .unwrap() + .mac_out(b"Test With Truncation", &mut out) + .unwrap(); assert_eq!(&Vec::from(out), &hex::decode("a3b6167473100ee06e0c796c2955552b").unwrap()); // RFC4231 Test Case 6 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), b"Test Using Larger Than Block-Size Key - Hash Key First", &hex::decode("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54").unwrap(), ); // RFC4231 Test Case 7 -- Test with a key and data that is larger than 128 bytes (= block-size // of SHA-384 and SHA-512) - test_framework.test_mac::>(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), b"This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.", &hex::decode("9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2").unwrap(), ); @@ -564,7 +595,6 @@ mod hmac_tests { #[test] fn hmac_sha384() { // test with zero-length key - let test_framework = TestFrameworkMAC::new(); let mut zero_length_key = KeyMaterial256::default(); do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) @@ -573,15 +603,14 @@ mod hmac_tests { assert_eq!(zero_length_key.key_len(), 0); assert_eq!(zero_length_key.key_type(), KeyType::MACKey); - test_framework.test_mac::>( + test_hmac::( &zero_length_key, b"Hello, world", &hex::decode("fbd41442f749049355175277afbaff610539e5bfa874c9cf86ef867a43a30b09a5eac6578d5c0cb1ceddc95f97598af7").unwrap(), ); // RFC4231 Test Case 1 - let test_framework = TestFrameworkMAC::new(); - test_framework.test_mac::>( + test_hmac::( &KeyMaterial256::from_bytes_as_type( &hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(), KeyType::MACKey, @@ -591,7 +620,7 @@ mod hmac_tests { ); // RFC4231 Test Case 2 -- Test with a key shorter than the length of the HMAC output. - test_framework.test_mac::>( + test_hmac::( &KeyMaterial256::from_bytes_as_type(b"Jefe", KeyType::MACKey).unwrap(), b"what do ya want for nothing?", &hex::decode("af45d2e376484031617f78d2b58a6b1b9c7ef464f5a01b47e42ec3736322445e8e2240ca5e69e2c78b3239ecfab21649").unwrap(), @@ -599,14 +628,14 @@ mod hmac_tests { // RFC4231 Test Case 3 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial256::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial256::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), &hex::decode("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd").unwrap(), &hex::decode("88062608d3e6ad8a0aa2ace014c8a86f0aa635d947ac9febe83ef4e55966144b2a5ab39dc13814b94e3ab6e101a34f27").unwrap(), ); // RFC4231 Test Case 4 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial256::from_bytes_as_type(&hex::decode("0102030405060708090a0b0c0d0e0f10111213141516171819").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial256::from_bytes_as_type(&hex::decode("0102030405060708090a0b0c0d0e0f10111213141516171819").unwrap(), KeyType::MACKey).unwrap(), &hex::decode("cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd").unwrap(), &hex::decode("3e8a69b7783c25851933ab6290af6ca77a9981480850009cc5577c6e1f573b4e6801dd23c4a7d679ccf8a386c674cffb").unwrap(), ); @@ -618,21 +647,22 @@ mod hmac_tests { ) .unwrap(); let mut out = [0u8; 128 / 8]; - // Key is shorter than HMAC security strength, so it needs to use new_allow_weak_keys() - let hmac = HMAC::::new_allow_weak_key(&key).unwrap(); + // Key is shorter than HMAC security strength, so it needs the weak form + let hmac = + HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); hmac.mac_out(b"Test With Truncation", &mut out).unwrap(); assert_eq!(&Vec::from(out), &hex::decode("3abf34c3503b2a23a46efc619baef897").unwrap()); // RFC4231 Test Case 6 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), b"Test Using Larger Than Block-Size Key - Hash Key First", &hex::decode("4ece084485813e9088d2c63a041bc5b44f9ef1012a2b588f3cd11f05033ac4c60c2ef6ab4030fe8296248df163f44952").unwrap(), ); // RFC4231 Test Case 7 -- Test with a key and data that is larger than 128 bytes (= block-size // of SHA-384 and SHA-512) - test_framework.test_mac::>(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), b"This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.", &hex::decode("6617178e941f020d351e2f254e8fd32c602420feb0b8fb9adccebb82461e99c5a678cc31e799176d3860e6110c46523e").unwrap(), ); @@ -641,7 +671,6 @@ mod hmac_tests { #[test] fn hmac_sha512() { // test with zero-length key - let test_framework = TestFrameworkMAC::new(); let mut zero_length_key = KeyMaterial256::default(); do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) @@ -650,15 +679,14 @@ mod hmac_tests { assert_eq!(zero_length_key.key_len(), 0); assert_eq!(zero_length_key.key_type(), KeyType::MACKey); - test_framework.test_mac::>( + test_hmac::( &zero_length_key, b"Hello, world", &hex::decode("e8f7176e01bf9bb883f71f42c143681e86cfafe0b61f3bc0d824e2cde13b5f80199e82d865aebb725461c86a54086aeacac37a86a9f1cf07db567ba5a10f1cc1").unwrap(), ); // RFC4231 Test Case 1 - let test_framework = TestFrameworkMAC::new(); - test_framework.test_mac::>( + test_hmac::( &KeyMaterial256::from_bytes_as_type( &hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(), KeyType::MACKey, @@ -669,7 +697,7 @@ mod hmac_tests { ); // RFC4231 Test Case 2 -- Test with a key shorter than the length of the HMAC output. - test_framework.test_mac::>( + test_hmac::( &KeyMaterial256::from_bytes_as_type(b"Jefe", KeyType::MACKey).unwrap(), b"what do ya want for nothing?", &hex::decode("164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737").unwrap(), @@ -677,14 +705,14 @@ mod hmac_tests { // RFC4231 Test Case 3 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial256::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial256::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), &hex::decode("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd").unwrap(), &hex::decode("fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb").unwrap(), ); // RFC4231 Test Case 4 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial256::from_bytes_as_type(&hex::decode("0102030405060708090a0b0c0d0e0f10111213141516171819").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial256::from_bytes_as_type(&hex::decode("0102030405060708090a0b0c0d0e0f10111213141516171819").unwrap(), KeyType::MACKey).unwrap(), &hex::decode("cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd").unwrap(), &hex::decode("b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd").unwrap(), ); @@ -696,21 +724,22 @@ mod hmac_tests { ) .unwrap(); let mut out = [0u8; 128 / 8]; - // Key is shorter than HMAC security strength, so need to use new_allow_weak_keys() - let hmac = HMAC::::new_allow_weak_key(&key).unwrap(); + // Key is shorter than HMAC security strength, so it needs the weak form + let hmac = + HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); hmac.mac_out(b"Test With Truncation", &mut out).unwrap(); assert_eq!(&Vec::from(out), &hex::decode("415fad6271580a531d4179bc891d87a6").unwrap()); // RFC4231 Test Case 6 -- Test with a combined length of key and data that is larger than 64 // bytes (= block-size of SHA-224 and SHA-256). - test_framework.test_mac::>(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), b"Test Using Larger Than Block-Size Key - Hash Key First", &hex::decode("80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598").unwrap(), ); // RFC4231 Test Case 7 -- Test with a key and data that is larger than 128 bytes (= block-size // of SHA-384 and SHA-512) - test_framework.test_mac::>(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), + test_hmac::(&KeyMaterial::<131>::from_bytes_as_type(&hex::decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), KeyType::MACKey).unwrap(), b"This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.", &hex::decode("e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58").unwrap(), ); @@ -724,10 +753,9 @@ mod hmac_tests { #[test] fn hmac_sm3_known_answers() { use bouncycastle_core::key_material::KeyMaterial; - let test_framework = TestFrameworkMAC::new(); // RFC4231 Test Case 1 key/message - test_framework.test_mac::( + test_hmac::( &KeyMaterial::<20>::from_bytes_as_type( &hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(), KeyType::MACKey, @@ -738,24 +766,26 @@ mod hmac_tests { .unwrap(), ); // RFC4231 Test Case 2 key/message - test_framework.test_mac::( + test_hmac::( &KeyMaterial::<4>::from_bytes_as_type(b"Jefe", KeyType::MACKey).unwrap(), b"what do ya want for nothing?", &hex::decode("2e87f1d16862e6d964b50a5200bf2b10b764faa9680a296a2405f24bec39f882") .unwrap(), ); // RFC4231 Test Case 6 key/message: key larger than the 64-byte block, so it is hashed first - test_framework.test_mac::( + test_hmac::( &KeyMaterial::<131>::from_bytes_as_type(&[0xaa; 131], KeyType::MACKey).unwrap(), b"Test Using Larger Than Block-Size Key - Hash Key First", &hex::decode("b4fd844e13342002f0b2e0690ea7741f1497d993a70494cea601e657bedf67a0") .unwrap(), ); - // zero-length key (weak; needs new_allow_weak_key) + // zero-length key (weak; needs the weak form) let mut zero_length_key = KeyMaterial256::default(); do_hazardous_operations(&mut zero_length_key, |k| k.set_key_type(KeyType::MACKey)).unwrap(); - let mut mac = HMAC_SM3::new_allow_weak_key(&zero_length_key).unwrap(); + let mut mac = HmacKey::::from_key_material_allow_weak(&zero_length_key) + .unwrap() + .create_generator(); mac.do_update(b"abc"); assert_eq!( mac.do_final(), @@ -764,10 +794,11 @@ mod hmac_tests { ); assert_eq!( - HMAC_SM3::new( + HmacKey::::from_key_material( &KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::MACKey).unwrap() ) .unwrap() + .create_generator() .output_len(), 32 ); @@ -826,17 +857,41 @@ mod hmac_tests { } } - round_trip(HMAC_SHA256::new(&key).unwrap(), &key, msg); - round_trip(HMAC_SHA512::new(&key).unwrap(), &key, msg); - round_trip(HMAC_SHA512_224::new(&key).unwrap(), &key, msg); - round_trip(HMAC_SHA512_256::new(&key).unwrap(), &key, msg); - round_trip(HMAC_SHA3_256::new(&key).unwrap(), &key, msg); - round_trip(HMAC_SM3::new(&key).unwrap(), &key, msg); + round_trip( + HmacKey::::from_key_material(&key).unwrap().create_generator(), + &key, + msg, + ); + round_trip( + HmacKey::::from_key_material(&key).unwrap().create_generator(), + &key, + msg, + ); + round_trip( + HmacKey::::from_key_material(&key).unwrap().create_generator(), + &key, + msg, + ); + round_trip( + HmacKey::::from_key_material(&key).unwrap().create_generator(), + &key, + msg, + ); + round_trip( + HmacKey::::from_key_material(&key).unwrap().create_generator(), + &key, + msg, + ); + round_trip(HmacKey::::from_key_material(&key).unwrap().create_generator(), &key, msg); // test suspend / resume with a key larger than block size let long_key = KeyMaterial::<200>::from_bytes_as_type(&DUMMY_SEED[..200], KeyType::MACKey).unwrap(); - round_trip(HMAC_SHA256::new(&long_key).unwrap(), &long_key, msg); + round_trip( + HmacKey::::from_key_material(&long_key).unwrap().create_generator(), + &long_key, + msg, + ); } /// Tests that no private data is displayed @@ -847,7 +902,7 @@ mod hmac_tests { KeyType::MACKey, ) .unwrap(); - let hmac = HMAC_SHA256::new(&key).unwrap(); + let hmac = HmacKey::::from_key_material(&key).unwrap().create_generator(); // test fmt let fmt_str = format!("{}", &hmac); @@ -860,12 +915,12 @@ mod hmac_tests { /// Exercises the `keygen_from_rng()` function of each HMAC type alias: /// * the generated key must not be the all-zero array, /// * `keygen_from_rng()` returns a ready-to-use `KeyType::MACKey` key, so that - /// * `HMAC::new(&key)` accepts the freshly generated key, without error. + /// * `HmacKey::from_key_material(&key)` accepts the freshly generated key, without error. /// /// HashDRBG_SHA512 is used throughout because it is the only built-in DRBG that meets the /// 256-bit strength that HMAC-SHA512 and HMAC-SHA3-512 claim; see `keygen_rejects_weak_rng`. macro_rules! keygen_test { - ($test_name:ident, $hmac:ident, $n:literal) => { + ($test_name:ident, $hmac:ident, $hash:ident, $n:literal) => { #[test] fn $test_name() { let mut rng = HashDRBG_SHA512::new_from_os(); @@ -878,22 +933,23 @@ mod hmac_tests { "keygen produced an all-zero key" ); - $hmac::new(&key).expect("HMAC::new should accept a freshly generated key"); + HmacKey::<$hash>::from_key_material(&key) + .expect("HmacKey should accept a freshly generated key"); } }; } - keygen_test!(keygen_hmac_sha224, HMAC_SHA224, 28); - keygen_test!(keygen_hmac_sha256, HMAC_SHA256, 32); - keygen_test!(keygen_hmac_sha384, HMAC_SHA384, 48); - keygen_test!(keygen_hmac_sha512, HMAC_SHA512, 64); - keygen_test!(keygen_hmac_sha512_224, HMAC_SHA512_224, 28); - keygen_test!(keygen_hmac_sha512_256, HMAC_SHA512_256, 32); - keygen_test!(keygen_hmac_sha3_224, HMAC_SHA3_224, 28); - keygen_test!(keygen_hmac_sha3_256, HMAC_SHA3_256, 32); - keygen_test!(keygen_hmac_sha3_384, HMAC_SHA3_384, 48); - keygen_test!(keygen_hmac_sha3_512, HMAC_SHA3_512, 64); - keygen_test!(keygen_hmac_sm3, HMAC_SM3, 32); + keygen_test!(keygen_hmac_sha224, HMAC_SHA224, SHA224, 28); + keygen_test!(keygen_hmac_sha256, HMAC_SHA256, SHA256, 32); + keygen_test!(keygen_hmac_sha384, HMAC_SHA384, SHA384, 48); + keygen_test!(keygen_hmac_sha512, HMAC_SHA512, SHA512, 64); + keygen_test!(keygen_hmac_sha512_224, HMAC_SHA512_224, SHA512_224, 28); + keygen_test!(keygen_hmac_sha512_256, HMAC_SHA512_256, SHA512_256, 32); + keygen_test!(keygen_hmac_sha3_224, HMAC_SHA3_224, SHA3_224, 28); + keygen_test!(keygen_hmac_sha3_256, HMAC_SHA3_256, SHA3_256, 32); + keygen_test!(keygen_hmac_sha3_384, HMAC_SHA3_384, SHA3_384, 48); + keygen_test!(keygen_hmac_sha3_512, HMAC_SHA3_512, SHA3_512, 64); + keygen_test!(keygen_hmac_sm3, HMAC_SM3, SM3, 32); /// `keygen_from_rng` must refuse an RNG whose security strength is below the strength the HMAC /// claims, otherwise the returned key would be tagged stronger than the entropy behind it. diff --git a/crypto/hmac/tests/hmac_wycheproof.rs b/crypto/hmac/tests/hmac_wycheproof.rs index 05d4bcf34..652e254ed 100644 --- a/crypto/hmac/tests/hmac_wycheproof.rs +++ b/crypto/hmac/tests/hmac_wycheproof.rs @@ -8,23 +8,24 @@ //! suites. //! //! Each file has a full-length-tag group and a truncated-tag group (half the output length). A -//! full-length tag is checked through `verify`. A truncated tag is checked through `mac_out` into a -//! buffer of the tag's length, and `verify` must reject it: it requires the full output length. +//! full-length tag is checked through the key's `verify`. A truncated tag is checked through +//! `mac_out` into a buffer of the tag's length, and `verify` must reject it: it requires the full +//! output length. use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::MAC; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; +use bouncycastle_core::traits::{Hash, MAC}; use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; -use bouncycastle_sha2::hmac::{ - HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, HMAC_SHA512, HMAC_SHA512_224, HMAC_SHA512_256, -}; -use bouncycastle_sha3::hmac::{HMAC_SHA3_224, HMAC_SHA3_256, HMAC_SHA3_384, HMAC_SHA3_512}; -use bouncycastle_sm3::hmac::HMAC_SM3; +use bouncycastle_hmac::HmacKey; +use bouncycastle_sha2::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; +use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; +use bouncycastle_sm3::SM3; /// The longest key in any of the files is 65 bytes. const MAX_KEY_LEN: usize = 128; -/// Runs every case in one `hmac_*_test.json` file through `M`. -fn run(filename: &str, algorithm: &str) { +/// Runs every case in one `hmac_*_test.json` file through `HmacKey`. +fn run(filename: &str, algorithm: &str) { let Some(doc) = wycheproof_json(filename) else { return }; assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some(algorithm), "{filename}"); @@ -44,25 +45,25 @@ fn run(filename: &str, algorithm: &str) { }; // Some keys are shorter than the hash's security strength (128-bit keys for // HMAC-SHA-512, say); the key-strength policy is tested in `hmac_tests.rs`. - let key = KeyMaterial::::from_bytes_as_type( + let material = KeyMaterial::::from_bytes_as_type( &hex_field(test, "key", tc_id), KeyType::MACKey, ) .expect("a MAC key"); - let mac = || M::new_allow_weak_key(&key).expect("an HMAC instance"); + let key = HmacKey::::from_key_material_allow_weak(&material).expect("an HMAC key"); let ctx = format!("{filename} tcId {tc_id}"); - if tag_len == mac().output_len() { - assert_eq!(mac().verify(&msg, &tag), valid, "{ctx}: verify"); + if tag_len == key.create_generator().output_len() { + assert_eq!(key.verify(&msg, &tag), valid, "{ctx}: verify"); if valid { - assert_eq!(mac().mac(&msg), tag, "{ctx}: mac"); + assert_eq!(key.mac(&msg), tag, "{ctx}: mac"); } full += 1; } else { let mut out = vec![0u8; tag_len]; - assert_eq!(mac().mac_out(&msg, &mut out).expect("mac_out"), tag_len, "{ctx}"); + assert_eq!(key.mac_out(&msg, &mut out).expect("mac_out"), tag_len, "{ctx}"); assert_eq!(out == tag, valid, "{ctx}: truncated mac_out"); - assert!(!mac().verify(&msg, &tag), "{ctx}: verify takes only a full-length tag"); + assert!(!key.verify(&msg, &tag), "{ctx}: verify takes only a full-length tag"); truncated += 1; } invalid += usize::from(!valid); @@ -79,55 +80,55 @@ fn run(filename: &str, algorithm: &str) { #[test] fn wycheproof_hmac_sha224() { - run::("hmac_sha224_test.json", "HMACSHA224"); + run::("hmac_sha224_test.json", "HMACSHA224"); } #[test] fn wycheproof_hmac_sha256() { - run::("hmac_sha256_test.json", "HMACSHA256"); + run::("hmac_sha256_test.json", "HMACSHA256"); } #[test] fn wycheproof_hmac_sha384() { - run::("hmac_sha384_test.json", "HMACSHA384"); + run::("hmac_sha384_test.json", "HMACSHA384"); } #[test] fn wycheproof_hmac_sha512() { - run::("hmac_sha512_test.json", "HMACSHA512"); + run::("hmac_sha512_test.json", "HMACSHA512"); } #[test] fn wycheproof_hmac_sha512_224() { - run::("hmac_sha512_224_test.json", "HMACSHA512/224"); + run::("hmac_sha512_224_test.json", "HMACSHA512/224"); } #[test] fn wycheproof_hmac_sha512_256() { - run::("hmac_sha512_256_test.json", "HMACSHA512/256"); + run::("hmac_sha512_256_test.json", "HMACSHA512/256"); } #[test] fn wycheproof_hmac_sha3_224() { - run::("hmac_sha3_224_test.json", "HMACSHA3-224"); + run::("hmac_sha3_224_test.json", "HMACSHA3-224"); } #[test] fn wycheproof_hmac_sha3_256() { - run::("hmac_sha3_256_test.json", "HMACSHA3-256"); + run::("hmac_sha3_256_test.json", "HMACSHA3-256"); } #[test] fn wycheproof_hmac_sha3_384() { - run::("hmac_sha3_384_test.json", "HMACSHA3-384"); + run::("hmac_sha3_384_test.json", "HMACSHA3-384"); } #[test] fn wycheproof_hmac_sha3_512() { - run::("hmac_sha3_512_test.json", "HMACSHA3-512"); + run::("hmac_sha3_512_test.json", "HMACSHA3-512"); } #[test] fn wycheproof_hmac_sm3() { - run::("hmac_sm3_test.json", "HMACSM3"); + run::("hmac_sm3_test.json", "HMACSM3"); } diff --git a/crypto/sha2/benches/hmac_sha2_benches.rs b/crypto/sha2/benches/hmac_sha2_benches.rs index d8ccf63b5..5b526dd0b 100644 --- a/crypto/sha2/benches/hmac_sha2_benches.rs +++ b/crypto/sha2/benches/hmac_sha2_benches.rs @@ -1,7 +1,9 @@ use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterial512, KeyType}; -use bouncycastle_core::traits::{MAC, RNG}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::RNG; +use bouncycastle_hmac::HmacKey; use bouncycastle_rng as rng; -use bouncycastle_sha2::hmac::{HMAC_SHA256, HMAC_SHA512}; +use bouncycastle_sha2::{SHA256, SHA512}; use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; @@ -15,13 +17,14 @@ fn bench_hmac_sha256(c: &mut Criterion) { } let hmac_key = KeyMaterial256::from_bytes_as_type(&data_block[..32], KeyType::MACKey).unwrap(); + let key = HmacKey::::from_key_material(&hmac_key).unwrap(); let mut out = [0u8; 32]; - let mut group = c.benchmark_group("hmac::HMAC_SHA256::mac_out() -- 16x1024 one-shot"); + let mut group = c.benchmark_group("hmac::HmacKey::mac_out() -- 16x1024 one-shot"); group.throughput(Throughput::Bytes(big_data.len() as u64)); - group.bench_function(format!("{} bytes -- ::hashes()", big_data.len() as u64), |b| { + group.bench_function(format!("{} bytes -- key.mac_out()", big_data.len() as u64), |b| { b.iter(|| { - HMAC_SHA256::new(&hmac_key).unwrap().mac_out(black_box(&big_data), &mut out).unwrap(); + key.mac_out(black_box(&big_data), &mut out).unwrap(); black_box(&out); }) }); @@ -38,13 +41,14 @@ fn bench_hmac_sha512(c: &mut Criterion) { } let hmac_key = KeyMaterial512::from_bytes_as_type(&data_block[..64], KeyType::MACKey).unwrap(); + let key = HmacKey::::from_key_material(&hmac_key).unwrap(); let mut out = [0u8; 64]; - let mut group = c.benchmark_group("hmac::HMAC_SHA512::mac_out() -- 16x1024 one-shot"); + let mut group = c.benchmark_group("hmac::HmacKey::mac_out() -- 16x1024 one-shot"); group.throughput(Throughput::Bytes(big_data.len() as u64)); - group.bench_function(format!("{} bytes -- ::hashes()", big_data.len() as u64), |b| { + group.bench_function(format!("{} bytes -- key.mac_out()", big_data.len() as u64), |b| { b.iter(|| { - HMAC_SHA512::new(&hmac_key).unwrap().mac_out(black_box(&big_data), &mut out).unwrap(); + key.mac_out(black_box(&big_data), &mut out).unwrap(); black_box(&out); }) }); diff --git a/crypto/sha2/src/hkdf.rs b/crypto/sha2/src/hkdf.rs index cc7a7a4af..7d73b596a 100644 --- a/crypto/sha2/src/hkdf.rs +++ b/crypto/sha2/src/hkdf.rs @@ -213,8 +213,8 @@ //! does not create entropy: asking for 200 bytes of output from a 128-bit seed yields 200 bytes at //! a 128-bit security level, not a 1600-bit key. //! * RFC 5869 Section 3.1 recommends a random salt where one is available; SP 800-56Cr2 permits an -//! all-zero salt. An all-zero salt is not a [`KeyType::MACKey`], so it needs -//! `MAC::new_allow_weak_key` semantics -- which is exactly what the extract phase does internally. +//! all-zero salt. An all-zero salt is below any HMAC's strength, so it needs the HMAC key's +//! `from_key_material_allow_weak` -- which is exactly what the extract phase does internally. use crate::hmac::{ SUSPENDED_HMAC_SHA256_STATE_LEN, SUSPENDED_HMAC_SHA384_STATE_LEN, SUSPENDED_HMAC_SHA512_STATE_LEN, diff --git a/crypto/sha2/src/hmac.rs b/crypto/sha2/src/hmac.rs index f3e25b03c..94dc95e47 100644 --- a/crypto/sha2/src/hmac.rs +++ b/crypto/sha2/src/hmac.rs @@ -10,98 +10,88 @@ //! rather than as part of library's public API. //! //! A key of any length is accepted: per RFC 2104, a key longer than the hash's block is pre-hashed -//! to the output length. The key's two pad states are computed once, when an -//! [`HmacKey`](bouncycastle_hmac::HmacKey) is built or a worker is constructed from key material. +//! to the output length. The key's two pad states are computed once, when the [`HmacKey`] is built. //! //! # Usage //! -//! An HMAC object (and the [`MAC`] trait in general) is used in three phases: +//! An [`HmacKey`] is built once from key material and hands out [`MAC`] workers, one per message: //! -//! * The initialization phase where you specify the underlying hash function and the key material. -//! * The update phase where you feed in the content being MAC'd, either in one-shot or in chunks. -//! * The finalization phase where you either obtain the MAC value or verify an existing MAC value. -//! -//! The initialization phase is primarily performed via the [`MAC::new`] function which performs -//! checks on the provided key to ensure that it is of the correct type [`KeyType::MACKey`] and tagged -//! at the correct security level for the chosen hash function. In cases where you need to use HMAC -//! with an intentially week key (such as an all-zero salt), the alternative constructor -//! [`MAC::new_allow_weak_key`] can be used. -//! -//! The update phase supports streaming of the content via the repeated calls to the [`MAC::do_update`] function. -//! One-shot APIs are provided that combine the update and finalization phases into a single function call. +//! * [`FromAnyKeyMaterial::from_key_material`] checks that the material is a [`KeyType::MACKey`] +//! tagged at the security strength of the chosen hash. Where a protocol calls for a weak key +//! (an all-zero salt, say), [`FromAnyKeyMaterial::from_key_material_allow_weak`] skips the +//! strength check. +//! * [`MacGenerationKey::create_generator`] / [`MacVerificationKey::create_verifier`] start a +//! worker, whose [`MAC::do_update`] takes the content in chunks and whose [`MAC::do_final`] / +//! [`MAC::do_verify_final`] finish it. The key's own [`MacGenerationKey::mac`] and +//! [`MacVerificationKey::verify`] are the one-shots. //! //! # Usage Examples //! -//! ## Constructing an HMAC object +//! ## Building the key //! -//! Instantiation of an HMAC object is straightforward. A key of the right length for the chosen hash -//! can be generated with [`HMAC::keygen_from_rng`]: +//! A key of the right length for the chosen hash is drawn with [`FromAnyKeyMaterial::generate`]: //! //! ``` -//! use bouncycastle_core::key_material::KeyMaterial256; -//! use bouncycastle_core::traits::MAC; +//! use bouncycastle_core::symmetric_key::FromAnyKeyMaterial; +//! use bouncycastle_hmac::HmacKey; //! use bouncycastle_rng::HashDRBG_SHA256; -//! use bouncycastle_sha2::hmac::HMAC_SHA256; +//! use bouncycastle_sha2::SHA256; //! //! let mut rng = HashDRBG_SHA256::new_from_os(); -//! let key: KeyMaterial256 = HMAC_SHA256::keygen_from_rng(&mut rng) +//! let key = HmacKey::::generate(&mut rng) //! .expect("Will only fail if the system RNG can't start up."); -//! -//! let hmac = HMAC_SHA256::new(&key).expect( -//! "Should succeed because key is long enough and tagged KeyType::MACKey"); //! ``` //! -//! Alternatively, if you have key material from somewhere else, you can create the key manually, -//! like so: +//! Alternatively, if you have key material from somewhere else, you can build the key from it: //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::MAC; -//! use bouncycastle_sha2::hmac::HMAC_SHA256; +//! use bouncycastle_core::symmetric_key::FromAnyKeyMaterial; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sha2::SHA256; //! -//! let key = KeyMaterial256::from_bytes_as_type( +//! let key_material = KeyMaterial256::from_bytes_as_type( //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); //! -//! let hmac = HMAC_SHA256::new(&key).expect( +//! let key = HmacKey::::from_key_material(&key_material).expect( //! "Should succeed because key is long enough and tagged KeyType::MACKey"); //! ``` //! //! ## Computing a MAC //! -//! MAC functionality is accessed via the [`MAC`] trait. -//! -//! The simplest usage is via the one-shot functions. +//! The simplest usage is the key's one-shot: //! //! ``` -//! use bouncycastle_core::key_material::KeyMaterial256; -//! use bouncycastle_core::traits::MAC; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +//! use bouncycastle_hmac::HmacKey; //! use bouncycastle_rng::HashDRBG_SHA256; -//! use bouncycastle_sha2::hmac::HMAC_SHA256; +//! use bouncycastle_sha2::SHA256; //! //! let mut rng = HashDRBG_SHA256::new_from_os(); -//! let key: KeyMaterial256 = HMAC_SHA256::keygen_from_rng(&mut rng) +//! let key = HmacKey::::generate(&mut rng) //! .expect("Will only fail if the system RNG can't start up."); //! //! let data: &[u8] = b"Hello, world!"; -//! let hmac = HMAC_SHA256::new(&key).expect("Should succeed because key is long enough and tagged KeyType::MACKey"); -//! let output: Vec = hmac.mac(data); +//! let output: Vec = key.mac(data); //! ``` //! -//! More advanced usage will require creating an HMAC object to hold state between successive calls, -//! for example if input is received in chunks and not all available at the same time: +//! A worker holds state between successive calls, for example if input is received in chunks and +//! not all available at the same time: //! //! ``` -//! use bouncycastle_core::key_material::KeyMaterial256; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; //! use bouncycastle_core::traits::MAC; +//! use bouncycastle_hmac::HmacKey; //! use bouncycastle_rng::HashDRBG_SHA256; +//! use bouncycastle_sha2::SHA256; //! use bouncycastle_sha2::hmac::HMAC_SHA256; //! //! let mut rng = HashDRBG_SHA256::new_from_os(); -//! let key: KeyMaterial256 = HMAC_SHA256::keygen_from_rng(&mut rng) +//! let key = HmacKey::::generate(&mut rng) //! .expect("Will only fail if the system RNG can't start up."); //! -//! let mut hmac = HMAC_SHA256::new(&key).expect("Should succeed because key is long enough and tagged KeyType::MACKey"); +//! let mut hmac: HMAC_SHA256 = key.create_generator(); //! hmac.do_update(b"Hello,"); //! hmac.do_update(b" world!"); //! let output: Vec = hmac.do_final(); @@ -109,31 +99,30 @@ //! //! ## Verifying a MAC //! -//! The [`MAC`] trait also provides functions for MAC verification. The built-in verification -//! functions use constant-time comparisons and so are *strongly recommended* rather than -//! re-computing the MAC value and comparing it yourself. +//! The built-in verification functions use constant-time comparisons and so are *strongly +//! recommended* rather than re-computing the MAC value and comparing it yourself. //! -//! The simplest usage is via the one-shot functions. +//! The simplest usage is the key's one-shot: //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::MAC; -//! use bouncycastle_sha2::hmac::HMAC_SHA256; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacVerificationKey}; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sha2::SHA256; //! //! // For this example to work, we are hard-coding both the key and the MAC value that it generates //! // for this data. -//! let key = KeyMaterial256::from_bytes_as_type( +//! let key_material = KeyMaterial256::from_bytes_as_type( //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); +//! let key = HmacKey::::from_key_material(&key_material).unwrap(); //! //! let data: &[u8] = b"Hello, world!"; //! //! // .verify() returns a bool: true if the MAC is valid, false otherwise. -//! if HMAC_SHA256::new(&key).unwrap() -//! .verify(data, -//! b"\xa2\xd1\x2e\xcf\xfc\x41\xba\xf1\x23\xd6\x3e\x44\xfc\x27\x88\x90 -//! \x47\xcd\x08\xe7\x05\xd7\x0f\xa3\xb8\xaa\x8a\x5c\x18\x7c\x6c\xa9" -//! ) +//! if key.verify(data, +//! b"\xa2\xd1\x2e\xcf\xfc\x41\xba\xf1\x23\xd6\x3e\x44\xfc\x27\x88\x90 +//! \x47\xcd\x08\xe7\x05\xd7\x0f\xa3\xb8\xaa\x8a\x5c\x18\x7c\x6c\xa9") //! { //! println!("MAC is valid!"); //! } else { @@ -146,15 +135,18 @@ //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacVerificationKey}; //! use bouncycastle_core::traits::MAC; -//! use bouncycastle_sha2::hmac::HMAC_SHA256; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sha2::SHA256; //! //! // For this example to work, we are hard-coding both the key and the MAC value that it generates //! // for this data. -//! let key = KeyMaterial256::from_bytes_as_type( +//! let key_material = KeyMaterial256::from_bytes_as_type( //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); -//! let mut hmac = HMAC_SHA256::new(&key).unwrap(); +//! let key = HmacKey::::from_key_material(&key_material).unwrap(); +//! let mut hmac = key.create_verifier(); //! hmac.do_update(b"Hello,"); //! hmac.do_update(b" world!"); //! if hmac.do_verify_final(b"\xa2\xd1\x2e\xcf\xfc\x41\xba\xf1\x23\xd6\x3e\x44\xfc\x27\x88\x90\x47\xcd\x08\xe7\x05\xd7\x0f\xa3\xb8\xaa\x8a\x5c\x18\x7c\x6c\xa9" @@ -181,7 +173,10 @@ //! //! ```rust //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; //! use bouncycastle_core::traits::{MAC, SuspendableKeyed}; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sha2::SHA256; //! use bouncycastle_sha2::hmac::HMAC_SHA256; //! //! let msg_part1 = b"The quick brown fox"; @@ -191,7 +186,7 @@ //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); //! -//! let mut hmac = HMAC_SHA256::new(&key).unwrap(); +//! let mut hmac = HmacKey::::from_key_material(&key).unwrap().create_generator(); //! hmac.do_update(msg_part1); //! //! // suspend the in-progress mac (the key is NOT included in the serialized state) @@ -228,10 +223,11 @@ //! //! # 🚨 Security Considerations 🚨 //! -//! * The key must carry at least the security strength claimed by the HMAC, and [`MAC::new`] -//! enforces that. [`MAC::new_allow_weak_key`] deliberately skips the check; use it only where a -//! weak or all-zero key is called for by the protocol (an all-zero HKDF salt, for example), not to -//! silence an error. +//! * The key must carry at least the security strength claimed by the HMAC, and +//! [`FromAnyKeyMaterial::from_key_material`] enforces that. +//! [`FromAnyKeyMaterial::from_key_material_allow_weak`] deliberately skips the check; use it only +//! where a weak or all-zero key is called for by the protocol (an all-zero HKDF salt, for +//! example), not to silence an error. //! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself //! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how //! many leading bytes matched. @@ -254,9 +250,11 @@ use bouncycastle_hmac::{HMAC, HMACParams}; #[allow(unused_imports)] use bouncycastle_core::key_material::KeyType; #[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; +#[allow(unused_imports)] use bouncycastle_core::traits::{MAC, SuspendableKeyed}; #[allow(unused_imports)] -use bouncycastle_hmac::MIN_FIPS_DIGEST_LEN; +use bouncycastle_hmac::{HmacKey, MIN_FIPS_DIGEST_LEN}; /*** String constants ***/ /// diff --git a/crypto/sha3/benches/hmac_sha3_benches.rs b/crypto/sha3/benches/hmac_sha3_benches.rs index badb2b7cf..c99f0f251 100644 --- a/crypto/sha3/benches/hmac_sha3_benches.rs +++ b/crypto/sha3/benches/hmac_sha3_benches.rs @@ -1,7 +1,9 @@ use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterial512, KeyType}; -use bouncycastle_core::traits::{MAC, RNG}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::RNG; +use bouncycastle_hmac::HmacKey; use bouncycastle_rng as rng; -use bouncycastle_sha3::hmac::{HMAC_SHA3_256, HMAC_SHA3_512}; +use bouncycastle_sha3::{SHA3_256, SHA3_512}; use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; @@ -15,13 +17,14 @@ fn bench_hmac_sha3_256(c: &mut Criterion) { } let hmac_key = KeyMaterial256::from_bytes_as_type(&data_block[..32], KeyType::MACKey).unwrap(); + let key = HmacKey::::from_key_material(&hmac_key).unwrap(); let mut out = [0u8; 32]; - let mut group = c.benchmark_group("hmac::HMAC_SHA256::mac_out() -- 16x1024 one-shot"); + let mut group = c.benchmark_group("hmac::HmacKey::mac_out() -- 16x1024 one-shot"); group.throughput(Throughput::Bytes(big_data.len() as u64)); - group.bench_function(format!("{} bytes -- ::hashes()", big_data.len() as u64), |b| { + group.bench_function(format!("{} bytes -- key.mac_out()", big_data.len() as u64), |b| { b.iter(|| { - HMAC_SHA3_256::new(&hmac_key).unwrap().mac_out(black_box(&big_data), &mut out).unwrap(); + key.mac_out(black_box(&big_data), &mut out).unwrap(); black_box(&out); }) }); @@ -38,13 +41,14 @@ fn bench_hmac_sha3_512(c: &mut Criterion) { } let hmac_key = KeyMaterial512::from_bytes_as_type(&data_block[..64], KeyType::MACKey).unwrap(); + let key = HmacKey::::from_key_material(&hmac_key).unwrap(); let mut out = [0u8; 64]; - let mut group = c.benchmark_group("hmac::HMAC_SHA512::mac_out() -- 16x1024 one-shot"); + let mut group = c.benchmark_group("hmac::HmacKey::mac_out() -- 16x1024 one-shot"); group.throughput(Throughput::Bytes(big_data.len() as u64)); - group.bench_function(format!("{} bytes -- ::hashes()", big_data.len() as u64), |b| { + group.bench_function(format!("{} bytes -- key.mac_out()", big_data.len() as u64), |b| { b.iter(|| { - HMAC_SHA3_512::new(&hmac_key).unwrap().mac_out(black_box(&big_data), &mut out).unwrap(); + key.mac_out(black_box(&big_data), &mut out).unwrap(); black_box(&out); }) }); diff --git a/crypto/sha3/src/hmac.rs b/crypto/sha3/src/hmac.rs index 5a68bb7c9..7c2d0dca1 100644 --- a/crypto/sha3/src/hmac.rs +++ b/crypto/sha3/src/hmac.rs @@ -12,98 +12,88 @@ //! A key of any length is accepted: per RFC 2104, a key longer than the hash's block -- for SHA-3 //! the sponge *rate*, FIPS 202 Table 3's "Input block sizes for HMAC", which shrinks as the output //! size grows -- is pre-hashed to the output length. The key's two pad states are computed once, -//! when an [`HmacKey`](bouncycastle_hmac::HmacKey) is built or a worker is constructed from key -//! material. +//! when the [`HmacKey`] is built. //! //! # Usage //! -//! An HMAC object (and the [`MAC`] trait in general) is used in three phases: +//! An [`HmacKey`] is built once from key material and hands out [`MAC`] workers, one per message: //! -//! * The initialization phase where you specify the underlying hash function and the key material. -//! * The update phase where you feed in the content being MAC'd, either in one-shot or in chunks. -//! * The finalization phase where you either obtain the MAC value or verify an existing MAC value. -//! -//! The initialization phase is primarily performed via the [`MAC::new`] function which performs -//! checks on the provided key to ensure that it is of the correct type [`KeyType::MACKey`] and tagged -//! at the correct security level for the chosen hash function. In cases where you need to use HMAC -//! with an intentially week key (such as an all-zero salt), the alternative constructor -//! [`MAC::new_allow_weak_key`] can be used. -//! -//! The update phase supports streaming of the content via the repeated calls to the [`MAC::do_update`] function. -//! One-shot APIs are provided that combine the update and finalization phases into a single function call. +//! * [`FromAnyKeyMaterial::from_key_material`] checks that the material is a [`KeyType::MACKey`] +//! tagged at the security strength of the chosen hash. Where a protocol calls for a weak key +//! (an all-zero salt, say), [`FromAnyKeyMaterial::from_key_material_allow_weak`] skips the +//! strength check. +//! * [`MacGenerationKey::create_generator`] / [`MacVerificationKey::create_verifier`] start a +//! worker, whose [`MAC::do_update`] takes the content in chunks and whose [`MAC::do_final`] / +//! [`MAC::do_verify_final`] finish it. The key's own [`MacGenerationKey::mac`] and +//! [`MacVerificationKey::verify`] are the one-shots. //! //! # Usage Examples //! -//! ## Constructing an HMAC object +//! ## Building the key //! -//! Instantiation of an HMAC object is straightforward. A key of the right length for the chosen hash -//! can be generated with [`HMAC::keygen_from_rng`]: +//! A key of the right length for the chosen hash is drawn with [`FromAnyKeyMaterial::generate`]: //! //! ``` -//! use bouncycastle_core::key_material::KeyMaterial256; -//! use bouncycastle_core::traits::MAC; +//! use bouncycastle_core::symmetric_key::FromAnyKeyMaterial; +//! use bouncycastle_hmac::HmacKey; //! use bouncycastle_rng::DefaultRNG; -//! use bouncycastle_sha3::hmac::HMAC_SHA3_256; +//! use bouncycastle_sha3::SHA3_256; //! //! let mut rng = DefaultRNG::new_from_os(); -//! let key: KeyMaterial256 = HMAC_SHA3_256::keygen_from_rng(&mut rng) +//! let key = HmacKey::::generate(&mut rng) //! .expect("Will only fail if the system RNG can't start up."); -//! -//! let hmac = HMAC_SHA3_256::new(&key).expect( -//! "Should succeed because key is long enough and tagged KeyType::MACKey"); //! ``` //! -//! Alternatively, if you have key material from somewhere else, you can create the key manually, -//! like so: +//! Alternatively, if you have key material from somewhere else, you can build the key from it: //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::MAC; -//! use bouncycastle_sha3::hmac::HMAC_SHA3_256; +//! use bouncycastle_core::symmetric_key::FromAnyKeyMaterial; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sha3::SHA3_256; //! -//! let key = KeyMaterial256::from_bytes_as_type( +//! let key_material = KeyMaterial256::from_bytes_as_type( //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); //! -//! let hmac = HMAC_SHA3_256::new(&key).expect( +//! let key = HmacKey::::from_key_material(&key_material).expect( //! "Should succeed because key is long enough and tagged KeyType::MACKey"); //! ``` //! //! ## Computing a MAC //! -//! MAC functionality is accessed via the [`MAC`] trait. -//! -//! The simplest usage is via the one-shot functions. +//! The simplest usage is the key's one-shot: //! //! ``` -//! use bouncycastle_core::key_material::KeyMaterial256; -//! use bouncycastle_core::traits::MAC; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +//! use bouncycastle_hmac::HmacKey; //! use bouncycastle_rng::DefaultRNG; -//! use bouncycastle_sha3::hmac::HMAC_SHA3_256; +//! use bouncycastle_sha3::SHA3_256; //! //! let mut rng = DefaultRNG::new_from_os(); -//! let key: KeyMaterial256 = HMAC_SHA3_256::keygen_from_rng(&mut rng) +//! let key = HmacKey::::generate(&mut rng) //! .expect("Will only fail if the system RNG can't start up."); //! //! let data: &[u8] = b"Hello, world!"; -//! let hmac = HMAC_SHA3_256::new(&key).expect("Should succeed because key is long enough and tagged KeyType::MACKey"); -//! let output: Vec = hmac.mac(data); +//! let output: Vec = key.mac(data); //! ``` //! -//! More advanced usage will require creating an HMAC object to hold state between successive calls, -//! for example if input is received in chunks and not all available at the same time: +//! A worker holds state between successive calls, for example if input is received in chunks and +//! not all available at the same time: //! //! ``` -//! use bouncycastle_core::key_material::KeyMaterial256; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; //! use bouncycastle_core::traits::MAC; +//! use bouncycastle_hmac::HmacKey; //! use bouncycastle_rng::DefaultRNG; +//! use bouncycastle_sha3::SHA3_256; //! use bouncycastle_sha3::hmac::HMAC_SHA3_256; //! //! let mut rng = DefaultRNG::new_from_os(); -//! let key: KeyMaterial256 = HMAC_SHA3_256::keygen_from_rng(&mut rng) +//! let key = HmacKey::::generate(&mut rng) //! .expect("Will only fail if the system RNG can't start up."); //! -//! let mut hmac = HMAC_SHA3_256::new(&key).expect("Should succeed because key is long enough and tagged KeyType::MACKey"); +//! let mut hmac: HMAC_SHA3_256 = key.create_generator(); //! hmac.do_update(b"Hello,"); //! hmac.do_update(b" world!"); //! let output: Vec = hmac.do_final(); @@ -111,31 +101,30 @@ //! //! ## Verifying a MAC //! -//! The [`MAC`] trait also provides functions for MAC verification. The built-in verification -//! functions use constant-time comparisons and so are *strongly recommended* rather than -//! re-computing the MAC value and comparing it yourself. +//! The built-in verification functions use constant-time comparisons and so are *strongly +//! recommended* rather than re-computing the MAC value and comparing it yourself. //! -//! The simplest usage is via the one-shot functions. +//! The simplest usage is the key's one-shot: //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::MAC; -//! use bouncycastle_sha3::hmac::HMAC_SHA3_256; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacVerificationKey}; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sha3::SHA3_256; //! //! // For this example to work, we are hard-coding both the key and the MAC value that it generates //! // for this data. -//! let key = KeyMaterial256::from_bytes_as_type( +//! let key_material = KeyMaterial256::from_bytes_as_type( //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); +//! let key = HmacKey::::from_key_material(&key_material).unwrap(); //! //! let data: &[u8] = b"Hello, world!"; //! //! // .verify() returns a bool: true if the MAC is valid, false otherwise. -//! if HMAC_SHA3_256::new(&key).unwrap() -//! .verify(data, -//! b"\x9c\x49\x05\x83\xff\xf3\x59\x6a\x59\x01\x4a\x0d\x95\xb4\x64\x00 -//! \x7d\x5b\xb7\x40\xb3\x84\x20\x7a\x3c\x76\x76\xd8\xc9\x93\xda\xd7" -//! ) +//! if key.verify(data, +//! b"\x9c\x49\x05\x83\xff\xf3\x59\x6a\x59\x01\x4a\x0d\x95\xb4\x64\x00 +//! \x7d\x5b\xb7\x40\xb3\x84\x20\x7a\x3c\x76\x76\xd8\xc9\x93\xda\xd7") //! { //! println!("MAC is valid!"); //! } else { @@ -148,15 +137,18 @@ //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacVerificationKey}; //! use bouncycastle_core::traits::MAC; -//! use bouncycastle_sha3::hmac::HMAC_SHA3_256; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sha3::SHA3_256; //! //! // For this example to work, we are hard-coding both the key and the MAC value that it generates //! // for this data. -//! let key = KeyMaterial256::from_bytes_as_type( +//! let key_material = KeyMaterial256::from_bytes_as_type( //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); -//! let mut hmac = HMAC_SHA3_256::new(&key).unwrap(); +//! let key = HmacKey::::from_key_material(&key_material).unwrap(); +//! let mut hmac = key.create_verifier(); //! hmac.do_update(b"Hello,"); //! hmac.do_update(b" world!"); //! if hmac.do_verify_final(b"\x9c\x49\x05\x83\xff\xf3\x59\x6a\x59\x01\x4a\x0d\x95\xb4\x64\x00\x7d\x5b\xb7\x40\xb3\x84\x20\x7a\x3c\x76\x76\xd8\xc9\x93\xda\xd7" @@ -186,7 +178,10 @@ //! //! ```rust //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; //! use bouncycastle_core::traits::{MAC, SuspendableKeyed}; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sha3::SHA3_256; //! use bouncycastle_sha3::hmac::HMAC_SHA3_256; //! //! let msg_part1 = b"The quick brown fox"; @@ -196,7 +191,7 @@ //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); //! -//! let mut hmac = HMAC_SHA3_256::new(&key).unwrap(); +//! let mut hmac = HmacKey::::from_key_material(&key).unwrap().create_generator(); //! hmac.do_update(msg_part1); //! //! // suspend the in-progress mac (the key is NOT included in the serialized state) @@ -230,9 +225,10 @@ //! //! # 🚨 Security Considerations 🚨 //! -//! * The key must carry at least the security strength claimed by the HMAC, and [`MAC::new`] -//! enforces that. [`MAC::new_allow_weak_key`] deliberately skips the check; use it only where a -//! weak or all-zero key is called for by the protocol, not to silence an error. +//! * The key must carry at least the security strength claimed by the HMAC, and +//! [`FromAnyKeyMaterial::from_key_material`] enforces that. +//! [`FromAnyKeyMaterial::from_key_material_allow_weak`] deliberately skips the check; use it only +//! where a weak or all-zero key is called for by the protocol, not to silence an error. //! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself //! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how //! many leading bytes matched. @@ -259,9 +255,11 @@ use bouncycastle_hmac::{HMAC, HMACParams}; #[allow(unused_imports)] use bouncycastle_core::key_material::KeyType; #[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; +#[allow(unused_imports)] use bouncycastle_core::traits::{MAC, SuspendableKeyed}; #[allow(unused_imports)] -use bouncycastle_hmac::MIN_FIPS_DIGEST_LEN; +use bouncycastle_hmac::{HmacKey, MIN_FIPS_DIGEST_LEN}; /*** String constants ***/ /// diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index 57d8eec28..6c6b5bec0 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -280,20 +280,6 @@ impl Algorithm for KMACInternal { } impl KMACInternal { - /// A worker for one message under a key built for this message alone: what - /// [`KMACKeyInternal::with_params`] does once, done per message. - pub(crate) fn new_with_params( - key: &dyn KeyMaterialTrait, - customization: &[u8], - output_len: usize, - allow_weak_key: bool, - ) -> Result { - Ok(KMACKeyInternal::::with_params_checked( - key, customization, output_len, allow_weak_key, - )? - .create_generator()) - } - /// Absorbs `right_encode(value)`, the length binding of Sec 4.3 step 1. fn absorb_right_encode(&mut self, value: u64) { let (buf, len) = right_encode(value); @@ -345,16 +331,6 @@ impl Suspendable for KMACInternal } impl MAC for KMACInternal { - /// A KMAC with no customization string, producing the nominal output length -- 32 bytes for - /// KMAC128 and 64 for KMAC256. [`KMACKeyInternal::with_params`] chooses either. - fn new(key: &impl KeyMaterialTrait) -> Result { - Ok(KMACKeyInternal::::from_key_material(key)?.create_generator()) - } - - fn new_allow_weak_key(key: &impl KeyMaterialTrait) -> Result { - Ok(KMACKeyInternal::::from_key_material_allow_weak(key)?.create_generator()) - } - fn output_len(&self) -> usize { self.output_len } @@ -468,7 +444,8 @@ impl KMACXOFInternal { ) -> Result { // The key binding is identical to KMAC's; only the length encoding differs, and that is // applied when output begins. - let kmac = KMACInternal::::new_with_params(key, customization, 0, allow_weak_key)?; + let kmac = + KMACKeyInternal::::with_params_checked(key, customization, 0, allow_weak_key)?; Ok(Self { cshake: kmac.cshake, strength: kmac.strength }) } } diff --git a/crypto/sm3/benches/hmac_sm3_benches.rs b/crypto/sm3/benches/hmac_sm3_benches.rs index 97cabfbb2..95e7bc50c 100644 --- a/crypto/sm3/benches/hmac_sm3_benches.rs +++ b/crypto/sm3/benches/hmac_sm3_benches.rs @@ -1,7 +1,9 @@ use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -use bouncycastle_core::traits::{MAC, RNG}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::RNG; +use bouncycastle_hmac::HmacKey; use bouncycastle_rng as rng; -use bouncycastle_sm3::hmac::HMAC_SM3; +use bouncycastle_sm3::SM3; use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; @@ -15,13 +17,14 @@ fn bench_hmac_sm3(c: &mut Criterion) { } let hmac_key = KeyMaterial256::from_bytes_as_type(&data_block[..32], KeyType::MACKey).unwrap(); + let key = HmacKey::::from_key_material(&hmac_key).unwrap(); let mut out = [0u8; 32]; - let mut group = c.benchmark_group("hmac::HMAC_SM3::mac_out() -- 16x1024 one-shot"); + let mut group = c.benchmark_group("hmac::HmacKey::mac_out() -- 16x1024 one-shot"); group.throughput(Throughput::Bytes(big_data.len() as u64)); - group.bench_function(format!("{} bytes -- ::hashes()", big_data.len() as u64), |b| { + group.bench_function(format!("{} bytes -- key.mac_out()", big_data.len() as u64), |b| { b.iter(|| { - HMAC_SM3::new(&hmac_key).unwrap().mac_out(black_box(&big_data), &mut out).unwrap(); + key.mac_out(black_box(&big_data), &mut out).unwrap(); black_box(&out); }) }); diff --git a/crypto/sm3/src/hmac.rs b/crypto/sm3/src/hmac.rs index 0a1dd1b59..559ebacba 100644 --- a/crypto/sm3/src/hmac.rs +++ b/crypto/sm3/src/hmac.rs @@ -11,18 +11,19 @@ //! //! A key of any length is accepted: per RFC 2104, a key longer than the hash's block -- the 512-bit //! block GB/T 32905-2016 s. 5.2 pads to -- is pre-hashed to the output length. The key's two pad -//! states are computed once, when an [`HmacKey`](bouncycastle_hmac::HmacKey) is built or a worker -//! is constructed from key material. +//! states are computed once, when the [`HmacKey`] is built. //! //! # Usage Examples //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::MAC; -//! use bouncycastle_sm3::hmac::HMAC_SM3; +//! use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +//! use bouncycastle_hmac::HmacKey; +//! use bouncycastle_sm3::SM3; //! -//! let key = KeyMaterial256::from_bytes_as_type(&[0x0b; 32], KeyType::MACKey).unwrap(); -//! let tag = HMAC_SM3::new(&key).unwrap().mac(b"Hi There"); +//! let key_material = KeyMaterial256::from_bytes_as_type(&[0x0b; 32], KeyType::MACKey).unwrap(); +//! let key = HmacKey::::from_key_material(&key_material).unwrap(); +//! let tag = key.mac(b"Hi There"); //! assert_eq!(tag.len(), 32); //! ``` //! @@ -52,7 +53,7 @@ use bouncycastle_core::key_material::KeyType; #[allow(unused_imports)] use bouncycastle_core::traits::MAC; #[allow(unused_imports)] -use bouncycastle_hmac::MIN_FIPS_DIGEST_LEN; +use bouncycastle_hmac::{HmacKey, MIN_FIPS_DIGEST_LEN}; /*** end of doc-only imports ***/ /*** String constants ***/ diff --git a/mem_usage_benches/src/bench_ascon_mem_usage.rs b/mem_usage_benches/src/bench_ascon_mem_usage.rs index a32da9ab0..b8912b5b4 100644 --- a/mem_usage_benches/src/bench_ascon_mem_usage.rs +++ b/mem_usage_benches/src/bench_ascon_mem_usage.rs @@ -324,7 +324,7 @@ fn bench_aead_encrypt_update_aad() { measure(move || { let mut enc = enc; let op = black_box( - >::do_update_aad + >::do_update_aad as fn(&mut Enc, &[u8]) -> Result<(), CipherErr>, ); op(&mut enc, &aad).unwrap(); @@ -341,7 +341,7 @@ fn bench_aead_encrypt_out() { let mut enc = enc; let mut ct = [0u8; MSG_LEN]; let op = black_box( - >::do_encrypt_out + >::do_encrypt_out as fn(&mut Enc, &[u8], &mut [u8]) -> Result, ); let n = op(&mut enc, &pt, &mut ct).unwrap(); @@ -356,7 +356,7 @@ fn bench_aead_encrypt_final() { let enc = black_box(encryptor_after_data()); measure(move || { let op = black_box( - >::do_encrypt_final + >::do_encrypt_final as fn(Enc) -> Result<([u8; TAG_LEN], usize), CipherErr>, ); let (tag, n) = op(enc).unwrap(); @@ -371,7 +371,7 @@ fn bench_aead_encrypt_final_detached() { measure(move || { let mut last = [0u8; TAG_LEN]; let op = black_box( - >::do_encrypt_final_detached_out + >::do_encrypt_final_detached_out as fn(Enc, &mut [u8; TAG_LEN]) -> Result<(usize, [u8; TAG_LEN]), CipherErr>, ); let (_, tag) = op(enc, &mut last).unwrap(); @@ -427,7 +427,7 @@ fn bench_aead_decrypt_update_aad() { measure(move || { let mut dec = dec; let op = black_box( - >::do_update_aad + >::do_update_aad as fn(&mut Dec, &[u8]) -> Result<(), CipherErr>, ); op(&mut dec, &aad).unwrap(); @@ -446,7 +446,7 @@ fn bench_aead_decrypt_out() { let mut dec = dec; let mut pt = [0u8; SEALED_LEN]; let op = black_box( - >::do_decrypt_out + >::do_decrypt_out as fn(&mut Dec, &[u8], &mut [u8]) -> Result, ); let n = op(&mut dec, &ct, &mut pt).unwrap(); @@ -461,7 +461,7 @@ fn bench_aead_decrypt_final() { let dec = black_box(decryptor_holding_tag()); measure(move || { let op = black_box( - >::do_decrypt_final + >::do_decrypt_final as fn(Dec) -> Result<([u8; TAG_LEN], usize), CipherErr>, ); let (_, n) = op(dec).unwrap(); @@ -478,7 +478,7 @@ fn bench_aead_decrypt_final_detached() { measure(move || { let mut last = [0u8; TAG_LEN]; let op = black_box( - >::do_decrypt_final_detached_out + >::do_decrypt_final_detached_out as fn(Dec, &[u8; TAG_LEN], &mut [u8; TAG_LEN]) -> Result, ); let n = op(dec, &tag, &mut last).unwrap(); From 8db1e8c4c993416eaf3c25cf4f45e6edd1ea4b32 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 17:04:51 +1100 Subject: [PATCH 14/54] core, cipher: CCM_Packet names the fixed-frame CCM pair, and the declared-length side loses "packet" (prototype) The fixed-frame mode type is CCM_Packet, was CCMFrame, spelled as the spec spells it with #[allow(non_camel_case_types)] like Ascon_AEAD128. Its workers are CcmPacketEncryptor / CcmPacketDecryptor, taking the names from the any-length pair, which becomes CcmEncryptor / CcmDecryptor over the crate-private engine, now Ccm. The core traits that pair implements go from AeadPacket{Encrypt,Decrypt}Mode and AeadPacket{Encryption,Decryption}Key to AeadDeclared*, after the lengths they take per message. The test and bench aliases (Frame, ToyCcmFrame, Aes128CcmFrameKey) follow as Packet, ToyCcmPacket and Aes128CcmPacketKey, and "packet" in the docs, test names and the CLI bound follows the same split. The GCM packet bench keeps its word, which is the ordinary one. Assisted-by: Claude:claude-mythos-5-1 --- CLAUDE.md | 6 +- cli/src/aes_ccm_cmd.rs | 8 +- crypto/aes/src/ccm.rs | 38 ++++----- crypto/aes/tests/ccm_bc-test-data.rs | 4 +- crypto/aes/tests/ccm_key_tests.rs | 22 ++--- crypto/aes/tests/ccm_wycheproof.rs | 2 +- crypto/aes/tests/mode_key_tests.rs | 12 +-- crypto/aes/tests/sp800_38c_tests.rs | 88 +++++++++++--------- crypto/cipher/src/modes/mod.rs | 31 +++---- crypto/cipher/tests/ccm_suspend_tests.rs | 12 +-- crypto/cipher/tests/common/toy_key.rs | 12 +-- crypto/cipher/tests/modes/alg_name_tests.rs | 10 +-- crypto/cipher/tests/modes/ccm_tests.rs | 24 +++--- crypto/cipher/tests/suspend_tests.rs | 17 ++-- crypto/core/src/hazmat/mod.rs | 2 +- crypto/core/src/hazmat/mode.rs | 10 +-- crypto/core/src/provided.rs | 2 +- crypto/core/src/symmetric_key.rs | 10 +-- mem_usage_benches/src/bench_ccm_mem_usage.rs | 49 ++++++----- 19 files changed, 191 insertions(+), 168 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 35484c1f9..4073e3590 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -196,7 +196,7 @@ taught, as mechanics to apply next time a cipher type is shaped or reshaped: a CLI path, run it against a known-answer vector (`--nonce` on `ascon-aead128` reproduces the LWC KAT tag). - **A rename stays in the crate it was asked for.** `do_*_update` to `do_*_inplace` was done for - Ascon; the CCM packet type still has its namesake. Note the follow-up rather than widening the + Ascon; the CCM type still has its namesake. Note the follow-up rather than widening the change. ## Lessons from the key-object rework (October 2026) @@ -212,9 +212,9 @@ What that taught, as mechanics for the next trait that sheds a constructor: a worker trait. - **Mode types carry the role.** A key is `AESKey`; which role pairs it implements is decided by `M`'s mode-trait impls in `core::hazmat::mode` (`SymmetricEncryptMode`, - `BlockEncryptMode`, `EcbEncryptMode`, `AeadEncryptMode`, `AeadPacketEncryptMode`, `MacMode`). + `BlockEncryptMode`, `EcbEncryptMode`, `AeadEncryptMode`, `AeadDeclaredEncryptMode`, `MacMode`). Padding and frame parameters live in the mode type (`CBC`, `CBC`, - `ECB`, `CTR<12>`, `CCMFrame<12, 16, 64, 1024>`), not on a worker alias. + `ECB`, `CTR<12>`, `CCM_Packet<12, 16, 64, 1024>`), not on a worker alias. - **A key that needs the permutation's key length cannot be generic over it.** `Mode

` cannot carry `KEY_LEN` while `ElectronicCodeBook` does, so the GCM, GMAC and CMAC mode impls are written per AES key length; the cipher crate's tests use local `ToyGCM` / diff --git a/cli/src/aes_ccm_cmd.rs b/cli/src/aes_ccm_cmd.rs index 94a140a90..6e4394ffd 100644 --- a/cli/src/aes_ccm_cmd.rs +++ b/cli/src/aes_ccm_cmd.rs @@ -20,7 +20,7 @@ //! `ascon-aead128`, are the streaming alternatives. //! //! The AAD is read whole too: the key's one-shot takes it as a slice. (A.2.2 needs its length -//! before its first byte, so streaming it would need the declared-length constructor the packet +//! before its first byte, so streaming it would need the declared-length constructor the declared-length //! type keeps for itself.) //! //! # The nonce is supplied, not generated @@ -53,7 +53,7 @@ use bouncycastle::cipher::modes::CCM; use bouncycastle::core::errors::SymmetricCipherError; use bouncycastle::core::key_material::KeyMaterial; use bouncycastle::core::symmetric_key::{ - AeadPacketDecryptionKey, AeadPacketEncryptionKey, FromKeyMaterial, + AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, FromKeyMaterial, }; use bouncycastle::hex; @@ -68,8 +68,8 @@ use crate::helpers::block_mode_helpers::{CipherDirection, load_key}; trait CcmKeys { /// The key type at nonce length `N` and tag length `T`. type Key: FromKeyMaterial - + AeadPacketEncryptionKey - + AeadPacketDecryptionKey; + + AeadDeclaredEncryptionKey + + AeadDeclaredDecryptionKey; } struct Aes128; diff --git a/crypto/aes/src/ccm.rs b/crypto/aes/src/ccm.rs index 4e920f01e..79b71000e 100644 --- a/crypto/aes/src/ccm.rs +++ b/crypto/aes/src/ccm.rs @@ -10,14 +10,14 @@ //! `create_encryptor` is handed no lengths, so [`AES_CCM_128`] and friends fix the payload length //! in the type as the const parameter `DATA_LEN` -- the fixed frame of a packet protocol -- hold //! back nothing but up to `AAD_LEN` bytes of AAD, and refuse any other amount of payload. They -//! come from a key, [`AES128Key`]`>` and friends, +//! come from a key, [`AES128Key`]`>` and friends, //! through the [`AeadEncryptionKey`] / [`AeadDecryptionKey`] pair, whose one-shots take the //! frame length too. The nonce is drawn from the caller's RNG unless supplied through the //! `_nonce` forms, which CCM permits because it requires the nonce to be unique but not random //! (Sec 5.3), so a caller with a counter can do better than a draw from a DRBG. //! //! A message of any length goes through the other key, [`AES128Key`]`>` -//! and friends: its [`AeadPacketEncryptionKey`] / [`AeadPacketDecryptionKey`] constructors take +//! and friends: its [`AeadDeclaredEncryptionKey`] / [`AeadDeclaredDecryptionKey`] constructors take //! the AAD and payload lengths, so its workers hold nothing back, and its in-place one-shots //! take any length with no second buffer. //! @@ -43,26 +43,26 @@ //! //! ```text //! // 12-byte nonce, 16-byte tag, up to 64 bytes of AAD, 2 KiB frames -//! AES128Key> +//! AES128Key> //! ``` //! //! Though some alternative choices do exist, for example: //! ```text //! // IEEE 802.11 CCMP's 13 byte nonce and 8 byte tag -//! AES128Key> +//! AES128Key> //! ``` //! //! # Usage Examples //! //! ## Fixed frames, through the generic AEAD pair //! -//! For code written against [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], the `CCMFrame` +//! For code written against [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], the `CCM_Packet` //! key draws the nonce and returns it. Its streaming methods and one-shots take exactly //! `DATA_LEN` bytes of payload: //! //! ``` //! use bouncycastle_aes::AES128Key; -//! use bouncycastle_cipher::modes::CCMFrame; +//! use bouncycastle_cipher::modes::CCM_Packet; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; //! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; @@ -73,7 +73,7 @@ //! .expect("a 16-byte symmetric cipher key"); //! // Up to 64 bytes of AAD, and frames of exactly 2 KiB -- comfortably above an 802.11 frame, //! // the packet size CCM was designed for. -//! let key = AES128Key::>::from_key_material(&material) +//! let key = AES128Key::>::from_key_material(&material) //! .expect("a valid AES-128 key"); //! //! let frame = [0x5Au8; 2048]; @@ -109,7 +109,7 @@ //! use bouncycastle_aes::{AES256Key, CCM_NONCE_LEN, CCM_TAG_LEN}; //! use bouncycastle_cipher::modes::CCM; //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::symmetric_key::{AeadPacketDecryptionKey, AeadPacketEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_core::symmetric_key::{AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, FromKeyMaterial}; //! //! let material = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); @@ -144,7 +144,7 @@ //! use bouncycastle_aes::AES128Key; //! use bouncycastle_cipher::modes::CCM; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::symmetric_key::{AeadPacketDecryptionKey, AeadPacketEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_core::symmetric_key::{AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, FromKeyMaterial}; //! //! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); @@ -190,7 +190,7 @@ use crate::AES_BLOCK_LEN; use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_cipher::Direction; -use bouncycastle_cipher::modes::{CcmDecryptor, CcmEncryptor}; +use bouncycastle_cipher::modes::{CcmPacketDecryptor, CcmPacketEncryptor}; // Imports needed for docs #[allow(unused_imports)] @@ -199,7 +199,7 @@ use crate::AES128Key; use bouncycastle_cipher::{Decrypting, Encrypting}; #[allow(unused_imports)] use bouncycastle_core::symmetric_key::{ - AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, + AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, AeadDecryptionKey, AeadEncryptionKey, }; #[allow(unused_imports)] use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; @@ -219,11 +219,11 @@ pub const CCM_TAG_LEN: usize = 16; /// /// This is the fixed-frame pair, for code written against the generic AEAD traits: the streaming /// methods accept exactly `DATA_LEN` bytes of payload and up to `AAD_LEN` of AAD. It comes from -/// an [`AES128Key`]`>`, whose `create_encryptor` +/// an [`AES128Key`]`>`, whose `create_encryptor` /// draws the nonce and whose `create_encryptor_nonce` and `_nonce` one-shots take the caller's; /// a drawn nonce needs `NONCE_LEN` of at least 12, a supplied one any A.1 length. `NONCE_LEN` must be 7..=13 /// and `TAG_LEN` one of 4, 6, 8, 10, 12, 14, 16 (A.1); anything else is a compile error. Use -/// [`CCM_NONCE_LEN`] and [`CCM_TAG_LEN`] if you have no reason to choose. See [`CcmEncryptor`] +/// [`CCM_NONCE_LEN`] and [`CCM_TAG_LEN`] if you have no reason to choose. See [`CcmPacketEncryptor`] /// for the rules. #[allow(non_camel_case_types)] pub type AES_CCM_128< @@ -233,8 +233,8 @@ pub type AES_CCM_128< const AAD_LEN: usize, const DATA_LEN: usize, > =

::Select< - CcmEncryptor, - CcmDecryptor, + CcmPacketEncryptor, + CcmPacketDecryptor, >; /// AES-192 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`. See [`AES_CCM_128`]. @@ -246,8 +246,8 @@ pub type AES_CCM_192< const AAD_LEN: usize, const DATA_LEN: usize, > = ::Select< - CcmEncryptor, - CcmDecryptor, + CcmPacketEncryptor, + CcmPacketDecryptor, >; /// AES-256 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`. See [`AES_CCM_128`]. @@ -259,6 +259,6 @@ pub type AES_CCM_256< const AAD_LEN: usize, const DATA_LEN: usize, > = ::Select< - CcmEncryptor, - CcmDecryptor, + CcmPacketEncryptor, + CcmPacketDecryptor, >; diff --git a/crypto/aes/tests/ccm_bc-test-data.rs b/crypto/aes/tests/ccm_bc-test-data.rs index f7a98dfb7..bd5e705c6 100644 --- a/crypto/aes/tests/ccm_bc-test-data.rs +++ b/crypto/aes/tests/ccm_bc-test-data.rs @@ -48,7 +48,9 @@ use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_cipher::modes::{CCM, ModeNames}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::{AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook}; +use bouncycastle_core::hazmat::{ + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, ElectronicCodeBook, +}; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; use std::collections::BTreeMap; diff --git a/crypto/aes/tests/ccm_key_tests.rs b/crypto/aes/tests/ccm_key_tests.rs index 008edfd50..c36fbb5e4 100644 --- a/crypto/aes/tests/ccm_key_tests.rs +++ b/crypto/aes/tests/ccm_key_tests.rs @@ -1,4 +1,4 @@ -//! The mode-bound CCM key: the packet role traits, whose constructors take the AAD and payload +//! The mode-bound CCM key: the declared-length role traits, whose constructors take the AAD and payload //! lengths, their one-shots at any length, and agreement with the generic-trait pair. //! //! Only the worker traits are in scope here, since the pair's cipher-trait methods share their @@ -8,10 +8,10 @@ use bouncycastle_aes::hazmat::AES128Internal; use bouncycastle_aes::{AES128Key, AES192Key}; use bouncycastle_cipher::modes::CCM; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::{AeadPacketEncryptMode, ElectronicCodeBook}; +use bouncycastle_core::hazmat::{AeadDeclaredEncryptMode, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{ - AeadPacketDecryptionKey, AeadPacketEncryptionKey, FromKeyMaterial, + AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, FromKeyMaterial, }; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, @@ -190,17 +190,17 @@ fn a_short_nonce_is_reachable_through_the_supplied_nonce_forms() { assert_eq!(buf, msg); } -/// The workers are the mode's packet state, 264/296/328 B for AES-128/192/256, with the +/// The workers are the mode's `Ccm` state, 264/296/328 B for AES-128/192/256, with the /// decryptor adding the tag it holds back and that tag's length; the aes docs quote these. #[test] fn worker_sizes_match_the_documented_memory_table() { use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; - use bouncycastle_cipher::modes::{CcmPacketDecryptor, CcmPacketEncryptor}; + use bouncycastle_cipher::modes::{CcmDecryptor, CcmEncryptor}; use core::mem::size_of; - assert_eq!(size_of::>(), 264); - assert_eq!(size_of::>(), 296); - assert_eq!(size_of::>(), 328); - assert_eq!(size_of::>(), 288); - assert_eq!(size_of::>(), 320); - assert_eq!(size_of::>(), 352); + assert_eq!(size_of::>(), 264); + assert_eq!(size_of::>(), 296); + assert_eq!(size_of::>(), 328); + assert_eq!(size_of::>(), 288); + assert_eq!(size_of::>(), 320); + assert_eq!(size_of::>(), 352); } diff --git a/crypto/aes/tests/ccm_wycheproof.rs b/crypto/aes/tests/ccm_wycheproof.rs index f44694edf..1c12afd6a 100644 --- a/crypto/aes/tests/ccm_wycheproof.rs +++ b/crypto/aes/tests/ccm_wycheproof.rs @@ -37,7 +37,7 @@ use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_cipher::modes::{CCM, ModeNames}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ - AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook, do_hazardous_operations, + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, ElectronicCodeBook, do_hazardous_operations, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; diff --git a/crypto/aes/tests/mode_key_tests.rs b/crypto/aes/tests/mode_key_tests.rs index f007d5e24..99ea2f9c5 100644 --- a/crypto/aes/tests/mode_key_tests.rs +++ b/crypto/aes/tests/mode_key_tests.rs @@ -8,7 +8,7 @@ use bouncycastle_aes::hazmat::{AES128Internal, AES256Internal}; use bouncycastle_aes::{AES128Key, AES256Key}; use bouncycastle_cipher::modes::hazmat::ecb::ECB; -use bouncycastle_cipher::modes::{Blocks, CBC, CCMFrame, CFB, CFB8, CTR, ModeNames}; +use bouncycastle_cipher::modes::{Blocks, CBC, CCM_Packet, CFB, CFB8, CTR, ModeNames}; use bouncycastle_cipher::padding::{NoPadding, PKCS7}; use bouncycastle_core::hazmat::{EcbEncryptionKey, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; @@ -28,7 +28,7 @@ use bouncycastle_core_test_framework::symmetric_ciphers::{ use bouncycastle_rng::HashDRBG_SHA512; /// A 32-byte frame for the fixed-frame CCM key, within the framework's seed buffer. -type Frame = CCMFrame<12, 16, 64, 32>; +type Packet = CCM_Packet<12, 16, 64, 32>; fn material() -> KeyMaterial { let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); @@ -71,7 +71,7 @@ fn every_mode_key_enforces_the_material_policy() { construction_suite::(AES128Internal::CFB_ALG_NAME, AES256Internal::CFB_ALG_NAME); construction_suite::(AES128Internal::CFB8_ALG_NAME, AES256Internal::CFB8_ALG_NAME); construction_suite::(AES128Internal::CTR_ALG_NAME, AES256Internal::CTR_ALG_NAME); - construction_suite::(AES128Internal::CCM_ALG_NAME, AES256Internal::CCM_ALG_NAME); + construction_suite::(AES128Internal::CCM_ALG_NAME, AES256Internal::CCM_ALG_NAME); } #[test] @@ -124,8 +124,8 @@ fn stream_mode_keys_pass_the_stream_cipher_framework() { fn ccm_frame_keys_pass_the_aead_framework() { let mut framework = TestFrameworkAEADCipher::new(); framework.fixed_message_len = Some(32); - framework.test_key::<16, 12, 16, 16, _>(&key128::(), &mut rng()); - framework.test_key::<32, 12, 16, 16, _>(&key256::(), &mut rng()); + framework.test_key::<16, 12, 16, 16, _>(&key128::(), &mut rng()); + framework.test_key::<32, 12, 16, 16, _>(&key256::(), &mut rng()); } /// ECB has no init data, so the `ECB` key makes its workers with no RNG and nothing to @@ -136,7 +136,7 @@ fn ecb_keys_take_no_init_data() { let key = key128::>(); let mut padded_hello = *b"hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"; let mut enc = key.create_encryptor(); - enc.do_encrypt_inplace(&mut padded_hello).unwrap(); + enc.do_encrypt_blocks_inplace(core::slice::from_mut(&mut padded_hello)).unwrap(); let mut again = *b"hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b"; assert_eq!(key.encrypt_inplace(&mut again).unwrap(), 16); assert_eq!(again, padded_hello, "the worker and the one-shot are the same codebook"); diff --git a/crypto/aes/tests/sp800_38c_tests.rs b/crypto/aes/tests/sp800_38c_tests.rs index 307eca370..469b66b82 100644 --- a/crypto/aes/tests/sp800_38c_tests.rs +++ b/crypto/aes/tests/sp800_38c_tests.rs @@ -19,13 +19,17 @@ use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; -use bouncycastle_cipher::modes::{CCM, CCMFrame, CcmDecryptor, CcmEncryptor, ModeNames}; +use bouncycastle_cipher::modes::{ + CCM, CCM_Packet, CcmPacketDecryptor, CcmPacketEncryptor, ModeNames, +}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::{AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook}; +use bouncycastle_core::hazmat::{ + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, ElectronicCodeBook, +}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ - AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, + AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial, }; use bouncycastle_core::traits::{ @@ -47,11 +51,11 @@ fn material(hex_key: &str) -> KeyMaterial { .expect("a symmetric cipher key") } -/// The appendix key bound to the fixed-frame pair: its streams are [`CcmEncryptor`] / -/// [`CcmDecryptor`] at `(NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN)`, and its one-shots take exactly +/// The appendix key bound to the fixed-frame pair: its streams are [`CcmPacketEncryptor`] / +/// [`CcmPacketDecryptor`] at `(NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN)`, and its one-shots take exactly /// `DATA_LEN` bytes. type FrameKey = - AES128Key>; + AES128Key>; fn frame_key() -> FrameKey { @@ -266,8 +270,8 @@ fn framework(data_len: usize) -> TestFrameworkAEADCipher { } /// The whole [`AeadEncryptionKey`] / [`AeadDecryptionKey`] contract, through the shared -/// framework, for a key bound to [`CCMFrame`], whose streams are the fixed-frame -/// [`CcmEncryptor`] / [`CcmDecryptor`] pair. +/// framework, for a key bound to [`CCM_Packet`], whose streams are the fixed-frame +/// [`CcmPacketEncryptor`] / [`CcmPacketDecryptor`] pair. /// /// `DATA_LEN` is 240, well above the few multiples of the tag the suite's one-shots try, so the /// one-shots and the streaming methods are exercised at exactly it and refused at every other @@ -283,11 +287,11 @@ fn framework_streaming_contract() { fn framework_streaming_contract_other_parameter_sets() { let mut rng = os_rng(); framework(240).test_key::<24, 12, 16, 16, _>( - &AES192Key::>::generate(&mut rng).expect("a generated key"), + &AES192Key::>::generate(&mut rng).expect("a generated key"), &mut rng, ); framework(240).test_key::<32, 12, 16, 16, _>( - &AES256Key::>::generate(&mut rng).expect("a generated key"), + &AES256Key::>::generate(&mut rng).expect("a generated key"), &mut rng, ); // A 13-byte nonce (q = 2) with an 8-byte tag: the parameterization IEEE 802.11 CCMP uses, and @@ -620,15 +624,15 @@ fn the_decryptor_releases_the_payload_and_holds_back_only_the_tag() { } /// The frame key's one-shots are written over its streams, so `DATA_LEN` and `AAD_LEN` bind -/// them: at the frame's length, with AAD at and under the capacity, they agree with the packet +/// them: at the frame's length, with AAD at and under the capacity, they agree with the declared-length /// key's in-place one-shots byte for byte, on every form -- detached and inline, drawing and /// supplied-nonce, with and without AAD, and the three decrypting forms -- and at any other /// length, or with AAD past the capacity, every form is refused. A failed tag leaves the output -/// zeroized. The packet key, [`CCM`], is the one that takes any length. +/// zeroized. The declared-length key, [`CCM`], is the one that takes any length. #[test] -fn frame_key_one_shots_take_the_frame_and_agree_with_the_packet_key() { +fn packet_key_one_shots_take_the_frame_and_agree_with_the_declared_length_key() { let key = frame_key::<12, 16, 48, 48>(); - let packet = AES128Key::>::from_key_material(&material::<16>(APPENDIX_C_KEY)) + let declared = AES128Key::>::from_key_material(&material::<16>(APPENDIX_C_KEY)) .expect("the appendix key"); let rng = || FixedSeedRNG::<12>::new([0x24u8; 12]); @@ -645,7 +649,7 @@ fn frame_key_one_shots_take_the_frame_and_agree_with_the_packet_key() { assert_eq!(written, msg_len); let mut direct = msg.clone(); let direct_tag = - packet.encrypt_detached_in_place_nonce(&nonce, &aad, &mut direct).expect("in place"); + declared.encrypt_detached_in_place_nonce(&nonce, &aad, &mut direct).expect("in place"); assert_eq!((&detached, tag), (&direct, direct_tag), "{what}: detached agrees"); let mut inline = vec![0u8; msg_len + 16]; @@ -668,7 +672,7 @@ fn frame_key_one_shots_take_the_frame_and_agree_with_the_packet_key() { msg_len ); assert_eq!(pt, msg, "{what}: inline round trip"); - packet.decrypt_detached_in_place(&nonce, &aad, &mut direct, &tag).expect("in place"); + declared.decrypt_detached_in_place(&nonce, &aad, &mut direct, &tag).expect("in place"); assert_eq!(direct, msg, "{what}: in-place round trip"); // The supplied-nonce forms, each opened by the matching decrypt. @@ -681,7 +685,7 @@ fn frame_key_one_shots_take_the_frame_and_agree_with_the_packet_key() { } // One byte either side of the frame, nothing, well past it, and AAD past the capacity: every - // form refuses, and the packet key does not. + // form refuses, and the declared-length key does not. for (msg_len, aad_len) in [(47usize, 0usize), (49, 0), (0, 5), (1000, 200), (48, 49)] { let msg = vec![0xA5u8; msg_len]; let aad = vec![0x3Cu8; aad_len]; @@ -708,10 +712,10 @@ fn frame_key_one_shots_take_the_frame_and_agree_with_the_packet_key() { ); let mut direct = msg.clone(); - let tag = packet + let tag = declared .encrypt_detached_in_place_nonce(&nonce, &aad, &mut direct) - .unwrap_or_else(|e| panic!("{what}: the packet key takes any length: {e:?}")); - let n = packet.encrypt_with_aad_nonce_out(&nonce, &aad, &msg, &mut inline).unwrap(); + .unwrap_or_else(|e| panic!("{what}: the declared-length key takes any length: {e:?}")); + let n = declared.encrypt_with_aad_nonce_out(&nonce, &aad, &msg, &mut inline).unwrap(); assert_eq!(n, msg_len + 16); let mut pt = vec![0xEEu8; msg_len]; assert!( @@ -753,7 +757,7 @@ fn frame_key_one_shots_take_the_frame_and_agree_with_the_packet_key() { assert_eq!(pt, [0u8; 48]); let mut buf = inline[..48].to_vec(); assert!(matches!( - packet.decrypt_detached_in_place(&nonce, b"hdr", &mut buf, &[0u8; 16]), + declared.decrypt_detached_in_place(&nonce, b"hdr", &mut buf, &[0u8; 16]), Err(SymmetricCipherError::AEADTagCheckFailed) )); assert_eq!(buf, [0u8; 48]); @@ -956,20 +960,20 @@ fn an_inline_ciphertext_shorter_than_the_tag_is_rejected() { assert_eq!(dec.do_decrypt_final().expect("an empty frame still verifies").1, 0); } -/// A truncated `C` on a frame that is not empty, where the frame key's stream and the packet +/// A truncated `C` on a frame that is not empty, where the frame key's stream and the declared-length /// key's one-shots part ways. The streaming decryptor knows the frame: `do_decrypt_out` releases /// the payload as it arrives, up to `DATA_LEN`, holds back only what follows as the tag, and the -/// final reports anything short of `DATA_LEN + TAG_LEN` as malformed. The packet key's one-shots +/// final reports anything short of `DATA_LEN + TAG_LEN` as malformed. The declared-length key's one-shots /// take the whole message and know no frame: a `C` shorter than the tag is malformed, and /// anything longer is a shorter message whose tag does not verify. `decrypt_out_len` is `C` less /// a tag, as for any AEAD. #[test] fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { const DATA_LEN: usize = 32; - type Enc = CcmEncryptor; - type Dec = CcmDecryptor; + type Enc = CcmPacketEncryptor; + type Dec = CcmPacketDecryptor; let key = frame_key::<12, 16, 16, DATA_LEN>(); - let packet = AES128Key::>::from_key_material(&material::<16>(APPENDIX_C_KEY)) + let declared = AES128Key::>::from_key_material(&material::<16>(APPENDIX_C_KEY)) .expect("the appendix key"); let frame = [0x5Au8; DATA_LEN]; let mut sealed = vec![0u8; Enc::encrypt_out_len(DATA_LEN)]; @@ -990,7 +994,7 @@ fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { }; let mut pt = vec![0u8; Dec::decrypt_out_len(len)]; assert!( - one_shot_says(packet.decrypt_with_aad_out(&nonce, b"hdr", short, &mut pt)), + one_shot_says(declared.decrypt_with_aad_out(&nonce, b"hdr", short, &mut pt)), "a {len}-byte C is too short for a tag, or a shorter message with the wrong one \ (decrypt_with_aad_out)" ); @@ -1006,7 +1010,7 @@ fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { // ...and the accepted length, through both, so the loop's bound is not off by one. let mut pt = vec![0u8; Dec::decrypt_out_len(sealed.len())]; - assert_eq!(packet.decrypt_with_aad_out(&nonce, b"hdr", &sealed, &mut pt).expect("open"), 32); + assert_eq!(declared.decrypt_with_aad_out(&nonce, b"hdr", &sealed, &mut pt).expect("open"), 32); assert_eq!(&pt[..], &frame[..]); assert_eq!(key.decrypt_with_aad_out(&nonce, b"hdr", &sealed, &mut pt).expect("open"), 32); assert_eq!(&pt[..], &frame[..]); @@ -1083,7 +1087,7 @@ fn each_direction_has_its_own_methods() { // ---- memory ------------------------------------------------------------------------------ -/// Pins the sizes: the fixed-frame pair is the mode's packet state (264/296/328 B for +/// Pins the sizes: the fixed-frame pair is the mode's `Ccm` state (264/296/328 B for /// AES-128/192/256, independent of `NONCE_LEN`/`TAG_LEN`) plus the `AAD_LEN` buffer and a few /// words of bookkeeping, independent of `DATA_LEN`. #[test] @@ -1091,28 +1095,34 @@ fn sizes_match_the_documented_memory_table() { use core::mem::size_of; // The pair holds no payload: a 4 KiB frame costs exactly what a 16-byte one does. - let enc_64 = size_of::>(); - assert_eq!(enc_64, size_of::>()); + let enc_64 = size_of::>(); + assert_eq!(enc_64, size_of::>()); assert_eq!( enc_64, 264 + 64 + 16, - "the packet state, the AAD buffer, its length and the phase flag" + "the `Ccm` state, the AAD buffer, its length and the phase flag" ); assert_eq!( - size_of::>() - enc_64, + size_of::>() - enc_64, 4096 - 64, "the value grows by exactly the AAD capacity" ); // Independent of NONCE_LEN and TAG_LEN: the nonce lives inside the counter template and the // tag is assembled at finalization, not held. - assert_eq!(enc_64, size_of::>()); - assert_eq!(enc_64, size_of::>()); + assert_eq!(enc_64, size_of::>()); + assert_eq!(enc_64, size_of::>()); // The key schedule is the difference between the key lengths. - assert_eq!(size_of::>(), enc_64 + 32); - assert_eq!(size_of::>(), enc_64 + 64); + assert_eq!( + size_of::>(), + enc_64 + 32 + ); + assert_eq!( + size_of::>(), + enc_64 + 64 + ); // The decryptor adds the tag it holds back and that tag's length. - let dec_64 = size_of::>(); - assert_eq!(dec_64, size_of::>()); + let dec_64 = size_of::>(); + assert_eq!(dec_64, size_of::>()); assert_eq!(dec_64, enc_64 + 16 + 8); } diff --git a/crypto/cipher/src/modes/mod.rs b/crypto/cipher/src/modes/mod.rs index aa6075b10..b6f4335cb 100644 --- a/crypto/cipher/src/modes/mod.rs +++ b/crypto/cipher/src/modes/mod.rs @@ -43,7 +43,7 @@ //! why it lives under [`hazmat`] -- see [`hazmat::ecb::Ecb`] and //! [Choosing between the modes](#choosing-between-the-modes). //! -//! Each mode is also a **mode type** -- [`CBC`], [`CCM`], [`CCMFrame`], [`CFB`], [`CFB8`], +//! Each mode is also a **mode type** -- [`CBC`], [`CCM`], [`CCM_Packet`], [`CFB`], [`CFB8`], //! [`CTR`], [`GCM`], [`hazmat::ecb::ECB`], and the MACs [`CMAC`] and [`GMAC`] -- the `M` a mode-bound //! key such as `bouncycastle_aes::AES128Key` is parameterised by. The mode type implements the //! hazmat mode traits of [`bouncycastle_core::hazmat`], which is how a key hands out the mode's @@ -82,30 +82,31 @@ //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_cipher::modes::{Cbc, CcmDecryptor, CcmEncryptor, Cfb, Cfb8, Ctr, Gcm}; +//! use bouncycastle_cipher::modes::{Cbc, CcmPacketDecryptor, CcmPacketEncryptor, Cfb, Cfb8, Ctr, Gcm}; //! -//! // CBC, CFB, and CFB8 take a permutation, a direction, key length, and a block length. -//! type ToyCbc = Cbc; -//! type ToyCfb = Cfb; -//! type ToyCfb8 = Cfb8; +//! // Every worker borrows its key state for `'k`. CBC, CFB, and CFB8 then take a permutation, a +//! // direction and a block length; the key length is the key's business, not the worker's. +//! type ToyCbc<'k, Dir> = Cbc<'k, ToyBlockCipher, Dir, 16>; +//! type ToyCfb<'k, Dir> = Cfb<'k, ToyBlockCipher, Dir, 16>; +//! type ToyCfb8<'k, Dir> = Cfb8<'k, ToyBlockCipher, Dir, 16>; //! //! // CTR takes one more parameter: the nonce length, which fixes the counter width at //! // `BLOCK_LEN - NONCE_LEN`. 12 bytes of nonce leaves the maximum 4-byte counter. -//! type ToyCtr = Ctr; +//! type ToyCtr<'k, Dir> = Ctr<'k, ToyBlockCipher, Dir, 16, 12>; //! -//! // CCM takes the permutation, key length and block length like the rest, plus the nonce -//! // length and the tag length -- both CCM-specific choices rather than cipher params. +//! // CCM takes the permutation and block length like the rest, plus the nonce length and the +//! // tag length -- both CCM-specific choices rather than cipher params. //! // The nonce length caps the payload (SP 800-38C A.1: `n + q = 15`, `p < 2^8q`) and the tag //! // length is the forgery bound; 12 and 16 are the usual pair. //! // Each direction is its own type here, and the frame follows: `AAD_LEN` of AAD capacity and //! // frames of exactly `DATA_LEN` bytes, since CCM commits to the payload length up front. -//! type ToyCcmEnc = CcmEncryptor; -//! type ToyCcmDec = CcmDecryptor; +//! type ToyCcmEnc<'k> = CcmPacketEncryptor<'k, ToyBlockCipher, 16, 12, 16, 64, 1024>; +//! type ToyCcmDec<'k> = CcmPacketDecryptor<'k, ToyBlockCipher, 16, 12, 16, 64, 1024>; //! //! // GCM mode is specified in NIST SP 800-38D. `Gcm` fixes the nonce at 12 bytes (Sec 5.2.1.1 //! // recommends restricting support to 96 bits), and the block is always 16, so neither is a //! // parameter. -//! type ToyGcm = Gcm; +//! type ToyGcm<'k, Dir> = Gcm<'k, ToyBlockCipher, Dir, 16>; //! ``` //! //! ## Encrypting and decrypting @@ -168,7 +169,9 @@ pub mod hazmat; mod iv; pub use cbc::{CBC, Cbc}; -pub use ccm::{CCM, CCMFrame, CcmDecryptor, CcmEncryptor, CcmPacketDecryptor, CcmPacketEncryptor}; +pub use ccm::{ + CCM, CCM_Packet, CcmDecryptor, CcmEncryptor, CcmPacketDecryptor, CcmPacketEncryptor, +}; pub use cfb::{CFB, Cfb}; pub use cfb8::{CFB8, Cfb8}; pub use cmac::{CMAC, CMAC_MIN_TAG_LEN, Cmac, CmacKeyState}; @@ -190,7 +193,7 @@ pub struct Blocks; pub trait ModeNames { /// Reported by [`Cbc`]. const CBC_ALG_NAME: &'static str; - /// Reported by [`CcmEncryptor`] and [`CcmDecryptor`]. + /// Reported by [`CcmPacketEncryptor`] and [`CcmPacketDecryptor`]. const CCM_ALG_NAME: &'static str; /// Reported by [`Cfb`]. const CFB_ALG_NAME: &'static str; diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index 1aa031741..e00ede4c6 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -1,4 +1,4 @@ -//! What a resumed CCM state refuses: the bounds the pair and its packet state check when they +//! What a resumed CCM state refuses: the bounds the pair and its `Ccm` state check when they //! are rebuilt from a suspended array, each tampered with on its own. //! //! The round trips in `suspend_tests.rs` show that a faithful state resumes. These show that an @@ -8,13 +8,13 @@ //! corrupts several at once would still pass if any one check were dropped. //! //! The offsets are those of the layout the `SuspendableComponent` impls write, in order: the -//! library version, the packet state (the keystream's counter template and counter index, the +//! library version, the `Ccm` state (the keystream's counter template and counter index, the //! stream cipher's pending block and its `used` count, then the chaining block, `mac_pos`, //! `aad_owed` and `owed`), the held-back AAD, its length and the phase flag; the decryptor adds //! the held-back tag and its length. Every integer is a little-endian `u64`. Spec references are //! to NIST SP 800-38C (May 2004, errata update 07-20-2007). -use bouncycastle_cipher::modes::{CCM, CCMFrame, CcmDecryptor, CcmEncryptor}; +use bouncycastle_cipher::modes::{CCM, CCM_Packet, CcmPacketDecryptor, CcmPacketEncryptor}; use bouncycastle_core::errors::SuspendableError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; @@ -37,8 +37,8 @@ const NONCE_LEN: usize = 12; const BLOCK_LEN: usize = 16; const AAD_LEN: usize = 16; const DATA_LEN: usize = 32; -type ToyEnc = CcmEncryptor; -type ToyDec = CcmDecryptor; +type ToyEnc = CcmPacketEncryptor; +type ToyDec = CcmPacketDecryptor; const N: usize = ToyEnc::SUSPENDED_STATE_LEN; const ND: usize = ToyDec::SUSPENDED_STATE_LEN; @@ -62,7 +62,7 @@ fn key() -> KeyMaterial<16> { } /// The key the pair is made from; resume still takes the material, through `key()`. -fn frame_key() -> ToyKey> { +fn frame_key() -> ToyKey> { ToyKey::from_key_material(&key()).unwrap() } diff --git a/crypto/cipher/tests/common/toy_key.rs b/crypto/cipher/tests/common/toy_key.rs index 603915d3f..b05f14df0 100644 --- a/crypto/cipher/tests/common/toy_key.rs +++ b/crypto/cipher/tests/common/toy_key.rs @@ -15,14 +15,14 @@ use bouncycastle_cipher::modes::{ use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ - AeadDecryptMode, AeadEncryptMode, AeadPacketDecryptMode, AeadPacketEncryptMode, + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbDecryptionKey, EcbEncryptMode, EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, SymmetricDecryptMode, SymmetricEncryptMode, do_hazardous_operations, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::symmetric_key::{ - AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, + AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, AeadDecryptionKey, AeadEncryptionKey, BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, MacGenerationKey, MacVerificationKey, SymmetricDecryptionKey, SymmetricEncryptionKey, }; @@ -250,9 +250,9 @@ impl< const NONCE_LEN: usize, const TAG_LEN: usize, const FINAL_LEN: usize, -> AeadPacketEncryptionKey for ToyKey +> AeadDeclaredEncryptionKey for ToyKey where - M: AeadPacketEncryptMode, + M: AeadDeclaredEncryptMode, { type Encryptor = M::Encryptor; @@ -300,9 +300,9 @@ impl< const NONCE_LEN: usize, const TAG_LEN: usize, const FINAL_LEN: usize, -> AeadPacketDecryptionKey for ToyKey +> AeadDeclaredDecryptionKey for ToyKey where - M: AeadPacketDecryptMode, + M: AeadDeclaredDecryptMode, { type Decryptor = M::Decryptor; diff --git a/crypto/cipher/tests/modes/alg_name_tests.rs b/crypto/cipher/tests/modes/alg_name_tests.rs index 8235837c6..0e61bc6c0 100644 --- a/crypto/cipher/tests/modes/alg_name_tests.rs +++ b/crypto/cipher/tests/modes/alg_name_tests.rs @@ -8,8 +8,8 @@ mod common; use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{ - Blocks, CBC, CCM, CCMFrame, CFB, CFB8, CTR, Cbc, CcmDecryptor, CcmEncryptor, Cfb, Cfb8, Cmac, - Ctr, Gcm, Gmac, ModeNames, + Blocks, CBC, CCM, CCM_Packet, CFB, CFB8, CTR, Cbc, CcmPacketDecryptor, CcmPacketEncryptor, Cfb, + Cfb8, Cmac, Ctr, Gcm, Gmac, ModeNames, }; use bouncycastle_cipher::padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, @@ -58,8 +58,8 @@ fn each_mode_reports_its_own_name() { check::>(Toy::CFB8_ALG_NAME); check::>(Toy::GCM_ALG_NAME); check::>(Toy::GCM_ALG_NAME); - check::>(Toy::CCM_ALG_NAME); - check::>(Toy::CCM_ALG_NAME); + check::>(Toy::CCM_ALG_NAME); + check::>(Toy::CCM_ALG_NAME); check::>(Toy::CMAC_ALG_NAME); check::>(Toy::GMAC_ALG_NAME); check::>(Toy::GMAC_ALG_NAME); @@ -102,5 +102,5 @@ fn each_mode_type_reports_its_mode_name() { check::(Toy::CTR_ALG_NAME); check::>(Toy::CTR_ALG_NAME); check::>(Toy::CCM_ALG_NAME); - check::>(Toy::CCM_ALG_NAME); + check::>(Toy::CCM_ALG_NAME); } diff --git a/crypto/cipher/tests/modes/ccm_tests.rs b/crypto/cipher/tests/modes/ccm_tests.rs index c0b798da7..87f2f850f 100644 --- a/crypto/cipher/tests/modes/ccm_tests.rs +++ b/crypto/cipher/tests/modes/ccm_tests.rs @@ -6,7 +6,7 @@ //! stream holds to its frame, and which entry points release unauthenticated plaintext -- //! independently of the known-answer vectors in the `aes` crate's `sp800_38c_tests.rs`, //! `ccm_bc-test-data.rs` and `ccm_wycheproof.rs`. The Appendix C file also carries the -//! fixed-frame `CcmEncryptor` / `CcmDecryptor` pair's contract, the shared framework run and the +//! fixed-frame `CcmPacketEncryptor` / `CcmPacketDecryptor` pair's contract, the shared framework run and the //! memory table, so none of those is repeated here. //! //! The one-shots are the mode's in-place pair on [`CCM`], which take any length; the streams are @@ -18,13 +18,13 @@ mod common; #[path = "../common/toy_key.rs"] mod toy_key; -use bouncycastle_cipher::modes::{CCM, CCMFrame, ModeNames}; +use bouncycastle_cipher::modes::{CCM, CCM_Packet, ModeNames}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ - AeadDecryptionKey, AeadEncryptionKey, AeadPacketDecryptionKey, AeadPacketEncryptionKey, + AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial, }; use bouncycastle_core::traits::{ @@ -41,10 +41,10 @@ const TAG_LEN: usize = 16; const AAD_LEN: usize = 64; /// The toy key under the in-place pair, which takes any length. -type PacketKey

= ToyKey>; +type DeclaredKey

= ToyKey>; /// The toy key under the fixed-frame streams, whose frame is `DATA_LEN`. type FrameKey = - ToyKey>; + ToyKey>; fn pinned_nonce() -> [u8; NONCE_LEN] { core::array::from_fn(|i| 0xA0 ^ (i as u8)) @@ -54,8 +54,8 @@ fn message(len: usize) -> Vec { (0..len).map(|i| (i as u8).wrapping_mul(3).wrapping_add(1)).collect() } -fn packet_key + ModeNames + Clone>() -> PacketKey

{ - PacketKey::from_key_material(&toy_key()).expect("the toy key") +fn declared_key + ModeNames + Clone>() -> DeclaredKey

{ + DeclaredKey::from_key_material(&toy_key()).expect("the toy key") } fn frame_key + ModeNames + Clone, const DATA_LEN: usize>() @@ -70,7 +70,7 @@ fn seal + ModeNames + Clone>( plaintext: &[u8], ) -> (Vec, [u8; TAG_LEN]) { let mut buf = plaintext.to_vec(); - let tag = packet_key::

().encrypt_detached_in_place_nonce(nonce, aad, &mut buf).unwrap(); + let tag = declared_key::

().encrypt_detached_in_place_nonce(nonce, aad, &mut buf).unwrap(); (buf, tag) } @@ -82,7 +82,7 @@ fn open + ModeNames + Clone>( tag: &[u8; TAG_LEN], ) -> Result, SymmetricCipherError> { let mut buf = ciphertext.to_vec(); - packet_key::

().decrypt_detached_in_place(nonce, aad, &mut buf, tag)?; + declared_key::

().decrypt_detached_in_place(nonce, aad, &mut buf, tag)?; Ok(buf) } @@ -444,7 +444,7 @@ fn every_permitted_nonce_length_works() { /// Sec 6.2: "When the error message INVALID is returned, the payload P and the MAC T shall not /// be revealed." The in-place one-shot honours that -- the caller's buffer comes back zeroized -- /// because it has the whole ciphertext before it starts. The stream cannot: Sec 6.2 recovers `P` -/// (step 5) before it can verify it (step 10), and the fixed-frame `CcmDecryptor` releases `P` +/// (step 5) before it can verify it (step 10), and the fixed-frame `CcmPacketDecryptor` releases `P` /// as it is recovered rather than hold the frame back, so by the time the final rejects the tag /// the plaintext is already in the caller's buffer, as its docs warn. Pinning the difference /// makes it a documented property rather than an accident. @@ -459,7 +459,7 @@ fn the_one_shot_releases_nothing_on_forgery_but_the_stream_does() { // The one-shot: verify-then-return, so a forged tag leaves nothing but zeros. let mut buf = ct.clone(); assert!(matches!( - packet_key::().decrypt_detached_in_place(&nonce, b"aad", &mut buf, &tag), + declared_key::().decrypt_detached_in_place(&nonce, b"aad", &mut buf, &tag), Err(SymmetricCipherError::AEADTagCheckFailed) )); assert_eq!(buf, [0u8; LEN], "the one-shot must zeroize its buffer on a forged tag"); @@ -509,6 +509,6 @@ fn a_large_payload_round_trips_in_place() { #[test] fn the_keys_enforce_the_material_policy() { let framework = TestFrameworkFromKeyMaterial::new(); - framework.test::>(Toy::CCM_ALG_NAME, SecurityStrength::_128bit); + framework.test::>(Toy::CCM_ALG_NAME, SecurityStrength::_128bit); framework.test::>(Toy::CCM_ALG_NAME, SecurityStrength::_128bit); } diff --git a/crypto/cipher/tests/suspend_tests.rs b/crypto/cipher/tests/suspend_tests.rs index 0279d7a39..b74a74061 100644 --- a/crypto/cipher/tests/suspend_tests.rs +++ b/crypto/cipher/tests/suspend_tests.rs @@ -10,7 +10,8 @@ use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{ - Blocks, CBC, CCMFrame, CFB, CFB8, CTR, Cbc, CcmDecryptor, CcmEncryptor, Cfb, Cfb8, Ctr, Gcm, + Blocks, CBC, CCM_Packet, CFB, CFB8, CTR, Cbc, CcmPacketDecryptor, CcmPacketEncryptor, Cfb, + Cfb8, Ctr, Gcm, }; use bouncycastle_cipher::padding::{PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use bouncycastle_cipher::{Decrypting, Encrypting}; @@ -41,9 +42,9 @@ type ToyCfb8

= Cfb8; type ToyCtr = Ctr; type ToyGcm = Gcm; // A 64-byte AAD capacity and 37-byte frames, the message `ccm_both_directions` uses. -type ToyCcmFrame = CCMFrame<12, 16, 64, 37>; -type ToyCcmEnc = CcmEncryptor; -type ToyCcmDec = CcmDecryptor; +type ToyCcmPacket = CCM_Packet<12, 16, 64, 37>; +type ToyCcmEnc = CcmPacketEncryptor; +type ToyCcmDec = CcmPacketDecryptor; type ToyPaddedEnc = PaddedBlockCipherEncryptor, PKCS7, 16, 16, 16>; type ToyPaddedDec = PaddedBlockCipherDecryptor, PKCS7, 16, 16, 16>; @@ -222,7 +223,7 @@ fn ccm_both_directions() { // Suspended with half the AAD held back and no payload yet, and again mid-payload; both // must continue to the one answer. - let mut enc = key_for::().create_encryptor_nonce(&nonce); + let mut enc = key_for::().create_encryptor_nonce(&nonce); enc.do_update_aad(&aad[..3]).unwrap(); let sealed = round_trip::<{ ToyCcmEnc::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { e.do_update_aad(&aad[3..]).unwrap(); @@ -232,7 +233,7 @@ fn ccm_both_directions() { out.extend_from_slice(&tag); out }); - let mut enc = key_for::().create_encryptor_nonce(&nonce); + let mut enc = key_for::().create_encryptor_nonce(&nonce); enc.do_update_aad(aad).unwrap(); let mut head = vec![0u8; 9]; enc.do_encrypt_out(&msg[..9], &mut head).unwrap(); @@ -247,7 +248,7 @@ fn ccm_both_directions() { let (ct, tag) = sealed.split_at(37); let tag: [u8; 16] = tag.try_into().unwrap(); - let mut dec = key_for::().create_decryptor(&nonce); + let mut dec = key_for::().create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut opened_head = vec![0u8; 9]; dec.do_decrypt_out(&ct[..9], &mut opened_head).unwrap(); @@ -260,7 +261,7 @@ fn ccm_both_directions() { assert_eq!([&opened_head[..], &plain[..]].concat(), msg); // ...and with part of an inline tag already held back at the suspend point. - let mut dec = key_for::().create_decryptor(&nonce); + let mut dec = key_for::().create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut opened = vec![0u8; 37]; dec.do_decrypt_out(&sealed[..40], &mut opened).unwrap(); diff --git a/crypto/core/src/hazmat/mod.rs b/crypto/core/src/hazmat/mod.rs index b8292167d..d9a37ba22 100644 --- a/crypto/core/src/hazmat/mod.rs +++ b/crypto/core/src/hazmat/mod.rs @@ -22,7 +22,7 @@ pub use electronic_code_book::ElectronicCodeBook; pub use hazardous_operations::do_hazardous_operations; pub use key_stream::KeyStream; pub use mode::{ - AeadDecryptMode, AeadEncryptMode, AeadPacketDecryptMode, AeadPacketEncryptMode, + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbEncryptMode, MacMode, Mode, SymmetricDecryptMode, SymmetricEncryptMode, }; diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index 153bc9ae9..08b4de961 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -6,8 +6,8 @@ //! There are two shapes of constructor. [`AeadEncryptMode`] / [`AeadDecryptMode`] are handed a //! nonce and nothing else, which is every mode that can take its message as it comes. A mode that //! must commit to the lengths before its first byte -- CCM, whose `B0` carries the payload length -//! and whose AAD is length-prefixed -- implements [`AeadPacketEncryptMode`] / -//! [`AeadPacketDecryptMode`] instead, whose constructors take the AAD and payload lengths and +//! and whose AAD is length-prefixed -- implements [`AeadDeclaredEncryptMode`] / +//! [`AeadDeclaredDecryptMode`] instead, whose constructors take the AAD and payload lengths and //! whose workers then need no buffer for either. The unauthenticated modes have the same two //! shapes with no tag: [`SymmetricEncryptMode`] / [`SymmetricDecryptMode`] for a mode that takes //! data of any length (padded CBC, CFB, CFB8, CTR), [`BlockEncryptMode`] / [`BlockDecryptMode`] @@ -143,7 +143,7 @@ pub trait AeadDecryptMode< /// The encryption side of an AEAD mode over `P` whose worker must know the AAD and payload /// lengths before its first byte. The constructors take both; the worker then accepts exactly /// that much of each, in any chunking, and holds nothing back. -pub trait AeadPacketEncryptMode< +pub trait AeadDeclaredEncryptMode< P, const KEY_LEN: usize, const NONCE_LEN: usize, @@ -214,8 +214,8 @@ pub trait AeadPacketEncryptMode< } /// The decryption side of an AEAD mode over `P` whose worker must know the AAD and payload -/// lengths before its first byte; see [`AeadPacketEncryptMode`]. -pub trait AeadPacketDecryptMode< +/// lengths before its first byte; see [`AeadDeclaredEncryptMode`]. +pub trait AeadDeclaredDecryptMode< P, const KEY_LEN: usize, const NONCE_LEN: usize, diff --git a/crypto/core/src/provided.rs b/crypto/core/src/provided.rs index d2f81961b..bc4022e7e 100644 --- a/crypto/core/src/provided.rs +++ b/crypto/core/src/provided.rs @@ -1,5 +1,5 @@ //! The provided bodies of the role keys' and mode traits' one-shots, over a worker that has -//! already been constructed, so that the drawing, supplied-nonce and packet forms share them. +//! already been constructed, so that the drawing, supplied-nonce and declared-length forms share them. //! //! The `_out` bodies stream the message from one slice to another. The in-place bodies run a //! message in its own buffer through the same split-slice update methods: each chunk is copied out diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index 668d46f70..9e5c8fa69 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -12,7 +12,7 @@ //! `KEY_LEN` bytes, [`FromAnyKeyMaterial`] for a construction that accepts any byte string. //! A key that lives in a hardware token implements neither; it has its own way in. //! * **Roles**, one trait per direction: [`AeadEncryptionKey`] / [`AeadDecryptionKey`], their -//! packet forms [`AeadPacketEncryptionKey`] / [`AeadPacketDecryptionKey`] for a mode that must +//! declared-length forms [`AeadDeclaredEncryptionKey`] / [`AeadDeclaredDecryptionKey`] for a mode that must //! know the lengths up front, [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] for the //! unauthenticated modes that take data of any length (padded CBC, CFB, CFB8, CTR), //! [`BlockEncryptionKey`] / [`BlockDecryptionKey`] for the whole-block modes, the ECB pair in @@ -22,7 +22,7 @@ //! worker's streaming methods, the cipher traits in [`crate::traits`]. //! //! Which role a cipher key carries is decided by its mode type through the mode traits in -//! [`crate::hazmat`] ([`Mode`], [`AeadEncryptMode`], [`AeadDecryptMode`] and their packet, +//! [`crate::hazmat`] ([`Mode`], [`AeadEncryptMode`], [`AeadDecryptMode`] and their declared-length, //! symmetric, block and ECB forms); the permutation crate connects the two with a forwarding //! impl per role. A mode implements exactly one pair, so `key.create_encryptor(..)` on a key //! bound to it is never ambiguous. @@ -456,7 +456,7 @@ pub trait AeadDecryptionKey< /// its first byte: CCM. The constructors take both, and the worker then needs no buffer for /// either and holds nothing back. The one-shots have the lengths in hand, so they are provided /// exactly as [`AeadEncryptionKey`]'s are and behave the same. -pub trait AeadPacketEncryptionKey< +pub trait AeadDeclaredEncryptionKey< const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, @@ -658,9 +658,9 @@ pub trait AeadPacketEncryptionKey< } /// The decryption half of an AEAD key whose mode must know the lengths up front; see -/// [`AeadPacketEncryptionKey`]. For the inline layout the payload length is the ciphertext less +/// [`AeadDeclaredEncryptionKey`]. For the inline layout the payload length is the ciphertext less /// its tag, and a ciphertext too short for a tag is malformed. -pub trait AeadPacketDecryptionKey< +pub trait AeadDeclaredDecryptionKey< const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, diff --git a/mem_usage_benches/src/bench_ccm_mem_usage.rs b/mem_usage_benches/src/bench_ccm_mem_usage.rs index 7fa5262d0..fc5ad87b3 100644 --- a/mem_usage_benches/src/bench_ccm_mem_usage.rs +++ b/mem_usage_benches/src/bench_ccm_mem_usage.rs @@ -30,9 +30,9 @@ use bouncycastle::aes::AES128Key; use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle::cipher::modes::{CCM, CCMFrame, CcmDecryptor, CcmEncryptor}; +use bouncycastle::cipher::modes::{CCM, CCM_Packet, CcmPacketDecryptor, CcmPacketEncryptor}; use bouncycastle::core::hazmat::{ - AeadPacketDecryptMode, AeadPacketEncryptMode, ElectronicCodeBook, + AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, ElectronicCodeBook, }; use bouncycastle::core::key_material::{KeyMaterial, KeyType}; use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; @@ -57,11 +57,11 @@ const MESSAGE_LEN: usize = DATA_LEN; /// The mode's in-place one-shots over the keyed permutation: the direct path. type Aes128CcmMode = CCM; /// The key bound to the fixed frame, whose streams are the pair below. -type Aes128CcmFrameKey = AES128Key>; +type Aes128CcmPacketKey = AES128Key>; type Aes128CcmEncryptor = - CcmEncryptor; + CcmPacketEncryptor; type Aes128CcmDecryptor = - CcmDecryptor; + CcmPacketDecryptor; fn key() -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&[0x42u8; N], KeyType::SymmetricCipherKey).unwrap() @@ -71,8 +71,8 @@ fn perm() -> AES128Internal { AES128Internal::new(&key::<16>()).unwrap() } -fn frame_key() -> Aes128CcmFrameKey { - Aes128CcmFrameKey::from_key_material(&key::<16>()).unwrap() +fn frame_key() -> Aes128CcmPacketKey { + Aes128CcmPacketKey::from_key_material(&key::<16>()).unwrap() } /// The message every bench processes, filled at run time and then only ever reached through a @@ -96,7 +96,7 @@ fn bench_do_nothing() { /// Prints the in-memory size of each CCM value: the persistent cost of holding one open. /// -/// A mode-bound key is the keyed permutation alone; the trait pair is the mode's packet state +/// A mode-bound key is the keyed permutation alone; the trait pair is the mode's `Ccm` state /// (the permutation, three blocks and five counters, independent of `NONCE_LEN` and `TAG_LEN`) /// plus the `AAD_LEN` buffer and a few words, at any `DATA_LEN`. #[inline(never)] @@ -109,27 +109,34 @@ fn print_struct_sizes() { eprintln!("AES256Internal {:>7} B", size_of::()); eprintln!( - "--- the trait pair: packet state + AAD_LEN + bookkeeping, independent of DATA_LEN ---" + "--- the trait pair: `Ccm` state + AAD_LEN + bookkeeping, independent of DATA_LEN ---" ); - eprintln!("CcmEncryptor<.., {AAD_LEN}, {DATA_LEN}> {:>7} B", size_of::()); - eprintln!("CcmDecryptor<.., {AAD_LEN}, {DATA_LEN}> {:>7} B", size_of::()); eprintln!( - "CcmEncryptor<.., 64, 240> {:>7} B", - size_of::>() + "CcmPacketEncryptor<.., {AAD_LEN}, {DATA_LEN}> {:>7} B", + size_of::() ); eprintln!( - "CcmEncryptor<.., 7, 4, ..> {:>7} B", - size_of::>() + "CcmPacketDecryptor<.., {AAD_LEN}, {DATA_LEN}> {:>7} B", + size_of::() ); eprintln!( - "CcmEncryptor {:>7} B", - size_of::>() + "CcmPacketEncryptor<.., 64, 240> {:>7} B", + size_of::>() + ); + eprintln!( + "CcmPacketEncryptor<.., 7, 4, ..> {:>7} B", + size_of::>() + ); + eprintln!( + "CcmPacketEncryptor {:>7} B", + size_of::>( + ) ); print!("{}", size_of::()); } -/// The in-place path over the message: the packet state plus the caller's one buffer, and +/// The in-place path over the message: the `Ccm` state plus the caller's one buffer, and /// nothing else. This is the baseline for `bench_streaming_encrypt`, `bench_streaming_decrypt` /// and `bench_oneshot_encrypt_out_detached`. #[inline(never)] @@ -150,7 +157,7 @@ fn bench_in_place_encrypt() { #[inline(never)] fn bench_streaming_encrypt() { eprintln!( - "CcmEncryptor create_encryptor/do_encrypt_out/do_encrypt_final_detached_out, {MESSAGE_LEN} B in 1 KiB chunks" + "CcmPacketEncryptor create_encryptor/do_encrypt_out/do_encrypt_final_detached_out, {MESSAGE_LEN} B in 1 KiB chunks" ); let k = frame_key(); @@ -176,7 +183,7 @@ fn bench_streaming_encrypt() { #[inline(never)] fn bench_streaming_decrypt() { eprintln!( - "CcmDecryptor create_decryptor/do_decrypt_out/do_decrypt_final, {MESSAGE_LEN} B in 1 KiB chunks" + "CcmPacketDecryptor create_decryptor/do_decrypt_out/do_decrypt_final, {MESSAGE_LEN} B in 1 KiB chunks" ); let k = frame_key(); @@ -208,7 +215,7 @@ fn bench_streaming_decrypt() { /// measures plus that DRBG. #[inline(never)] fn bench_oneshot_encrypt_out_detached() { - eprintln!("AES128Key::encrypt_detached_out, {MESSAGE_LEN} B"); + eprintln!("AES128Key::encrypt_detached_out, {MESSAGE_LEN} B"); let k = frame_key(); let mut rng = HashDRBG_SHA512::new_from_os(); From 279449039efc92369f4681d045a603c4e110fdac Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 02:58:24 +1100 Subject: [PATCH 15/54] core, cipher: workers borrow the key state instead of copying the schedule (prototype) The role traits in core::symmetric_key and the mode traits in core::hazmat hand out their workers through a generic associated type with a lifetime, `type Encryptor<'k>`, and `create_encryptor` takes `&'k self`; every worker in the cipher crate (Cbc, Cfb, Cfb8, CtrKeyStream, Ecb, Gcm, Gmac, Cmac, the CCM engine and both CCM pairs) holds `&'k` to the mode's key state, so making a worker copies the per-message state and nothing else, and a worker cannot outlive its key. The mode traits spell the bound `where P: 'k`: rustc rejects a generic impl whose key state is a struct over `P` under `Self::KeyState: 'k`. What that removes: `KeyStream::new` and its `KEY_LEN`, `Gmac::new` / `new_nonce` (the last material route), `Cmac::init`, and every `P::new` on resume. SuspendableKeyed and SuspendableComponent gain the lifetime and take the key by `&'k`; a cipher-mode worker's `Key` is now the mode's `KeyState`, which a key object exposes through the new `core::hazmat::ModeKey::state` (AESKey and the test ToyKey implement it), and the suspended layouts are unchanged. The test framework's keystream suite takes a constructor closure and leaves the key-policy checks to the construction suite. Ascon, HMAC, KMAC and HKDF keep owned workers (HKDF embeds an HMAC worker) and implement the traits for every `'k`. Sizes: the CCM workers go from 264/296/328 B to 96 B at every key length, the CCM_Packet pair from 344 B to 176 B, the CMAC workers from 248/280/312 B to 48 B; the CCM peak-stack table is re-measured with massif (in place 19 328 -> 18 608 B). Tests bind the key before the worker, since the worker borrows it. quality_stats.sh, ./crypto: unwraps in core code 798 -> 784, Err() 446 -> 440, test ratio .98 unchanged. Assisted-by: Claude:claude-mythos-5-1 --- CLAUDE.md | 14 +- QUALITY_AND_STYLE.md | 2 + crypto/aes/benches/aes_gcm_packet_benches.rs | 43 ++- crypto/aes/src/cbc.rs | 18 +- crypto/aes/src/ccm.rs | 88 +++++- crypto/aes/src/cfb.rs | 6 +- crypto/aes/src/cfb8.rs | 6 +- crypto/aes/src/cmac.rs | 22 +- crypto/aes/src/ctr.rs | 6 +- crypto/aes/src/gcm.rs | 86 +++--- crypto/aes/src/gmac.rs | 60 ++-- crypto/aes/src/hazmat/ecb.rs | 18 +- crypto/aes/tests/alg_name_tests.rs | 156 ++++++---- crypto/aes/tests/ccm_key_tests.rs | 17 +- crypto/aes/tests/cfb8_bc-test-data.rs | 5 +- crypto/aes/tests/cfb_bc-test-data.rs | 5 +- crypto/aes/tests/cmac_key_tests.rs | 2 +- crypto/aes/tests/cmac_tests.rs | 8 +- crypto/aes/tests/cmac_wycheproof.rs | 2 +- crypto/aes/tests/ctr_bc-test-data.rs | 5 +- crypto/aes/tests/ctr_bc_java_tests.rs | 9 +- crypto/aes/tests/ctr_vector_tests.rs | 5 +- crypto/aes/tests/gmac_key_tests.rs | 29 +- crypto/aes/tests/sp800_38a_cfb8_tests.rs | 6 +- crypto/aes/tests/sp800_38a_cfb_tests.rs | 6 +- crypto/aes/tests/sp800_38c_tests.rs | 39 +-- crypto/ascon/src/ascon_aead128.rs | 32 +- crypto/cipher/src/lib.rs | 9 +- crypto/cipher/src/modes/cmac.rs | 96 ++---- crypto/cipher/src/modes/ctr.rs | 31 +- crypto/cipher/src/modes/gmac.rs | 133 ++------- .../cipher/src/modes/hazmat/ctr_key_stream.rs | 118 ++++---- crypto/cipher/tests/ccm_suspend_tests.rs | 23 +- crypto/cipher/tests/common/toy_key.rs | 177 +++++++---- crypto/cipher/tests/modes/alg_name_tests.rs | 48 ++- crypto/cipher/tests/modes/ccm_tests.rs | 36 ++- crypto/cipher/tests/modes/cfb8_tests.rs | 45 ++- crypto/cipher/tests/modes/cfb_tests.rs | 65 ++-- crypto/cipher/tests/modes/cmac_tests.rs | 34 +-- crypto/cipher/tests/modes/ctr_tests.rs | 79 +++-- crypto/cipher/tests/modes/gcm_tests.rs | 12 +- crypto/cipher/tests/modes/gmac_tests.rs | 43 +-- .../tests/modes/symmetric_cipher_api_tests.rs | 12 +- crypto/cipher/tests/padding/padded_tests.rs | 12 +- crypto/cipher/tests/suspend_tests.rs | 277 +++++++++++------- crypto/core-test-framework/src/key_stream.rs | 82 +----- .../src/suspendable_state.rs | 5 +- crypto/core/src/hazmat/ecb_key.rs | 16 +- crypto/core/src/hazmat/key_stream.rs | 25 +- crypto/core/src/hazmat/mod.rs | 2 +- crypto/core/src/hazmat/mode.rs | 162 +++++++--- crypto/core/src/symmetric_key.rs | 261 ++++++++++++----- crypto/hkdf/src/lib.rs | 6 +- crypto/hkdf/tests/hkdf_tests.rs | 7 +- crypto/hmac/src/lib.rs | 20 +- crypto/hmac/tests/hmac_tests.rs | 6 +- crypto/sha3/src/cshake.rs | 8 +- crypto/sha3/src/kmac.rs | 22 +- crypto/sha3/src/parallelhash.rs | 8 +- crypto/sha3/src/tuplehash.rs | 8 +- crypto/utils/src/suspendable_state.rs | 18 +- mem_usage_benches/src/bench_ccm_mem_usage.rs | 27 +- 62 files changed, 1453 insertions(+), 1175 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4073e3590..992722129 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -224,8 +224,18 @@ What that taught, as mechanics for the next trait that sheds a constructor: and the construction suites in `core_test_framework::from_key_material`; a test of "a key of the wrong type is rejected" belongs to the construction suite, once, not to each mode's tests. Known-answer suites keep pinning their IV through `create_encryptor` and a `FixedSeedRNG`. -- **Resume still takes the material.** `SuspendableKeyed::Key` stays `KeyMaterial`; - resuming from a key object is a follow-up, not something to fold into the deletion. +- **Workers borrow the key.** The role and mode traits hand out workers through a generic + associated type with a lifetime, `type Encryptor<'k>`, and the cipher-crate workers hold + `&'k` to the mode's key state, so `create_encryptor` copies no schedule and a worker cannot + outlive its key. Spell the mode traits' GAT bound `where P: 'k`, not `where Self::KeyState: 'k`: + rustc rejects a generic impl whose key state is a struct over `P` (E0276) under the latter. A + function handed `key: &'k K` that names the worker is generic over `'k`; one that builds its + key locally bounds `for<'k> K::Encryptor<'k>: ..` with `K: 'static`. A test binds the key + before the worker, since `key_fn().create_encryptor(..)` borrows a temporary. +- **Resume takes the key state.** `SuspendableKeyed<'k, N>` and `SuspendableComponent<'k>` take + the key by `&'k`, and a cipher-mode worker's `Key` is the mode's `KeyState`, reached through + `hazmat::ModeKey::state`; the suspended layouts did not change. A worker that owns a copy of its + key (Ascon, HMAC, KMAC) implements the traits for every `'k` and ignores it. - **What has no key-path twin is dropped, and said so.** `new_allow_weak_key` on a cipher-based MAC, a wrong-length `KeyMaterial512` into a fixed-length key, GCM's verify-before-decrypt one-shot override: each went with a line in the commit message rather than a shim. diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index 603a99634..2e2bf6993 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -93,6 +93,8 @@ reached through a key object (`AES128Key`, `HmacKey`, `KMAC128Key`, `KeyMaterial` by `FromKeyMaterial` / `FromAnyKeyMaterial`, and its one-shots and `create_*` constructors are methods on that key. The cipher and MAC traits in `core::traits` describe a worker that already exists and carry no constructor and no key-taking method, so a key's validation and expansion happen once, where the key is made, rather than per call. +A worker borrows the key it came from, for the lifetime on the role trait's associated worker type, so making one +copies no schedule and a worker cannot outlive its key. Other version of Bouncy Castle have a design pattern where stateful objects follow a pattern of new () -> init () -> do_update () -> do_final (), and then optionally reset () that sets the object back to an unitialized state. Instead, diff --git a/crypto/aes/benches/aes_gcm_packet_benches.rs b/crypto/aes/benches/aes_gcm_packet_benches.rs index 41f7ef5e3..e9504797d 100644 --- a/crypto/aes/benches/aes_gcm_packet_benches.rs +++ b/crypto/aes/benches/aes_gcm_packet_benches.rs @@ -32,8 +32,8 @@ const PACKET_LEN: usize = 16 * 1024; const NONCE_LEN: usize = 12; const TAG_LEN: usize = 16; -type Enc = AES_GCM_256; -type Dec = AES_GCM_256; +type Enc<'k> = AES_GCM_256<'k, Encrypting>; +type Dec<'k> = AES_GCM_256<'k, Decrypting>; type Key = AES256Key; fn material() -> KeyMaterial<32> { @@ -81,10 +81,8 @@ fn bench_encrypt_packets(c: &mut Criterion) { b.iter_batched_ref( || [0u8; PACKET_LEN], |out| { - let (enc, nonce) = Key::from_key_material(black_box(&km)) - .unwrap() - .create_encryptor(&mut rng) - .unwrap(); + let per_packet = Key::from_key_material(black_box(&km)).unwrap(); + let (enc, nonce) = per_packet.create_encryptor(&mut rng).unwrap(); black_box((nonce, encrypt_packet(enc, black_box(&packet), out))) }, BatchSize::LargeInput, @@ -107,10 +105,8 @@ fn bench_encrypt_packets(c: &mut Criterion) { || [0u8; PACKET_LEN], |out| { let mut os_rng = HashDRBG_SHA512::new_from_os(); - let (enc, nonce) = Key::from_key_material(black_box(&km)) - .unwrap() - .create_encryptor(&mut os_rng) - .unwrap(); + let per_packet = Key::from_key_material(black_box(&km)).unwrap(); + let (enc, nonce) = per_packet.create_encryptor(&mut os_rng).unwrap(); black_box((nonce, encrypt_packet(enc, black_box(&packet), out))) }, BatchSize::LargeInput, @@ -149,7 +145,8 @@ fn bench_decrypt_packets(c: &mut Criterion) { b.iter_batched_ref( || [0u8; PACKET_LEN], |out| { - let dec = Key::from_key_material(black_box(&km)).unwrap().create_decryptor(&nonce); + let per_packet = Key::from_key_material(black_box(&km)).unwrap(); + let dec = per_packet.create_decryptor(&nonce); black_box(decrypt_packet(dec, black_box(&ct), &tag, out)) }, BatchSize::LargeInput, @@ -185,12 +182,11 @@ fn bench_init(c: &mut Criterion) { "AES256Key: from_key_material + create_encryptor (per packet)", |b| { b.iter(|| { - black_box( - Key::from_key_material(black_box(&km)) - .unwrap() - .create_encryptor(&mut rng) - .unwrap(), - ) + let per_packet = Key::from_key_material(black_box(&km)).unwrap(); + let (enc, nonce) = per_packet.create_encryptor(&mut rng).unwrap(); + // The worker borrows the key, so what a per-packet key costs is what it takes + // to make the worker and finish it. + black_box((nonce, enc.do_encrypt_final().unwrap())) }) }, ); @@ -201,7 +197,9 @@ fn bench_init(c: &mut Criterion) { "AES256Key: from_key_material + create_decryptor (per packet)", |b| { b.iter(|| { - black_box(Key::from_key_material(black_box(&km)).unwrap().create_decryptor(&nonce)) + let per_packet = Key::from_key_material(black_box(&km)).unwrap(); + let dec = per_packet.create_decryptor(&nonce); + black_box(dec.do_decrypt_final().is_ok()) }) }, ); @@ -213,12 +211,9 @@ fn bench_init(c: &mut Criterion) { |b| { b.iter(|| { let mut os_rng = HashDRBG_SHA512::new_from_os(); - black_box( - Key::from_key_material(black_box(&km)) - .unwrap() - .create_encryptor(&mut os_rng) - .unwrap(), - ) + let per_packet = Key::from_key_material(black_box(&km)).unwrap(); + let (enc, nonce) = per_packet.create_encryptor(&mut os_rng).unwrap(); + black_box((nonce, enc.do_encrypt_final().unwrap())) }) }, ); diff --git a/crypto/aes/src/cbc.rs b/crypto/aes/src/cbc.rs index bf490210e..9ded1e7e0 100644 --- a/crypto/aes/src/cbc.rs +++ b/crypto/aes/src/cbc.rs @@ -154,16 +154,16 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// AES-128 in CBC mode with a padding scheme. #[allow(non_camel_case_types)] -pub type AES_CBC_128 = ::Select< +pub type AES_CBC_128<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Cbc, + Cbc<'k, AES128Internal, Encrypting, 16, AES_BLOCK_LEN>, Pad, 16, AES_BLOCK_LEN, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Cbc, + Cbc<'k, AES128Internal, Decrypting, 16, AES_BLOCK_LEN>, Pad, 16, AES_BLOCK_LEN, @@ -173,16 +173,16 @@ pub type AES_CBC_128 = ::Select< /// AES-192 in CBC mode with a padding scheme. #[allow(non_camel_case_types)] -pub type AES_CBC_192 = ::Select< +pub type AES_CBC_192<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Cbc, + Cbc<'k, AES192Internal, Encrypting, 24, AES_BLOCK_LEN>, Pad, 24, AES_BLOCK_LEN, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Cbc, + Cbc<'k, AES192Internal, Decrypting, 24, AES_BLOCK_LEN>, Pad, 24, AES_BLOCK_LEN, @@ -192,16 +192,16 @@ pub type AES_CBC_192 = ::Select< /// AES-256 in CBC mode with a padding scheme. See [`AES_CBC_128`]. #[allow(non_camel_case_types)] -pub type AES_CBC_256 = ::Select< +pub type AES_CBC_256<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Cbc, + Cbc<'k, AES256Internal, Encrypting, 32, AES_BLOCK_LEN>, Pad, 32, AES_BLOCK_LEN, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Cbc, + Cbc<'k, AES256Internal, Decrypting, 32, AES_BLOCK_LEN>, Pad, 32, AES_BLOCK_LEN, diff --git a/crypto/aes/src/ccm.rs b/crypto/aes/src/ccm.rs index 79b71000e..d8b1fd916 100644 --- a/crypto/aes/src/ccm.rs +++ b/crypto/aes/src/ccm.rs @@ -165,22 +165,23 @@ //! | Type | AES-128 | AES-192 | AES-256 | //! |---|---|---|---| //! | [`AES128Key`]`>` and friends: the key schedule | 176 B | 208 B | 240 B | -//! | its workers, encrypting / decrypting | 264 B / 288 B | 296 B / 320 B | 328 B / 352 B | -//! | [`AES_CCM_128`] and friends, encrypting, `AAD_LEN = 64` | 344 B | 376 B | 408 B | -//! | [`AES_CCM_128`] and friends, decrypting, `AAD_LEN = 64` | 368 B | 400 B | 432 B | +//! | its workers, encrypting / decrypting | 96 B / 120 B | 96 B / 120 B | 96 B / 120 B | +//! | [`AES_CCM_128`] and friends, encrypting, `AAD_LEN = 64` | 176 B | 176 B | 176 B | +//! | [`AES_CCM_128`] and friends, decrypting, `AAD_LEN = 64` | 200 B | 200 B | 200 B | //! -//! The difference between the key sizes is the key schedule; the pair adds the mode's state, the -//! `AAD_LEN`-byte AAD buffer and, on the decrypting side, the held-back tag. Peak stack over a +//! The difference between the key sizes is the key schedule, which the workers borrow rather +//! than copy; the pair adds the mode's state, the `AAD_LEN`-byte AAD buffer and, on the +//! decrypting side, the held-back tag. Peak stack over a //! 16 KiB frame with AES-128, measured with massif on x86-64 in release mode by //! `mem_usage_benches/src/bench_ccm_mem_usage.rs`, including the caller's own 16 KiB arrays: //! //! | Path | Peak stack | //! |---|---| //! | process start-up alone | 7 680 B | -//! | `AES128Key>` in place, one array | 19 328 B | -//! | `AES_CCM_128` streaming encrypt, two arrays | 37 400 B | -//! | `AES_CCM_128` streaming decrypt, two arrays | 36 368 B | -//! | `AES_CCM_128` one-shot, two arrays | 36 904 B | +//! | `AES128Key>` in place, one array | 18 608 B | +//! | `AES_CCM_128` streaming encrypt, two arrays | 36 216 B | +//! | `AES_CCM_128` streaming decrypt, two arrays | 35 424 B | +//! | `AES_CCM_128` one-shot, two arrays | 35 880 B | //! //! # 🚨 Security Considerations 🚨 //! @@ -227,38 +228,95 @@ pub const CCM_TAG_LEN: usize = 16; /// for the rules. #[allow(non_camel_case_types)] pub type AES_CCM_128< + 'k, Dir, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, > = ::Select< - CcmPacketEncryptor, - CcmPacketDecryptor, + CcmPacketEncryptor< + 'k, + AES128Internal, + 16, + AES_BLOCK_LEN, + NONCE_LEN, + TAG_LEN, + AAD_LEN, + DATA_LEN, + >, + CcmPacketDecryptor< + 'k, + AES128Internal, + 16, + AES_BLOCK_LEN, + NONCE_LEN, + TAG_LEN, + AAD_LEN, + DATA_LEN, + >, >; /// AES-192 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`. See [`AES_CCM_128`]. #[allow(non_camel_case_types)] pub type AES_CCM_192< + 'k, Dir, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, > = ::Select< - CcmPacketEncryptor, - CcmPacketDecryptor, + CcmPacketEncryptor< + 'k, + AES192Internal, + 24, + AES_BLOCK_LEN, + NONCE_LEN, + TAG_LEN, + AAD_LEN, + DATA_LEN, + >, + CcmPacketDecryptor< + 'k, + AES192Internal, + 24, + AES_BLOCK_LEN, + NONCE_LEN, + TAG_LEN, + AAD_LEN, + DATA_LEN, + >, >; /// AES-256 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`. See [`AES_CCM_128`]. #[allow(non_camel_case_types)] pub type AES_CCM_256< + 'k, Dir, const NONCE_LEN: usize, const TAG_LEN: usize, const AAD_LEN: usize, const DATA_LEN: usize, > = ::Select< - CcmPacketEncryptor, - CcmPacketDecryptor, + CcmPacketEncryptor< + 'k, + AES256Internal, + 32, + AES_BLOCK_LEN, + NONCE_LEN, + TAG_LEN, + AAD_LEN, + DATA_LEN, + >, + CcmPacketDecryptor< + 'k, + AES256Internal, + 32, + AES_BLOCK_LEN, + NONCE_LEN, + TAG_LEN, + AAD_LEN, + DATA_LEN, + >, >; diff --git a/crypto/aes/src/cfb.rs b/crypto/aes/src/cfb.rs index 6fc70ba58..472a037fe 100644 --- a/crypto/aes/src/cfb.rs +++ b/crypto/aes/src/cfb.rs @@ -94,12 +94,12 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// AES-128 in CFB128 mode. #[allow(non_camel_case_types)] -pub type AES_CFB_128 = Cfb; +pub type AES_CFB_128<'k, Dir> = Cfb<'k, AES128Internal, Dir, 16, AES_BLOCK_LEN>; /// AES-192 in CFB128 mode. #[allow(non_camel_case_types)] -pub type AES_CFB_192 = Cfb; +pub type AES_CFB_192<'k, Dir> = Cfb<'k, AES192Internal, Dir, 24, AES_BLOCK_LEN>; /// AES-256 in CFB128 mode. See [`AES_CFB_128`]. #[allow(non_camel_case_types)] -pub type AES_CFB_256 = Cfb; +pub type AES_CFB_256<'k, Dir> = Cfb<'k, AES256Internal, Dir, 32, AES_BLOCK_LEN>; diff --git a/crypto/aes/src/cfb8.rs b/crypto/aes/src/cfb8.rs index e46870de0..e4de3178f 100644 --- a/crypto/aes/src/cfb8.rs +++ b/crypto/aes/src/cfb8.rs @@ -121,12 +121,12 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// AES-128 in CFB8 mode. #[allow(non_camel_case_types)] -pub type AES_CFB8_128 = Cfb8; +pub type AES_CFB8_128<'k, Dir> = Cfb8<'k, AES128Internal, Dir, 16, AES_BLOCK_LEN>; /// AES-192 in CFB8 mode. #[allow(non_camel_case_types)] -pub type AES_CFB8_192 = Cfb8; +pub type AES_CFB8_192<'k, Dir> = Cfb8<'k, AES192Internal, Dir, 24, AES_BLOCK_LEN>; /// AES-256 in CFB8 mode. See [`AES_CFB8_128`]. #[allow(non_camel_case_types)] -pub type AES_CFB8_256 = Cfb8; +pub type AES_CFB8_256<'k, Dir> = Cfb8<'k, AES256Internal, Dir, 32, AES_BLOCK_LEN>; diff --git a/crypto/aes/src/cmac.rs b/crypto/aes/src/cmac.rs index bd782cff8..9cbb45857 100644 --- a/crypto/aes/src/cmac.rs +++ b/crypto/aes/src/cmac.rs @@ -34,8 +34,8 @@ //! # Memory Usage //! //! A key is the expanded schedule plus the two 16-byte subkeys: 208, 240 and 272 bytes for -//! AES-128, -192 and -256. A worker is a clone of that plus the running `C_i`, the pending block -//! and its length: 248, 280 and 312 bytes. +//! AES-128, -192 and -256. A worker borrows that and adds the running `C_i`, the pending block +//! and its length: 48 bytes at every key length. //! //! [`AES128Key`]: crate::AES128Key //! [`MAC`]: bouncycastle_core::traits::MAC @@ -50,15 +50,15 @@ use bouncycastle_core::key_material::KeyType; /// AES-128 in CMAC. #[allow(non_camel_case_types)] -pub type AES_CMAC_128 = Cmac; +pub type AES_CMAC_128<'k> = Cmac<'k, AES128Internal, 16, 16>; /// AES-192 in CMAC. See [`AES_CMAC_128`]. #[allow(non_camel_case_types)] -pub type AES_CMAC_192 = Cmac; +pub type AES_CMAC_192<'k> = Cmac<'k, AES192Internal, 24, 16>; /// AES-256 in CMAC. See [`AES_CMAC_128`]. #[allow(non_camel_case_types)] -pub type AES_CMAC_256 = Cmac; +pub type AES_CMAC_256<'k> = Cmac<'k, AES256Internal, 32, 16>; // Per key length for the reason the GCM impls are (see `gcm.rs`): the subkey derivation needs the // permutation's `ElectronicCodeBook` bound, and `KEY_LEN` cannot be tied to @@ -74,9 +74,9 @@ impl Mode for CMAC { } impl MacMode for CMAC { - type Worker = AES_CMAC_128; + type Worker<'k> = AES_CMAC_128<'k>; - fn mac_init(state: &Self::KeyState) -> Self::Worker { + fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { Cmac::from_key_state(state) } } @@ -91,9 +91,9 @@ impl Mode for CMAC { } impl MacMode for CMAC { - type Worker = AES_CMAC_192; + type Worker<'k> = AES_CMAC_192<'k>; - fn mac_init(state: &Self::KeyState) -> Self::Worker { + fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { Cmac::from_key_state(state) } } @@ -108,9 +108,9 @@ impl Mode for CMAC { } impl MacMode for CMAC { - type Worker = AES_CMAC_256; + type Worker<'k> = AES_CMAC_256<'k>; - fn mac_init(state: &Self::KeyState) -> Self::Worker { + fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { Cmac::from_key_state(state) } } diff --git a/crypto/aes/src/ctr.rs b/crypto/aes/src/ctr.rs index 6cba5c5be..fcb37fb08 100644 --- a/crypto/aes/src/ctr.rs +++ b/crypto/aes/src/ctr.rs @@ -103,12 +103,12 @@ pub const CTR_NONCE_LEN: usize = 12; /// AES-128 in CTR mode with a 12-byte nonce. #[allow(non_camel_case_types)] -pub type AES_CTR_128 = Ctr; +pub type AES_CTR_128<'k, Dir> = Ctr<'k, AES128Internal, Dir, 16, AES_BLOCK_LEN, CTR_NONCE_LEN>; /// AES-192 in CTR mode with a 12-byte nonce. #[allow(non_camel_case_types)] -pub type AES_CTR_192 = Ctr; +pub type AES_CTR_192<'k, Dir> = Ctr<'k, AES192Internal, Dir, 24, AES_BLOCK_LEN, CTR_NONCE_LEN>; /// AES-256 in CTR mode with a 12-byte nonce. See [`AES_CTR_128`]. #[allow(non_camel_case_types)] -pub type AES_CTR_256 = Ctr; +pub type AES_CTR_256<'k, Dir> = Ctr<'k, AES256Internal, Dir, 32, AES_BLOCK_LEN, CTR_NONCE_LEN>; diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs index c5fbc691e..8a95f236a 100644 --- a/crypto/aes/src/gcm.rs +++ b/crypto/aes/src/gcm.rs @@ -155,19 +155,19 @@ use bouncycastle_core::traits::{ /// AES-128 in GCM with a 128-bit tag. #[allow(non_camel_case_types)] -pub type AES_GCM_128 = Gcm; +pub type AES_GCM_128<'k, Dir> = Gcm<'k, AES128Internal, Dir, 16, 16>; /// AES-192 in GCM with a 128-bit tag. #[allow(non_camel_case_types)] -pub type AES_GCM_192 = Gcm; +pub type AES_GCM_192<'k, Dir> = Gcm<'k, AES192Internal, Dir, 24, 16>; /// AES-256 in GCM with a 128-bit tag. See [`AES_GCM_128`]. #[allow(non_camel_case_types)] -pub type AES_GCM_256 = Gcm; +pub type AES_GCM_256<'k, Dir> = Gcm<'k, AES256Internal, Dir, 32, 16>; // The `GCM` mode impls for the three AES permutations, which is what makes `AES128Key` and // friends AEAD keys. Written per key length rather than once over `AESInternal

`: the generic -// form needs `Gcm, Dir, { P::KEY_LEN }, TAG_LEN>`, a const expression stable Rust +// form needs `Gcm<'k, AESInternal

, Dir, { P::KEY_LEN }, TAG_LEN>`, a const expression stable Rust // rejects, and a generic impl in `cipher` over `P: ElectronicCodeBook` leaves // `KEY_LEN` unconstrained on `Mode

`. Both go once `KEY_LEN` leaves the cipher traits. @@ -183,27 +183,33 @@ impl Mode for GCM { impl AeadEncryptMode for GCM { - type Encryptor = Gcm; + type Encryptor<'k> = Gcm<'k, AES128Internal, Encrypting, 16, TAG_LEN>; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - Self::Encryptor::from_key_state(state, rng) + ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + >::from_key_state(state, rng) } - fn encrypt_init_nonce(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Encryptor { - Self::Encryptor::from_key_state_nonce(state, nonce) + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Encryptor<'k> { + >::from_key_state_nonce(state, nonce) } } impl AeadDecryptMode for GCM { - type Decryptor = Gcm; + type Decryptor<'k> = Gcm<'k, AES128Internal, Decrypting, 16, TAG_LEN>; - fn decrypt_init(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Decryptor { - Self::Decryptor::from_key_state(state, nonce) + fn decrypt_init<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Decryptor<'k> { + >::from_key_state(state, nonce) } } @@ -219,27 +225,33 @@ impl Mode for GCM { impl AeadEncryptMode for GCM { - type Encryptor = Gcm; + type Encryptor<'k> = Gcm<'k, AES192Internal, Encrypting, 24, TAG_LEN>; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - Self::Encryptor::from_key_state(state, rng) + ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + >::from_key_state(state, rng) } - fn encrypt_init_nonce(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Encryptor { - Self::Encryptor::from_key_state_nonce(state, nonce) + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Encryptor<'k> { + >::from_key_state_nonce(state, nonce) } } impl AeadDecryptMode for GCM { - type Decryptor = Gcm; + type Decryptor<'k> = Gcm<'k, AES192Internal, Decrypting, 24, TAG_LEN>; - fn decrypt_init(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Decryptor { - Self::Decryptor::from_key_state(state, nonce) + fn decrypt_init<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Decryptor<'k> { + >::from_key_state(state, nonce) } } @@ -255,26 +267,32 @@ impl Mode for GCM { impl AeadEncryptMode for GCM { - type Encryptor = Gcm; + type Encryptor<'k> = Gcm<'k, AES256Internal, Encrypting, 32, TAG_LEN>; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - Self::Encryptor::from_key_state(state, rng) + ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + >::from_key_state(state, rng) } - fn encrypt_init_nonce(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Encryptor { - Self::Encryptor::from_key_state_nonce(state, nonce) + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Encryptor<'k> { + >::from_key_state_nonce(state, nonce) } } impl AeadDecryptMode for GCM { - type Decryptor = Gcm; + type Decryptor<'k> = Gcm<'k, AES256Internal, Decrypting, 32, TAG_LEN>; - fn decrypt_init(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Decryptor { - Self::Decryptor::from_key_state(state, nonce) + fn decrypt_init<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Decryptor<'k> { + >::from_key_state(state, nonce) } } diff --git a/crypto/aes/src/gmac.rs b/crypto/aes/src/gmac.rs index 8b3c692b1..5a0cde73c 100644 --- a/crypto/aes/src/gmac.rs +++ b/crypto/aes/src/gmac.rs @@ -53,15 +53,15 @@ use bouncycastle_core::traits::RNG; /// AES-128 in GMAC with a 128-bit tag. `Dir` is [`Encrypting`] to generate a tag or /// [`Decrypting`] to verify one. #[allow(non_camel_case_types)] -pub type AES_GMAC_128

= Gmac; +pub type AES_GMAC_128<'k, Dir> = Gmac<'k, AES128Internal, Dir, 16, 16>; /// AES-192 in GMAC with a 128-bit tag. See [`AES_GMAC_128`]. #[allow(non_camel_case_types)] -pub type AES_GMAC_192 = Gmac; +pub type AES_GMAC_192<'k, Dir> = Gmac<'k, AES192Internal, Dir, 24, 16>; /// AES-256 in GMAC with a 128-bit tag. See [`AES_GMAC_128`]. #[allow(non_camel_case_types)] -pub type AES_GMAC_256 = Gmac; +pub type AES_GMAC_256<'k, Dir> = Gmac<'k, AES256Internal, Dir, 32, 16>; // Per key length for the reason the GCM impls are (see `gcm.rs`): the worker carries `KEY_LEN`, // which cannot be tied to `Mode

` from a generic impl. The pair is per key length for the same @@ -81,11 +81,11 @@ impl AES128Key> { /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. - pub fn create_generator( - &self, + pub fn create_generator<'k>( + &'k self, rng: &mut dyn RNG, ) -> Result< - (Gmac, [u8; GCM_NONCE_LEN]), + (Gmac<'k, AES128Internal, Encrypting, 16, TAG_LEN>, [u8; GCM_NONCE_LEN]), SymmetricCipherError, > { Gmac::::from_key_state(&self.state, rng) @@ -93,18 +93,18 @@ impl AES128Key> { /// Begins a tag under a nonce the caller supplies, who takes on the obligation that it never /// repeat under this key. - pub fn create_generator_nonce( - &self, + pub fn create_generator_nonce<'k>( + &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac { + ) -> Gmac<'k, AES128Internal, Encrypting, 16, TAG_LEN> { Gmac::::from_key_state_nonce(&self.state, nonce) } /// Begins the verification of a tag made under `nonce`. Infallible. - pub fn create_verifier( - &self, + pub fn create_verifier<'k>( + &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac { + ) -> Gmac<'k, AES128Internal, Decrypting, 16, TAG_LEN> { Gmac::::from_key_state(&self.state, nonce) } } @@ -123,29 +123,29 @@ impl AES192Key> { /// /// # Errors /// As [`AES128Key::create_generator`]. - pub fn create_generator( - &self, + pub fn create_generator<'k>( + &'k self, rng: &mut dyn RNG, ) -> Result< - (Gmac, [u8; GCM_NONCE_LEN]), + (Gmac<'k, AES192Internal, Encrypting, 24, TAG_LEN>, [u8; GCM_NONCE_LEN]), SymmetricCipherError, > { Gmac::::from_key_state(&self.state, rng) } /// As [`AES128Key::create_generator_nonce`]. - pub fn create_generator_nonce( - &self, + pub fn create_generator_nonce<'k>( + &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac { + ) -> Gmac<'k, AES192Internal, Encrypting, 24, TAG_LEN> { Gmac::::from_key_state_nonce(&self.state, nonce) } /// As [`AES128Key::create_verifier`]. - pub fn create_verifier( - &self, + pub fn create_verifier<'k>( + &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac { + ) -> Gmac<'k, AES192Internal, Decrypting, 24, TAG_LEN> { Gmac::::from_key_state(&self.state, nonce) } } @@ -164,29 +164,29 @@ impl AES256Key> { /// /// # Errors /// As [`AES128Key::create_generator`]. - pub fn create_generator( - &self, + pub fn create_generator<'k>( + &'k self, rng: &mut dyn RNG, ) -> Result< - (Gmac, [u8; GCM_NONCE_LEN]), + (Gmac<'k, AES256Internal, Encrypting, 32, TAG_LEN>, [u8; GCM_NONCE_LEN]), SymmetricCipherError, > { Gmac::::from_key_state(&self.state, rng) } /// As [`AES128Key::create_generator_nonce`]. - pub fn create_generator_nonce( - &self, + pub fn create_generator_nonce<'k>( + &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac { + ) -> Gmac<'k, AES256Internal, Encrypting, 32, TAG_LEN> { Gmac::::from_key_state_nonce(&self.state, nonce) } /// As [`AES128Key::create_verifier`]. - pub fn create_verifier( - &self, + pub fn create_verifier<'k>( + &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac { + ) -> Gmac<'k, AES256Internal, Decrypting, 32, TAG_LEN> { Gmac::::from_key_state(&self.state, nonce) } } diff --git a/crypto/aes/src/hazmat/ecb.rs b/crypto/aes/src/hazmat/ecb.rs index b14f0b621..7904ae3f5 100644 --- a/crypto/aes/src/hazmat/ecb.rs +++ b/crypto/aes/src/hazmat/ecb.rs @@ -191,16 +191,16 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// AES-128 in ECB mode with a padding scheme. #[allow(non_camel_case_types)] -pub type AES_ECB_128 =

::Select< +pub type AES_ECB_128<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Ecb, + Ecb<'k, AES128Internal, Encrypting, 16, AES_BLOCK_LEN>, Pad, 16, 0, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Ecb, + Ecb<'k, AES128Internal, Decrypting, 16, AES_BLOCK_LEN>, Pad, 16, 0, @@ -210,16 +210,16 @@ pub type AES_ECB_128 = ::Select< /// AES-192 in ECB mode with a padding scheme. #[allow(non_camel_case_types)] -pub type AES_ECB_192 = ::Select< +pub type AES_ECB_192<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Ecb, + Ecb<'k, AES192Internal, Encrypting, 24, AES_BLOCK_LEN>, Pad, 24, 0, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Ecb, + Ecb<'k, AES192Internal, Decrypting, 24, AES_BLOCK_LEN>, Pad, 24, 0, @@ -229,16 +229,16 @@ pub type AES_ECB_192 = ::Select< /// AES-256 in ECB mode with a padding scheme. See [`AES_ECB_128`]. #[allow(non_camel_case_types)] -pub type AES_ECB_256 = ::Select< +pub type AES_ECB_256<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Ecb, + Ecb<'k, AES256Internal, Encrypting, 32, AES_BLOCK_LEN>, Pad, 32, 0, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Ecb, + Ecb<'k, AES256Internal, Decrypting, 32, AES_BLOCK_LEN>, Pad, 32, 0, diff --git a/crypto/aes/tests/alg_name_tests.rs b/crypto/aes/tests/alg_name_tests.rs index 67378cd59..481a4bd3a 100644 --- a/crypto/aes/tests/alg_name_tests.rs +++ b/crypto/aes/tests/alg_name_tests.rs @@ -1,6 +1,4 @@ -//! Each public AES alias reports its own algorithm through `Algorithm`, not the AES permutation it -//! is built on: `AES_GCM_128` is `"AES_GCM_128"`, while `AES128Internal` stays `"AES-128"`. The -//! strength is the key's, so it is the same for every mode at a key size. +//! What each public AES alias is: the algorithm it reports and the type it resolves to. //! //! The direction and the padding are type parameters of one algorithm, not different algorithms, //! so every combination of them must report the same name, and so must every CCM frame. @@ -10,16 +8,22 @@ use bouncycastle_aes::{ AES_CBC_128, AES_CBC_192, AES_CBC_256, AES_CCM_128, AES_CCM_192, AES_CCM_256, AES_CFB_128, AES_CFB_192, AES_CFB_256, AES_CFB8_128, AES_CFB8_192, AES_CFB8_256, AES_CMAC_128, AES_CMAC_192, AES_CMAC_256, AES_CTR_128, AES_CTR_192, AES_CTR_256, AES_GCM_128, AES_GCM_192, AES_GCM_256, - AES_GMAC_128, AES_GMAC_192, AES_GMAC_256, CCM_NONCE_LEN, CCM_TAG_LEN, + AES_GMAC_128, AES_GMAC_192, AES_GMAC_256, AES128Key, CCM_NONCE_LEN, CCM_TAG_LEN, }; -use bouncycastle_cipher::modes::hazmat::ecb::Ecb; -use bouncycastle_cipher::modes::{Cbc, Gcm}; +use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; +use bouncycastle_cipher::modes::{CBC, Cbc, Gcm}; use bouncycastle_cipher::padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, }; use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; use bouncycastle_core::traits::Algorithm; +use bouncycastle_rng::HashDRBG_SHA512; use core::mem::size_of; /// Both directions of one algorithm report `name` and `strength`. @@ -32,68 +36,80 @@ fn check(name: &str, strength: SecurityStrength) #[test] fn cbc() { - type Enc = AES_CBC_128; + type Enc<'k> = AES_CBC_128<'k, Encrypting, NoPadding>; assert_eq!(Enc::ALG_NAME, "AES_CBC_128"); assert_eq!(Enc::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); use SecurityStrength::*; - check::, AES_CBC_128>( + check::, AES_CBC_128<'_, Decrypting, NoPadding>>( + "AES_CBC_128", _128bit, + ); + check::, AES_CBC_128<'_, Decrypting, PKCS7>>( "AES_CBC_128", _128bit, ); - check::, AES_CBC_128>("AES_CBC_128", _128bit); - check::, AES_CBC_192>( + check::, AES_CBC_192<'_, Decrypting, NoPadding>>( + "AES_CBC_192", _192bit, + ); + check::, AES_CBC_192<'_, Decrypting, PKCS7>>( "AES_CBC_192", _192bit, ); - check::, AES_CBC_192>("AES_CBC_192", _192bit); - check::, AES_CBC_256>( + check::, AES_CBC_256<'_, Decrypting, NoPadding>>( + "AES_CBC_256", _256bit, + ); + check::, AES_CBC_256<'_, Decrypting, PKCS7>>( "AES_CBC_256", _256bit, ); - check::, AES_CBC_256>("AES_CBC_256", _256bit); } #[test] fn ecb() { use SecurityStrength::*; - check::, AES_ECB_128>( + check::, AES_ECB_128<'_, Decrypting, NoPadding>>( + "AES_ECB_128", _128bit, + ); + check::, AES_ECB_128<'_, Decrypting, PKCS7>>( "AES_ECB_128", _128bit, ); - check::, AES_ECB_128>("AES_ECB_128", _128bit); - check::, AES_ECB_192>( + check::, AES_ECB_192<'_, Decrypting, NoPadding>>( + "AES_ECB_192", _192bit, + ); + check::, AES_ECB_192<'_, Decrypting, PKCS7>>( "AES_ECB_192", _192bit, ); - check::, AES_ECB_192>("AES_ECB_192", _192bit); - check::, AES_ECB_256>( + check::, AES_ECB_256<'_, Decrypting, NoPadding>>( + "AES_ECB_256", _256bit, + ); + check::, AES_ECB_256<'_, Decrypting, PKCS7>>( "AES_ECB_256", _256bit, ); - check::, AES_ECB_256>("AES_ECB_256", _256bit); } #[test] fn cfb_cfb8_ctr() { use SecurityStrength::*; - check::, AES_CFB_128>("AES_CFB_128", _128bit); - check::, AES_CFB_192>("AES_CFB_192", _192bit); - check::, AES_CFB_256>("AES_CFB_256", _256bit); + check::, AES_CFB_128<'_, Decrypting>>("AES_CFB_128", _128bit); + check::, AES_CFB_192<'_, Decrypting>>("AES_CFB_192", _192bit); + check::, AES_CFB_256<'_, Decrypting>>("AES_CFB_256", _256bit); - check::, AES_CFB8_128>("AES_CFB8_128", _128bit); - check::, AES_CFB8_192>("AES_CFB8_192", _192bit); - check::, AES_CFB8_256>("AES_CFB8_256", _256bit); + check::, AES_CFB8_128<'_, Decrypting>>("AES_CFB8_128", _128bit); + check::, AES_CFB8_192<'_, Decrypting>>("AES_CFB8_192", _192bit); + check::, AES_CFB8_256<'_, Decrypting>>("AES_CFB8_256", _256bit); - check::, AES_CTR_128>("AES_CTR_128", _128bit); - check::, AES_CTR_192>("AES_CTR_192", _192bit); - check::, AES_CTR_256>("AES_CTR_256", _256bit); + check::, AES_CTR_128<'_, Decrypting>>("AES_CTR_128", _128bit); + check::, AES_CTR_192<'_, Decrypting>>("AES_CTR_192", _192bit); + check::, AES_CTR_256<'_, Decrypting>>("AES_CTR_256", _256bit); } #[test] fn gcm() { - type Enc = AES_GCM_128; + type Enc<'k> = AES_GCM_128<'k, Encrypting>; assert_eq!(Enc::ALG_NAME, "AES_GCM_128"); assert_eq!(Enc::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); use SecurityStrength::*; - check::, AES_GCM_128>("AES_GCM_128", _128bit); - check::, AES_GCM_192>("AES_GCM_192", _192bit); - check::, AES_GCM_256>("AES_GCM_256", _256bit); + check::, AES_GCM_128<'_, Decrypting>>("AES_GCM_128", _128bit); + check::, AES_GCM_192<'_, Decrypting>>("AES_GCM_192", _192bit); + check::, AES_GCM_256<'_, Decrypting>>("AES_GCM_256", _256bit); } #[test] @@ -102,16 +118,16 @@ fn ccm() { const N: usize = CCM_NONCE_LEN; const T: usize = CCM_TAG_LEN; // The nonce and tag lengths are parameters too, not part of the name. - check::, AES_CCM_128>( + check::, AES_CCM_128<'_, Decrypting, 7, 4, 16, 32>>( "AES_CCM_128", _128bit, ); - check::, AES_CCM_128>( + check::, AES_CCM_128<'_, Decrypting, N, T, 16, 32>>( "AES_CCM_128", _128bit, ); - check::, AES_CCM_192>( + check::, AES_CCM_192<'_, Decrypting, N, T, 16, 32>>( "AES_CCM_192", _192bit, ); - check::, AES_CCM_256>( + check::, AES_CCM_256<'_, Decrypting, N, T, 16, 32>>( "AES_CCM_256", _256bit, ); } @@ -119,9 +135,9 @@ fn ccm() { #[test] fn gmac() { use SecurityStrength::*; - check::, AES_GMAC_128>("AES_GMAC_128", _128bit); - check::, AES_GMAC_192>("AES_GMAC_192", _192bit); - check::, AES_GMAC_256>("AES_GMAC_256", _256bit); + check::, AES_GMAC_128<'_, Decrypting>>("AES_GMAC_128", _128bit); + check::, AES_GMAC_192<'_, Decrypting>>("AES_GMAC_192", _192bit); + check::, AES_GMAC_256<'_, Decrypting>>("AES_GMAC_256", _256bit); } #[test] @@ -141,7 +157,14 @@ fn cmac() { ); } -/// The aliases must resolve to exactly the types they claim to, at both directions. +fn key_for>() -> AES128Key { + let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) + .expect("a 16-byte symmetric cipher key"); + AES128Key::from_key_material(&key).expect("a valid key") +} + +/// The aliases must resolve to exactly the types they claim to, at both directions, and be what +/// the keys hand out. /// /// A type alias that quietly resolved to something else -- the wrong padding, the wrong direction, /// the wrong key length -- would still compile everywhere it is used, so this pins the projection @@ -150,44 +173,51 @@ fn cmac() { #[test] fn the_aliases_name_the_expected_types() { assert_eq!( - size_of::>(), - size_of::< - PaddedBlockCipherEncryptor, PKCS7, 16, 16, 16>, - >() + size_of::>(), + size_of::, PKCS7, 16, 16>>( + ) ); assert_eq!( - size_of::>(), - size_of::< - PaddedBlockCipherDecryptor, PKCS7, 16, 16, 16>, - >() + size_of::>(), + size_of::, PKCS7, 16, 16>>( + ) ); // The two directions are genuinely different types, so the encryptor and the decryptor do not // have to agree in size -- and here they do not, which is itself evidence the projection // selected two different adapters rather than one. assert_ne!( - size_of::>(), - size_of::>() + size_of::>(), + size_of::>() ); assert_eq!( - size_of::>(), - size_of::< - PaddedBlockCipherEncryptor, PKCS7, 16, 0, 16>, - >() + size_of::>(), + size_of::, PKCS7, 0, 16>>( + ) ); assert_eq!( - size_of::>(), - size_of::< - PaddedBlockCipherDecryptor, PKCS7, 16, 0, 16>, - >() + size_of::>(), + size_of::, PKCS7, 0, 16>>( + ) ); assert_eq!( - size_of::>(), - size_of::>() + size_of::>(), + size_of::>() ); assert_eq!( - size_of::>(), - size_of::>() - ); + size_of::>(), + size_of::>() + ); + + // ...and the keys hand out exactly those types. + let mut rng = HashDRBG_SHA512::new_from_os(); + let key = key_for::>(); + let (enc, iv) = key.create_encryptor(&mut rng).unwrap(); + let _: AES_CBC_128<'_, Encrypting, PKCS7> = enc; + let _: AES_CBC_128<'_, Decrypting, PKCS7> = key.create_decryptor(&iv); + let key = key_for::>(); + let (enc, no_iv) = key.create_encryptor(&mut rng).unwrap(); + let _: AES_ECB_128<'_, Encrypting, PKCS7> = enc; + let _: AES_ECB_128<'_, Decrypting, PKCS7> = key.create_decryptor(&no_iv); } diff --git a/crypto/aes/tests/ccm_key_tests.rs b/crypto/aes/tests/ccm_key_tests.rs index c36fbb5e4..2ea90139a 100644 --- a/crypto/aes/tests/ccm_key_tests.rs +++ b/crypto/aes/tests/ccm_key_tests.rs @@ -190,17 +190,18 @@ fn a_short_nonce_is_reachable_through_the_supplied_nonce_forms() { assert_eq!(buf, msg); } -/// The workers are the mode's `Ccm` state, 264/296/328 B for AES-128/192/256, with the -/// decryptor adding the tag it holds back and that tag's length; the aes docs quote these. +/// The workers are the mode's `Ccm` state over a borrowed schedule, 96 B at every key length, +/// with the decryptor adding the tag it holds back and that tag's length; the aes docs quote +/// these. #[test] fn worker_sizes_match_the_documented_memory_table() { use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_cipher::modes::{CcmDecryptor, CcmEncryptor}; use core::mem::size_of; - assert_eq!(size_of::>(), 264); - assert_eq!(size_of::>(), 296); - assert_eq!(size_of::>(), 328); - assert_eq!(size_of::>(), 288); - assert_eq!(size_of::>(), 320); - assert_eq!(size_of::>(), 352); + assert_eq!(size_of::>(), 96); + assert_eq!(size_of::>(), 96); + assert_eq!(size_of::>(), 96); + assert_eq!(size_of::>(), 120); + assert_eq!(size_of::>(), 120); + assert_eq!(size_of::>(), 120); } diff --git a/crypto/aes/tests/cfb8_bc-test-data.rs b/crypto/aes/tests/cfb8_bc-test-data.rs index 3f54f4a57..098d26c86 100644 --- a/crypto/aes/tests/cfb8_bc-test-data.rs +++ b/crypto/aes/tests/cfb8_bc-test-data.rs @@ -91,11 +91,12 @@ fn run_case( grouping: Grouping, ) -> Vec where + K: 'static, K: FromKeyMaterial + SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + for<'k> K::Encryptor<'k>: StreamCipherEncryptor, + for<'k> K::Decryptor<'k>: StreamCipherDecryptor, { let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); diff --git a/crypto/aes/tests/cfb_bc-test-data.rs b/crypto/aes/tests/cfb_bc-test-data.rs index a4704d66d..462d57bcf 100644 --- a/crypto/aes/tests/cfb_bc-test-data.rs +++ b/crypto/aes/tests/cfb_bc-test-data.rs @@ -96,11 +96,12 @@ fn run_case( grouping: Grouping, ) -> Vec where + K: 'static, K: FromKeyMaterial + SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + for<'k> K::Encryptor<'k>: StreamCipherEncryptor, + for<'k> K::Decryptor<'k>: StreamCipherDecryptor, { let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); diff --git a/crypto/aes/tests/cmac_key_tests.rs b/crypto/aes/tests/cmac_key_tests.rs index ef90143ac..bf9ff2d37 100644 --- a/crypto/aes/tests/cmac_key_tests.rs +++ b/crypto/aes/tests/cmac_key_tests.rs @@ -136,6 +136,6 @@ fn sizes() { ); assert_eq!( [size_of::(), size_of::(), size_of::()], - [248, 280, 312] + [48, 48, 48] ); } diff --git a/crypto/aes/tests/cmac_tests.rs b/crypto/aes/tests/cmac_tests.rs index 10c739e86..0cdb9b450 100644 --- a/crypto/aes/tests/cmac_tests.rs +++ b/crypto/aes/tests/cmac_tests.rs @@ -6,7 +6,7 @@ use bouncycastle_aes::hazmat::{AES128Internal, AES256Internal}; use bouncycastle_aes::{AES_CMAC_128, AES_CMAC_256, AES128Key, AES192Key, AES256Key}; use bouncycastle_cipher::modes::{CMAC, CMAC_MIN_TAG_LEN, ModeNames}; -use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::hazmat::{ModeKey, do_hazardous_operations}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; @@ -131,8 +131,8 @@ fn truncation_to_the_minimum_tag() { fn algorithm_params() { assert_eq!(AES_CMAC_128::ALG_NAME, "AES_CMAC_128"); assert_eq!(AES_CMAC_256::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); - let cmac = - AES192Key::::from_key_material(&mac_key::<24>(KEY_192)).unwrap().create_generator(); + let key = AES192Key::::from_key_material(&mac_key::<24>(KEY_192)).unwrap(); + let cmac = key.create_generator(); assert_eq!(cmac.output_len(), 16); assert_eq!(cmac.max_security_strength(), SecurityStrength::_192bit); assert_eq!(STATE_LEN, 36); @@ -151,7 +151,7 @@ fn suspend_resume() { let mut cmac = key.create_generator(); cmac.do_update(&msg[..split]); let state: [u8; STATE_LEN] = cmac.suspend(); - let mut resumed = AES_CMAC_128::from_suspended(state, &material).unwrap(); + let mut resumed = AES_CMAC_128::from_suspended(state, key.state()).unwrap(); resumed.do_update(&msg[split..*mlen]); assert_eq!(resumed.do_final(), expected, "mlen {mlen}, suspended at {split}"); } diff --git a/crypto/aes/tests/cmac_wycheproof.rs b/crypto/aes/tests/cmac_wycheproof.rs index c5206d907..082623548 100644 --- a/crypto/aes/tests/cmac_wycheproof.rs +++ b/crypto/aes/tests/cmac_wycheproof.rs @@ -21,7 +21,7 @@ use cmac_helpers::mac_key; fn run_case(tc_id: u64, key: &[u8], msg: &[u8], tag: &[u8], valid: bool) where K: FromKeyMaterial + MacGenerationKey + MacVerificationKey, - ::Generator: MAC, + for<'k> ::Generator<'k>: MAC, { let material = mac_key::(key); let key = K::from_key_material(&material).unwrap_or_else(|e| panic!("tcId {tc_id}: {e:?}")); diff --git a/crypto/aes/tests/ctr_bc-test-data.rs b/crypto/aes/tests/ctr_bc-test-data.rs index 9615083f9..b6d6fbdd1 100644 --- a/crypto/aes/tests/ctr_bc-test-data.rs +++ b/crypto/aes/tests/ctr_bc-test-data.rs @@ -96,11 +96,12 @@ fn run_case( grouping: Grouping, ) -> Vec where + K: 'static, K: FromKeyMaterial + SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + for<'k> K::Encryptor<'k>: StreamCipherEncryptor, + for<'k> K::Decryptor<'k>: StreamCipherDecryptor, { let key = K::from_key_material(&cipher_key::(key_bytes)).expect("a valid key"); let mut data = input.to_vec(); diff --git a/crypto/aes/tests/ctr_bc_java_tests.rs b/crypto/aes/tests/ctr_bc_java_tests.rs index 43705a1fb..a1deacc9c 100644 --- a/crypto/aes/tests/ctr_bc_java_tests.rs +++ b/crypto/aes/tests/ctr_bc_java_tests.rs @@ -60,9 +60,9 @@ fn key() -> AES128Key> { fn keystream(nonce_hex: &str, blocks: usize) -> Vec { let nonce: [u8; NONCE_LEN] = hex::decode(nonce_hex).expect("valid hex").try_into().expect("nonce length"); - let (mut enc, got) = key::() - .create_encryptor(&mut FixedSeedRNG::::new(nonce)) - .expect("encrypt init"); + let key = key::(); + let (mut enc, got) = + key.create_encryptor(&mut FixedSeedRNG::::new(nonce)).expect("encrypt init"); assert_eq!(got, nonce, "the pinned RNG should reproduce the nonce"); let mut data = vec![0u8; blocks * 16]; @@ -152,7 +152,8 @@ fn three_byte_counter_matches_bc_java() { fn the_counter_limit_falls_where_bc_java_throws() { let nonce: [u8; 15] = hex::decode("5a5b5c5d5e5f606162636465666768").unwrap().try_into().unwrap(); - let (mut enc, _) = key::<15>().create_encryptor(&mut FixedSeedRNG::<15>::new(nonce)).unwrap(); + let key = key::<15>(); + let (mut enc, _) = key.create_encryptor(&mut FixedSeedRNG::<15>::new(nonce)).unwrap(); // BC Java encrypts 4096 bytes under this IV without complaint. let mut data = vec![0u8; 4096]; diff --git a/crypto/aes/tests/ctr_vector_tests.rs b/crypto/aes/tests/ctr_vector_tests.rs index fd84238ca..8fb04e8a3 100644 --- a/crypto/aes/tests/ctr_vector_tests.rs +++ b/crypto/aes/tests/ctr_vector_tests.rs @@ -99,11 +99,12 @@ const CHUNKINGS: [usize; 6] = [1, 5, 16, 17, 33, 69]; fn check(name: &str, key_hex: &str, expected_hex: &str) where + K: 'static, K: FromKeyMaterial + SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + for<'k> K::Encryptor<'k>: StreamCipherEncryptor, + for<'k> K::Decryptor<'k>: StreamCipherDecryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let nonce: [u8; NONCE_LEN] = unhex(NONCE).try_into().expect("a 12-byte nonce"); diff --git a/crypto/aes/tests/gmac_key_tests.rs b/crypto/aes/tests/gmac_key_tests.rs index 9bc0f6e18..ec77684bc 100644 --- a/crypto/aes/tests/gmac_key_tests.rs +++ b/crypto/aes/tests/gmac_key_tests.rs @@ -1,10 +1,8 @@ //! `AES*Key`: the key object holding `H`, and the pair it hands out producing exactly what -//! `Gmac::new` and `AES_GCM_*` over AAD alone produce from the same bytes. +//! `AES_GCM_*` over AAD alone produces from the same bytes. use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal}; -use bouncycastle_aes::{ - AES_GCM_128, AES_GMAC_128, AES_GMAC_192, AES_GMAC_256, AES128Key, AES192Key, AES256Key, -}; +use bouncycastle_aes::{AES_GCM_128, AES_GMAC_128, AES_GMAC_192, AES128Key, AES192Key, AES256Key}; use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN, GMAC, GcmKeyState, Gmac}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; @@ -24,8 +22,8 @@ fn rng() -> FixedSeedRNG { FixedSeedRNG::new(NONCE) } -/// The key object, the material constructor and GCM over AAD alone give one tag, at every key -/// length and through both generator constructors. +/// The key object and GCM over AAD alone give one tag, at every key length and through both +/// generator constructors. #[test] fn the_key_and_the_other_routes_agree_at_every_key_length() { macro_rules! agree { @@ -41,17 +39,8 @@ fn the_key_and_the_other_routes_agree_at_every_key_length() { "{} supplied nonce", $klen ); - assert_eq!( - $alias::::new_nonce(&mac_key, &NONCE).unwrap().mac(MSG), - tag, - "{} Gmac::new_nonce", - $klen - ); - let (gen_, nonce) = $alias::::new(&mac_key, &mut rng()).unwrap(); - assert_eq!((nonce, gen_.mac(MSG)), (NONCE, tag), "{} Gmac::new", $klen); assert!(key.create_verifier(&NONCE).verify(MSG, &tag)); assert!(!key.create_verifier(&NONCE).verify(b"another message", &tag)); - assert!($alias::::new(&mac_key, &NONCE).unwrap().verify(MSG, &tag)); tag }}; } @@ -136,10 +125,6 @@ fn a_key_below_its_own_strength_is_refused_at_construction() { AES256Key::::from_key_material(&weak), Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) )); - assert!(matches!( - AES_GMAC_256::::new_nonce(&weak, &NONCE), - Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) - )); } #[test] @@ -160,9 +145,9 @@ fn sizes() { [192, 224, 256] ); assert_eq!(size_of::>(), size_of::>()); - assert_eq!(size_of::>(), size_of::>()); + assert_eq!(size_of::>(), size_of::>()); assert_eq!( - size_of::>(), - size_of::>() + size_of::>(), + size_of::>() ); } diff --git a/crypto/aes/tests/sp800_38a_cfb8_tests.rs b/crypto/aes/tests/sp800_38a_cfb8_tests.rs index 78b637226..2b19bb0b0 100644 --- a/crypto/aes/tests/sp800_38a_cfb8_tests.rs +++ b/crypto/aes/tests/sp800_38a_cfb8_tests.rs @@ -136,8 +136,9 @@ const CHUNKINGS: [usize; 7] = [1, 3, 4, 8, 9, 17, 18]; /// how the calls are grouped. fn check_encrypt(section: &str, key_hex: &str, expected_hex: &str) where + K: 'static, K: FromKeyMaterial + SymmetricEncryptionKey, - K::Encryptor: StreamCipherEncryptor, + for<'k> K::Encryptor<'k>: StreamCipherEncryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); @@ -161,8 +162,9 @@ where /// Runs one Appendix F.3 CFB8 decrypt subsection. fn check_decrypt(section: &str, key_hex: &str, ciphertext_hex: &str) where + K: 'static, K: FromKeyMaterial + SymmetricDecryptionKey, - K::Decryptor: StreamCipherDecryptor, + for<'k> K::Decryptor<'k>: StreamCipherDecryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); diff --git a/crypto/aes/tests/sp800_38a_cfb_tests.rs b/crypto/aes/tests/sp800_38a_cfb_tests.rs index d17b10587..26db521f7 100644 --- a/crypto/aes/tests/sp800_38a_cfb_tests.rs +++ b/crypto/aes/tests/sp800_38a_cfb_tests.rs @@ -137,8 +137,9 @@ const ODD_CHUNKS: [usize; 3] = [5, 23, 63]; /// that straddle the segments -- the vector should not care how the calls are grouped. fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) where + K: 'static, K: FromKeyMaterial + SymmetricEncryptionKey, - K::Encryptor: StreamCipherEncryptor, + for<'k> K::Encryptor<'k>: StreamCipherEncryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); @@ -183,8 +184,9 @@ where /// straddle the segments. fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) where + K: 'static, K: FromKeyMaterial + SymmetricDecryptionKey, - K::Decryptor: StreamCipherDecryptor, + for<'k> K::Decryptor<'k>: StreamCipherDecryptor, { let key = K::from_key_material(&key_material::(key_hex)).expect("a valid key"); let iv = block(IV); diff --git a/crypto/aes/tests/sp800_38c_tests.rs b/crypto/aes/tests/sp800_38c_tests.rs index 469b66b82..ee7818a6c 100644 --- a/crypto/aes/tests/sp800_38c_tests.rs +++ b/crypto/aes/tests/sp800_38c_tests.rs @@ -970,8 +970,8 @@ fn an_inline_ciphertext_shorter_than_the_tag_is_rejected() { #[test] fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { const DATA_LEN: usize = 32; - type Enc = CcmPacketEncryptor; - type Dec = CcmPacketDecryptor; + type Enc<'k> = CcmPacketEncryptor<'k, AES128Internal, 16, 16, 12, 16, 16, DATA_LEN>; + type Dec<'k> = CcmPacketDecryptor<'k, AES128Internal, 16, 16, 12, 16, 16, DATA_LEN>; let key = frame_key::<12, 16, 16, DATA_LEN>(); let declared = AES128Key::>::from_key_material(&material::<16>(APPENDIX_C_KEY)) .expect("the appendix key"); @@ -1087,42 +1087,45 @@ fn each_direction_has_its_own_methods() { // ---- memory ------------------------------------------------------------------------------ -/// Pins the sizes: the fixed-frame pair is the mode's `Ccm` state (264/296/328 B for -/// AES-128/192/256, independent of `NONCE_LEN`/`TAG_LEN`) plus the `AAD_LEN` buffer and a few -/// words of bookkeeping, independent of `DATA_LEN`. +/// Pins the sizes: the fixed-frame pair is the mode's `Ccm` state over a borrowed schedule (96 B +/// at every key length, independent of `NONCE_LEN`/`TAG_LEN`) plus the `AAD_LEN` buffer and a +/// few words of bookkeeping, independent of `DATA_LEN`. #[test] fn sizes_match_the_documented_memory_table() { use core::mem::size_of; // The pair holds no payload: a 4 KiB frame costs exactly what a 16-byte one does. - let enc_64 = size_of::>(); - assert_eq!(enc_64, size_of::>()); + let enc_64 = size_of::>(); + assert_eq!(enc_64, size_of::>()); assert_eq!( enc_64, - 264 + 64 + 16, + 96 + 64 + 16, "the `Ccm` state, the AAD buffer, its length and the phase flag" ); assert_eq!( - size_of::>() - enc_64, + size_of::>() - enc_64, 4096 - 64, "the value grows by exactly the AAD capacity" ); // Independent of NONCE_LEN and TAG_LEN: the nonce lives inside the counter template and the // tag is assembled at finalization, not held. - assert_eq!(enc_64, size_of::>()); - assert_eq!(enc_64, size_of::>()); - // The key schedule is the difference between the key lengths. + assert_eq!(enc_64, size_of::>()); assert_eq!( - size_of::>(), - enc_64 + 32 + enc_64, + size_of::>() + ); + // The key schedule stays in the key, so the key length makes no difference. + assert_eq!( + size_of::>(), + enc_64 ); assert_eq!( - size_of::>(), - enc_64 + 64 + size_of::>(), + enc_64 ); // The decryptor adds the tag it holds back and that tag's length. - let dec_64 = size_of::>(); - assert_eq!(dec_64, size_of::>()); + let dec_64 = size_of::>(); + assert_eq!(dec_64, size_of::>()); assert_eq!(dec_64, enc_64 + 16 + 8); } diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 0fc3fbd2e..8651aca7d 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -652,14 +652,17 @@ impl FromKeyMaterial for AsconAEAD128Key { } impl AeadEncryptionKey for AsconAEAD128Key { - type Encryptor = Ascon_AEAD128; + type Encryptor<'k> + = Ascon_AEAD128 + where + Self: 'k; /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { let mut nonce = [0u8; NONCE_LEN]; rng.next_bytes_out(&mut nonce)?; Ok((Ascon_AEAD128::from_key_words(&self.key, &nonce), nonce)) @@ -667,15 +670,18 @@ impl AeadEncryptionKey for AsconAEAD128Key /// Under a nonce the caller supplies, who takes on the obligation that it never repeat /// under this key (SP 800-232 R3). - fn create_encryptor_nonce(&self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor { + fn create_encryptor_nonce<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor<'k> { Ascon_AEAD128::from_key_words(&self.key, nonce) } } impl AeadDecryptionKey for AsconAEAD128Key { - type Decryptor = Ascon_AEAD128; + type Decryptor<'k> + = Ascon_AEAD128 + where + Self: 'k; - fn create_decryptor(&self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor { + fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { Ascon_AEAD128::from_key_words(&self.key, nonce) } } @@ -719,7 +725,9 @@ const AEAD128_STATE_TAG: u8 = 0x04; const ENC_DIR_BITS: u8 = 0; const DEC_DIR_BITS: u8 = 4; -impl SuspendableKeyed for Ascon_AEAD128 { +impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN> + for Ascon_AEAD128 +{ // The 128-bit key must be re-supplied when resuming; it is never part of the serialized state, // and is re-validated exactly as construction validates it. type Key = KeyMaterial; @@ -731,7 +739,7 @@ impl SuspendableKeyed for Ascon_AEA /// A state suspended by the decrypting side is [`SuspendableError::InvalidData`] here. fn from_suspended( serialized_state: [u8; SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN], - key: &Self::Key, + key: &'k Self::Key, ) -> Result { Self::resume_as(serialized_state, key, ENC_DIR_BITS) } @@ -739,7 +747,9 @@ impl SuspendableKeyed for Ascon_AEA /// The core's state, then the held-back bytes and their count; see /// [`SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN`]. -impl SuspendableKeyed for Ascon_AEAD128 { +impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN> + for Ascon_AEAD128 +{ type Key = KeyMaterial; fn suspend(self) -> [u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN] { @@ -755,7 +765,7 @@ impl SuspendableKeyed for Ascon_AEA /// [`SuspendableError::InvalidData`]. fn from_suspended( serialized_state: [u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN], - key: &Self::Key, + key: &'k Self::Key, ) -> Result { // infallible: the slice is exactly CIPHER_STATE_LEN bytes by construction. let cipher_state: [u8; CIPHER_STATE_LEN] = diff --git a/crypto/cipher/src/lib.rs b/crypto/cipher/src/lib.rs index 375093ecc..351a1dee1 100644 --- a/crypto/cipher/src/lib.rs +++ b/crypto/cipher/src/lib.rs @@ -13,7 +13,8 @@ //! # Suspending and resuming execution //! //! Every mode and adapter implements `SuspendableKeyed`, so a message in progress can be suspended -//! to a byte array and resumed later with the re-supplied key. The length of that array is the +//! to a byte array and resumed later over the re-supplied key state, which a key object exposes +//! through `bouncycastle_core::hazmat::ModeKey::state`. The length of that array is the //! type's `SUSPENDED_STATE_LEN`, and a wrong length is a compile error; the mechanism is //! [`bouncycastle_utils::suspendable_state`]. A suspended state holds everything the message in //! progress depends on except the key -- a chaining block, live keystream, a running MAC -- so @@ -29,7 +30,7 @@ //! # mod toy { include!("../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; //! -//! type ToyCbc = Cbc; +//! type ToyCbc<'k> = Cbc<'k, ToyBlockCipher, Encrypting, 16>; //! const STATE_LEN: usize = ToyCbc::SUSPENDED_STATE_LEN; //! //! // The mode's key state, as a mode-bound key such as `AES128Key>` holds it. @@ -98,8 +99,8 @@ mod sealed { /// /// ```text /// pub type AES_CBC_128 = ::Select< -/// PaddedBlockCipherEncryptor, Pad, 16, 16, 16>, -/// PaddedBlockCipherDecryptor, Pad, 16, 16, 16>, +/// PaddedBlockCipherEncryptor, Pad, 16, 16>, +/// PaddedBlockCipherDecryptor, Pad, 16, 16>, /// >; /// ``` /// diff --git a/crypto/cipher/src/modes/cmac.rs b/crypto/cipher/src/modes/cmac.rs index 3ad63f50e..6e6489428 100644 --- a/crypto/cipher/src/modes/cmac.rs +++ b/crypto/cipher/src/modes/cmac.rs @@ -7,8 +7,8 @@ //! //! The way onto it is a mode-bound key, built with `from_key_material`, which holds a //! [`CmacKeyState`] -- the keyed permutation and the two subkeys, derived once -- and hands out -//! workers through [`CMAC`], the marker a key type such as `bouncycastle_aes::AES128Key` is -//! bound to. +//! workers that borrow it through [`CMAC`], the marker a key type such as +//! `bouncycastle_aes::AES128Key` is bound to. //! //! # Usage Examples //! @@ -23,7 +23,7 @@ //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::traits::MAC; //! -//! type ToyCmac = Cmac; +//! type ToyCmac<'k> = Cmac<'k, ToyBlockCipher, 16, 16>; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); //! let state = CmacKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); @@ -64,12 +64,14 @@ //! chosen block cipher", so the key's `from_key_material` accepts only a [`KeyType::MACKey`] //! key (or an all-zero key, as HMAC does), never a [`KeyType::SymmetricCipherKey`]. //! * **Replay** (Appendix C): a valid tag says nothing about freshness; that is the protocol's job. +//! +//! [`KeyType::MACKey`]: bouncycastle_core::key_material::KeyType::MACKey +//! [`KeyType::SymmetricCipherKey`]: bouncycastle_core::key_material::KeyType::SymmetricCipherKey use super::ModeNames; use alloc::vec::Vec; -use bouncycastle_core::errors::{KeyMaterialError, MACError, SuspendableError}; -use bouncycastle_core::hazmat::{ElectronicCodeBook, do_hazardous_operations}; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::errors::{MACError, SuspendableError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, MAC, SuspendableKeyed}; use bouncycastle_utils::ct::ct_eq_bytes; @@ -138,11 +140,11 @@ impl CmacKeyState { /// CMAC (SP 800-38B) over the permutation `P`, keyed by a `KEY_LEN`-byte key, with a `BLOCK_LEN` /// of 8 or 16 bytes. See the module docs. -pub struct Cmac +pub struct Cmac<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize> where P: ElectronicCodeBook + ModeNames, { - key: CmacKeyState, + key: &'k CmacKeyState, /// `C_i` of Sec 6.2 step 6 for the blocks absorbed so far; `C_0 = 0^b` (step 5). c: Secret<[u8; BLOCK_LEN]>, /// The last `pending_len` bytes of message seen. Held back even when full, because only @@ -152,7 +154,7 @@ where pending_len: usize, } -impl Cmac +impl<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize> Cmac<'k, P, KEY_LEN, BLOCK_LEN> where P: ElectronicCodeBook + ModeNames, { @@ -160,54 +162,12 @@ where /// pending block, and its length. pub const SUSPENDED_STATE_LEN: usize = 3 + 2 * BLOCK_LEN + 1; - /// Begins a MAC over a new message from a key state, which is cloned into the worker. - pub fn from_key_state(state: &CmacKeyState) -> Self - where - P: Clone, - { - Self::from_owned_state(state.clone()) - } - - fn from_owned_state(key: CmacKeyState) -> Self { + /// Begins a MAC over a new message over a key state, which the worker borrows: the + /// permutation and the subkeys stay in the key. + pub fn from_key_state(key: &'k CmacKeyState) -> Self { Self { key, c: Secret::new(), pending: Secret::new(), pending_len: 0 } } - /// Validates `key` and derives the subkeys. The checks run type, then strength, then length, - /// so a short-strength key is reported as such whatever its length. - fn init( - key: &(impl KeyMaterialTrait + ?Sized), - allow_weak_key: bool, - ) -> Result { - if !(key.key_type() == KeyType::MACKey || key.key_type() == KeyType::Zeroized) { - return Err(KeyMaterialError::InvalidKeyType("CMAC: key type must be a MAC key").into()); - } - if !allow_weak_key && key.security_strength() < P::MAX_SECURITY_STRENGTH { - return Err(KeyMaterialError::SecurityStrength( - "CMAC: provided key has a lower security strength than the block cipher", - ) - .into()); - } - if key.key_len() != KEY_LEN { - return Err(KeyMaterialError::InvalidLength.into()); - } - - // `P::new` accepts only a full-strength `SymmetricCipherKey`. The MAC key's type, strength - // and length were checked above, so this copy is retagged to pass that gate and is used for - // nothing else; `P::new` then has nothing left to reject. - let mut cipher_key = KeyMaterial::::from_bytes_as_type( - key.ref_to_bytes(), - KeyType::SymmetricCipherKey, - )?; - do_hazardous_operations(&mut cipher_key, |k| { - k.set_key_type(KeyType::SymmetricCipherKey)?; - k.set_security_strength(P::MAX_SECURITY_STRENGTH) - })?; - let perm = P::new(&cipher_key) - .map_err(|_| MACError::GenericError("CMAC: the block cipher rejected the key"))?; - - Ok(Self::from_owned_state(CmacKeyState::new(perm))) - } - /// Sec 6.2 step 6 for one block: `C_i = CIPH_K(C_{i-1} (+) M_i)`, in place in `c`. /// /// An associated function over the fields rather than a `&mut self` method, so that `pending` @@ -237,7 +197,8 @@ where } } -impl Algorithm for Cmac +impl<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize> Algorithm + for Cmac<'k, P, KEY_LEN, BLOCK_LEN> where P: ElectronicCodeBook + ModeNames, { @@ -245,7 +206,7 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl MAC for Cmac +impl<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize> MAC for Cmac<'k, P, KEY_LEN, BLOCK_LEN> where P: ElectronicCodeBook + ModeNames, { @@ -326,18 +287,17 @@ where } } -/// The state is `C_i` and the pending block; the subkeys are recomputed from the re-supplied key, -/// which must be the one the suspended `Cmac` was created with -- a different key cannot be -/// detected and silently produces a wrong tag. As for HMAC, the key's security strength is not -/// re-checked on resume. +/// The state is `C_i` and the pending block; the subkeys are the re-supplied key state's, which +/// must be the one the suspended `Cmac` was created with -- a different key cannot be detected +/// and silently produces a wrong tag. /// /// `STATE_LEN` must be [`Cmac::SUSPENDED_STATE_LEN`]; any other value fails to compile. -impl - SuspendableKeyed for Cmac +impl<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize, const STATE_LEN: usize> + SuspendableKeyed<'k, STATE_LEN> for Cmac<'k, P, KEY_LEN, BLOCK_LEN> where P: ElectronicCodeBook + ModeNames, { - type Key = dyn KeyMaterialTrait; + type Key = CmacKeyState; fn suspend(self) -> [u8; STATE_LEN] { const { @@ -357,9 +317,11 @@ where /// # Errors /// [`SuspendableError::IncompatibleVersion`] for a state from an incompatible library version, - /// and [`SuspendableError::InvalidData`] for a malformed state or a key the CMAC key's - /// `from_key_material_allow_weak` would reject. - fn from_suspended(state: [u8; STATE_LEN], key: &Self::Key) -> Result { + /// and [`SuspendableError::InvalidData`] for a malformed state. + fn from_suspended( + state: [u8; STATE_LEN], + key: &'k Self::Key, + ) -> Result { const { assert!( STATE_LEN == 3 + 2 * BLOCK_LEN + 1, @@ -371,7 +333,7 @@ where if pending_len > BLOCK_LEN { return Err(SuspendableError::InvalidData); } - let mut cmac = Self::init(key, true).map_err(|_| SuspendableError::InvalidData)?; + let mut cmac = Self::from_key_state(key); cmac.c.copy_from_slice(&body[..BLOCK_LEN]); cmac.pending.copy_from_slice(&body[BLOCK_LEN..2 * BLOCK_LEN]); cmac.pending_len = pending_len; diff --git a/crypto/cipher/src/modes/ctr.rs b/crypto/cipher/src/modes/ctr.rs index 48894896e..2b4a54b6b 100644 --- a/crypto/cipher/src/modes/ctr.rs +++ b/crypto/cipher/src/modes/ctr.rs @@ -102,7 +102,7 @@ //! [`Ctr`] implements [`SuspendableKeyed`](bouncycastle_core::traits::SuspendableKeyed), so a //! message in progress can be suspended to a byte array and resumed later with the re-supplied key. //! The state is the nonce, the next counter value and the partly used keystream block; the -//! permutation is rebuilt from the key. The array length is `Ctr::SUSPENDED_STATE_LEN`; see [the +//! permutation is the key's, re-supplied by reference. The array length is `Ctr::SUSPENDED_STATE_LEN`; see [the //! crate docs](crate#suspending-and-resuming-execution) for an example. //! //! # 🚨 Security Considerations 🚨 @@ -161,14 +161,17 @@ impl where P: ElectronicCodeBook + ModeNames + Clone, { - type Encryptor = Ctr; + type Encryptor<'k> + = Ctr<'k, P, Encrypting, KEY_LEN, BLOCK_LEN, NONCE_LEN> + where + P: 'k; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { let nonce = random_iv::(rng)?; - Ok((StreamCipher::from_keystream(CtrKeyStream::start(state.clone(), nonce)), nonce)) + Ok((StreamCipher::from_keystream(CtrKeyStream::from_perm(state, nonce)), nonce)) } } @@ -177,10 +180,16 @@ impl where P: ElectronicCodeBook + ModeNames + Clone, { - type Decryptor = Ctr; + type Decryptor<'k> + = Ctr<'k, P, Decrypting, KEY_LEN, BLOCK_LEN, NONCE_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState, init_data: &[u8; NONCE_LEN]) -> Self::Decryptor { - StreamCipher::from_keystream(CtrKeyStream::start(state.clone(), *init_data)) + fn decrypt_init<'k>( + state: &'k Self::KeyState, + init_data: &[u8; NONCE_LEN], + ) -> Self::Decryptor<'k> { + StreamCipher::from_keystream(CtrKeyStream::from_perm(state, *init_data)) } } @@ -215,9 +224,9 @@ where /// // 1-byte counter /// let _ = CTR::<15>::encrypt_init(& as Mode>::key_state(perm), &mut rng).unwrap(); /// ``` -pub type Ctr = +pub type Ctr<'k, P, Dir, const KEY_LEN: usize, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> = StreamCipher< - CtrKeyStream, + CtrKeyStream<'k, P, KEY_LEN, BLOCK_LEN, INIT_DATA_LEN>, Dir, KEY_LEN, INIT_DATA_LEN, diff --git a/crypto/cipher/src/modes/gmac.rs b/crypto/cipher/src/modes/gmac.rs index 23fa0e8ee..ca4e789ca 100644 --- a/crypto/cipher/src/modes/gmac.rs +++ b/crypto/cipher/src/modes/gmac.rs @@ -6,21 +6,21 @@ //! //! [`Gmac`] is a [`Gcm`] that is only ever given AAD, so it inherits `Gcm`'s 96-bit nonce and its //! `TAG_LEN` range of 12 to 16 bytes. Following that sentence of Sec 5.2, the direction markers -//! carry over too: `Gmac<.., Encrypting, ..>` generates a tag and `Gmac<.., Decrypting, ..>` +//! carry over too: `Gmac<'k, .., Encrypting, ..>` generates a tag and `Gmac<'k, .., Decrypting, ..>` //! verifies one. //! -//! Every tag is bound to a nonce, which is why GMAC implements neither [`MAC`] (whose `new` has -//! nowhere to put one) nor the MAC mode trait. The generating side draws the nonce from the RNG +//! Every tag is bound to a nonce, which is why GMAC implements neither [`MAC`] (whose workers take +//! no nonce) nor the MAC mode trait. The generating side draws the nonce from the RNG //! it is given and returns it, or takes one from a caller who manages its own; the verifying side //! takes the nonce the tag was made under. A mode-bound key, `AES128Key`, hands out both -//! sides from one stored `H`. +//! sides from one stored `H`, which the workers borrow. //! //! The key is [`KeyType::MACKey`] material, as CMAC's is, and so cannot also be a GCM key: a //! nonce shared between a GMAC tag and a GCM message under one key is a repeated nonce. //! //! # Usage Examples //! -//! From a key state, which is what a mode-bound key holds: +//! From a key state, which is what a mode-bound key holds and what the workers borrow: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } @@ -31,7 +31,7 @@ //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGmac = Gmac; +//! type ToyGmac<'k, Dir> = Gmac<'k, ToyBlockCipher, Dir, 16, 16>; //! //! let mut rng = HashDRBG_SHA512::new_from_os(); //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); @@ -47,25 +47,6 @@ //! assert!(verifier.do_verify_final(&tag)); //! ``` //! -//! From the material, for one message: -//! -//! ``` -//! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } -//! # use toy::ToyBlockCipher; -//! use bouncycastle_cipher::modes::Gmac; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_rng::HashDRBG_SHA512; -//! -//! type ToyGmac = Gmac; -//! -//! let mut rng = HashDRBG_SHA512::new_from_os(); -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::MACKey).unwrap(); -//! let (generator, nonce) = ToyGmac::::new(&key, &mut rng).unwrap(); -//! let tag = generator.mac(b"a message"); -//! assert!(ToyGmac::::new(&key, &nonce).unwrap().verify(b"a message", &tag)); -//! ``` -//! //! # 🚨 Security Considerations 🚨 //! //! Everything in the [`gcm`](crate::modes::gcm) module's security considerations applies: above @@ -79,9 +60,8 @@ use crate::modes::ModeNames; use crate::modes::gcm::{GCM_NONCE_LEN, Gcm, GcmKeyState}; use crate::{Decrypting, Encrypting}; -use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; -use bouncycastle_core::hazmat::{ElectronicCodeBook, do_hazardous_operations}; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, RNG}; @@ -96,39 +76,19 @@ pub struct GMAC; /// GMAC over the permutation `P`, generating (`Dir = Encrypting`) or verifying (`Dir = /// Decrypting`) a `TAG_LEN`-byte tag. See the module docs. -pub struct Gmac +pub struct Gmac<'k, P, Dir, const KEY_LEN: usize, const TAG_LEN: usize> where P: ElectronicCodeBook + ModeNames, { - gcm: Gcm, + gcm: Gcm<'k, P, Dir, KEY_LEN, TAG_LEN>, } /// `Gcm`'s AAD absorption fails only if AAD follows data, which GMAC never supplies, or if the /// running AAD length overflows a `u64`, which needs 2^64 bytes of input. const AAD_INFALLIBLE: &str = "GMAC: GCM rejects AAD only after data or past 2^64 bytes"; -/// The material route's key check: GMAC takes a `MACKey` of exactly `KEY_LEN` bytes, and the -/// permutation accepts only a `SymmetricCipherKey`, so the key is handed on as a retagged copy at -/// the same strength -- the permutation's strength check still applies -- used for nothing else. -fn mac_key_as_cipher_key( - key: &KeyMaterial, -) -> Result, SymmetricCipherError> { - if key.key_type() != KeyType::MACKey { - return Err(KeyMaterialError::InvalidKeyType("GMAC: key type must be a MAC key").into()); - } - let mut cipher_key = KeyMaterial::::from_bytes_as_type( - key.ref_to_bytes(), - KeyType::SymmetricCipherKey, - )?; - do_hazardous_operations(&mut cipher_key, |k| { - k.set_key_type(KeyType::SymmetricCipherKey)?; - k.set_security_strength(key.security_strength()) - })?; - Ok(cipher_key) -} - -impl Algorithm - for Gmac +impl<'k, P, Dir, const KEY_LEN: usize, const TAG_LEN: usize> Algorithm + for Gmac<'k, P, Dir, KEY_LEN, TAG_LEN> where P: ElectronicCodeBook + ModeNames, { @@ -136,7 +96,7 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl Gmac +impl<'k, P, const KEY_LEN: usize, const TAG_LEN: usize> Gmac<'k, P, Encrypting, KEY_LEN, TAG_LEN> where P: ElectronicCodeBook + ModeNames, { @@ -146,58 +106,19 @@ where /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. pub fn from_key_state( - state: &GcmKeyState

, + state: &'k GcmKeyState

, rng: &mut dyn RNG, - ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> - where - P: Clone, - { + ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { let (gcm, nonce) = Gcm::::from_key_state(state, rng)?; Ok((Self { gcm }, nonce)) } /// As [`from_key_state`](Self::from_key_state) under a nonce the caller supplies, who takes /// on the obligation that it never repeat under this key. - pub fn from_key_state_nonce(state: &GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self - where - P: Clone, - { + pub fn from_key_state_nonce(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { Self { gcm: Gcm::from_key_state_nonce(state, nonce) } } - /// Begins a tag from the material, under a nonce drawn from `rng`. - /// - /// # Errors - /// [`KeyMaterialError::InvalidKeyType`] if the key is not a - /// [`KeyType::MACKey`], the permutation's own refusals (a strength below the key length, - /// an all-zero key), or [`SymmetricCipherError::RNGError`] if the nonce draw fails. - pub fn new( - key: &KeyMaterial, - rng: &mut dyn RNG, - ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> - where - P: Clone, - { - let state = GcmKeyState::new::(P::new(&mac_key_as_cipher_key(key)?)?); - let (gcm, nonce) = Gcm::::from_key_state(&state, rng)?; - Ok((Self { gcm }, nonce)) - } - - /// As [`new`](Self::new) under a nonce the caller supplies. - /// - /// # Errors - /// As [`new`](Self::new), less the RNG. - pub fn new_nonce( - key: &KeyMaterial, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Result - where - P: Clone, - { - let state = GcmKeyState::new::(P::new(&mac_key_as_cipher_key(key)?)?); - Ok(Self { gcm: Gcm::from_key_state_nonce(&state, nonce) }) - } - /// Absorbs more of the message. pub fn do_update(&mut self, data: &[u8]) { self.gcm.absorb_aad(data).expect(AAD_INFALLIBLE); @@ -215,35 +136,17 @@ where } } -impl Gmac +impl<'k, P, const KEY_LEN: usize, const TAG_LEN: usize> Gmac<'k, P, Decrypting, KEY_LEN, TAG_LEN> where P: ElectronicCodeBook + ModeNames, { /// Begins verifying a tag made under `nonce` from a key's stored state: what a mode-bound /// key's `create_verifier` calls. Infallible: the state is keyed and the nonce length is in /// the type. - pub fn from_key_state(state: &GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self - where - P: Clone, - { + pub fn from_key_state(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { Self { gcm: Gcm::::from_key_state(state, nonce) } } - /// Begins verifying a tag made under `nonce`, from the material. - /// - /// # Errors - /// As the generating side's `new`, less the RNG. - pub fn new( - key: &KeyMaterial, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Result - where - P: Clone, - { - let state = GcmKeyState::new::(P::new(&mac_key_as_cipher_key(key)?)?); - Ok(Self { gcm: Gcm::::from_key_state(&state, nonce) }) - } - /// Absorbs more of the message. pub fn do_update(&mut self, data: &[u8]) { self.gcm.absorb_aad(data).expect(AAD_INFALLIBLE); diff --git a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs index 133fc2b44..2919bd1e6 100644 --- a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs +++ b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs @@ -3,9 +3,8 @@ use crate::modes::ModeNames; use crate::modes::ctr::apply_counter_blocks; -use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; -use bouncycastle_core::hazmat::{ElectronicCodeBook, KeyStream}; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::errors::SuspendableError; +use bouncycastle_core::hazmat::{BlockPermutation, KeyStream}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::Algorithm; use bouncycastle_utils::suspendable_state::{Cursor, CursorMut, SuspendableComponent}; @@ -17,7 +16,7 @@ use crate::modes::Ctr; use crate::stream::StreamCipher; // end of imports needed for docs -/// The CTR keystream `Oj = CIPH_K(Tj)` (SP 800-38A Sec 6.5) over any [`ElectronicCodeBook`], with +/// The CTR keystream `Oj = CIPH_K(Tj)` (SP 800-38A Sec 6.5) over any [`BlockPermutation`], with /// `Tj = N | [j]m` (SP 800-38A Appendix B.2); see the module docs. Use it through [`Ctr`]. /// /// # 🚨 Security Considerations 🚨 @@ -27,14 +26,15 @@ use crate::stream::StreamCipher; /// /// # State /// -/// The permutation, the nonce and the next counter value. The nonce and the counter are both -/// public, so they are plain fields; no keystream is kept between calls. +/// A reference to the permutation, which the key owns, the nonce and the next counter value. The +/// nonce and the counter are both public, so they are plain fields; no keystream is kept between +/// calls. #[derive(Clone)] -pub struct CtrKeyStream +pub struct CtrKeyStream<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - perm: P, + perm: &'k P, /// `N`: the message nonce, the leading bytes of every counter block. nonce: [u8; INIT_DATA_LEN], /// The counter of the *next* block to use, as an integer: `Tj = N | [next_counter]m`. @@ -46,10 +46,10 @@ where next_counter: u64, } -impl - CtrKeyStream +impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> + CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Bytes of counter at the end of each block: whatever the nonce leaves. const CTR_LEN: usize = BLOCK_LEN - INIT_DATA_LEN; @@ -79,20 +79,20 @@ where }; } - /// `T1 = N | [0]m`: the nonce, then a zero counter. What the [`CTR`](crate::modes::CTR) mode - /// impls build over a key's stored permutation. + /// `T1 = N | [0]m`: the nonce, then a zero counter, over a key's stored permutation, which + /// the keystream borrows. What the [`CTR`](crate::modes::CTR) mode impls build. #[inline] - pub(crate) fn start(perm: P, nonce: [u8; INIT_DATA_LEN]) -> Self { + pub fn from_perm(perm: &'k P, nonce: [u8; INIT_DATA_LEN]) -> Self { Self::start_at(perm, nonce, 0) } - /// As [`start`](Self::start), but the counter of the *next* block is `counter` instead of 0. + /// As [`from_perm`](Self::from_perm), but the counter of the *next* block is `counter` instead of 0. /// /// GCM's GCTR (SP 800-38D Sec 6.5) runs the data through this mode starting at `inc32(J0)`, /// whose counter field is `2` -- see `gcm.rs`. Crate-private because the public API's contract /// is that a message starts at counter 0; only `gcm.rs` needs otherwise. #[inline] - pub(crate) fn start_at(perm: P, nonce: [u8; INIT_DATA_LEN], counter: u64) -> Self { + pub(crate) fn start_at(perm: &'k P, nonce: [u8; INIT_DATA_LEN], counter: u64) -> Self { Self::check_shape(); debug_assert!( counter < Self::BLOCK_LIMIT, @@ -101,7 +101,7 @@ where Self { perm, nonce, next_counter: counter } } - /// The nonce `N` of a suspended keystream, read out of the state [`SuspendableComponent`] + /// The nonce `N` of a suspended keystream, read out of the state [`SuspendableComponent<'k>`] /// writes without rebuilding the keystream: it is the leading `INIT_DATA_LEN` bytes. For a /// composite that needs the nonce before it can afford the key schedule (GCM derives `H` and /// the tag mask from it). @@ -125,10 +125,10 @@ where } } -impl Algorithm - for CtrKeyStream +impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> Algorithm + for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// The permutation's name for this mode, [`ModeNames::CTR_ALG_NAME`]. const ALG_NAME: &'static str = P::CTR_ALG_NAME; @@ -136,22 +136,11 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl - KeyStream - for CtrKeyStream +impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> KeyStream + for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { - /// Expands the key; the keystream starts at `T1 = N | [0]m`. - fn new( - key: &KeyMaterial, - init_data: &[u8; INIT_DATA_LEN], - ) -> Result { - Self::check_shape(); - let perm = P::new(key)?; - Ok(Self::start(perm, *init_data)) - } - /// A whole block for every counter value left. fn remaining_blocks(&self) -> u64 { Self::BLOCK_LIMIT - self.next_counter @@ -162,7 +151,7 @@ where fn apply_blocks(&mut self, blocks: &mut [[u8; BLOCK_LEN]]) { let nonce = &self.nonce; apply_counter_blocks( - &self.perm, + self.perm, &mut self.next_counter, |j| Self::counter_block(nonce, j), blocks, @@ -170,15 +159,15 @@ where } } -/// The suspended state is the nonce and the next counter value; the permutation is rebuilt from -/// the re-supplied key. See [`bouncycastle_utils::suspendable_state`]. -impl - SuspendableComponent for CtrKeyStream +/// The suspended state is the nonce and the next counter value; the permutation is the key's, +/// re-supplied by reference. See [`bouncycastle_utils::suspendable_state`]. +impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> SuspendableComponent<'k> + for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const STATE_LEN: usize = INIT_DATA_LEN + 8; - type Key = KeyMaterial; + type Key = P; fn write_state(&self, out: &mut [u8]) { let mut w = CursorMut::new(out); @@ -187,9 +176,9 @@ where debug_assert!(w.is_done()); } - fn read_state(state: &[u8], key: &Self::Key) -> Result { + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { Self::check_shape(); - let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + let perm = key; let mut r = Cursor::new(state); let nonce = r.array::(); // The counter counts to `BLOCK_LIMIT` and stops there (that is the exhausted state, with @@ -229,8 +218,8 @@ mod tests { include!("../../../tests/common/toy_block_cipher.rs"); } - type ToyKeyStream = CtrKeyStream; - type ToyCtr = Ctr; + type ToyKeyStream<'k> = CtrKeyStream<'k, ToyBlockCipher, 16, 12>; + type ToyCtr<'k> = Ctr<'k, ToyBlockCipher, Encrypting, 16, 12>; fn key() -> KeyMaterial<16> { KeyMaterial::<16>::from_bytes_as_type(&[0x5Au8; 16], KeyType::SymmetricCipherKey) @@ -244,18 +233,12 @@ mod tests { fn start_at_matches_start_after_discarding_blocks() { let nonce = [0x11u8; 12]; - let mut from_start = ToyCtr::from_keystream(ToyKeyStream::start( - ToyBlockCipher::new(&key()).unwrap(), - nonce, - )); + let perm = ToyBlockCipher::new(&key()).unwrap(); + let mut from_start = ToyCtr::from_keystream(ToyKeyStream::from_perm(&perm, nonce)); let mut discarded = [0u8; 32]; from_start.do_encrypt_inplace(&mut discarded).unwrap(); - let mut from_start_at = ToyCtr::from_keystream(ToyKeyStream::start_at( - ToyBlockCipher::new(&key()).unwrap(), - nonce, - 2, - )); + let mut from_start_at = ToyCtr::from_keystream(ToyKeyStream::start_at(&perm, nonce, 2)); let mut a = [0x42u8; 48]; let mut b = a; @@ -269,7 +252,8 @@ mod tests { /// 128-bit blocks) that GCM relies on `Ctr`'s existing "counter exhausted" error to enforce. #[test] fn start_at_capacity_is_block_limit_minus_the_starting_counter() { - let ks = ToyKeyStream::start_at(ToyBlockCipher::new(&key()).unwrap(), [0u8; 12], 2); + let perm = ToyBlockCipher::new(&key()).unwrap(); + let ks = ToyKeyStream::start_at(&perm, [0u8; 12], 2); assert_eq!(ks.remaining_blocks(), ToyKeyStream::BLOCK_LIMIT - 2); } @@ -279,20 +263,24 @@ mod tests { #[test] fn ctr_keystream_conforms_to_the_key_stream_framework() { let framework = TestFrameworkKeyStream::new(); - framework.test::>(); - framework.test::>(); - framework - .test::>(); + let toy = Toy::new(&key()).unwrap(); + framework.test::<12, TOY_LEN, _>(|nonce| { + CtrKeyStream::::from_perm(&toy, *nonce) + }); + framework.test::<15, TOY_LEN, _>(|nonce| { + CtrKeyStream::::from_perm(&toy, *nonce) + }); + let forward_only = ForwardOnlyToy::new(&key()).unwrap(); + framework.test::<12, TOY_LEN, _>(|nonce| { + CtrKeyStream::::from_perm(&forward_only, *nonce) + }); } /// The keystream reports the permutation's CTR name, not its bare `ALG_NAME`; `Ctr` forwards /// it, which `tests/modes/alg_name_tests.rs` checks. #[test] fn ctr_keystream_reports_the_ctr_name() { - assert_eq!(CtrKeyStream::::ALG_NAME, Toy::CTR_ALG_NAME); - assert_eq!( - CtrKeyStream::::MAX_SECURITY_STRENGTH, - Toy::MAX_SECURITY_STRENGTH - ); + assert_eq!(CtrKeyStream::::ALG_NAME, Toy::CTR_ALG_NAME); + assert_eq!(CtrKeyStream::::MAX_SECURITY_STRENGTH, Toy::MAX_SECURITY_STRENGTH); } } diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index e00ede4c6..7ec3c8f24 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -37,8 +37,10 @@ const NONCE_LEN: usize = 12; const BLOCK_LEN: usize = 16; const AAD_LEN: usize = 16; const DATA_LEN: usize = 32; -type ToyEnc = CcmPacketEncryptor; -type ToyDec = CcmPacketDecryptor; +type ToyEnc<'k> = + CcmPacketEncryptor<'k, ToyBlockCipher, 16, BLOCK_LEN, NONCE_LEN, 16, AAD_LEN, DATA_LEN>; +type ToyDec<'k> = + CcmPacketDecryptor<'k, ToyBlockCipher, 16, BLOCK_LEN, NONCE_LEN, 16, AAD_LEN, DATA_LEN>; const N: usize = ToyEnc::SUSPENDED_STATE_LEN; const ND: usize = ToyDec::SUSPENDED_STATE_LEN; @@ -61,7 +63,7 @@ fn key() -> KeyMaterial<16> { KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap() } -/// The key the pair is made from; resume still takes the material, through `key()`. +/// The key the pair is made from; its workers borrow it, and resume takes its state. fn frame_key() -> ToyKey> { ToyKey::from_key_material(&key()).unwrap() } @@ -69,7 +71,8 @@ fn frame_key() -> ToyKey [u8; N] { - let mut enc = frame_key().create_encryptor_nonce(&[0x24u8; NONCE_LEN]); + let key = frame_key(); + let mut enc = key.create_encryptor_nonce(&[0x24u8; NONCE_LEN]); enc.do_update_aad(b"aad").unwrap(); enc.suspend() } @@ -77,7 +80,8 @@ fn unformatted() -> [u8; N] { /// An encryptor one block into its payload: the header is formatted, `next_ctr = 2`, the CBC-MAC /// block boundary has just been crossed, and 16 of the 32 bytes are still owed. fn mid_payload() -> [u8; N] { - let mut enc = frame_key().create_encryptor_nonce(&[0x24u8; NONCE_LEN]); + let key = frame_key(); + let mut enc = key.create_encryptor_nonce(&[0x24u8; NONCE_LEN]); enc.do_update_aad(b"aad").unwrap(); let mut out = [0u8; 16]; enc.do_encrypt_out(&[0x5Au8; 16], &mut out).unwrap(); @@ -90,7 +94,8 @@ fn with_u64(mut state: [u8; L], at: usize, value: u64) -> [u8; L } fn resumes(state: [u8; N]) -> Result<(), SuspendableError> { - ToyEnc::from_suspended(state, &key()).map(|_| ()) + let key = frame_key(); + ToyEnc::from_suspended(state, key.state()).map(|_| ()) } /// `mac_pos` is how much of the current CBC-MAC block has been XORed in, and a full block is @@ -195,12 +200,14 @@ fn the_aad_length_is_held_to_its_capacity() { /// on the tag bytes it holds back. #[test] fn the_decrypting_direction_checks_the_same_bounds() { - let mut dec = frame_key().create_decryptor(&[0x24u8; NONCE_LEN]); + let key = frame_key(); + let mut dec = key.create_decryptor(&[0x24u8; NONCE_LEN]); dec.do_update_aad(b"aad").unwrap(); let mut out = [0u8; 16]; dec.do_decrypt_out(&[0x5Au8; 16], &mut out).unwrap(); let state = dec.suspend(); - let resumes = |state: [u8; ND]| ToyDec::from_suspended(state, &key()).map(|_| ()); + let key = frame_key(); + let resumes = |state: [u8; ND]| ToyDec::from_suspended(state, key.state()).map(|_| ()); assert!(resumes(state).is_ok()); assert!(resumes(with_u64(state, OWED, DATA_LEN as u64 + 1)).is_err()); diff --git a/crypto/cipher/tests/common/toy_key.rs b/crypto/cipher/tests/common/toy_key.rs index b05f14df0..73b96f060 100644 --- a/crypto/cipher/tests/common/toy_key.rs +++ b/crypto/cipher/tests/common/toy_key.rs @@ -17,7 +17,7 @@ use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbDecryptionKey, EcbEncryptMode, - EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, SymmetricDecryptMode, + EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, ModeKey, SymmetricDecryptMode, SymmetricEncryptMode, do_hazardous_operations, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; @@ -54,6 +54,12 @@ impl> ToyKey { } } +impl> ModeKey for ToyKey { + fn state(&self) -> &M::KeyState { + &self.state + } +} + /// The permutation takes only a `SymmetricCipherKey`, so a MAC mode's `MACKey` is retagged for it /// at the same strength, as the AES key does. fn as_cipher_key( @@ -99,12 +105,15 @@ impl, { - type Encryptor = M::Encryptor; + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { M::encrypt_init(&self.state, rng) } } @@ -114,9 +123,12 @@ impl, { - type Decryptor = M::Decryptor; + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; - fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor { + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { M::decrypt_init(&self.state, init_data) } } @@ -126,12 +138,15 @@ impl, { - type Encryptor = M::Encryptor; + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { M::encrypt_init(&self.state, rng) } } @@ -141,9 +156,12 @@ impl, { - type Decryptor = M::Decryptor; + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; - fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor { + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { M::decrypt_init(&self.state, init_data) } } @@ -153,9 +171,12 @@ impl EcbEncryptionKey, { - type Encryptor = M::Encryptor; + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; - fn create_encryptor(&self) -> Self::Encryptor { + fn create_encryptor<'k>(&'k self) -> Self::Encryptor<'k> { M::encrypt_init(&self.state) } } @@ -165,9 +186,12 @@ impl EcbDecryptionKey, { - type Decryptor = M::Decryptor; + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; - fn create_decryptor(&self) -> Self::Decryptor { + fn create_decryptor<'k>(&'k self) -> Self::Decryptor<'k> { M::decrypt_init(&self.state) } } @@ -183,16 +207,19 @@ impl< where M: AeadEncryptMode, { - type Encryptor = M::Encryptor; + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { M::encrypt_init(&self.state, rng) } - fn create_encryptor_nonce(&self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor { + fn create_encryptor_nonce<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor<'k> { M::encrypt_init_nonce(&self.state, nonce) } @@ -226,9 +253,12 @@ impl< where M: AeadDecryptMode, { - type Decryptor = M::Decryptor; + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; - fn create_decryptor(&self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor { + fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { M::decrypt_init(&self.state, nonce) } @@ -254,23 +284,26 @@ impl< where M: AeadDeclaredEncryptMode, { - type Encryptor = M::Encryptor; + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, aad_len: usize, payload_len: usize, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError> { + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { M::encrypt_init(&self.state, rng, aad_len, payload_len) } - fn create_encryptor_nonce( - &self, + fn create_encryptor_nonce<'k>( + &'k self, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result { + ) -> Result, SymmetricCipherError> { M::encrypt_init_nonce(&self.state, nonce, aad_len, payload_len) } @@ -304,14 +337,17 @@ impl< where M: AeadDeclaredDecryptMode, { - type Decryptor = M::Decryptor; + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; - fn create_decryptor( - &self, + fn create_decryptor<'k>( + &'k self, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result { + ) -> Result, SymmetricCipherError> { M::decrypt_init(&self.state, nonce, aad_len, payload_len) } @@ -327,17 +363,23 @@ where } impl> MacGenerationKey for ToyKey { - type Generator = M::Worker; + type Generator<'k> + = M::Worker<'k> + where + Self: 'k; - fn create_generator(&self) -> Self::Generator { + fn create_generator<'k>(&'k self) -> Self::Generator<'k> { M::mac_init(&self.state) } } impl> MacVerificationKey for ToyKey { - type Verifier = M::Worker; + type Verifier<'k> + = M::Worker<'k> + where + Self: 'k; - fn create_verifier(&self) -> Self::Verifier { + fn create_verifier<'k>(&'k self) -> Self::Verifier<'k> { M::mac_init(&self.state) } } @@ -364,17 +406,23 @@ impl AeadEncryptMode + ModeNames + Clone, { - type Encryptor = Gcm; + type Encryptor<'k> + = Gcm<'k, P, Encrypting, TOY_KEY_LEN, TAG_LEN> + where + P: 'k; - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - Self::Encryptor::from_key_state(state, rng) + ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + >::from_key_state(state, rng) } - fn encrypt_init_nonce(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Encryptor { - Self::Encryptor::from_key_state_nonce(state, nonce) + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Encryptor<'k> { + >::from_key_state_nonce(state, nonce) } } @@ -383,10 +431,16 @@ impl AeadDecryptMode + ModeNames + Clone, { - type Decryptor = Gcm; + type Decryptor<'k> + = Gcm<'k, P, Decrypting, TOY_KEY_LEN, TAG_LEN> + where + P: 'k; - fn decrypt_init(state: &Self::KeyState, nonce: &[u8; GCM_NONCE_LEN]) -> Self::Decryptor { - Self::Decryptor::from_key_state(state, nonce) + fn decrypt_init<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Decryptor<'k> { + >::from_key_state(state, nonce) } } @@ -410,27 +464,27 @@ impl ToyKey> where P: ElectronicCodeBook + ModeNames + Clone, { - pub fn create_generator( - &self, + pub fn create_generator<'k>( + &'k self, rng: &mut dyn RNG, ) -> Result< - (Gmac, [u8; GCM_NONCE_LEN]), + (Gmac<'k, P, Encrypting, TOY_KEY_LEN, TAG_LEN>, [u8; GCM_NONCE_LEN]), SymmetricCipherError, > { Gmac::::from_key_state(&self.state, rng) } - pub fn create_generator_nonce( - &self, + pub fn create_generator_nonce<'k>( + &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac { + ) -> Gmac<'k, P, Encrypting, TOY_KEY_LEN, TAG_LEN> { Gmac::::from_key_state_nonce(&self.state, nonce) } - pub fn create_verifier( - &self, + pub fn create_verifier<'k>( + &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac { + ) -> Gmac<'k, P, Decrypting, TOY_KEY_LEN, TAG_LEN> { Gmac::::from_key_state(&self.state, nonce) } } @@ -454,9 +508,12 @@ impl

MacMode

for ToyCMAC where P: ElectronicCodeBook + ModeNames + Clone, { - type Worker = Cmac; + type Worker<'k> + = Cmac<'k, P, TOY_KEY_LEN, 16> + where + P: 'k; - fn mac_init(state: &Self::KeyState) -> Self::Worker { + fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { Cmac::from_key_state(state) } } diff --git a/crypto/cipher/tests/modes/alg_name_tests.rs b/crypto/cipher/tests/modes/alg_name_tests.rs index 0e61bc6c0..03f5b32a6 100644 --- a/crypto/cipher/tests/modes/alg_name_tests.rs +++ b/crypto/cipher/tests/modes/alg_name_tests.rs @@ -48,48 +48,46 @@ fn the_toy_names_every_mode_differently() { #[test] fn each_mode_reports_its_own_name() { - check::>(Toy::CBC_ALG_NAME); - check::>(Toy::CBC_ALG_NAME); - check::>(Toy::ECB_ALG_NAME); - check::>(Toy::ECB_ALG_NAME); - check::>(Toy::CFB_ALG_NAME); - check::>(Toy::CFB_ALG_NAME); - check::>(Toy::CFB8_ALG_NAME); - check::>(Toy::CFB8_ALG_NAME); - check::>(Toy::GCM_ALG_NAME); - check::>(Toy::GCM_ALG_NAME); - check::>(Toy::CCM_ALG_NAME); - check::>(Toy::CCM_ALG_NAME); - check::>(Toy::CMAC_ALG_NAME); - check::>(Toy::GMAC_ALG_NAME); - check::>(Toy::GMAC_ALG_NAME); + check::>(Toy::CBC_ALG_NAME); + check::>(Toy::CBC_ALG_NAME); + check::>(Toy::ECB_ALG_NAME); + check::>(Toy::ECB_ALG_NAME); + check::>(Toy::CFB_ALG_NAME); + check::>(Toy::CFB_ALG_NAME); + check::>(Toy::CFB8_ALG_NAME); + check::>(Toy::CFB8_ALG_NAME); + check::>(Toy::GCM_ALG_NAME); + check::>(Toy::GCM_ALG_NAME); + check::>(Toy::CCM_ALG_NAME); + check::>(Toy::CCM_ALG_NAME); + check::>(Toy::CMAC_ALG_NAME); + check::>(Toy::GMAC_ALG_NAME); + check::>(Toy::GMAC_ALG_NAME); } /// The padding layer and the stream-cipher wrapper add no name of their own. #[test] fn the_wrappers_forward_the_inner_name() { - check::>(Toy::CTR_ALG_NAME); - check::>(Toy::CTR_ALG_NAME); - check::, PKCS7, 16, 16, 16>>( + check::>(Toy::CTR_ALG_NAME); + check::>(Toy::CTR_ALG_NAME); + check::, PKCS7, 16, 16>>( Toy::CBC_ALG_NAME, ); - check::, PKCS7, 16, 16, 16>>( + check::, PKCS7, 16, 16>>( Toy::CBC_ALG_NAME, ); - check::, NoPadding, 16, 0, 16>>( + check::, NoPadding, 0, 16>>( Toy::ECB_ALG_NAME, ); - check::, NoPadding, 16, 0, 16>>( + check::, NoPadding, 0, 16>>( Toy::ECB_ALG_NAME, ); } -/// A mode type's key reports the mode's name and asks for a cipher key, whatever the padding or -/// frame parameters. +/// A mode type asks for a cipher key, whatever the padding or frame parameters. #[test] -fn each_mode_type_reports_its_mode_name() { +fn each_mode_type_asks_for_a_cipher_key() { fn check>(name: &str) { - assert_eq!(M::ALG_NAME, name); assert_eq!(M::KEY_TYPE, KeyType::SymmetricCipherKey, "{name}"); } check::>(Toy::CBC_ALG_NAME); diff --git a/crypto/cipher/tests/modes/ccm_tests.rs b/crypto/cipher/tests/modes/ccm_tests.rs index 87f2f850f..878f294e2 100644 --- a/crypto/cipher/tests/modes/ccm_tests.rs +++ b/crypto/cipher/tests/modes/ccm_tests.rs @@ -97,7 +97,8 @@ fn stream_encrypt< plaintext: &[u8], chunk: usize, ) -> (Vec, [u8; TAG_LEN]) { - let mut enc = frame_key::().create_encryptor_nonce(nonce); + let key = frame_key::(); + let mut enc = key.create_encryptor_nonce(nonce); enc.do_update_aad(aad).unwrap(); let mut out = vec![0u8; plaintext.len()]; let mut written = 0; @@ -120,7 +121,8 @@ fn stream_decrypt< tag: &[u8; TAG_LEN], chunk: usize, ) -> Result, SymmetricCipherError> { - let mut dec = frame_key::().create_decryptor(nonce); + let key = frame_key::(); + let mut dec = key.create_decryptor(nonce); dec.do_update_aad(aad)?; let mut out = vec![0u8; ciphertext.len()]; let mut written = 0; @@ -196,7 +198,8 @@ fn the_pair_path_is_really_used_in_both_directions_and_the_mac_never_batches() { // Decryption: the swapped keystream recovers the wrong plaintext (Sec 6.2 step 5), which is // what the MAC then absorbs (step 9), so the tag check fails. The stream releases the // plaintext before the final, so the wrong recovery can be seen. - let mut dec = frame_key::().create_decryptor(&nonce); + let key = frame_key::(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut back = vec![0u8; LEN]; dec.do_decrypt_out(&ct, &mut back).unwrap(); @@ -227,7 +230,9 @@ fn the_four_block_path_is_really_used_in_both_directions() { assert_eq!(pairs_ct, ct, "pairs must not use the four path"); assert_eq!(pairs_tag, tag); - let mut dec = frame_key::().create_decryptor(&nonce); + let key = frame_key::(); + + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut back = vec![0u8; LEN]; dec.do_decrypt_out(&ct, &mut back).unwrap(); @@ -259,7 +264,8 @@ fn every_split_agrees_with_the_one_shot_in_both_directions() { let (ct, tag) = seal::(&nonce, aad, &plaintext); for split in 0..=LEN { - let mut enc = frame_key::().create_encryptor_nonce(&nonce); + let key = frame_key::(); + let mut enc = key.create_encryptor_nonce(&nonce); enc.do_update_aad(aad).unwrap(); let mut streamed = vec![0u8; LEN]; enc.do_encrypt_out(&plaintext[..split], &mut streamed).unwrap(); @@ -268,7 +274,9 @@ fn every_split_agrees_with_the_one_shot_in_both_directions() { assert_eq!(streamed_tag, tag, "tag, split at {split}"); assert_eq!(streamed, ct, "ciphertext, split at {split}"); - let mut dec = frame_key::().create_decryptor(&nonce); + let key = frame_key::(); + + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut back = vec![0u8; LEN]; dec.do_decrypt_out(&ct[..split], &mut back).unwrap(); @@ -290,7 +298,8 @@ fn both_streams_hold_to_the_frame() { let (ct, tag) = seal::(&nonce, &[], &plaintext); // Encrypting: too much is refused, and finalizing short is refused. - let mut enc = frame_key::().create_encryptor_nonce(&nonce); + let key = frame_key::(); + let mut enc = key.create_encryptor_nonce(&nonce); let mut out = [0u8; 9]; assert!( matches!( @@ -307,7 +316,8 @@ fn both_streams_hold_to_the_frame() { // Decrypting: the stream's bound is the frame plus an inline tag, and a final that has not // seen the frame reports a malformed ciphertext. - let mut dec = frame_key::().create_decryptor(&nonce); + let key = frame_key::(); + let mut dec = key.create_decryptor(&nonce); let mut out = [0u8; LEN]; assert!( matches!( @@ -321,7 +331,9 @@ fn both_streams_hold_to_the_frame() { dec.do_decrypt_final_detached(&tag).unwrap(); assert_eq!(out, plaintext[..]); - let mut dec = frame_key::().create_decryptor(&nonce); + let key = frame_key::(); + + let mut dec = key.create_decryptor(&nonce); dec.do_decrypt_out(&ct[..4], &mut out).unwrap(); assert!( matches!(dec.do_decrypt_final_detached(&tag), Err(SymmetricCipherError::DecryptionFailed)), @@ -467,7 +479,8 @@ fn the_one_shot_releases_nothing_on_forgery_but_the_stream_does() { // The stream: the payload is released by the update that brings it, the finals release // nothing, and a rejected tag cannot take it back. The detached final leaves its (unused) // buffer alone rather than zeroizing it, since there is nothing of the plaintext in it. - let mut dec = frame_key::().create_decryptor(&nonce); + let key = frame_key::(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut streamed = [0u8; LEN]; assert_eq!(dec.do_decrypt_out(&ct, &mut streamed).unwrap(), LEN, "released mid-stream"); @@ -482,7 +495,8 @@ fn the_one_shot_releases_nothing_on_forgery_but_the_stream_does() { let mut inline = ct.clone(); inline.extend_from_slice(&tag); - let mut dec = frame_key::().create_decryptor(&nonce); + let key = frame_key::(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); let mut streamed = [0u8; LEN]; assert_eq!(dec.do_decrypt_out(&inline, &mut streamed).unwrap(), LEN); diff --git a/crypto/cipher/tests/modes/cfb8_tests.rs b/crypto/cipher/tests/modes/cfb8_tests.rs index d82f90e16..999691225 100644 --- a/crypto/cipher/tests/modes/cfb8_tests.rs +++ b/crypto/cipher/tests/modes/cfb8_tests.rs @@ -30,7 +30,7 @@ use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCiph use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; use toy_key::{ToyKey, os_rng}; -type ToyCfb8

= Cfb8; +type ToyCfb8<'k, Dir> = Cfb8<'k, Toy, Dir, TOY_LEN, TOY_LEN>; type ToyCfb8Key = ToyKey; @@ -38,6 +38,10 @@ fn cfb8_key() -> ToyCfb8Key { ToyCfb8Key::from_key_material(&toy_key()).expect("the toy key") } +/// The key the worker helpers below hand out borrowed workers over; a worker cannot outlive +/// its key, so the helpers' key lives for the whole test binary. +static KEY: std::sync::LazyLock = std::sync::LazyLock::new(cfb8_key); + fn swapped_key() -> ToyKey { ToyKey::from_key_material(&toy_key()).expect("the toy key") } @@ -87,14 +91,14 @@ fn pinned_rng(iv: [u8; TOY_LEN]) -> FixedSeedRNG { FixedSeedRNG::::new(iv) } -fn pinned_encryptor(iv: [u8; TOY_LEN]) -> ToyCfb8 { - let (enc, got) = cfb8_key().create_encryptor(&mut pinned_rng(iv)).expect("encrypt init"); +fn pinned_encryptor(iv: [u8; TOY_LEN]) -> ToyCfb8<'static, Encrypting> { + let (enc, got) = KEY.create_encryptor(&mut pinned_rng(iv)).expect("encrypt init"); assert_eq!(got, iv, "the pinned RNG should reproduce the IV"); enc } -fn pinned_decryptor(iv: [u8; TOY_LEN]) -> ToyCfb8 { - cfb8_key().create_decryptor(&iv) +fn pinned_decryptor(iv: [u8; TOY_LEN]) -> ToyCfb8<'static, Decrypting> { + KEY.create_decryptor(&iv) } /// A test message of `len` bytes with no repeating structure at the block size. @@ -278,15 +282,19 @@ fn neither_direction_uses_the_inverse_cipher() { let iv = pinned_iv(); let plaintext = message(19); - let (mut e, _) = forward_only_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); + let key = forward_only_key(); + + let (mut e, _) = key.create_encryptor(&mut pinned_rng(iv)).unwrap(); let ct = enc(&mut e, &plaintext); // One call: four fours, then a pair, then a single byte. - let mut d = forward_only_key().create_decryptor(&iv); + let key = forward_only_key(); + let mut d = key.create_decryptor(&iv); assert_eq!(dec(&mut d, &ct), plaintext, "all paths, forward cipher only"); // Byte by byte: the single-byte path only. - let mut d = forward_only_key().create_decryptor(&iv); + let key = forward_only_key(); + let mut d = key.create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "single-byte path, forward cipher only"); // The forward-only toy must agree with the real one, or the above proves nothing. @@ -446,15 +454,18 @@ fn the_pair_path_is_really_used() { assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); // The swapped-pair toy encrypts identically -- CFB8 encryption is serial and never batches. - let (mut e, _) = swapped_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); + let key = swapped_key(); + let (mut e, _) = key.create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(enc(&mut e, &plaintext), ct, "CFB8 encryption must not use the pair path"); // ...but decrypting two bytes together must now be wrong, because the pair path is used. - let mut d = swapped_key().create_decryptor(&iv); + let key = swapped_key(); + let mut d = key.create_decryptor(&iv); assert_ne!(dec(&mut d, &ct), plaintext, "a pair must go through encrypt_2blocks"); // One byte at a time avoids the pair path, so it is correct even for this toy. - let mut d = swapped_key().create_decryptor(&iv); + let key = swapped_key(); + let mut d = key.create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "the single-byte path must not pair"); } @@ -473,21 +484,25 @@ fn the_four_byte_path_is_really_used() { assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); // The rotated-four toy encrypts identically: CFB8 encryption is serial and never batches. - let (mut e, _) = swapped_four_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); + let key = swapped_four_key(); + let (mut e, _) = key.create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(enc(&mut e, &plaintext), ct, "CFB8 encryption must not use the four path"); // ...but five bytes together must now be wrong, because the first four go through // encrypt_4blocks. - let mut d = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut d = key.create_decryptor(&iv); assert_ne!(dec(&mut d, &ct), plaintext, "five bytes must go through encrypt_4blocks"); // Two bytes use the pair path only, so they are correct even for this toy... let two = &ct[..2]; - let mut d = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut d = key.create_decryptor(&iv); assert_eq!(dec(&mut d, two), plaintext[..2], "pairs must not use the four path"); // ...and so is one byte at a time. - let mut d = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut d = key.create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "the single-byte path must not batch"); } diff --git a/crypto/cipher/tests/modes/cfb_tests.rs b/crypto/cipher/tests/modes/cfb_tests.rs index 8f85870e6..c688074c8 100644 --- a/crypto/cipher/tests/modes/cfb_tests.rs +++ b/crypto/cipher/tests/modes/cfb_tests.rs @@ -33,7 +33,7 @@ use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCiph use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; use toy_key::{ToyKey, os_rng}; -type ToyCfb = Cfb; +type ToyCfb<'k, Dir> = Cfb<'k, Toy, Dir, TOY_LEN, TOY_LEN>; type ToyCfbKey = ToyKey; @@ -41,6 +41,10 @@ fn cfb_key() -> ToyCfbKey { ToyCfbKey::from_key_material(&toy_key()).expect("the toy key") } +/// The key the worker helpers below hand out borrowed workers over; a worker cannot outlive +/// its key, so the helpers' key lives for the whole test binary. +static KEY: std::sync::LazyLock = std::sync::LazyLock::new(cfb_key); + fn swapped_key() -> ToyKey { ToyKey::from_key_material(&toy_key()).expect("the toy key") } @@ -103,14 +107,14 @@ fn pinned_rng(iv: [u8; TOY_LEN]) -> FixedSeedRNG { FixedSeedRNG::::new(iv) } -fn pinned_encryptor(iv: [u8; TOY_LEN]) -> ToyCfb { - let (enc, got) = cfb_key().create_encryptor(&mut pinned_rng(iv)).expect("encrypt init"); +fn pinned_encryptor(iv: [u8; TOY_LEN]) -> ToyCfb<'static, Encrypting> { + let (enc, got) = KEY.create_encryptor(&mut pinned_rng(iv)).expect("encrypt init"); assert_eq!(got, iv, "the pinned RNG should reproduce the IV"); enc } -fn pinned_decryptor(iv: [u8; TOY_LEN]) -> ToyCfb { - cfb_key().create_decryptor(&iv) +fn pinned_decryptor(iv: [u8; TOY_LEN]) -> ToyCfb<'static, Decrypting> { + KEY.create_decryptor(&iv) } /// A test message of `len` bytes with no repeating structure at the block size. @@ -210,10 +214,8 @@ fn the_mode_matches_the_spec_equations() { ); // ...and CFB is not CBC: CBC computes `CIPH_K(P1 XOR IV)`, CFB computes `P1 XOR CIPH_K(IV)`. - let (mut cbc, _) = ToyKey::>::from_key_material(&toy_key()) - .unwrap() - .create_encryptor(&mut pinned_rng(iv)) - .unwrap(); + let cbc_key = ToyKey::>::from_key_material(&toy_key()).unwrap(); + let (mut cbc, _) = cbc_key.create_encryptor(&mut pinned_rng(iv)).unwrap(); let mut cbc_c1: [u8; TOY_LEN] = plaintext[..TOY_LEN].try_into().unwrap(); cbc.do_encrypt_blocks_inplace(cbc_c1.as_chunks_mut().0).unwrap(); assert_ne!(&cbc_c1[..], &ct[..TOY_LEN], "CFB must not agree with CBC"); @@ -296,15 +298,19 @@ fn neither_direction_uses_the_inverse_cipher() { let iv = pinned_iv(); let plaintext = message(11 * TOY_LEN + 5); - let (mut e, _) = forward_only_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); + let key = forward_only_key(); + + let (mut e, _) = key.create_encryptor(&mut pinned_rng(iv)).unwrap(); let ct = enc(&mut e, &plaintext); // One call: two fours, then a pair, then a single, then the short segment. - let mut d = forward_only_key().create_decryptor(&iv); + let key = forward_only_key(); + let mut d = key.create_decryptor(&iv); assert_eq!(dec(&mut d, &ct), plaintext, "all paths, forward cipher only"); // Byte by byte: the byte path only. - let mut d = forward_only_key().create_decryptor(&iv); + let key = forward_only_key(); + let mut d = key.create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "byte path, forward cipher only"); // The forward-only toy must agree with the real one, or the above proves nothing. @@ -478,20 +484,24 @@ fn the_pair_path_is_really_used() { // The swapped-pair toy encrypts identically -- CFB encryption is serial and never pairs, so its // `encrypt_2blocks` override is not reached from the encryptor at all. - let (mut e, _) = swapped_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); + let key = swapped_key(); + let (mut e, _) = key.create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(enc(&mut e, &plaintext), ct, "CFB encryption must not use the pair path"); // ...but decrypting the pair together must now be wrong, because the pair path is used. - let mut d = swapped_key().create_decryptor(&iv); + let key = swapped_key(); + let mut d = key.create_decryptor(&iv); assert_ne!(dec(&mut d, &ct), plaintext, "decrypting a pair must go through encrypt_2blocks"); // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. - let mut d = swapped_key().create_decryptor(&iv); + let key = swapped_key(); + let mut d = key.create_decryptor(&iv); assert_eq!(dec_chunked(&mut d, &ct, TOY_LEN), plaintext, "the single-block path must not pair"); // So does splitting the pair across a segment boundary: 5 bytes, then 27. The second call has // an 11-byte head, one whole block and no tail, so there is no pair to form. - let mut d = swapped_key().create_decryptor(&iv); + let key = swapped_key(); + let mut d = key.create_decryptor(&iv); let mut got = ct.clone(); d.do_decrypt_inplace(&mut got[..5]).unwrap(); d.do_decrypt_inplace(&mut got[5..]).unwrap(); @@ -515,16 +525,19 @@ fn the_four_block_path_is_really_used() { assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); // The rotated-four toy encrypts identically: CFB encryption is serial and never batches. - let (mut e, _) = swapped_four_key().create_encryptor(&mut pinned_rng(iv)).unwrap(); + let key = swapped_four_key(); + let (mut e, _) = key.create_encryptor(&mut pinned_rng(iv)).unwrap(); assert_eq!(enc(&mut e, &plaintext), ct, "CFB encryption must not use the four path"); // ...but five blocks together must now be wrong, because the first four go through // encrypt_4blocks. - let mut d = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut d = key.create_decryptor(&iv); assert_ne!(dec(&mut d, &ct), plaintext, "five blocks must go through encrypt_4blocks"); // Pairs use the pair path only, so they are correct even for this toy... - let mut d = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut d = key.create_decryptor(&iv); assert_eq!( dec_chunked(&mut d, &ct, 2 * TOY_LEN), plaintext, @@ -532,7 +545,8 @@ fn the_four_block_path_is_really_used() { ); // ...and so is one block at a time. - let mut d = swapped_four_key().create_decryptor(&iv); + let key = swapped_four_key(); + let mut d = key.create_decryptor(&iv); assert_eq!( dec_chunked(&mut d, &ct, TOY_LEN), plaintext, @@ -759,15 +773,16 @@ fn every_length_round_trips_without_padding() { // ---- memory ------------------------------------------------------------------------------ -/// Pins the "Memory Usage" statement in the module docs -- the state is the permutation, one -/// block and a byte count -- and the claim that CFB costs one `usize` more than CBC: the block that -/// is `Ij`, `Oj` and `I_{j+1}` in turn, plus the count of how much of it has been used. +/// Pins the "Memory Usage" statement in the module docs -- the state is a reference to the +/// permutation, one block and a byte count -- and the claim that CFB costs one `usize` more than +/// CBC: the block that is `Ij`, `Oj` and `I_{j+1}` in turn, plus the count of how much of it has +/// been used. #[test] fn sizes_match_the_documented_memory_table() { use core::mem::size_of; // The general rule the docs state. - assert_eq!(size_of::>(), size_of::() + TOY_LEN + size_of::()); + assert_eq!(size_of::>(), size_of::<&Toy>() + TOY_LEN + size_of::()); // The direction marker is free, and does not change the layout. assert_eq!(size_of::>(), size_of::>()); @@ -775,6 +790,6 @@ fn sizes_match_the_documented_memory_table() { // The docs say CFB is one `usize` bigger than CBC. assert_eq!( size_of::>(), - size_of::>() + size_of::() + size_of::>() + size_of::() ); } diff --git a/crypto/cipher/tests/modes/cmac_tests.rs b/crypto/cipher/tests/modes/cmac_tests.rs index 36921b3e9..0ba08be7b 100644 --- a/crypto/cipher/tests/modes/cmac_tests.rs +++ b/crypto/cipher/tests/modes/cmac_tests.rs @@ -19,7 +19,7 @@ use bouncycastle_core_test_framework::mac::TestFrameworkMAC; use common::{TOY_LEN, Toy, toy_key}; use toy_key::{ToyCMAC, ToyKey}; -type ToyCmac = Cmac; +type ToyCmac<'k> = Cmac<'k, Toy, TOY_LEN, TOY_LEN>; type ToyCmacKey = ToyKey; fn cmac_key() -> ToyCmacKey { @@ -141,15 +141,15 @@ fn truncation() { #[test] fn suspend_resume() { - let key = mac_key(); + let key = cmac_key(); for mlen in [0, 16, 20, 64] { let msg = message(mlen); - let expected = cmac_key().mac(&msg); + let expected = key.mac(&msg); for split in 0..=mlen { - let mut cmac = cmac_key().create_generator(); + let mut cmac = key.create_generator(); cmac.do_update(&msg[..split]); let state: [u8; STATE_LEN] = cmac.suspend(); - let mut resumed = ToyCmac::from_suspended(state, &key).unwrap(); + let mut resumed = ToyCmac::from_suspended(state, key.state()).unwrap(); resumed.do_update(&msg[split..]); assert_eq!(resumed.do_final(), expected, "mlen {mlen}, suspended at {split}"); } @@ -158,34 +158,26 @@ fn suspend_resume() { #[test] fn suspend_resume_rejects() { - let key = mac_key(); - let mut cmac = cmac_key().create_generator(); + let key = cmac_key(); + let mut cmac = key.create_generator(); cmac.do_update(b"some data"); let state: [u8; STATE_LEN] = cmac.suspend(); assert_eq!(STATE_LEN, 3 + 2 * TOY_LEN + 1); let mut bad_len = state; bad_len[STATE_LEN - 1] = TOY_LEN as u8 + 1; - assert_eq!(ToyCmac::from_suspended(bad_len, &key).err(), Some(SuspendableError::InvalidData)); + assert_eq!( + ToyCmac::from_suspended(bad_len, key.state()).err(), + Some(SuspendableError::InvalidData) + ); let mut full_len = state; full_len[STATE_LEN - 1] = TOY_LEN as u8; - assert!(ToyCmac::from_suspended(full_len, &key).is_ok()); + assert!(ToyCmac::from_suspended(full_len, key.state()).is_ok()); let mut no_ver = state; no_ver[..3].fill(0); assert_eq!( - ToyCmac::from_suspended(no_ver, &key).err(), + ToyCmac::from_suspended(no_ver, key.state()).err(), Some(SuspendableError::IncompatibleVersion) ); - - let wrong_len = KeyMaterial::<32>::from_bytes_as_type(&[7u8; 32], KeyType::MACKey).unwrap(); - assert_eq!( - ToyCmac::from_suspended(state, &wrong_len).err(), - Some(SuspendableError::InvalidData) - ); - assert_eq!( - ToyCmac::from_suspended(state, &toy_key()).err(), - Some(SuspendableError::InvalidData), - "a cipher key is the wrong type" - ); } diff --git a/crypto/cipher/tests/modes/ctr_tests.rs b/crypto/cipher/tests/modes/ctr_tests.rs index be3bd4175..7d8b0c63d 100644 --- a/crypto/cipher/tests/modes/ctr_tests.rs +++ b/crypto/cipher/tests/modes/ctr_tests.rs @@ -28,7 +28,7 @@ mod toy_key; use bouncycastle_cipher::modes::{CTR, Ctr, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ @@ -43,12 +43,12 @@ use toy_key::{ToyKey, os_rng}; /// The default shape under test: a 12-byte nonce, so a 4-byte counter. const NONCE_LEN: usize = 12; -type ToyCtr = Ctr; +type ToyCtr<'k, Dir> = Ctr<'k, Toy, Dir, TOY_LEN, NONCE_LEN>; /// A 15-byte nonce leaves a **1-byte** counter, so the whole counter space is 256 blocks -- 4 KiB /// of keystream. That makes the exhaustion behaviour reachable in a test. const SHORT_CTR_NONCE_LEN: usize = 15; -type TinyCtr = Ctr; +type TinyCtr<'k, Dir> = Ctr<'k, Toy, Dir, TOY_LEN, SHORT_CTR_NONCE_LEN>; type ToyCtrKey = ToyKey>; @@ -56,6 +56,10 @@ fn ctr_key() -> ToyCtrKey { ctr_key_n() } +/// The key the worker helpers below hand out borrowed workers over; a worker cannot outlive +/// its key, so the helpers' key lives for the whole test binary. +static KEY: std::sync::LazyLock = std::sync::LazyLock::new(ctr_key); + /// The toy key under CTR at any permitted nonce length. fn ctr_key_n() -> ToyKey> { ToyKey::from_key_material(&toy_key()).expect("the toy key") @@ -111,14 +115,14 @@ fn pinned_rng(nonce: [u8; NONCE_LEN]) -> FixedSeedRNG { FixedSeedRNG::::new(nonce) } -fn pinned_encryptor(nonce: [u8; NONCE_LEN]) -> ToyCtr { - let (e, got) = ctr_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); +fn pinned_encryptor(nonce: [u8; NONCE_LEN]) -> ToyCtr<'static, Encrypting> { + let (e, got) = KEY.create_encryptor(&mut pinned_rng(nonce)).unwrap(); assert_eq!(got, nonce, "the pinned RNG should reproduce the nonce"); e } -fn pinned_decryptor(nonce: [u8; NONCE_LEN]) -> ToyCtr { - ctr_key().create_decryptor(&nonce) +fn pinned_decryptor(nonce: [u8; NONCE_LEN]) -> ToyCtr<'static, Decrypting> { + KEY.create_decryptor(&nonce) } fn message(len: usize) -> Vec { @@ -244,8 +248,9 @@ fn check_counter_blocks(blocks: usize) { let perm = >::new(&toy_key()).unwrap(); let nonce: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(13).wrapping_add(5)); - let (mut e, got) = - ctr_key_n::().create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); + let key = ctr_key_n::(); + + let (mut e, got) = key.create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); assert_eq!(got, nonce); let mut keystream = vec![0u8; blocks * TOY_LEN]; e.do_encrypt_inplace(&mut keystream).expect("the run must fit in the counter space"); @@ -340,10 +345,10 @@ fn the_f5_counter_blocks_have_the_shape_this_type_assumes() { fn the_counter_limit_is_enforced() { let nonce: [u8; SHORT_CTR_NONCE_LEN] = core::array::from_fn(|i| 0x5A ^ (i as u8)); + let key = tiny_key(); let encryptor = || { - let (e, got) = tiny_key() - .create_encryptor(&mut FixedSeedRNG::::new(nonce)) - .unwrap(); + let (e, got) = + key.create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); assert_eq!(got, nonce); e }; @@ -408,9 +413,9 @@ fn the_counter_limit_is_enforced_at_two_bytes_too() { const CAPACITY: usize = 65536 * TOY_LEN; let nonce: [u8; NONCE] = core::array::from_fn(|i| 0x3C ^ (i as u8)); + let key = ctr_key_n::(); let encryptor = || { - let (e, got) = - ctr_key_n::().create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); + let (e, got) = key.create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); assert_eq!(got, nonce); e }; @@ -433,7 +438,8 @@ fn the_counter_limit_is_enforced_at_two_bytes_too() { #[test] fn the_counter_limit_is_enforced_when_decrypting_too() { let nonce: [u8; SHORT_CTR_NONCE_LEN] = core::array::from_fn(|i| 0x5A ^ (i as u8)); - let mut d = tiny_key().create_decryptor(&nonce); + let key = tiny_key(); + let mut d = key.create_decryptor(&nonce); let mut data = vec![0u8; TINY_CAPACITY + 1]; assert!( d.do_decrypt_inplace(&mut data).is_err(), @@ -451,18 +457,22 @@ fn neither_direction_uses_the_inverse_cipher() { let nonce = pinned_nonce(); let plaintext = message(11 * TOY_LEN + 5); - let (mut e, _) = - forward_only_key().create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); + let key = forward_only_key(); + + let (mut e, _) = key.create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); let mut ct = plaintext.clone(); e.do_encrypt_inplace(&mut ct).unwrap(); - let mut d = forward_only_key().create_decryptor(&nonce); + let key = forward_only_key(); + + let mut d = key.create_decryptor(&nonce); let mut back = ct.clone(); d.do_decrypt_inplace(&mut back).unwrap(); assert_eq!(back, plaintext, "all paths, forward cipher only"); // Byte by byte, so the single-block path runs too. - let mut d = forward_only_key().create_decryptor(&nonce); + let key = forward_only_key(); + let mut d = key.create_decryptor(&nonce); let mut back = ct.clone(); for piece in back.chunks_mut(1) { d.do_decrypt_inplace(piece).unwrap(); @@ -584,13 +594,15 @@ fn the_pair_path_is_really_used_in_both_directions() { assert_eq!(dec(&mut pinned_decryptor(nonce), &ct), plaintext); // Encryption: two blocks together must go through encrypt_2blocks, so the swapped toy differs. - let (mut e, _) = swapped_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); + let key = swapped_key(); + let (mut e, _) = key.create_encryptor(&mut pinned_rng(nonce)).unwrap(); let mut swapped = plaintext.clone(); e.do_encrypt_inplace(&mut swapped).unwrap(); assert_ne!(swapped, ct, "CTR encryption must use the pair path"); // ...but one block at a time avoids it, and then it agrees with the correct toy. - let (mut e, _) = swapped_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); + let key = swapped_key(); + let (mut e, _) = key.create_encryptor(&mut pinned_rng(nonce)).unwrap(); let mut single = plaintext.clone(); for piece in single.chunks_mut(TOY_LEN) { e.do_encrypt_inplace(piece).unwrap(); @@ -598,7 +610,8 @@ fn the_pair_path_is_really_used_in_both_directions() { assert_eq!(single, ct, "the single-block path must not pair"); // Decryption: the same, on the correct ciphertext. - let mut d = swapped_key().create_decryptor(&nonce); + let key = swapped_key(); + let mut d = key.create_decryptor(&nonce); let mut back = ct.clone(); d.do_decrypt_inplace(&mut back).unwrap(); assert_ne!(back, plaintext, "CTR decryption must use the pair path"); @@ -612,20 +625,25 @@ fn the_four_block_path_is_really_used_in_both_directions() { let ct = enc(&mut pinned_encryptor(nonce), &plaintext); - let (mut e, _) = swapped_four_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); + let key = swapped_four_key(); + + let (mut e, _) = key.create_encryptor(&mut pinned_rng(nonce)).unwrap(); let mut swapped = plaintext.clone(); e.do_encrypt_inplace(&mut swapped).unwrap(); assert_ne!(swapped, ct, "five blocks must go through encrypt_4blocks"); // Two blocks at a time uses pairs only, so the rotated-four toy is correct there. - let (mut e, _) = swapped_four_key().create_encryptor(&mut pinned_rng(nonce)).unwrap(); + let key = swapped_four_key(); + let (mut e, _) = key.create_encryptor(&mut pinned_rng(nonce)).unwrap(); let mut pairs = plaintext.clone(); for piece in pairs.chunks_mut(2 * TOY_LEN) { e.do_encrypt_inplace(piece).unwrap(); } assert_eq!(pairs, ct, "pairs must not use the four path"); - let mut d = swapped_four_key().create_decryptor(&nonce); + let key = swapped_four_key(); + + let mut d = key.create_decryptor(&nonce); let mut back = ct.clone(); d.do_decrypt_inplace(&mut back).unwrap(); assert_ne!(back, plaintext, "decryption must batch fours too"); @@ -701,8 +719,9 @@ fn every_permitted_nonce_length_works() { let nonce: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(11).wrapping_add(3)); let plaintext = (0..100u8).collect::>(); - let (mut e, got) = - ctr_key_n::().create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); + let key = ctr_key_n::(); + + let (mut e, got) = key.create_encryptor(&mut FixedSeedRNG::::new(nonce)).unwrap(); assert_eq!(got, nonce); let mut ct = plaintext.clone(); e.do_encrypt_inplace(&mut ct).unwrap(); @@ -720,15 +739,15 @@ fn every_permitted_nonce_length_works() { // ---- memory --------------------------------------------------------------------------------- -/// Pins the layout: permutation + nonce + counter (`u64`) + keystream block + the used offset, -/// rounded up to the `u64`'s alignment. +/// Pins the layout: a reference to the permutation + nonce + counter (`u64`) + keystream block + +/// the used offset, rounded up to the `u64`'s alignment. #[test] fn sizes_match_the_documented_memory_table() { use core::mem::{align_of, size_of}; assert_eq!( size_of::>(), - (size_of::() + NONCE_LEN + size_of::() + TOY_LEN + size_of::()) + (size_of::<&Toy>() + NONCE_LEN + size_of::() + TOY_LEN + size_of::()) .next_multiple_of(align_of::()) ); diff --git a/crypto/cipher/tests/modes/gcm_tests.rs b/crypto/cipher/tests/modes/gcm_tests.rs index 7af30c953..efe264bea 100644 --- a/crypto/cipher/tests/modes/gcm_tests.rs +++ b/crypto/cipher/tests/modes/gcm_tests.rs @@ -10,7 +10,7 @@ mod common; #[path = "../common/toy_key.rs"] mod toy_key; -use bouncycastle_cipher::modes::ModeNames; +use bouncycastle_cipher::modes::{GCM, ModeNames}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; @@ -20,9 +20,9 @@ use bouncycastle_core::traits::{ use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use common::{ForwardOnlyToy, TOY_LEN, Toy, toy_key}; -use toy_key::{ToyGCM, ToyKey, os_rng}; +use toy_key::{ToyKey, os_rng}; -type ToyGcmKey = ToyKey>; +type ToyGcmKey = ToyKey>; fn gcm_key() -> ToyGcmKey { ToyGcmKey::from_key_material(&toy_key()).expect("the toy key") @@ -239,12 +239,12 @@ fn one_shot_releases_nothing_on_forgery_but_streaming_does() { /// checks the tag, so on a forged tag it must zeroize that buffer before returning the error. #[test] fn streaming_detached_final_zeroizes_its_buffer_on_forgery() { - let key = toy_key(); + let key = ToyGcmKey::<16>::from_key_material(&toy_key()).unwrap(); let message = *b"do not trust me yet"; let (nonce, ct, mut tag) = toy_encrypt::<16>(b"aad", &message, [0xAFu8; 12]); tag[0] ^= 0xFF; // forge it - let mut dec = ToyGcm::::do_decrypt_init(&key, &nonce).unwrap(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(b"aad").unwrap(); dec.do_decrypt_out(&ct, &mut [0u8; 19]).unwrap(); // Pre-filled with a non-zero byte, so a buffer the decryptor never wrote is not mistaken for @@ -268,7 +268,7 @@ fn neither_direction_uses_the_inverse_cipher() { where P: bouncycastle_core::hazmat::ElectronicCodeBook + ModeNames + Clone, { - let key = ToyKey::>::from_key_material(&toy_key()).unwrap(); + let key = ToyKey::>::from_key_material(&toy_key()).unwrap(); let aad = b"associated data of no particular length"; let message = b"a message that is not a whole number of blocks!!"; diff --git a/crypto/cipher/tests/modes/gmac_tests.rs b/crypto/cipher/tests/modes/gmac_tests.rs index 8e08c02c7..00d80bfd6 100644 --- a/crypto/cipher/tests/modes/gmac_tests.rs +++ b/crypto/cipher/tests/modes/gmac_tests.rs @@ -1,5 +1,6 @@ -//! Structural tests for GMAC, driven by a toy permutation: both routes onto it give the tag `Gcm` -//! gives over AAD alone, streaming splits, tag, nonce and message sensitivity, and key validation. +//! Structural tests for GMAC, driven by a toy permutation: the key and the key state give the tag +//! `Gcm` gives over AAD alone, streaming splits, tag, nonce and message sensitivity, and key +//! validation. //! The known-answer tests (ACVP, Wycheproof) are in the `aes` crate, `crypto/aes/tests/gmac_*.rs`. mod common; @@ -8,7 +9,6 @@ mod toy_key; use bouncycastle_cipher::modes::{GCM_NONCE_LEN, GcmKeyState, Gmac, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; @@ -18,7 +18,7 @@ use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMat use common::{TOY_LEN, Toy, toy_key}; use toy_key::{ToyGCM, ToyGMAC, ToyKey}; -type ToyGmac = Gmac; +type ToyGmac<'k, Dir, const TAG_LEN: usize = 16> = Gmac<'k, Toy, Dir, TOY_LEN, TAG_LEN>; type ToyGmacKey = ToyKey>; fn gmac_key() -> ToyGmacKey { @@ -75,17 +75,7 @@ fn every_route_gives_gcm_s_aad_only_tag() { expected, "from_key_state_nonce, len {len}" ); - let (gen_, nonce) = ToyGmac::::new(&mac_key(), &mut rng()).unwrap(); - assert_eq!(nonce, NONCE); - assert_eq!(gen_.mac(&msg), expected, "new, len {len}"); - assert_eq!( - ToyGmac::::new_nonce(&mac_key(), &NONCE).unwrap().mac(&msg), - expected, - "new_nonce, len {len}" - ); - assert!(ToyGmac::::from_key_state(&state, &NONCE).verify(&msg, &expected)); - assert!(ToyGmac::::new(&mac_key(), &NONCE).unwrap().verify(&msg, &expected)); let short = ToyGmac::::from_key_state_nonce(&state, &NONCE).mac(&msg); assert_eq!(short[..], expected[..12], "len {len}"); @@ -150,31 +140,6 @@ fn a_fresh_nonce_per_generator() { assert_ne!(g1.mac(b"m"), g2.mac(b"m")); } -/// The material route takes a `MACKey` at the permutation's strength and nothing else. -#[test] -fn key_validation() { - let cipher_key = toy_key(); - let is_type_error = |r: Result<(), SymmetricCipherError>| { - matches!( - r, - Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) - ) - }; - assert!(is_type_error(ToyGmac::::new(&cipher_key, &mut rng()).map(drop))); - assert!(is_type_error(ToyGmac::::new_nonce(&cipher_key, &NONCE).map(drop))); - assert!(is_type_error(ToyGmac::::new(&cipher_key, &NONCE).map(drop))); - - let mut weak = mac_key(); - weak.set_security_strength(SecurityStrength::_112bit).unwrap(); - assert!(matches!( - ToyGmac::::new_nonce(&weak, &NONCE), - Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) - )); - - let zero = KeyMaterial::::from_bytes_as_type(&[0; TOY_LEN], KeyType::MACKey).unwrap(); - assert!(ToyGmac::::new(&zero, &NONCE).is_err()); -} - /// The key's construction suite: a `MACKey` of the permutation's length and strength, bound to /// the GMAC name or to nothing. #[test] diff --git a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs index 9a824208c..668c26689 100644 --- a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs +++ b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs @@ -34,7 +34,7 @@ use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkSymmetricC use common::{TOY_LEN, Toy, toy_key}; use toy_key::{ToyKey, os_rng}; -type ToyCtr = Ctr; +type ToyCtr<'k, Dir> = Ctr<'k, Toy, Dir, TOY_LEN, TOY_LEN, 12>; /// The toy key under the stream mode `M`. fn key_for>() -> ToyKey { @@ -63,12 +63,12 @@ fn the_stream_modes_conform_to_the_symmetric_cipher_suite() { /// the check that the bridge adds nothing and loses nothing. #[test] fn the_two_apis_agree_byte_for_byte() { - fn check(name: &str, key: &K) + fn check<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>(name: &str, key: &'k K) where K: SymmetricEncryptionKey + SymmetricDecryptionKey, - K::Encryptor: StreamCipherEncryptor, - K::Decryptor: StreamCipherDecryptor, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, { for len in [0usize, 1, 15, 16, 17, 63, 64, 171] { let plaintext: Vec = (0..len).map(|i| (i * 7 + 1) as u8).collect(); @@ -103,7 +103,9 @@ fn the_input_buffer_is_not_modified() { let plaintext: Vec = (0..100u8).collect(); let original = plaintext.clone(); - let (mut enc, _init) = key_for::().create_encryptor(&mut os_rng()).unwrap(); + let key = key_for::(); + + let (mut enc, _init) = key.create_encryptor(&mut os_rng()).unwrap(); let mut ciphertext = vec![0u8; plaintext.len()]; enc.do_encrypt_out(&plaintext, &mut ciphertext).unwrap(); diff --git a/crypto/cipher/tests/padding/padded_tests.rs b/crypto/cipher/tests/padding/padded_tests.rs index 119526ca7..dbf949bbf 100644 --- a/crypto/cipher/tests/padding/padded_tests.rs +++ b/crypto/cipher/tests/padding/padded_tests.rs @@ -30,12 +30,12 @@ use toy_key::{ToyKey, os_rng}; const B: usize = TOY_BLOCK_LEN; type Key = ToyKey>; -type Enc = >::Encryptor; -type Dec = >::Decryptor; +type Enc<'k> = >::Encryptor<'k>; +type Dec<'k> = >::Decryptor<'k>; /// The same adapters over `NoPadding`: an alignment check rather than a padding scheme. type KeyNP = ToyKey>; -type EncNP = >::Encryptor; -type DecNP = >::Decryptor; +type EncNP<'k> = >::Encryptor<'k>; +type DecNP<'k> = >::Decryptor<'k>; fn material() -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&[0x5a; B], KeyType::SymmetricCipherKey).unwrap() @@ -288,8 +288,8 @@ fn no_padding_adds_nothing_to_aligned_data() { // Byte for byte the bare cipher's output under the same IV. let mut bare = pt.clone(); - let (mut enc, _) = - key_for::>().create_encryptor(&mut FixedSeedRNG::::new(iv)).unwrap(); + let key = key_for::>(); + let (mut enc, _) = key.create_encryptor(&mut FixedSeedRNG::::new(iv)).unwrap(); let (blocks_mut, _) = bare.as_chunks_mut::(); enc.do_encrypt_blocks_inplace(blocks_mut).unwrap(); assert_eq!(ct, bare, "{blocks} blocks: the adapter must not alter the ciphertext"); diff --git a/crypto/cipher/tests/suspend_tests.rs b/crypto/cipher/tests/suspend_tests.rs index b74a74061..f20b5136b 100644 --- a/crypto/cipher/tests/suspend_tests.rs +++ b/crypto/cipher/tests/suspend_tests.rs @@ -11,7 +11,7 @@ use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{ Blocks, CBC, CCM_Packet, CFB, CFB8, CTR, Cbc, CcmPacketDecryptor, CcmPacketEncryptor, Cfb, - Cfb8, Ctr, Gcm, + Cfb8, Ctr, GCM, Gcm, }; use bouncycastle_cipher::padding::{PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use bouncycastle_cipher::{Decrypting, Encrypting}; @@ -33,26 +33,26 @@ mod toy_block_cipher; #[path = "common/toy_key.rs"] mod toy_key; use toy_block_cipher::ToyBlockCipher; -use toy_key::{ToyGCM, ToyKey, os_rng}; +use toy_key::{ToyKey, os_rng}; -type ToyEcb = Ecb; -type ToyCbc = Cbc; -type ToyCfb = Cfb; -type ToyCfb8 = Cfb8; -type ToyCtr = Ctr; -type ToyGcm = Gcm; +type ToyEcb<'k, Dir> = Ecb<'k, ToyBlockCipher, Dir, 16>; +type ToyCbc<'k, Dir> = Cbc<'k, ToyBlockCipher, Dir, 16>; +type ToyCfb<'k, Dir> = Cfb<'k, ToyBlockCipher, Dir, 16>; +type ToyCfb8<'k, Dir> = Cfb8<'k, ToyBlockCipher, Dir, 16>; +type ToyCtr<'k, Dir> = Ctr<'k, ToyBlockCipher, Dir, 16, 12>; +type ToyGcm<'k, Dir> = Gcm<'k, ToyBlockCipher, Dir, 16>; // A 64-byte AAD capacity and 37-byte frames, the message `ccm_both_directions` uses. type ToyCcmPacket = CCM_Packet<12, 16, 64, 37>; -type ToyCcmEnc = CcmPacketEncryptor; -type ToyCcmDec = CcmPacketDecryptor; -type ToyPaddedEnc = PaddedBlockCipherEncryptor, PKCS7, 16, 16, 16>; -type ToyPaddedDec = PaddedBlockCipherDecryptor, PKCS7, 16, 16, 16>; +type ToyCcmEnc<'k> = CcmPacketEncryptor<'k, ToyBlockCipher, 16, 12, 16, 64, 37>; +type ToyCcmDec<'k> = CcmPacketDecryptor<'k, ToyBlockCipher, 16, 12, 16, 64, 37>; +type ToyPaddedEnc<'k> = PaddedBlockCipherEncryptor, PKCS7, 16, 16>; +type ToyPaddedDec<'k> = PaddedBlockCipherDecryptor, PKCS7, 16, 16>; fn key() -> KeyMaterial<16> { KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap() } -/// The toy key under the mode `M`; resume still takes the material, through `key()`. +/// The toy key under the mode `M`; its workers borrow it, and resume takes its state. fn key_for>() -> ToyKey { ToyKey::from_key_material(&key()).expect("the toy key") } @@ -61,15 +61,19 @@ fn message(len: usize) -> Vec { (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(3)).collect() } -/// Runs the framework suite on `cipher`, then suspends a clone, resumes it, and finishes both -/// with `finish`. Whatever `finish` returns must be identical for the two. -fn round_trip(cipher: C, finish: impl Fn(C) -> Vec) -> Vec +/// Runs the framework suite on `cipher`, then suspends a clone, resumes it over `key`, the mode's +/// key state the cipher borrows, and finishes both with `finish`. Whatever `finish` returns must +/// be identical for the two. +fn round_trip<'k, const N: usize, C>( + cipher: C, + key: &'k C::Key, + finish: impl Fn(C) -> Vec, +) -> Vec where - C: SuspendableKeyed> + Clone, + C: SuspendableKeyed<'k, N> + Clone, { - let key = key(); - TestFrameworkSuspendableKeyedState::new().test(&cipher, &key); - let resumed = C::from_suspended(cipher.clone().suspend(), &key).unwrap(); + TestFrameworkSuspendableKeyedState::new().test(&cipher, key); + let resumed = C::from_suspended(cipher.clone().suspend(), key).unwrap(); let original_output = finish(cipher); assert_eq!(original_output, finish(resumed), "the resumed cipher must continue identically"); original_output @@ -77,37 +81,49 @@ where #[test] fn cbc_both_directions() { - let (mut enc, iv) = key_for::>().create_encryptor(&mut os_rng()).unwrap(); + let key = key_for::>(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let mut first = [0x11u8; 16]; enc.do_encrypt_blocks_inplace(first.as_chunks_mut().0).unwrap(); - let rest = round_trip::<{ ToyCbc::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { - let mut data = [0x22u8; 48]; - e.do_encrypt_blocks_inplace(data.as_chunks_mut().0).unwrap(); - data.to_vec() - }); + let rest = round_trip::<{ ToyCbc::::SUSPENDED_STATE_LEN }, _>( + enc, + key.state(), + |mut e| { + let mut data = [0x22u8; 48]; + e.do_encrypt_blocks_inplace(data.as_chunks_mut().0).unwrap(); + data.to_vec() + }, + ); - let mut dec = key_for::>().create_decryptor(&iv); + let key = key_for::>(); + let mut dec = key.create_decryptor(&iv); dec.do_decrypt_blocks_inplace(first.as_chunks_mut().0).unwrap(); assert_eq!(first, [0x11u8; 16]); - let plain = round_trip::<{ ToyCbc::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { - let mut data: [u8; 48] = rest.as_slice().try_into().unwrap(); - d.do_decrypt_blocks_inplace(data.as_chunks_mut().0).unwrap(); - data.to_vec() - }); + let plain = round_trip::<{ ToyCbc::::SUSPENDED_STATE_LEN }, _>( + dec, + key.state(), + |mut d| { + let mut data: [u8; 48] = rest.as_slice().try_into().unwrap(); + d.do_decrypt_blocks_inplace(data.as_chunks_mut().0).unwrap(); + data.to_vec() + }, + ); assert_eq!(plain, vec![0x22u8; 48]); } #[test] fn ecb_both_directions() { - let mut enc = key_for::>().create_encryptor(); + let key = key_for::>(); + let mut enc = key.create_encryptor(); enc.do_encrypt_blocks_inplace(&mut [[0x11u8; 16]]).unwrap(); - round_trip::<{ ToyEcb::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + round_trip::<{ ToyEcb::::SUSPENDED_STATE_LEN }, _>(enc, key.state(), |mut e| { let mut data = [0x22u8; 32]; e.do_encrypt_blocks_inplace(data.as_chunks_mut().0).unwrap(); data.to_vec() }); - let dec = key_for::>().create_decryptor(); - round_trip::<{ ToyEcb::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let key = key_for::>(); + let dec = key.create_decryptor(); + round_trip::<{ ToyEcb::::SUSPENDED_STATE_LEN }, _>(dec, key.state(), |mut d| { let mut data = [0x33u8; 32]; d.do_decrypt_blocks_inplace(data.as_chunks_mut().0).unwrap(); data.to_vec() @@ -117,65 +133,95 @@ fn ecb_both_directions() { #[test] fn cfb_mid_segment_both_directions() { let msg = message(40); - let (mut enc, iv) = key_for::().create_encryptor(&mut os_rng()).unwrap(); + let key = key_for::(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let mut head = msg[..7].to_vec(); enc.do_encrypt_inplace(&mut head).unwrap(); - let tail = round_trip::<{ ToyCfb::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { - let mut data = msg[7..].to_vec(); - e.do_encrypt_inplace(&mut data).unwrap(); - data - }); + let tail = round_trip::<{ ToyCfb::::SUSPENDED_STATE_LEN }, _>( + enc, + key.state(), + |mut e| { + let mut data = msg[7..].to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data + }, + ); - let mut dec = key_for::().create_decryptor(&iv); + let key = key_for::(); + let mut dec = key.create_decryptor(&iv); dec.do_decrypt_inplace(&mut head).unwrap(); assert_eq!(head, msg[..7]); - let plain = round_trip::<{ ToyCfb::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { - let mut data = tail.clone(); - d.do_decrypt_inplace(&mut data).unwrap(); - data - }); + let plain = round_trip::<{ ToyCfb::::SUSPENDED_STATE_LEN }, _>( + dec, + key.state(), + |mut d| { + let mut data = tail.clone(); + d.do_decrypt_inplace(&mut data).unwrap(); + data + }, + ); assert_eq!(plain, msg[7..]); } #[test] fn cfb8_both_directions() { let msg = message(20); - let (mut enc, iv) = key_for::().create_encryptor(&mut os_rng()).unwrap(); + let key = key_for::(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let mut head = msg[..5].to_vec(); enc.do_encrypt_inplace(&mut head).unwrap(); - let tail = round_trip::<{ ToyCfb8::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { - let mut data = msg[5..].to_vec(); - e.do_encrypt_inplace(&mut data).unwrap(); - data - }); - let mut dec = key_for::().create_decryptor(&iv); + let tail = round_trip::<{ ToyCfb8::::SUSPENDED_STATE_LEN }, _>( + enc, + key.state(), + |mut e| { + let mut data = msg[5..].to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data + }, + ); + let key = key_for::(); + let mut dec = key.create_decryptor(&iv); dec.do_decrypt_inplace(&mut head).unwrap(); - let plain = round_trip::<{ ToyCfb8::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { - let mut data = tail.clone(); - d.do_decrypt_inplace(&mut data).unwrap(); - data - }); + let plain = round_trip::<{ ToyCfb8::::SUSPENDED_STATE_LEN }, _>( + dec, + key.state(), + |mut d| { + let mut data = tail.clone(); + d.do_decrypt_inplace(&mut data).unwrap(); + data + }, + ); assert_eq!(plain, msg[5..]); } #[test] fn ctr_mid_block_both_directions() { let msg = message(50); - let (mut enc, nonce) = key_for::>().create_encryptor(&mut os_rng()).unwrap(); + let key = key_for::>(); + let (mut enc, nonce) = key.create_encryptor(&mut os_rng()).unwrap(); let mut head = msg[..7].to_vec(); enc.do_encrypt_inplace(&mut head).unwrap(); - let tail = round_trip::<{ ToyCtr::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { - let mut data = msg[7..].to_vec(); - e.do_encrypt_inplace(&mut data).unwrap(); - data - }); - let mut dec = key_for::>().create_decryptor(&nonce); + let tail = round_trip::<{ ToyCtr::::SUSPENDED_STATE_LEN }, _>( + enc, + key.state(), + |mut e| { + let mut data = msg[7..].to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data + }, + ); + let key = key_for::>(); + let mut dec = key.create_decryptor(&nonce); dec.do_decrypt_inplace(&mut head).unwrap(); - let plain = round_trip::<{ ToyCtr::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { - let mut data = tail.clone(); - d.do_decrypt_inplace(&mut data).unwrap(); - data - }); + let plain = round_trip::<{ ToyCtr::::SUSPENDED_STATE_LEN }, _>( + dec, + key.state(), + |mut d| { + let mut data = tail.clone(); + d.do_decrypt_inplace(&mut data).unwrap(); + data + }, + ); assert_eq!(plain, msg[7..]); } @@ -184,34 +230,44 @@ fn gcm_both_directions_with_aad() { let msg = message(45); let aad = b"authenticated header"; - let (mut enc, nonce) = key_for::>().create_encryptor(&mut os_rng()).unwrap(); + let key = key_for::>(); + let (mut enc, nonce) = key.create_encryptor(&mut os_rng()).unwrap(); enc.do_update_aad(aad).unwrap(); let mut head = [0u8; 5]; enc.do_encrypt_out(&msg[..5], &mut head).unwrap(); // Ciphertext of the rest, then the tag. - let tail = round_trip::<{ ToyGcm::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { - let mut out = vec![0u8; 40]; - e.do_encrypt_out(&msg[5..], &mut out).unwrap(); - let (tag, tag_len) = e.do_encrypt_final().unwrap(); - out.extend_from_slice(&tag[..tag_len]); - out - }); + let tail = round_trip::<{ ToyGcm::::SUSPENDED_STATE_LEN }, _>( + enc, + key.state(), + |mut e| { + let mut out = vec![0u8; 40]; + e.do_encrypt_out(&msg[5..], &mut out).unwrap(); + let (tag, tag_len) = e.do_encrypt_final().unwrap(); + out.extend_from_slice(&tag[..tag_len]); + out + }, + ); let mut ciphertext = head.to_vec(); ciphertext.extend_from_slice(&tail); // The decryptor holds the last 16 bytes back, so after 10 bytes nothing has been released, // but data has started and the AAD phase is closed: a state worth suspending. - let mut dec = key_for::>().create_decryptor(&nonce); + let key = key_for::>(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut nothing = [0u8; 0]; assert_eq!(dec.do_decrypt_out(&ciphertext[..10], &mut nothing).unwrap(), 0); - let plain = round_trip::<{ ToyGcm::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { - let mut out = vec![0u8; ciphertext.len()]; - let n = d.do_decrypt_out(&ciphertext[10..], &mut out).unwrap(); - let (_, last) = d.do_decrypt_final().expect("the tag must verify after a resume"); - out.truncate(n + last); - out - }); + let plain = round_trip::<{ ToyGcm::::SUSPENDED_STATE_LEN }, _>( + dec, + key.state(), + |mut d| { + let mut out = vec![0u8; ciphertext.len()]; + let n = d.do_decrypt_out(&ciphertext[10..], &mut out).unwrap(); + let (_, last) = d.do_decrypt_final().expect("the tag must verify after a resume"); + out.truncate(n + last); + out + }, + ); assert_eq!(plain, msg); } @@ -223,9 +279,10 @@ fn ccm_both_directions() { // Suspended with half the AAD held back and no payload yet, and again mid-payload; both // must continue to the one answer. - let mut enc = key_for::().create_encryptor_nonce(&nonce); + let key = key_for::(); + let mut enc = key.create_encryptor_nonce(&nonce); enc.do_update_aad(&aad[..3]).unwrap(); - let sealed = round_trip::<{ ToyCcmEnc::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let sealed = round_trip::<{ ToyCcmEnc::SUSPENDED_STATE_LEN }, _>(enc, key.state(), |mut e| { e.do_update_aad(&aad[3..]).unwrap(); let mut out = vec![0u8; 37]; e.do_encrypt_out(&msg, &mut out).unwrap(); @@ -233,11 +290,12 @@ fn ccm_both_directions() { out.extend_from_slice(&tag); out }); - let mut enc = key_for::().create_encryptor_nonce(&nonce); + let key = key_for::(); + let mut enc = key.create_encryptor_nonce(&nonce); enc.do_update_aad(aad).unwrap(); let mut head = vec![0u8; 9]; enc.do_encrypt_out(&msg[..9], &mut head).unwrap(); - let tail = round_trip::<{ ToyCcmEnc::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let tail = round_trip::<{ ToyCcmEnc::SUSPENDED_STATE_LEN }, _>(enc, key.state(), |mut e| { let mut out = vec![0u8; 28]; e.do_encrypt_out(&msg[9..], &mut out).unwrap(); let (_, _, tag) = e.do_encrypt_final_detached().unwrap(); @@ -248,11 +306,12 @@ fn ccm_both_directions() { let (ct, tag) = sealed.split_at(37); let tag: [u8; 16] = tag.try_into().unwrap(); - let mut dec = key_for::().create_decryptor(&nonce); + let key = key_for::(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut opened_head = vec![0u8; 9]; dec.do_decrypt_out(&ct[..9], &mut opened_head).unwrap(); - let plain = round_trip::<{ ToyCcmDec::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let plain = round_trip::<{ ToyCcmDec::SUSPENDED_STATE_LEN }, _>(dec, key.state(), |mut d| { let mut out = vec![0u8; 28]; d.do_decrypt_out(&ct[9..], &mut out).unwrap(); d.do_decrypt_final_detached(&tag).expect("the tag must verify after a resume"); @@ -261,11 +320,12 @@ fn ccm_both_directions() { assert_eq!([&opened_head[..], &plain[..]].concat(), msg); // ...and with part of an inline tag already held back at the suspend point. - let mut dec = key_for::().create_decryptor(&nonce); + let key = key_for::(); + let mut dec = key.create_decryptor(&nonce); dec.do_update_aad(aad).unwrap(); let mut opened = vec![0u8; 37]; dec.do_decrypt_out(&sealed[..40], &mut opened).unwrap(); - round_trip::<{ ToyCcmDec::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + round_trip::<{ ToyCcmDec::SUSPENDED_STATE_LEN }, _>(dec, key.state(), |mut d| { d.do_decrypt_out(&sealed[40..], &mut []).unwrap(); d.do_decrypt_final().expect("the held-back tag must survive a resume"); Vec::new() @@ -276,11 +336,12 @@ fn ccm_both_directions() { #[test] fn padded_cbc_both_directions() { let msg = message(45); - let (mut enc, iv) = key_for::>().create_encryptor(&mut os_rng()).unwrap(); + let key = key_for::>(); + let (mut enc, iv) = key.create_encryptor(&mut os_rng()).unwrap(); let mut head = [0u8; 16]; // 20 bytes in: one block out, four buffered. assert_eq!(enc.do_encrypt_out(&msg[..20], &mut head).unwrap(), 16); - let tail = round_trip::<{ ToyPaddedEnc::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let tail = round_trip::<{ ToyPaddedEnc::SUSPENDED_STATE_LEN }, _>(enc, key.state(), |mut e| { let mut out = vec![0u8; 32]; let n = e.do_encrypt_out(&msg[20..], &mut out).unwrap(); out.truncate(n); @@ -293,17 +354,19 @@ fn padded_cbc_both_directions() { assert_eq!(ciphertext.len(), 48); // 20 bytes in: one block released, one held back, four buffered. - let mut dec = key_for::>().create_decryptor(&iv); + let key = key_for::>(); + let mut dec = key.create_decryptor(&iv); let mut first = [0u8; 16]; assert_eq!(dec.do_decrypt_out(&ciphertext[..36], &mut first).unwrap(), 16); - let plain = round_trip::<{ ToyPaddedDec::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { - let mut out = vec![0u8; 32]; - let n = d.do_decrypt_out(&ciphertext[36..], &mut out).unwrap(); - out.truncate(n); - let (last, data_len) = d.do_decrypt_final().unwrap(); - out.extend_from_slice(&last[..data_len]); - out - }); + let plain = + round_trip::<{ ToyPaddedDec::SUSPENDED_STATE_LEN }, _>(dec, key.state(), |mut d| { + let mut out = vec![0u8; 32]; + let n = d.do_decrypt_out(&ciphertext[36..], &mut out).unwrap(); + out.truncate(n); + let (last, data_len) = d.do_decrypt_final().unwrap(); + out.extend_from_slice(&last[..data_len]); + out + }); let mut recovered = first.to_vec(); recovered.extend_from_slice(&plain); assert_eq!(recovered, msg); diff --git a/crypto/core-test-framework/src/key_stream.rs b/crypto/core-test-framework/src/key_stream.rs index 4d9019106..3f46646ac 100644 --- a/crypto/core-test-framework/src/key_stream.rs +++ b/crypto/core-test-framework/src/key_stream.rs @@ -1,11 +1,7 @@ //! Shared conformance tests for [`KeyStream`] implementors. use crate::DUMMY_SEED; -use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::KeyStream; -use bouncycastle_core::hazmat::do_hazardous_operations; -use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::security_strength::SecurityStrength; /// Instance of the test framework. pub struct TestFrameworkKeyStream { @@ -24,7 +20,8 @@ impl TestFrameworkKeyStream { Self {} } - /// Exercises the trait contract for one implementor. + /// Exercises the trait contract for one implementor, built by `new` over the init data it is + /// given, over a permutation the caller keys once. /// /// Checks, in order: /// * `apply_blocks` XORs: applied to [`DUMMY_SEED`] it gives `DUMMY_SEED` XOR the keystream it @@ -34,31 +31,25 @@ impl TestFrameworkKeyStream { /// * every chunking of the blocks into `apply_blocks` calls gives the one-call answer -- /// including chunk sizes that are not multiples of any batch width the implementor uses; /// * `remaining_blocks` goes down by exactly one per block applied (a keystream that reports - /// `u64::MAX`, no practical limit, may stay there); - /// * a key of the wrong [`KeyType`] is rejected; - /// * the security-strength policy matches [`Algorithm::MAX_SECURITY_STRENGTH`]. + /// `u64::MAX`, no practical limit, may stay there). /// - /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH + /// The key-policy checks (type, strength, binding) belong to the key object that owns the + /// permutation, and are `TestFrameworkFromKeyMaterial`'s. pub fn test< - const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, - KS: KeyStream, + KS: KeyStream, >( &self, + new: impl Fn(&[u8; INIT_DATA_LEN]) -> KS, ) { - let key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); let init_data = [0xA5u8; INIT_DATA_LEN]; let blocks = DUMMY_SEED.as_chunks::().0; let n = blocks.len(); // The keystream itself: XORed into zeros. let mut keystream = vec![[0u8; BLOCK_LEN]; n]; - KS::new(&key, &init_data).unwrap().apply_blocks(&mut keystream); + new(&init_data).apply_blocks(&mut keystream); assert!( keystream.iter().any(|b| b.iter().any(|&x| x != 0)), "apply_blocks must produce keystream, not leave its input unchanged" @@ -66,7 +57,7 @@ impl TestFrameworkKeyStream { // XOR semantics: applied to data, the result is data XOR keystream. let mut data = blocks.to_vec(); - KS::new(&key, &init_data).unwrap().apply_blocks(&mut data); + new(&init_data).apply_blocks(&mut data); for ((d, k), p) in data.iter().zip(keystream.iter()).zip(blocks.iter()) { let expected: [u8; BLOCK_LEN] = core::array::from_fn(|i| p[i] ^ k[i]); assert_eq!(d, &expected, "apply_blocks must XOR the keystream into its input"); @@ -74,18 +65,18 @@ impl TestFrameworkKeyStream { // Deterministic in (key, init data), and dependent on the init data. let mut again = vec![[0u8; BLOCK_LEN]; n]; - KS::new(&key, &init_data).unwrap().apply_blocks(&mut again); + new(&init_data).apply_blocks(&mut again); assert_eq!(again, keystream, "the same key and init data must give the same keystream"); if INIT_DATA_LEN > 0 { let mut other = vec![[0u8; BLOCK_LEN]; n]; - KS::new(&key, &[0x5Au8; INIT_DATA_LEN]).unwrap().apply_blocks(&mut other); + new(&[0x5Au8; INIT_DATA_LEN]).apply_blocks(&mut other); assert_ne!(other, keystream, "different init data must give a different keystream"); } // Every chunking agrees with the single call, and `remaining_blocks` counts down by one per // block. The chunk sizes straddle the batch widths an implementor is likely to use. for chunk in [1usize, 2, 3, 4, 5, 7, 8, 9, n - 1, n] { - let mut ks = KS::new(&key, &init_data).unwrap(); + let mut ks = new(&init_data); let mut chunked = vec![[0u8; BLOCK_LEN]; n]; for piece in chunked.chunks_mut(chunk) { let before = ks.remaining_blocks(); @@ -103,59 +94,12 @@ impl TestFrameworkKeyStream { } // An empty call produces nothing and consumes nothing. - let mut ks = KS::new(&key, &init_data).unwrap(); + let mut ks = new(&init_data); let before = ks.remaining_blocks(); ks.apply_blocks(&mut []); assert_eq!(ks.remaining_blocks(), before, "an empty call must not consume keystream"); let mut first = [[0u8; BLOCK_LEN]]; ks.apply_blocks(&mut first); assert_eq!(first[0], keystream[0], "an empty call must not advance the keystream"); - - // error case: KeyMaterial of the wrong type - let mac_key = - KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) - .unwrap(); - match KS::new(&mac_key, &init_data) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("A key that is not a SymmetricCipherKey should have been rejected"), - }; - - // error case: security strengths too weak, and strong enough - let mut key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - let security_strengths = [ - SecurityStrength::None, - SecurityStrength::_112bit, - SecurityStrength::_128bit, - SecurityStrength::_192bit, - SecurityStrength::_256bit, - ]; - for ss in security_strengths.iter() { - // `set_security_strength` enforces its key-length guard even inside a - // do_hazardous_operations() closure, so skip the strengths a KEY_LEN-byte key cannot - // carry. Do NOT relax that guard in `KeyMaterial`: core's - // `test_hazardous_ops_error_handling` requires it to stay enforced. - if ss > &SecurityStrength::from_bytes(KEY_LEN) { - continue; - } - - // Tag the key at an arbitrary strength for the purpose of this test. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); - - match KS::new(&key, &init_data) { - Ok(_) => assert!( - ss >= &KS::MAX_SECURITY_STRENGTH, - "should have required a key at least as strong as the algorithm" - ), - Err(SymmetricCipherError::KeyMaterialError(_)) => assert!( - ss < &KS::MAX_SECURITY_STRENGTH, - "should not have rejected a key strong enough for the algorithm" - ), - _ => panic!("Unexpected error"), - }; - } } } diff --git a/crypto/core-test-framework/src/suspendable_state.rs b/crypto/core-test-framework/src/suspendable_state.rs index d9fa5e1a2..aa52789d3 100644 --- a/crypto/core-test-framework/src/suspendable_state.rs +++ b/crypto/core-test-framework/src/suspendable_state.rs @@ -107,12 +107,13 @@ impl TestFrameworkSuspendableKeyedState { /// /// Expects ta be handed an instance of the object that has some in-progress state to be serialized. pub fn test< + 'k, const SERIALIZED_STATE_LEN: usize, - S: SuspendableKeyed + Clone, + S: SuspendableKeyed<'k, SERIALIZED_STATE_LEN> + Clone, >( &self, instance: &S, - key: &S::Key, + key: &'k S::Key, ) { // There's not a lot we can test here in the abstract, but we can test a few things to // ensure that the SerializableState trait has been impl'd correctly. diff --git a/crypto/core/src/hazmat/ecb_key.rs b/crypto/core/src/hazmat/ecb_key.rs index 3a46309f9..5a3b5b2d2 100644 --- a/crypto/core/src/hazmat/ecb_key.rs +++ b/crypto/core/src/hazmat/ecb_key.rs @@ -12,11 +12,13 @@ use crate::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; /// Equal plaintext blocks give equal ciphertext blocks; see [`crate::hazmat`] for the supported /// uses. pub trait EcbEncryptionKey { - /// The streaming encryptor this key produces. - type Encryptor: BlockCipherEncryptor<0, BLOCK_LEN>; + /// The streaming encryptor this key produces, borrowing the key for `'k`. + type Encryptor<'k>: BlockCipherEncryptor<0, BLOCK_LEN> + where + Self: 'k; /// Begins an encryption. - fn create_encryptor(&self) -> Self::Encryptor; + fn create_encryptor<'k>(&'k self) -> Self::Encryptor<'k>; /// One-shot: encrypts `LEN` bytes, a whole number of blocks, in place, and returns the number /// of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. @@ -33,11 +35,13 @@ pub trait EcbEncryptionKey { /// A key that can begin an ECB decryption; see [`EcbEncryptionKey`]. pub trait EcbDecryptionKey { - /// The streaming decryptor this key produces. - type Decryptor: BlockCipherDecryptor<0, BLOCK_LEN>; + /// The streaming decryptor this key produces, borrowing the key for `'k`. + type Decryptor<'k>: BlockCipherDecryptor<0, BLOCK_LEN> + where + Self: 'k; /// Begins a decryption. - fn create_decryptor(&self) -> Self::Decryptor; + fn create_decryptor<'k>(&'k self) -> Self::Decryptor<'k>; /// One-shot: decrypts `LEN` bytes, a whole number of blocks, in place, and returns the number /// of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. diff --git a/crypto/core/src/hazmat/key_stream.rs b/crypto/core/src/hazmat/key_stream.rs index fccb2f4e4..3566bb0d3 100644 --- a/crypto/core/src/hazmat/key_stream.rs +++ b/crypto/core/src/hazmat/key_stream.rs @@ -1,8 +1,6 @@ //! A keyed keystream generator: the raw primitive under a stream cipher, as //! [`ElectronicCodeBook`] is the raw primitive under a block cipher mode. -use crate::errors::SymmetricCipherError; -use crate::key_material::KeyMaterial; use crate::traits::Algorithm; // Imports needed for docs @@ -17,8 +15,8 @@ use crate::traits::{BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEnc /// A keyed keystream generator: the raw primitive under a stream cipher, as /// [`ElectronicCodeBook`] is the raw primitive under a block cipher mode. /// -/// It is constructed from a key and init data and XORs successive keystream blocks into whatever -/// it is handed. It has no direction and no init-data policy: generating the nonce, buffering a +/// It is positioned by its init data over an already-keyed permutation it borrows, and XORs +/// successive keystream blocks into whatever it is handed. It has no direction and no init-data policy: generating the nonce, buffering a /// partly-used block between calls, and refusing a call that would run past the end of the /// keystream all belong to `bouncycastle_cipher::stream::StreamCipher`, which turns any /// `KeyStream` into a [`StreamCipherEncryptor`] / [`StreamCipherDecryptor`] pair. @@ -27,8 +25,8 @@ use crate::traits::{BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEnc /// keystream block is the encryption of the last ciphertext block. /// /// # 🚨 Security Considerations 🚨 -/// [`KeyStream::new`] takes the init data from the caller, so nothing stops a caller reusing a -/// nonce under a key -- which repeats the keystream and reveals the XOR of the two plaintexts -- +/// A keystream's constructor takes the init data from the caller, so nothing stops a caller +/// reusing a nonce under a key -- which repeats the keystream and reveals the XOR of the two plaintexts -- /// and nothing stops it running past [`KeyStream::remaining_blocks`]. `StreamCipher` generates /// the init data and enforces the limit; use it. See the [module docs](crate::hazmat) for the /// supported uses of the raw trait. @@ -36,20 +34,7 @@ use crate::traits::{BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEnc /// Implementors hold the key in a zeroize-on-drop wrapper, as for [`ElectronicCodeBook`]. Any /// keystream they produce into scratch space of their own is live key material until it has been /// XORed in, and gets the same treatment. -pub trait KeyStream: - Algorithm + Sized -{ - /// Expands the key and positions the keystream at its first block for `init_data`. - /// - /// # 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, - init_data: &[u8; INIT_DATA_LEN], - ) -> Result; - +pub trait KeyStream: Algorithm + Sized { /// How many more keystream blocks this value can produce before its keystream would repeat. /// A keystream with no practical limit returns `u64::MAX`. fn remaining_blocks(&self) -> u64; diff --git a/crypto/core/src/hazmat/mod.rs b/crypto/core/src/hazmat/mod.rs index d9a37ba22..0c4960c7a 100644 --- a/crypto/core/src/hazmat/mod.rs +++ b/crypto/core/src/hazmat/mod.rs @@ -23,6 +23,6 @@ pub use hazardous_operations::do_hazardous_operations; pub use key_stream::KeyStream; pub use mode::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, - BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbEncryptMode, MacMode, Mode, + BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbEncryptMode, MacMode, Mode, ModeKey, SymmetricDecryptMode, SymmetricEncryptMode, }; diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index 08b4de961..4fd356b14 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -14,6 +14,12 @@ //! for one that takes whole blocks (CBC with no padding layer), and [`EcbEncryptMode`] / //! [`EcbDecryptMode`] for ECB, which has no init data and so takes no RNG. A MAC mode -- CMAC -- //! implements [`MacMode`], whose worker takes no nonce and serves both roles. +//! +//! Every worker borrows the key state it is made from, for the lifetime `'k` on the mode's +//! associated worker type: the schedule stays in the key and is read through the reference, so +//! making a worker copies nothing but the per-message state, and a worker cannot outlive its +//! key. Resuming a suspended worker takes the same reference, which a key object exposes through +//! [`ModeKey::state`]. use crate::errors::SymmetricCipherError; use crate::key_material::KeyType; @@ -41,6 +47,15 @@ pub trait Mode

{ fn key_state(perm: P) -> Self::KeyState; } +/// A key object's mode state, borrowed: what a suspended worker is resumed over +/// (`SuspendableKeyed::Key` on a cipher mode's worker is the mode's `KeyState`). Hazmat because +/// the state is the bare keyed permutation, which the mode traits take directly; the safe route to +/// a worker is the key's `create_*` method. +pub trait ModeKey> { + /// The mode's per-key state this key holds. + fn state(&self) -> &M::KeyState; +} + // `KEY_LEN` is on these traits only while the cipher traits still carry it: the permutation bound // a mode impl needs, `P: ElectronicCodeBook`, must be tied to the impl's trait. It // goes when the constructors leave the cipher traits. @@ -54,23 +69,30 @@ pub trait AeadEncryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming encryptor this mode produces over `P`. - type Encryptor: AEADCipherEncryptor; + /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Encryptor<'k>: AEADCipherEncryptor + where + P: 'k; /// Begins an encryption from `state` under a nonce drawn from `rng`. /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError>; + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError>; /// Begins an encryption from `state` under a nonce the caller supplies. Infallible, as /// [`AeadDecryptMode::decrypt_init`] is: the state is keyed and the nonce length is in the /// type. The caller takes on the obligation [`encrypt_init`](Self::encrypt_init) discharges /// for it: the nonce must never repeat under this key. - fn encrypt_init_nonce(state: &Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor; + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; NONCE_LEN], + ) -> Self::Encryptor<'k>; /// Encrypts `buf` in place from `state` under `aad` and a nonce drawn from `rng`, returning /// the nonce and the detached tag. Provided by streaming `buf` through @@ -114,12 +136,16 @@ pub trait AeadDecryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming decryptor this mode produces over `P`. - type Decryptor: AEADCipherDecryptor; + /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Decryptor<'k>: AEADCipherDecryptor + where + P: 'k; /// Begins the decryption of a message encrypted under `nonce` from `state`. Infallible: the /// state is already keyed and the nonce length is in the type. - fn decrypt_init(state: &Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor; + fn decrypt_init<'k>(state: &'k Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k>; /// Decrypts `buf` in place from `state` under `nonce` and `aad`, and checks `tag`. On any /// error `buf` is left zeroized, since the plaintext was written into it before the tag could @@ -151,8 +177,12 @@ pub trait AeadDeclaredEncryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming encryptor this mode produces over `P`. - type Encryptor: AEADCipherEncryptor; + /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Encryptor<'k>: AEADCipherEncryptor + where + P: 'k; /// Begins an encryption of `payload_len` bytes under `aad_len` bytes of AAD, from `state` /// under a nonce drawn from `rng`. @@ -160,24 +190,24 @@ pub trait AeadDeclaredEncryptMode< /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails, or the mode's own refusal of /// a length it cannot encode. - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, aad_len: usize, payload_len: usize, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError>; + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError>; /// As [`encrypt_init`](Self::encrypt_init) under a nonce the caller supplies, who takes on /// the obligation that it never repeat under this key. /// /// # Errors /// The mode's own refusal of a length it cannot encode. - fn encrypt_init_nonce( - state: &Self::KeyState, + fn encrypt_init_nonce<'k>( + state: &'k Self::KeyState, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result; + ) -> Result, SymmetricCipherError>; /// Encrypts `buf` in place from `state` under `aad` and a nonce drawn from `rng`, returning /// the nonce and the detached tag. Provided by streaming `buf` through the encryptor a chunk @@ -223,20 +253,24 @@ pub trait AeadDeclaredDecryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming decryptor this mode produces over `P`. - type Decryptor: AEADCipherDecryptor; + /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Decryptor<'k>: AEADCipherDecryptor + where + P: 'k; /// Begins the decryption of `payload_len` bytes of ciphertext under `aad_len` bytes of AAD, /// encrypted under `nonce`, from `state`. /// /// # Errors /// The mode's own refusal of a length it cannot encode. - fn decrypt_init( - state: &Self::KeyState, + fn decrypt_init<'k>( + state: &'k Self::KeyState, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result; + ) -> Result, SymmetricCipherError>; /// Decrypts `buf` in place from `state` under `nonce` and `aad`, and checks `tag`. On any /// error after construction `buf` is left zeroized, since the plaintext was written into it @@ -266,17 +300,21 @@ pub trait SymmetricEncryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming encryptor this mode produces over `P`. - type Encryptor: SymmetricCipherEncryptor; + /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Encryptor<'k>: SymmetricCipherEncryptor + where + P: 'k; /// Begins an encryption from `state` under init data drawn from `rng`. /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the draw fails; nothing else can. - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError>; } /// The decryption side of an unauthenticated mode over `P` that takes data of any length. @@ -287,12 +325,19 @@ pub trait SymmetricDecryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming decryptor this mode produces over `P`. - type Decryptor: SymmetricCipherDecryptor; + /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Decryptor<'k>: SymmetricCipherDecryptor + where + P: 'k; /// Begins the decryption of a message encrypted under `init_data` from `state`. Infallible: /// the state is already keyed and the init data length is in the type. - fn decrypt_init(state: &Self::KeyState, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; + fn decrypt_init<'k>( + state: &'k Self::KeyState, + init_data: &[u8; INIT_DATA_LEN], + ) -> Self::Decryptor<'k>; } /// The encryption side of a whole-block mode over `P`: the mode's own constructor for its @@ -304,17 +349,21 @@ pub trait BlockEncryptMode< const BLOCK_LEN: usize, >: Mode

{ - /// The streaming encryptor this mode produces over `P`. - type Encryptor: BlockCipherEncryptor; + /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Encryptor<'k>: BlockCipherEncryptor + where + P: 'k; /// Begins an encryption from `state` under init data drawn from `rng`. /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the draw fails; nothing else can. - fn encrypt_init( - state: &Self::KeyState, + fn encrypt_init<'k>( + state: &'k Self::KeyState, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError>; } /// The decryption side of a whole-block mode over `P`. @@ -325,41 +374,60 @@ pub trait BlockDecryptMode< const BLOCK_LEN: usize, >: Mode

{ - /// The streaming decryptor this mode produces over `P`. - type Decryptor: BlockCipherDecryptor; + /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Decryptor<'k>: BlockCipherDecryptor + where + P: 'k; /// Begins the decryption of a message encrypted under `init_data` from `state`. Infallible. - fn decrypt_init(state: &Self::KeyState, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; + fn decrypt_init<'k>( + state: &'k Self::KeyState, + init_data: &[u8; INIT_DATA_LEN], + ) -> Self::Decryptor<'k>; } /// The encryption side of ECB over `P`: a whole-block mode with no init data, so its constructor /// takes no RNG and cannot fail. Its own pair rather than [`BlockEncryptMode`] with /// `INIT_DATA_LEN = 0`, so that nothing is handed an RNG it has no use for. pub trait EcbEncryptMode: Mode

{ - /// The streaming encryptor this mode produces over `P`. - type Encryptor: BlockCipherEncryptor<0, BLOCK_LEN>; + /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Encryptor<'k>: BlockCipherEncryptor<0, BLOCK_LEN> + where + P: 'k; /// Begins an encryption from `state`. - fn encrypt_init(state: &Self::KeyState) -> Self::Encryptor; + fn encrypt_init<'k>(state: &'k Self::KeyState) -> Self::Encryptor<'k>; } /// The decryption side of ECB over `P`; see [`EcbEncryptMode`]. pub trait EcbDecryptMode: Mode

{ - /// The streaming decryptor this mode produces over `P`. - type Decryptor: BlockCipherDecryptor<0, BLOCK_LEN>; + /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Decryptor<'k>: BlockCipherDecryptor<0, BLOCK_LEN> + where + P: 'k; /// Begins a decryption from `state`. - fn decrypt_init(state: &Self::KeyState) -> Self::Decryptor; + fn decrypt_init<'k>(state: &'k Self::KeyState) -> Self::Decryptor<'k>; } /// A MAC mode over `P`: the mode's own constructor for its worker, which generates and verifies /// alike. The role traits [`crate::symmetric_key::MacGenerationKey`] and /// [`crate::symmetric_key::MacVerificationKey`] forward to it. pub trait MacMode

: Mode

{ - /// The streaming MAC worker this mode produces over `P`. - type Worker: MAC; + /// The streaming MAC worker this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Worker<'k>: MAC + where + P: 'k; /// Begins a MAC over a new message from `state`. Infallible: the state is keyed and there is /// nothing to draw. - fn mac_init(state: &Self::KeyState) -> Self::Worker; + fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k>; } diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index 9e5c8fa69..65274eb4c 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -4,7 +4,8 @@ //! `HmacKey` -- and is validated once, when it is made. It holds whatever the construction //! keeps per key (an expanded schedule, HMAC's two pad states) and hands out one worker per //! message: an encryptor, a decryptor, a MAC generator or a MAC verifier. The worker traits in -//! [`crate::traits`] then describe only an object that already exists. +//! [`crate::traits`] then describe only an object that already exists. A worker borrows the key +//! it came from: the schedule is never copied into it, and it cannot outlive the key. //! //! Two kinds of trait live here: //! @@ -138,8 +139,12 @@ pub trait AeadEncryptionKey< const FINAL_LEN: usize, > { - /// The streaming encryptor this key produces. - type Encryptor: AEADCipherEncryptor; + /// The streaming encryptor this key produces. It borrows the key for `'k`: the key's per-key + /// state is read through the reference rather than copied into the worker, and the worker + /// cannot outlive the key. + type Encryptor<'k>: AEADCipherEncryptor + where + Self: 'k; /// Begins an encryption under a nonce drawn from `rng`. The nonce is returned alongside the /// encryptor and must reach the recipient, who passes it to @@ -147,14 +152,14 @@ pub trait AeadEncryptionKey< /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError>; + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError>; /// Begins an encryption under a nonce the caller supplies. Infallible, as /// [`AeadDecryptionKey::create_decryptor`] is. - fn create_encryptor_nonce(&self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor; + fn create_encryptor_nonce<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor<'k>; /// One-shot with the tag inline: encrypts `plaintext` under `aad` into `ciphertext`, which /// needs `encrypt_out_len(plaintext.len())` bytes of the encryptor type, and returns the number @@ -187,7 +192,13 @@ pub trait AeadEncryptionKey< aad: &[u8], plaintext: &[u8], ) -> Result<([u8; NONCE_LEN], Vec), SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as SymmetricCipherEncryptor< + NONCE_LEN, + FINAL_LEN, + >>::encrypt_out_len(plaintext.len()) + ]; let (written, nonce) = self.encrypt_with_aad_out(rng, aad, plaintext, &mut ciphertext)?; ciphertext.truncate(written); Ok((nonce, ciphertext)) @@ -222,7 +233,14 @@ pub trait AeadEncryptionKey< aad: &[u8], plaintext: &[u8], ) -> Result, SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_detached_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as AEADCipherEncryptor< + NONCE_LEN, + TAG_LEN, + FINAL_LEN, + >>::encrypt_detached_out_len(plaintext.len()) + ]; let (written, nonce, tag) = self.encrypt_detached_out(rng, aad, plaintext, &mut ciphertext)?; ciphertext.truncate(written); @@ -279,7 +297,13 @@ pub trait AeadEncryptionKey< aad: &[u8], plaintext: &[u8], ) -> Result, SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as SymmetricCipherEncryptor< + NONCE_LEN, + FINAL_LEN, + >>::encrypt_out_len(plaintext.len()) + ]; let written = self.encrypt_with_aad_nonce_out(nonce, aad, plaintext, &mut ciphertext)?; ciphertext.truncate(written); Ok(ciphertext) @@ -317,7 +341,14 @@ pub trait AeadEncryptionKey< aad: &[u8], plaintext: &[u8], ) -> Result<(Vec, [u8; TAG_LEN]), SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_detached_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as AEADCipherEncryptor< + NONCE_LEN, + TAG_LEN, + FINAL_LEN, + >>::encrypt_detached_out_len(plaintext.len()) + ]; let (written, tag) = self.encrypt_detached_nonce_out(nonce, aad, plaintext, &mut ciphertext)?; ciphertext.truncate(written); @@ -350,11 +381,15 @@ pub trait AeadDecryptionKey< const FINAL_LEN: usize, > { - /// The streaming decryptor this key produces. - type Decryptor: AEADCipherDecryptor; + /// The streaming decryptor this key produces. It borrows the key for `'k`: the key's per-key + /// state is read through the reference rather than copied into the worker, and the worker + /// cannot outlive the key. + type Decryptor<'k>: AEADCipherDecryptor + where + Self: 'k; /// Begins the decryption of a message whose encryptor returned `nonce`. - fn create_decryptor(&self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor; + fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k>; /// One-shot with the tag inline: decrypts `ciphertext` under `nonce` and `aad` into /// `plaintext`, which needs `decrypt_out_len(ciphertext.len())` bytes of the decryptor type, @@ -387,7 +422,13 @@ pub trait AeadDecryptionKey< aad: &[u8], ciphertext: &[u8], ) -> Result, SymmetricCipherError> { - let mut plaintext = vec![0u8; Self::Decryptor::decrypt_out_len(ciphertext.len())]; + let mut plaintext = vec![ + 0u8; + as SymmetricCipherDecryptor< + NONCE_LEN, + FINAL_LEN, + >>::decrypt_out_len(ciphertext.len()) + ]; let written = self.decrypt_with_aad_out(nonce, aad, ciphertext, &mut plaintext)?; plaintext.truncate(written); Ok(plaintext) @@ -428,7 +469,14 @@ pub trait AeadDecryptionKey< ciphertext: &[u8], tag: &[u8; TAG_LEN], ) -> Result, SymmetricCipherError> { - let mut plaintext = vec![0u8; Self::Decryptor::decrypt_detached_out_len(ciphertext.len())]; + let mut plaintext = vec![ + 0u8; + as AEADCipherDecryptor< + NONCE_LEN, + TAG_LEN, + FINAL_LEN, + >>::decrypt_detached_out_len(ciphertext.len()) + ]; let written = self.decrypt_detached_out(nonce, aad, ciphertext, tag, &mut plaintext)?; plaintext.truncate(written); Ok(plaintext) @@ -463,8 +511,12 @@ pub trait AeadDeclaredEncryptionKey< const FINAL_LEN: usize, > { - /// The streaming encryptor this key produces. - type Encryptor: AEADCipherEncryptor; + /// The streaming encryptor this key produces. It borrows the key for `'k`: the key's per-key + /// state is read through the reference rather than copied into the worker, and the worker + /// cannot outlive the key. + type Encryptor<'k>: AEADCipherEncryptor + where + Self: 'k; /// Begins an encryption of `payload_len` bytes under `aad_len` bytes of AAD and a nonce /// drawn from `rng`, which is returned. The worker then takes exactly those amounts. @@ -472,24 +524,24 @@ pub trait AeadDeclaredEncryptionKey< /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails, or the mode's refusal of a /// length it cannot encode. - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, aad_len: usize, payload_len: usize, - ) -> Result<(Self::Encryptor, [u8; NONCE_LEN]), SymmetricCipherError>; + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError>; /// As [`create_encryptor`](Self::create_encryptor) under a nonce the caller supplies, who /// takes on the obligation that it never repeat under this key. /// /// # Errors /// The mode's refusal of a length it cannot encode. - fn create_encryptor_nonce( - &self, + fn create_encryptor_nonce<'k>( + &'k self, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result; + ) -> Result, SymmetricCipherError>; /// As [`AeadEncryptionKey::encrypt_with_aad_out`]. /// @@ -519,7 +571,13 @@ pub trait AeadDeclaredEncryptionKey< aad: &[u8], plaintext: &[u8], ) -> Result<([u8; NONCE_LEN], Vec), SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as SymmetricCipherEncryptor< + NONCE_LEN, + FINAL_LEN, + >>::encrypt_out_len(plaintext.len()) + ]; let (written, nonce) = self.encrypt_with_aad_out(rng, aad, plaintext, &mut ciphertext)?; ciphertext.truncate(written); Ok((nonce, ciphertext)) @@ -552,7 +610,14 @@ pub trait AeadDeclaredEncryptionKey< aad: &[u8], plaintext: &[u8], ) -> Result, SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_detached_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as AEADCipherEncryptor< + NONCE_LEN, + TAG_LEN, + FINAL_LEN, + >>::encrypt_detached_out_len(plaintext.len()) + ]; let (written, nonce, tag) = self.encrypt_detached_out(rng, aad, plaintext, &mut ciphertext)?; ciphertext.truncate(written); @@ -602,7 +667,13 @@ pub trait AeadDeclaredEncryptionKey< aad: &[u8], plaintext: &[u8], ) -> Result, SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as SymmetricCipherEncryptor< + NONCE_LEN, + FINAL_LEN, + >>::encrypt_out_len(plaintext.len()) + ]; let written = self.encrypt_with_aad_nonce_out(nonce, aad, plaintext, &mut ciphertext)?; ciphertext.truncate(written); Ok(ciphertext) @@ -634,7 +705,14 @@ pub trait AeadDeclaredEncryptionKey< aad: &[u8], plaintext: &[u8], ) -> Result<(Vec, [u8; TAG_LEN]), SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_detached_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as AEADCipherEncryptor< + NONCE_LEN, + TAG_LEN, + FINAL_LEN, + >>::encrypt_detached_out_len(plaintext.len()) + ]; let (written, tag) = self.encrypt_detached_nonce_out(nonce, aad, plaintext, &mut ciphertext)?; ciphertext.truncate(written); @@ -667,20 +745,24 @@ pub trait AeadDeclaredDecryptionKey< const FINAL_LEN: usize, > { - /// The streaming decryptor this key produces. - type Decryptor: AEADCipherDecryptor; + /// The streaming decryptor this key produces. It borrows the key for `'k`: the key's per-key + /// state is read through the reference rather than copied into the worker, and the worker + /// cannot outlive the key. + type Decryptor<'k>: AEADCipherDecryptor + where + Self: 'k; /// Begins the decryption of `payload_len` bytes of ciphertext under `aad_len` bytes of AAD, /// encrypted under `nonce`. /// /// # Errors /// The mode's refusal of a length it cannot encode. - fn create_decryptor( - &self, + fn create_decryptor<'k>( + &'k self, nonce: &[u8; NONCE_LEN], aad_len: usize, payload_len: usize, - ) -> Result; + ) -> Result, SymmetricCipherError>; /// As [`AeadDecryptionKey::decrypt_with_aad_out`]. /// @@ -710,7 +792,13 @@ pub trait AeadDeclaredDecryptionKey< aad: &[u8], ciphertext: &[u8], ) -> Result, SymmetricCipherError> { - let mut plaintext = vec![0u8; Self::Decryptor::decrypt_out_len(ciphertext.len())]; + let mut plaintext = vec![ + 0u8; + as SymmetricCipherDecryptor< + NONCE_LEN, + FINAL_LEN, + >>::decrypt_out_len(ciphertext.len()) + ]; let written = self.decrypt_with_aad_out(nonce, aad, ciphertext, &mut plaintext)?; plaintext.truncate(written); Ok(plaintext) @@ -745,7 +833,14 @@ pub trait AeadDeclaredDecryptionKey< ciphertext: &[u8], tag: &[u8; TAG_LEN], ) -> Result, SymmetricCipherError> { - let mut plaintext = vec![0u8; Self::Decryptor::decrypt_detached_out_len(ciphertext.len())]; + let mut plaintext = vec![ + 0u8; + as AEADCipherDecryptor< + NONCE_LEN, + TAG_LEN, + FINAL_LEN, + >>::decrypt_detached_out_len(ciphertext.len()) + ]; let written = self.decrypt_detached_out(nonce, aad, ciphertext, tag, &mut plaintext)?; plaintext.truncate(written); Ok(plaintext) @@ -782,8 +877,12 @@ pub trait SymmetricEncryptionKey< const FINAL_LEN: usize, > { - /// The streaming encryptor this key produces. - type Encryptor: SymmetricCipherEncryptor; + /// The streaming encryptor this key produces. It borrows the key for `'k`: the key's per-key + /// state is read through the reference rather than copied into the worker, and the worker + /// cannot outlive the key. + type Encryptor<'k>: SymmetricCipherEncryptor + where + Self: 'k; /// Begins an encryption under init data drawn from `rng`, which is returned alongside the /// encryptor and must reach the recipient, who passes it to @@ -791,10 +890,10 @@ pub trait SymmetricEncryptionKey< /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the draw fails; nothing else can. - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError>; /// One-shot: encrypts `plaintext` into `ciphertext`, which needs `encrypt_out_len` bytes of /// the encryptor type, and returns the number of bytes written with the generated init data. @@ -824,7 +923,13 @@ pub trait SymmetricEncryptionKey< rng: &mut dyn RNG, plaintext: &[u8], ) -> Result<([u8; INIT_DATA_LEN], Vec), SymmetricCipherError> { - let mut ciphertext = vec![0u8; Self::Encryptor::encrypt_out_len(plaintext.len())]; + let mut ciphertext = vec![ + 0u8; + as SymmetricCipherEncryptor< + INIT_DATA_LEN, + FINAL_LEN, + >>::encrypt_out_len(plaintext.len()) + ]; let (written, init_data) = self.encrypt_out(rng, plaintext, &mut ciphertext)?; ciphertext.truncate(written); Ok((init_data, ciphertext)) @@ -837,13 +942,13 @@ pub trait SymmetricEncryptionKey< /// # Errors /// As [`create_encryptor`](Self::create_encryptor), or /// [`StreamCipherEncryptor::do_encrypt_inplace`]'s. - fn encrypt_inplace( - &self, + fn encrypt_inplace<'k>( + &'k self, rng: &mut dyn RNG, data: &mut [u8], ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> where - Self::Encryptor: StreamCipherEncryptor, + Self::Encryptor<'k>: StreamCipherEncryptor, { let (mut enc, init_data) = self.create_encryptor(rng)?; let written = StreamCipherEncryptor::do_encrypt_inplace(&mut enc, data)?; @@ -860,11 +965,15 @@ pub trait SymmetricDecryptionKey< const FINAL_LEN: usize, > { - /// The streaming decryptor this key produces. - type Decryptor: SymmetricCipherDecryptor; + /// The streaming decryptor this key produces. It borrows the key for `'k`: the key's per-key + /// state is read through the reference rather than copied into the worker, and the worker + /// cannot outlive the key. + type Decryptor<'k>: SymmetricCipherDecryptor + where + Self: 'k; /// Begins the decryption of a message whose encryptor returned `init_data`. - fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k>; /// One-shot: decrypts `ciphertext` under `init_data` into `plaintext`, which needs /// `decrypt_out_len(ciphertext.len())` bytes of the decryptor type, and returns the number @@ -897,7 +1006,13 @@ pub trait SymmetricDecryptionKey< init_data: &[u8; INIT_DATA_LEN], ciphertext: &[u8], ) -> Result, SymmetricCipherError> { - let mut plaintext = vec![0u8; Self::Decryptor::decrypt_out_len(ciphertext.len())]; + let mut plaintext = vec![ + 0u8; + as SymmetricCipherDecryptor< + INIT_DATA_LEN, + FINAL_LEN, + >>::decrypt_out_len(ciphertext.len()) + ]; let written = self.decrypt_out(init_data, ciphertext, &mut plaintext)?; plaintext.truncate(written); Ok(plaintext) @@ -908,13 +1023,13 @@ pub trait SymmetricDecryptionKey< /// /// # Errors /// [`StreamCipherDecryptor::do_decrypt_inplace`]'s. - fn decrypt_inplace( - &self, + fn decrypt_inplace<'k>( + &'k self, init_data: &[u8; INIT_DATA_LEN], data: &mut [u8], ) -> Result where - Self::Decryptor: StreamCipherDecryptor, + Self::Decryptor<'k>: StreamCipherDecryptor, { let mut dec = self.create_decryptor(init_data); StreamCipherDecryptor::do_decrypt_inplace(&mut dec, data) @@ -931,8 +1046,12 @@ pub trait BlockEncryptionKey< const BLOCK_LEN: usize, > { - /// The streaming encryptor this key produces. - type Encryptor: BlockCipherEncryptor; + /// The streaming encryptor this key produces. It borrows the key for `'k`: the key's per-key + /// state is read through the reference rather than copied into the worker, and the worker + /// cannot outlive the key. + type Encryptor<'k>: BlockCipherEncryptor + where + Self: 'k; /// Begins an encryption under init data drawn from `rng`, which is returned alongside the /// encryptor and must reach the recipient, who passes it to @@ -940,10 +1059,10 @@ pub trait BlockEncryptionKey< /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the draw fails; nothing else can. - fn create_encryptor( - &self, + fn create_encryptor<'k>( + &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError>; /// One-shot: encrypts `LEN` bytes, a whole number of blocks, in place under init data drawn /// from `rng`, and returns the number of bytes written with the init data. `LEN % BLOCK_LEN @@ -973,11 +1092,15 @@ pub trait BlockDecryptionKey< const BLOCK_LEN: usize, > { - /// The streaming decryptor this key produces. - type Decryptor: BlockCipherDecryptor; + /// The streaming decryptor this key produces. It borrows the key for `'k`: the key's per-key + /// state is read through the reference rather than copied into the worker, and the worker + /// cannot outlive the key. + type Decryptor<'k>: BlockCipherDecryptor + where + Self: 'k; /// Begins the decryption of a message whose encryptor returned `init_data`. - fn create_decryptor(&self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor; + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k>; /// One-shot: decrypts `LEN` bytes, a whole number of blocks, in place under `init_data`, and /// returns the number of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. @@ -1001,16 +1124,18 @@ pub trait BlockDecryptionKey< // which is ambiguous at every call site that has both in scope (`do_update` is on `MAC`, `do_final` // would be on both). The split happens when `MAC` loses its constructors and finals. -/// A key that can generate MACs. Each call to [`create_generator`](Self::create_generator) clones -/// the key's per-key state into a fresh worker, so any number of generators can be in flight from +/// A key that can generate MACs. Each call to [`create_generator`](Self::create_generator) starts +/// a fresh worker over the key's per-key state, so any number of generators can be in flight from /// one key and finish in any order; the key is never changed by use. pub trait MacGenerationKey { - /// The streaming worker this key produces for generation. - type Generator: MAC; + /// The streaming worker this key produces for generation, borrowing the key for `'k`. + type Generator<'k>: MAC + where + Self: 'k; /// Begins a MAC over a new message. Infallible: there is nothing to draw and nothing left to /// check. - fn create_generator(&self) -> Self::Generator; + fn create_generator<'k>(&'k self) -> Self::Generator<'k>; /// One-shot: the MAC of `data` into `out`, which is zeroized first; returns the number of /// bytes written. @@ -1036,11 +1161,13 @@ pub trait MacGenerationKey { /// A key that can verify MACs; see [`MacGenerationKey`] for the lifetime of the workers. A key /// may implement this and not [`MacGenerationKey`], which is what a verify-only token key is. pub trait MacVerificationKey { - /// The streaming worker this key produces for verification. - type Verifier: MAC; + /// The streaming worker this key produces for verification, borrowing the key for `'k`. + type Verifier<'k>: MAC + where + Self: 'k; /// Begins the verification of a new message. Infallible. - fn create_verifier(&self) -> Self::Verifier; + fn create_verifier<'k>(&'k self) -> Self::Verifier<'k>; /// One-shot: whether `mac` is the MAC of `data`, compared in constant time. fn verify(&self, data: &[u8], mac: &[u8]) -> bool { diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index cbeb3845e..4aaa8c4a4 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -743,8 +743,8 @@ impl SuspendableKeyed - for HKDF +impl<'k, H, const HASH_STATE_LEN: usize, const HKDF_STATE_LEN: usize> + SuspendableKeyed<'k, HKDF_STATE_LEN> for HKDF where H: Hash + HashAlgParams + Default + Suspendable, { @@ -781,7 +781,7 @@ where fn from_suspended( state: [u8; HKDF_STATE_LEN], - salt: &Self::Key, + salt: &'k Self::Key, ) -> Result { debug_assert_eq!(HKDF_STATE_LEN, HASH_STATE_LEN + 14); diff --git a/crypto/hkdf/tests/hkdf_tests.rs b/crypto/hkdf/tests/hkdf_tests.rs index 9736a1e82..82c46db2f 100644 --- a/crypto/hkdf/tests/hkdf_tests.rs +++ b/crypto/hkdf/tests/hkdf_tests.rs @@ -743,13 +743,14 @@ mod hkdf_tests { // A helper that exercises the full round-trip for one HKDF variant. A concrete `&KeyMaterial128` // works for `do_extract_init` (which wants a `Sized` `&impl KeyMaterialTrait`) and coerces to // `&dyn KeyMaterialTrait` for the serialization APIs. - fn round_trip( - salt: &KeyMaterial128, + fn round_trip<'k, const HASH_LEN: usize, const LEN: usize, H>( + salt: &'k KeyMaterial128, part1: &[u8], part2: &[u8], ) where H: Hash + HashAlgParams + Default, - HKDF: Clone + SuspendableKeyed, + HKDF: + Clone + SuspendableKeyed<'k, LEN, Key = dyn KeyMaterialTrait + 'static>, { let hkdf = HKDF::::new(); diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index fdd803013..aa1425f33 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -315,17 +315,23 @@ impl FromAnyKeyMaterial for HmacKey { } impl MacGenerationKey for HmacKey { - type Generator = HMAC; + type Generator<'k> + = HMAC + where + Self: 'k; - fn create_generator(&self) -> Self::Generator { + fn create_generator<'k>(&'k self) -> Self::Generator<'k> { HMAC { hasher: self.inner.clone(), outer: self.outer.clone() } } } impl MacVerificationKey for HmacKey { - type Verifier = HMAC; + type Verifier<'k> + = HMAC + where + Self: 'k; - fn create_verifier(&self) -> Self::Verifier { + fn create_verifier<'k>(&'k self) -> Self::Verifier<'k> { self.create_generator() } } @@ -448,8 +454,8 @@ impl MAC for HMAC { /// alone and is rebuilt from the re-supplied key on resume, so the state carries one secret fewer. /// The key is *not* authenticated against the state: resuming with the wrong key cannot be /// detected and produces a wrong MAC. -impl> - SuspendableKeyed for HMAC +impl<'k, const HASH_STATE_LEN: usize, HASH: Hash + Default + Suspendable> + SuspendableKeyed<'k, HASH_STATE_LEN> for HMAC { type Key = dyn KeyMaterialTrait; @@ -459,7 +465,7 @@ impl Result { let hasher = HASH::from_suspended(state)?; let HmacKey { outer, .. } = HmacKey::::from_key_bytes(key.ref_to_bytes()); diff --git a/crypto/hmac/tests/hmac_tests.rs b/crypto/hmac/tests/hmac_tests.rs index d8029e205..64ab8562a 100644 --- a/crypto/hmac/tests/hmac_tests.rs +++ b/crypto/hmac/tests/hmac_tests.rs @@ -820,12 +820,12 @@ mod hmac_tests { // The `+ 'static` on the trait object matches the associated type `type Key = dyn // KeyMaterialTrait` (a bare `dyn` in an associated type defaults to `'static`). The concrete // key types are owned, so they satisfy it. - fn round_trip( + fn round_trip<'k, const N: usize, H>( mut hmac: H, - key: &(dyn KeyMaterialTrait + 'static), + key: &'k (dyn KeyMaterialTrait + 'static), input: &[u8], ) where - H: MAC + Clone + SuspendableKeyed, + H: MAC + Clone + SuspendableKeyed<'k, N, Key = dyn KeyMaterialTrait + 'static>, { hmac.do_update(&input[..10]); diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs index fafa1f8a9..6ace1b04a 100644 --- a/crypto/sha3/src/cshake.rs +++ b/crypto/sha3/src/cshake.rs @@ -200,7 +200,7 @@ impl CSHAKEInternal { } } -impl SuspendableComponent for CSHAKEInternal { +impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for CSHAKEInternal { const STATE_LEN: usize = CSHAKE_COMPONENT_LEN; type Key = (); @@ -208,7 +208,7 @@ impl SuspendableComponent for CSHAKEInternal { self.write_tagged(PARAMS::CSHAKE_STATE_TAG, out) } - fn read_state(state: &[u8], _key: &()) -> Result { + fn read_state(state: &[u8], _key: &'k ()) -> Result { Self::read_tagged(state, PARAMS::CSHAKE_STATE_TAG) } } @@ -470,7 +470,7 @@ impl CSHAKESqueezer { /// Both phases suspend. The sponge's own phase flag records which, so the state is the cSHAKE /// layout under one tag, and a resumed `Unbound` squeezer still has its first read to make. -impl SuspendableComponent for CSHAKESqueezer { +impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for CSHAKESqueezer { const STATE_LEN: usize = CSHAKE_COMPONENT_LEN; type Key = (); @@ -490,7 +490,7 @@ impl SuspendableComponent for CSHAKESqueezer { } } - fn read_state(state: &[u8], _key: &()) -> Result { + fn read_state(state: &[u8], _key: &'k ()) -> Result { let (family, rest) = state.split_at(SHA3_FAMILY_STATE_LEN); let shake = SHAKEInternal::::read_family_state( family, diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index 6c6b5bec0..2420eb710 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -228,9 +228,12 @@ impl FromAnyKeyMaterial for KMACKeyInternal { } impl MacGenerationKey for KMACKeyInternal { - type Generator = KMACInternal; + type Generator<'k> + = KMACInternal + where + Self: 'k; - fn create_generator(&self) -> Self::Generator { + fn create_generator<'k>(&'k self) -> Self::Generator<'k> { KMACInternal { cshake: self.cshake.clone(), output_len: self.output_len, @@ -240,9 +243,12 @@ impl MacGenerationKey for KMACKeyInternal { } impl MacVerificationKey for KMACKeyInternal { - type Verifier = KMACInternal; + type Verifier<'k> + = KMACInternal + where + Self: 'k; - fn create_verifier(&self) -> Self::Verifier { + fn create_verifier<'k>(&'k self) -> Self::Verifier<'k> { self.create_generator() } } @@ -287,7 +293,7 @@ impl KMACInternal { } } -impl SuspendableComponent for KMACInternal { +impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for KMACInternal { const STATE_LEN: usize = CSHAKE_COMPONENT_LEN + 8; type Key = (); @@ -299,7 +305,7 @@ impl SuspendableComponent for KMACInternal { debug_assert!(w.is_done()); } - fn read_state(state: &[u8], _key: &()) -> Result { + fn read_state(state: &[u8], _key: &'k ()) -> Result { let (cshake, rest) = state.split_at(CSHAKE_COMPONENT_LEN); let cshake = CSHAKEInternal::read_tagged_customized(cshake, PARAMS::KMAC_STATE_TAG)?; let mut r = Cursor::new(rest); @@ -450,7 +456,7 @@ impl KMACXOFInternal { } } -impl SuspendableComponent for KMACXOFInternal { +impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for KMACXOFInternal { const STATE_LEN: usize = CSHAKE_COMPONENT_LEN; type Key = (); @@ -458,7 +464,7 @@ impl SuspendableComponent for KMACXOFInternal { self.cshake.write_tagged(PARAMS::KMACXOF_STATE_TAG, out) } - fn read_state(state: &[u8], _key: &()) -> Result { + fn read_state(state: &[u8], _key: &'k ()) -> Result { let cshake = CSHAKEInternal::read_tagged_customized(state, PARAMS::KMACXOF_STATE_TAG)?; Ok(Self { cshake, strength: SecurityStrength::from_bits(PARAMS::SIZE as usize) }) } diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs index b975c97c0..92be07334 100644 --- a/crypto/sha3/src/parallelhash.rs +++ b/crypto/sha3/src/parallelhash.rs @@ -250,7 +250,7 @@ impl ParallelHashInternal { } } -impl SuspendableComponent for ParallelHashInternal { +impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for ParallelHashInternal { const STATE_LEN: usize = PARALLEL_STATE_LEN + 8; type Key = (); @@ -262,7 +262,7 @@ impl SuspendableComponent for ParallelHashInternal debug_assert!(w.is_done()); } - fn read_state(state: &[u8], _key: &()) -> Result { + fn read_state(state: &[u8], _key: &'k ()) -> Result { let (parallel, rest) = state.split_at(PARALLEL_STATE_LEN); let state = ParallelState::read_state(parallel, PARAMS::PARALLELHASH_STATE_TAG)?; let mut r = Cursor::new(rest); @@ -384,7 +384,7 @@ impl ParallelHashXOFInternal { } } -impl SuspendableComponent for ParallelHashXOFInternal { +impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for ParallelHashXOFInternal { const STATE_LEN: usize = PARALLEL_STATE_LEN; type Key = (); @@ -392,7 +392,7 @@ impl SuspendableComponent for ParallelHashXOFInternal Result { + fn read_state(state: &[u8], _key: &'k ()) -> Result { Ok(Self { state: ParallelState::read_state(state, PARAMS::PARALLELHASHXOF_STATE_TAG)? }) } } diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs index 1c22fa897..391ce8813 100644 --- a/crypto/sha3/src/tuplehash.rs +++ b/crypto/sha3/src/tuplehash.rs @@ -140,7 +140,7 @@ impl TupleHashInternal { } } -impl SuspendableComponent for TupleHashInternal { +impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for TupleHashInternal { const STATE_LEN: usize = CSHAKE_COMPONENT_LEN + 8; type Key = (); @@ -152,7 +152,7 @@ impl SuspendableComponent for TupleHashInternal { debug_assert!(w.is_done()); } - fn read_state(state: &[u8], _key: &()) -> Result { + fn read_state(state: &[u8], _key: &'k ()) -> Result { let (cshake, rest) = state.split_at(CSHAKE_COMPONENT_LEN); let cshake = CSHAKEInternal::read_tagged_customized(cshake, PARAMS::TUPLEHASH_STATE_TAG)?; let mut r = Cursor::new(rest); @@ -293,7 +293,7 @@ impl TupleHashXOFInternal { } } -impl SuspendableComponent for TupleHashXOFInternal { +impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for TupleHashXOFInternal { const STATE_LEN: usize = CSHAKE_COMPONENT_LEN; type Key = (); @@ -301,7 +301,7 @@ impl SuspendableComponent for TupleHashXOFInternal self.cshake.write_tagged(PARAMS::TUPLEHASHXOF_STATE_TAG, out) } - fn read_state(state: &[u8], _key: &()) -> Result { + fn read_state(state: &[u8], _key: &'k ()) -> Result { let cshake = CSHAKEInternal::read_tagged_customized(state, PARAMS::TUPLEHASHXOF_STATE_TAG)?; Ok(Self { cshake }) } diff --git a/crypto/utils/src/suspendable_state.rs b/crypto/utils/src/suspendable_state.rs index d22ce2e9c..9e676bd24 100644 --- a/crypto/utils/src/suspendable_state.rs +++ b/crypto/utils/src/suspendable_state.rs @@ -35,7 +35,7 @@ //! /// A toy: a counter that must never exceed 100, and a key it is checked against on resume. //! struct Counter { count: usize } //! -//! impl SuspendableComponent for Counter { +//! impl<'k> SuspendableComponent<'k> for Counter { //! const STATE_LEN: usize = 8; //! type Key = u8; //! fn write_state(&self, out: &mut [u8]) { @@ -185,10 +185,12 @@ pub fn check_lib_ver( /// `write_state` and `read_state` are given exactly [`STATE_LEN`](Self::STATE_LEN) bytes. The /// version header is not part of it: a composite writes one header for the whole state, through /// [`suspend_component`] and [`resume_component`]. -pub trait SuspendableComponent: Sized { +pub trait SuspendableComponent<'k>: Sized { /// The number of bytes `write_state` fills and `read_state` reads. const STATE_LEN: usize; - /// The key that must be re-supplied to resume. It is never written into the state. + /// The key that must be re-supplied to resume. It is never written into the state. A + /// component that borrows its key for `'k` names the key's type here and is rebuilt over the + /// reference it is given; one that owns a copy ignores the lifetime. type Key: ?Sized; /// Writes the state into `out`, which is exactly `STATE_LEN` bytes. fn write_state(&self, out: &mut [u8]); @@ -197,13 +199,15 @@ pub trait SuspendableComponent: Sized { /// # Errors /// [`SuspendableError::InvalidData`] if `state` is not one this component could have /// written, or `key` is not a key the component accepts. - fn read_state(state: &[u8], key: &Self::Key) -> Result; + fn read_state(state: &[u8], key: &'k Self::Key) -> Result; } /// The `suspend` of a component: the version header, then its state. /// /// `N` must be `LIB_VERSION_LEN + C::STATE_LEN`, checked at compile time. -pub fn suspend_component(component: &C) -> [u8; N] { +pub fn suspend_component<'k, C: SuspendableComponent<'k>, const N: usize>( + component: &C, +) -> [u8; N] { const { assert!( N == LIB_VERSION_LEN + C::STATE_LEN, @@ -222,9 +226,9 @@ pub fn suspend_component(component: &C) /// # Errors /// [`SuspendableError::IncompatibleVersion`] from the header check, otherwise whatever /// [`SuspendableComponent::read_state`] returns. -pub fn resume_component( +pub fn resume_component<'k, C: SuspendableComponent<'k>, const N: usize>( state: &[u8; N], - key: &C::Key, + key: &'k C::Key, ) -> Result { const { assert!( diff --git a/mem_usage_benches/src/bench_ccm_mem_usage.rs b/mem_usage_benches/src/bench_ccm_mem_usage.rs index fc5ad87b3..d24202b51 100644 --- a/mem_usage_benches/src/bench_ccm_mem_usage.rs +++ b/mem_usage_benches/src/bench_ccm_mem_usage.rs @@ -58,10 +58,10 @@ const MESSAGE_LEN: usize = DATA_LEN; type Aes128CcmMode = CCM; /// The key bound to the fixed frame, whose streams are the pair below. type Aes128CcmPacketKey = AES128Key>; -type Aes128CcmEncryptor = - CcmPacketEncryptor; -type Aes128CcmDecryptor = - CcmPacketDecryptor; +type Aes128CcmEncryptor<'k> = + CcmPacketEncryptor<'k, AES128Internal, 16, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>; +type Aes128CcmDecryptor<'k> = + CcmPacketDecryptor<'k, AES128Internal, 16, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>; fn key() -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&[0x42u8; N], KeyType::SymmetricCipherKey).unwrap() @@ -112,25 +112,26 @@ fn print_struct_sizes() { "--- the trait pair: `Ccm` state + AAD_LEN + bookkeeping, independent of DATA_LEN ---" ); eprintln!( - "CcmPacketEncryptor<.., {AAD_LEN}, {DATA_LEN}> {:>7} B", + "CcmPacketEncryptor<'_, .., {AAD_LEN}, {DATA_LEN}> {:>7} B", size_of::() ); eprintln!( - "CcmPacketDecryptor<.., {AAD_LEN}, {DATA_LEN}> {:>7} B", + "CcmPacketDecryptor<'_, .., {AAD_LEN}, {DATA_LEN}> {:>7} B", size_of::() ); eprintln!( - "CcmPacketEncryptor<.., 64, 240> {:>7} B", - size_of::>() + "CcmPacketEncryptor<'_, .., 64, 240> {:>7} B", + size_of::>() ); eprintln!( - "CcmPacketEncryptor<.., 7, 4, ..> {:>7} B", - size_of::>() + "CcmPacketEncryptor<'_, .., 7, 4, ..> {:>7} B", + size_of::>() ); eprintln!( - "CcmPacketEncryptor {:>7} B", - size_of::>( - ) + "CcmPacketEncryptor<'_, AES256Internal> {:>7} B", + size_of::< + CcmPacketEncryptor<'_, AES256Internal, 32, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, + >() ); print!("{}", size_of::()); From bd97aeb9707351c922ed3d77e86071427750190a Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 06:12:28 +1100 Subject: [PATCH 16/54] core, cipher: BlockPermutation splits from ElectronicCodeBook; the key length leaves the workers (prototype) `BlockPermutation` now carries the block methods and `ElectronicCodeBook` adds only `new`. Every worker in the cipher crate and every `Mode

` impl bound on the former and no longer name a key length, so the GCM, GMAC and CMAC impls that `aes` wrote three times over are written once in `cipher`, generic over the permutation, and the cipher crate's tests lose their ToyGCM / ToyGMAC / ToyCMAC stand-ins. CMAC takes the block length as a parameter, `CMAC`, as CTR does. The AES GMAC pair becomes one inherent impl over `AESParams`. With that, `KEY_LEN` goes from every worker type (Cbc, Cfb, Cfb8, Ecb, CtrKeyStream, Ctr, StreamCipher, the padding adapters, Gcm, Gmac, Cmac, Ccm and both CCM pairs) and from `GcmKeyState::new` / `CmacKeyState::new`; the AES aliases follow. It stays on the mode traits' worker-producing halves, where `P: ElectronicCodeBook` is the tie between a key's length and its permutation's and is what keeps a key object's role impls to the one length. No layout, vector or size changes. quality_stats.sh, ./crypto: code lines 29792 -> 29432; unwraps and Err() unchanged at 784 / 440. Assisted-by: Claude:claude-mythos-5-1 --- CLAUDE.md | 13 +- crypto/aes/src/cbc.rs | 18 +-- crypto/aes/src/ccm.rs | 66 +------- crypto/aes/src/cfb.rs | 6 +- crypto/aes/src/cfb8.rs | 6 +- crypto/aes/src/cmac.rs | 65 +------- crypto/aes/src/ctr.rs | 6 +- crypto/aes/src/gcm.rs | 149 ++---------------- crypto/aes/src/gmac.rs | 141 ++++------------- crypto/aes/src/hazmat/aes_internal.rs | 10 +- crypto/aes/src/hazmat/ecb.rs | 29 ++-- crypto/aes/tests/cbc_bc-test-data.rs | 2 +- crypto/aes/tests/ccm_key_tests.rs | 12 +- crypto/aes/tests/cfb8_bc-test-data.rs | 2 +- crypto/aes/tests/cfb_bc-test-data.rs | 2 +- crypto/aes/tests/cmac_key_tests.rs | 6 +- crypto/aes/tests/ecb_bc-test-data.rs | 2 +- crypto/aes/tests/fips197_tests.rs | 2 +- crypto/aes/tests/gmac_key_tests.rs | 2 +- crypto/aes/tests/sp800_38a_cfb8_tests.rs | 2 +- crypto/aes/tests/sp800_38a_ecb_tests.rs | 2 +- crypto/aes/tests/sp800_38c_tests.rs | 31 ++-- crypto/cipher/src/modes/cmac.rs | 78 ++++++--- crypto/cipher/src/modes/ctr.rs | 28 ++-- crypto/cipher/src/modes/gmac.rs | 49 +++--- crypto/cipher/tests/ccm_suspend_tests.rs | 4 +- .../cipher/tests/common/toy_block_cipher.rs | 4 +- crypto/cipher/tests/common/toy_key.rs | 138 ++-------------- crypto/cipher/tests/modes/cfb8_tests.rs | 6 +- crypto/cipher/tests/modes/cfb_tests.rs | 8 +- crypto/cipher/tests/modes/cmac_tests.rs | 10 +- crypto/cipher/tests/modes/common/mod.rs | 10 +- crypto/cipher/tests/modes/gmac_tests.rs | 12 +- .../tests/modes/symmetric_cipher_api_tests.rs | 2 +- .../core/src/hazmat/electronic_code_book.rs | 46 +++--- crypto/core/src/hazmat/mod.rs | 2 +- crypto/core/src/hazmat/mode.rs | 8 +- mem_usage_benches/src/bench_aes_mem_usage.rs | 2 +- mem_usage_benches/src/bench_ccm_mem_usage.rs | 13 +- 39 files changed, 299 insertions(+), 695 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 992722129..0ab8b4324 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -215,11 +215,14 @@ What that taught, as mechanics for the next trait that sheds a constructor: `BlockEncryptMode`, `EcbEncryptMode`, `AeadEncryptMode`, `AeadDeclaredEncryptMode`, `MacMode`). Padding and frame parameters live in the mode type (`CBC`, `CBC`, `ECB`, `CTR<12>`, `CCM_Packet<12, 16, 64, 1024>`), not on a worker alias. -- **A key that needs the permutation's key length cannot be generic over it.** `Mode

` cannot - carry `KEY_LEN` while `ElectronicCodeBook` does, so the GCM, GMAC and CMAC - mode impls are written per AES key length; the cipher crate's tests use local `ToyGCM` / - `ToyCMAC` marker types for the same reason. A `BlockPermutation` split is the open - follow-up. +- **The key length stops at the key.** `BlockPermutation` carries the block methods + and `ElectronicCodeBook` adds only `new`, so a worker and a mode's + `Mode

` impl bound on the former and never name a key length; the GCM, GMAC and CMAC impls + are generic in `cipher` for that reason. `KEY_LEN` stays on the mode traits' worker-producing + halves as the tie between a key's length and its permutation's, through + `P: ElectronicCodeBook`, which is what keeps a key object's role impls to the one + length. A mode that must know the block length (`CMAC`, `CTR`) takes + it as a parameter, since stable Rust cannot read it off the permutation inside the impl. - **Test the key, not the worker pair.** The framework runners are `test_key` over a role pair and the construction suites in `core_test_framework::from_key_material`; a test of "a key of the wrong type is rejected" belongs to the construction suite, once, not to each mode's tests. diff --git a/crypto/aes/src/cbc.rs b/crypto/aes/src/cbc.rs index 9ded1e7e0..d21b99c65 100644 --- a/crypto/aes/src/cbc.rs +++ b/crypto/aes/src/cbc.rs @@ -156,16 +156,14 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti #[allow(non_camel_case_types)] pub type AES_CBC_128<'k, Dir, Pad> =

::Select< PaddedBlockCipherEncryptor< - Cbc<'k, AES128Internal, Encrypting, 16, AES_BLOCK_LEN>, + Cbc<'k, AES128Internal, Encrypting, AES_BLOCK_LEN>, Pad, - 16, AES_BLOCK_LEN, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Cbc<'k, AES128Internal, Decrypting, 16, AES_BLOCK_LEN>, + Cbc<'k, AES128Internal, Decrypting, AES_BLOCK_LEN>, Pad, - 16, AES_BLOCK_LEN, AES_BLOCK_LEN, >, @@ -175,16 +173,14 @@ pub type AES_CBC_128<'k, Dir, Pad> = ::Select< #[allow(non_camel_case_types)] pub type AES_CBC_192<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Cbc<'k, AES192Internal, Encrypting, 24, AES_BLOCK_LEN>, + Cbc<'k, AES192Internal, Encrypting, AES_BLOCK_LEN>, Pad, - 24, AES_BLOCK_LEN, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Cbc<'k, AES192Internal, Decrypting, 24, AES_BLOCK_LEN>, + Cbc<'k, AES192Internal, Decrypting, AES_BLOCK_LEN>, Pad, - 24, AES_BLOCK_LEN, AES_BLOCK_LEN, >, @@ -194,16 +190,14 @@ pub type AES_CBC_192<'k, Dir, Pad> = ::Select< #[allow(non_camel_case_types)] pub type AES_CBC_256<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Cbc<'k, AES256Internal, Encrypting, 32, AES_BLOCK_LEN>, + Cbc<'k, AES256Internal, Encrypting, AES_BLOCK_LEN>, Pad, - 32, AES_BLOCK_LEN, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Cbc<'k, AES256Internal, Decrypting, 32, AES_BLOCK_LEN>, + Cbc<'k, AES256Internal, Decrypting, AES_BLOCK_LEN>, Pad, - 32, AES_BLOCK_LEN, AES_BLOCK_LEN, >, diff --git a/crypto/aes/src/ccm.rs b/crypto/aes/src/ccm.rs index d8b1fd916..196cffc7b 100644 --- a/crypto/aes/src/ccm.rs +++ b/crypto/aes/src/ccm.rs @@ -235,26 +235,8 @@ pub type AES_CCM_128< const AAD_LEN: usize, const DATA_LEN: usize, > = ::Select< - CcmPacketEncryptor< - 'k, - AES128Internal, - 16, - AES_BLOCK_LEN, - NONCE_LEN, - TAG_LEN, - AAD_LEN, - DATA_LEN, - >, - CcmPacketDecryptor< - 'k, - AES128Internal, - 16, - AES_BLOCK_LEN, - NONCE_LEN, - TAG_LEN, - AAD_LEN, - DATA_LEN, - >, + CcmPacketEncryptor<'k, AES128Internal, AES_BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, + CcmPacketDecryptor<'k, AES128Internal, AES_BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, >; /// AES-192 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`. See [`AES_CCM_128`]. @@ -267,26 +249,8 @@ pub type AES_CCM_192< const AAD_LEN: usize, const DATA_LEN: usize, > = ::Select< - CcmPacketEncryptor< - 'k, - AES192Internal, - 24, - AES_BLOCK_LEN, - NONCE_LEN, - TAG_LEN, - AAD_LEN, - DATA_LEN, - >, - CcmPacketDecryptor< - 'k, - AES192Internal, - 24, - AES_BLOCK_LEN, - NONCE_LEN, - TAG_LEN, - AAD_LEN, - DATA_LEN, - >, + CcmPacketEncryptor<'k, AES192Internal, AES_BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, + CcmPacketDecryptor<'k, AES192Internal, AES_BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, >; /// AES-256 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`. See [`AES_CCM_128`]. @@ -299,24 +263,6 @@ pub type AES_CCM_256< const AAD_LEN: usize, const DATA_LEN: usize, > = ::Select< - CcmPacketEncryptor< - 'k, - AES256Internal, - 32, - AES_BLOCK_LEN, - NONCE_LEN, - TAG_LEN, - AAD_LEN, - DATA_LEN, - >, - CcmPacketDecryptor< - 'k, - AES256Internal, - 32, - AES_BLOCK_LEN, - NONCE_LEN, - TAG_LEN, - AAD_LEN, - DATA_LEN, - >, + CcmPacketEncryptor<'k, AES256Internal, AES_BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, + CcmPacketDecryptor<'k, AES256Internal, AES_BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, >; diff --git a/crypto/aes/src/cfb.rs b/crypto/aes/src/cfb.rs index 472a037fe..189f45184 100644 --- a/crypto/aes/src/cfb.rs +++ b/crypto/aes/src/cfb.rs @@ -94,12 +94,12 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// AES-128 in CFB128 mode. #[allow(non_camel_case_types)] -pub type AES_CFB_128<'k, Dir> = Cfb<'k, AES128Internal, Dir, 16, AES_BLOCK_LEN>; +pub type AES_CFB_128<'k, Dir> = Cfb<'k, AES128Internal, Dir, AES_BLOCK_LEN>; /// AES-192 in CFB128 mode. #[allow(non_camel_case_types)] -pub type AES_CFB_192<'k, Dir> = Cfb<'k, AES192Internal, Dir, 24, AES_BLOCK_LEN>; +pub type AES_CFB_192<'k, Dir> = Cfb<'k, AES192Internal, Dir, AES_BLOCK_LEN>; /// AES-256 in CFB128 mode. See [`AES_CFB_128`]. #[allow(non_camel_case_types)] -pub type AES_CFB_256<'k, Dir> = Cfb<'k, AES256Internal, Dir, 32, AES_BLOCK_LEN>; +pub type AES_CFB_256<'k, Dir> = Cfb<'k, AES256Internal, Dir, AES_BLOCK_LEN>; diff --git a/crypto/aes/src/cfb8.rs b/crypto/aes/src/cfb8.rs index e4de3178f..397ca600d 100644 --- a/crypto/aes/src/cfb8.rs +++ b/crypto/aes/src/cfb8.rs @@ -121,12 +121,12 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// AES-128 in CFB8 mode. #[allow(non_camel_case_types)] -pub type AES_CFB8_128<'k, Dir> = Cfb8<'k, AES128Internal, Dir, 16, AES_BLOCK_LEN>; +pub type AES_CFB8_128<'k, Dir> = Cfb8<'k, AES128Internal, Dir, AES_BLOCK_LEN>; /// AES-192 in CFB8 mode. #[allow(non_camel_case_types)] -pub type AES_CFB8_192<'k, Dir> = Cfb8<'k, AES192Internal, Dir, 24, AES_BLOCK_LEN>; +pub type AES_CFB8_192<'k, Dir> = Cfb8<'k, AES192Internal, Dir, AES_BLOCK_LEN>; /// AES-256 in CFB8 mode. See [`AES_CFB8_128`]. #[allow(non_camel_case_types)] -pub type AES_CFB8_256<'k, Dir> = Cfb8<'k, AES256Internal, Dir, 32, AES_BLOCK_LEN>; +pub type AES_CFB8_256<'k, Dir> = Cfb8<'k, AES256Internal, Dir, AES_BLOCK_LEN>; diff --git a/crypto/aes/src/cmac.rs b/crypto/aes/src/cmac.rs index 9cbb45857..9ec8451ad 100644 --- a/crypto/aes/src/cmac.rs +++ b/crypto/aes/src/cmac.rs @@ -44,73 +44,16 @@ //! [`KeyType::MACKey`]: bouncycastle_core::key_material::KeyType::MACKey use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::{CMAC, Cmac, CmacKeyState}; -use bouncycastle_core::hazmat::{MacMode, Mode}; -use bouncycastle_core::key_material::KeyType; +use bouncycastle_cipher::modes::Cmac; /// AES-128 in CMAC. #[allow(non_camel_case_types)] -pub type AES_CMAC_128<'k> = Cmac<'k, AES128Internal, 16, 16>; +pub type AES_CMAC_128<'k> = Cmac<'k, AES128Internal, 16>; /// AES-192 in CMAC. See [`AES_CMAC_128`]. #[allow(non_camel_case_types)] -pub type AES_CMAC_192<'k> = Cmac<'k, AES192Internal, 24, 16>; +pub type AES_CMAC_192<'k> = Cmac<'k, AES192Internal, 16>; /// AES-256 in CMAC. See [`AES_CMAC_128`]. #[allow(non_camel_case_types)] -pub type AES_CMAC_256<'k> = Cmac<'k, AES256Internal, 32, 16>; - -// Per key length for the reason the GCM impls are (see `gcm.rs`): the subkey derivation needs the -// permutation's `ElectronicCodeBook` bound, and `KEY_LEN` cannot be tied to -// `Mode

` from a generic impl. - -impl Mode for CMAC { - type KeyState = CmacKeyState; - const KEY_TYPE: KeyType = KeyType::MACKey; - - fn key_state(perm: AES128Internal) -> Self::KeyState { - CmacKeyState::new::<16>(perm) - } -} - -impl MacMode for CMAC { - type Worker<'k> = AES_CMAC_128<'k>; - - fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { - Cmac::from_key_state(state) - } -} - -impl Mode for CMAC { - type KeyState = CmacKeyState; - const KEY_TYPE: KeyType = KeyType::MACKey; - - fn key_state(perm: AES192Internal) -> Self::KeyState { - CmacKeyState::new::<24>(perm) - } -} - -impl MacMode for CMAC { - type Worker<'k> = AES_CMAC_192<'k>; - - fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { - Cmac::from_key_state(state) - } -} - -impl Mode for CMAC { - type KeyState = CmacKeyState; - const KEY_TYPE: KeyType = KeyType::MACKey; - - fn key_state(perm: AES256Internal) -> Self::KeyState { - CmacKeyState::new::<32>(perm) - } -} - -impl MacMode for CMAC { - type Worker<'k> = AES_CMAC_256<'k>; - - fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { - Cmac::from_key_state(state) - } -} +pub type AES_CMAC_256<'k> = Cmac<'k, AES256Internal, 16>; diff --git a/crypto/aes/src/ctr.rs b/crypto/aes/src/ctr.rs index fcb37fb08..45843f66e 100644 --- a/crypto/aes/src/ctr.rs +++ b/crypto/aes/src/ctr.rs @@ -103,12 +103,12 @@ pub const CTR_NONCE_LEN: usize = 12; /// AES-128 in CTR mode with a 12-byte nonce. #[allow(non_camel_case_types)] -pub type AES_CTR_128<'k, Dir> = Ctr<'k, AES128Internal, Dir, 16, AES_BLOCK_LEN, CTR_NONCE_LEN>; +pub type AES_CTR_128<'k, Dir> = Ctr<'k, AES128Internal, Dir, AES_BLOCK_LEN, CTR_NONCE_LEN>; /// AES-192 in CTR mode with a 12-byte nonce. #[allow(non_camel_case_types)] -pub type AES_CTR_192<'k, Dir> = Ctr<'k, AES192Internal, Dir, 24, AES_BLOCK_LEN, CTR_NONCE_LEN>; +pub type AES_CTR_192<'k, Dir> = Ctr<'k, AES192Internal, Dir, AES_BLOCK_LEN, CTR_NONCE_LEN>; /// AES-256 in CTR mode with a 12-byte nonce. See [`AES_CTR_128`]. #[allow(non_camel_case_types)] -pub type AES_CTR_256<'k, Dir> = Ctr<'k, AES256Internal, Dir, 32, AES_BLOCK_LEN, CTR_NONCE_LEN>; +pub type AES_CTR_256<'k, Dir> = Ctr<'k, AES256Internal, Dir, AES_BLOCK_LEN, CTR_NONCE_LEN>; diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs index 8a95f236a..17ba6e3af 100644 --- a/crypto/aes/src/gcm.rs +++ b/crypto/aes/src/gcm.rs @@ -137,12 +137,13 @@ //! All security considerations from [`bouncycastle_cipher::modes::gcm`] apply. use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN, Gcm, GcmKeyState}; +use bouncycastle_cipher::modes::Gcm; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::modes::GCM_NONCE_LEN; +#[allow(unused_imports)] use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::{AeadDecryptMode, AeadEncryptMode, Mode}; -use bouncycastle_core::key_material::KeyType; -use bouncycastle_core::traits::RNG; // Imports needed for docs #[allow(unused_imports)] @@ -155,144 +156,12 @@ use bouncycastle_core::traits::{ /// AES-128 in GCM with a 128-bit tag. #[allow(non_camel_case_types)] -pub type AES_GCM_128<'k, Dir> = Gcm<'k, AES128Internal, Dir, 16, 16>; +pub type AES_GCM_128<'k, Dir> = Gcm<'k, AES128Internal, Dir, 16>; /// AES-192 in GCM with a 128-bit tag. #[allow(non_camel_case_types)] -pub type AES_GCM_192<'k, Dir> = Gcm<'k, AES192Internal, Dir, 24, 16>; +pub type AES_GCM_192<'k, Dir> = Gcm<'k, AES192Internal, Dir, 16>; /// AES-256 in GCM with a 128-bit tag. See [`AES_GCM_128`]. #[allow(non_camel_case_types)] -pub type AES_GCM_256<'k, Dir> = Gcm<'k, AES256Internal, Dir, 32, 16>; - -// The `GCM` mode impls for the three AES permutations, which is what makes `AES128Key` and -// friends AEAD keys. Written per key length rather than once over `AESInternal

`: the generic -// form needs `Gcm<'k, AESInternal

, Dir, { P::KEY_LEN }, TAG_LEN>`, a const expression stable Rust -// rejects, and a generic impl in `cipher` over `P: ElectronicCodeBook` leaves -// `KEY_LEN` unconstrained on `Mode

`. Both go once `KEY_LEN` leaves the cipher traits. - -impl Mode for GCM { - type KeyState = GcmKeyState; - const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; - - fn key_state(perm: AES128Internal) -> Self::KeyState { - GcmKeyState::new::<16>(perm) - } -} - -impl AeadEncryptMode - for GCM -{ - type Encryptor<'k> = Gcm<'k, AES128Internal, Encrypting, 16, TAG_LEN>; - - fn encrypt_init<'k>( - state: &'k Self::KeyState, - rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - >::from_key_state(state, rng) - } - - fn encrypt_init_nonce<'k>( - state: &'k Self::KeyState, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Self::Encryptor<'k> { - >::from_key_state_nonce(state, nonce) - } -} - -impl AeadDecryptMode - for GCM -{ - type Decryptor<'k> = Gcm<'k, AES128Internal, Decrypting, 16, TAG_LEN>; - - fn decrypt_init<'k>( - state: &'k Self::KeyState, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Self::Decryptor<'k> { - >::from_key_state(state, nonce) - } -} - -impl Mode for GCM { - type KeyState = GcmKeyState; - const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; - - fn key_state(perm: AES192Internal) -> Self::KeyState { - GcmKeyState::new::<24>(perm) - } -} - -impl AeadEncryptMode - for GCM -{ - type Encryptor<'k> = Gcm<'k, AES192Internal, Encrypting, 24, TAG_LEN>; - - fn encrypt_init<'k>( - state: &'k Self::KeyState, - rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - >::from_key_state(state, rng) - } - - fn encrypt_init_nonce<'k>( - state: &'k Self::KeyState, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Self::Encryptor<'k> { - >::from_key_state_nonce(state, nonce) - } -} - -impl AeadDecryptMode - for GCM -{ - type Decryptor<'k> = Gcm<'k, AES192Internal, Decrypting, 24, TAG_LEN>; - - fn decrypt_init<'k>( - state: &'k Self::KeyState, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Self::Decryptor<'k> { - >::from_key_state(state, nonce) - } -} - -impl Mode for GCM { - type KeyState = GcmKeyState; - const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; - - fn key_state(perm: AES256Internal) -> Self::KeyState { - GcmKeyState::new::<32>(perm) - } -} - -impl AeadEncryptMode - for GCM -{ - type Encryptor<'k> = Gcm<'k, AES256Internal, Encrypting, 32, TAG_LEN>; - - fn encrypt_init<'k>( - state: &'k Self::KeyState, - rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - >::from_key_state(state, rng) - } - - fn encrypt_init_nonce<'k>( - state: &'k Self::KeyState, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Self::Encryptor<'k> { - >::from_key_state_nonce(state, nonce) - } -} - -impl AeadDecryptMode - for GCM -{ - type Decryptor<'k> = Gcm<'k, AES256Internal, Decrypting, 32, TAG_LEN>; - - fn decrypt_init<'k>( - state: &'k Self::KeyState, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Self::Decryptor<'k> { - >::from_key_state(state, nonce) - } -} +pub type AES_GCM_256<'k, Dir> = Gcm<'k, AES256Internal, Dir, 16>; diff --git a/crypto/aes/src/gmac.rs b/crypto/aes/src/gmac.rs index 5a0cde73c..a8002b126 100644 --- a/crypto/aes/src/gmac.rs +++ b/crypto/aes/src/gmac.rs @@ -1,5 +1,5 @@ -//! Type aliases for AES in GMAC (NIST SP 800-38D Sec 5.2), and the `GMAC` mode impls and the -//! generator/verifier pair that make [`AES128Key`] and friends GMAC keys. +//! Type aliases for AES in GMAC (NIST SP 800-38D Sec 5.2), and the generator/verifier pair that +//! makes [`AES128Key`] and friends GMAC keys. //! //! See [`bouncycastle_cipher::modes::gmac`] for the construction, the nonce and the security //! considerations. [`AES_GMAC_128`] and friends pin the permutation and its key length and fix @@ -41,42 +41,39 @@ //! //! [`AES128Key`]: crate::AES128Key -use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use crate::{AES128Key, AES192Key, AES256Key}; -use bouncycastle_cipher::modes::{GCM_NONCE_LEN, GMAC, GcmKeyState, Gmac}; +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal, AESInternal}; +use crate::{AESKey, AESParams}; +use bouncycastle_cipher::modes::{GCM_NONCE_LEN, GMAC, Gmac, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::Mode; -use bouncycastle_core::key_material::KeyType; +use bouncycastle_core::hazmat::BlockPermutation; use bouncycastle_core::traits::RNG; +// Imports needed for docs +#[allow(unused_imports)] +use crate::AES128Key; +// end of imports needed for docs + /// AES-128 in GMAC with a 128-bit tag. `Dir` is [`Encrypting`] to generate a tag or /// [`Decrypting`] to verify one. #[allow(non_camel_case_types)] -pub type AES_GMAC_128<'k, Dir> = Gmac<'k, AES128Internal, Dir, 16, 16>; +pub type AES_GMAC_128<'k, Dir> = Gmac<'k, AES128Internal, Dir, 16>; /// AES-192 in GMAC with a 128-bit tag. See [`AES_GMAC_128`]. #[allow(non_camel_case_types)] -pub type AES_GMAC_192<'k, Dir> = Gmac<'k, AES192Internal, Dir, 24, 16>; +pub type AES_GMAC_192<'k, Dir> = Gmac<'k, AES192Internal, Dir, 16>; /// AES-256 in GMAC with a 128-bit tag. See [`AES_GMAC_128`]. #[allow(non_camel_case_types)] -pub type AES_GMAC_256<'k, Dir> = Gmac<'k, AES256Internal, Dir, 32, 16>; - -// Per key length for the reason the GCM impls are (see `gcm.rs`): the worker carries `KEY_LEN`, -// which cannot be tied to `Mode

` from a generic impl. The pair is per key length for the same -// reason, since its return types name the worker. - -impl Mode for GMAC { - type KeyState = GcmKeyState; - const KEY_TYPE: KeyType = KeyType::MACKey; - - fn key_state(perm: AES128Internal) -> Self::KeyState { - GcmKeyState::new::<16>(perm) - } -} - -impl AES128Key> { +pub type AES_GMAC_256<'k, Dir> = Gmac<'k, AES256Internal, Dir, 16>; + +// The pair is inherent on the key rather than a role trait, since a GMAC tag is bound to a nonce; +// one impl over every AES key length. +impl AESKey> +where + P: AESParams, + AESInternal

: BlockPermutation<16> + ModeNames + Clone, +{ /// Begins a tag under a nonce drawn from `rng`, which is returned with the generator. /// /// # Errors @@ -85,10 +82,10 @@ impl AES128Key> { &'k self, rng: &mut dyn RNG, ) -> Result< - (Gmac<'k, AES128Internal, Encrypting, 16, TAG_LEN>, [u8; GCM_NONCE_LEN]), + (Gmac<'k, AESInternal

, Encrypting, TAG_LEN>, [u8; GCM_NONCE_LEN]), SymmetricCipherError, > { - Gmac::::from_key_state(&self.state, rng) + Gmac::, Encrypting, TAG_LEN>::from_key_state(&self.state, rng) } /// Begins a tag under a nonce the caller supplies, who takes on the obligation that it never @@ -96,97 +93,15 @@ impl AES128Key> { pub fn create_generator_nonce<'k>( &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac<'k, AES128Internal, Encrypting, 16, TAG_LEN> { - Gmac::::from_key_state_nonce(&self.state, nonce) + ) -> Gmac<'k, AESInternal

, Encrypting, TAG_LEN> { + Gmac::, Encrypting, TAG_LEN>::from_key_state_nonce(&self.state, nonce) } /// Begins the verification of a tag made under `nonce`. Infallible. pub fn create_verifier<'k>( &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac<'k, AES128Internal, Decrypting, 16, TAG_LEN> { - Gmac::::from_key_state(&self.state, nonce) - } -} - -impl Mode for GMAC { - type KeyState = GcmKeyState; - const KEY_TYPE: KeyType = KeyType::MACKey; - - fn key_state(perm: AES192Internal) -> Self::KeyState { - GcmKeyState::new::<24>(perm) - } -} - -impl AES192Key> { - /// As [`AES128Key::create_generator`]. - /// - /// # Errors - /// As [`AES128Key::create_generator`]. - pub fn create_generator<'k>( - &'k self, - rng: &mut dyn RNG, - ) -> Result< - (Gmac<'k, AES192Internal, Encrypting, 24, TAG_LEN>, [u8; GCM_NONCE_LEN]), - SymmetricCipherError, - > { - Gmac::::from_key_state(&self.state, rng) - } - - /// As [`AES128Key::create_generator_nonce`]. - pub fn create_generator_nonce<'k>( - &'k self, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac<'k, AES192Internal, Encrypting, 24, TAG_LEN> { - Gmac::::from_key_state_nonce(&self.state, nonce) - } - - /// As [`AES128Key::create_verifier`]. - pub fn create_verifier<'k>( - &'k self, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac<'k, AES192Internal, Decrypting, 24, TAG_LEN> { - Gmac::::from_key_state(&self.state, nonce) - } -} - -impl Mode for GMAC { - type KeyState = GcmKeyState; - const KEY_TYPE: KeyType = KeyType::MACKey; - - fn key_state(perm: AES256Internal) -> Self::KeyState { - GcmKeyState::new::<32>(perm) - } -} - -impl AES256Key> { - /// As [`AES128Key::create_generator`]. - /// - /// # Errors - /// As [`AES128Key::create_generator`]. - pub fn create_generator<'k>( - &'k self, - rng: &mut dyn RNG, - ) -> Result< - (Gmac<'k, AES256Internal, Encrypting, 32, TAG_LEN>, [u8; GCM_NONCE_LEN]), - SymmetricCipherError, - > { - Gmac::::from_key_state(&self.state, rng) - } - - /// As [`AES128Key::create_generator_nonce`]. - pub fn create_generator_nonce<'k>( - &'k self, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac<'k, AES256Internal, Encrypting, 32, TAG_LEN> { - Gmac::::from_key_state_nonce(&self.state, nonce) - } - - /// As [`AES128Key::create_verifier`]. - pub fn create_verifier<'k>( - &'k self, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac<'k, AES256Internal, Decrypting, 32, TAG_LEN> { - Gmac::::from_key_state(&self.state, nonce) + ) -> Gmac<'k, AESInternal

, Decrypting, TAG_LEN> { + Gmac::, Decrypting, TAG_LEN>::from_key_state(&self.state, nonce) } } diff --git a/crypto/aes/src/hazmat/aes_internal.rs b/crypto/aes/src/hazmat/aes_internal.rs index 8133c4a5a..cc8abc276 100644 --- a/crypto/aes/src/hazmat/aes_internal.rs +++ b/crypto/aes/src/hazmat/aes_internal.rs @@ -10,7 +10,7 @@ //! ``` //! use bouncycastle_aes::hazmat::AES128Internal; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; //! //! let key = KeyMaterial::<16>::from_bytes_as_type( //! &[0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, @@ -44,15 +44,15 @@ //! block: `u16` planes hold one block, `u32` planes two and `u64` planes four (see the //! `bitslice` module in the source). The round functions cost about the same whatever the //! width, so on a 64-bit machine four independent blocks cost little more than one. Where a -//! caller has them, [`ElectronicCodeBook::encrypt_4blocks`] is 3.0x the throughput of four -//! [`ElectronicCodeBook::encrypt_block`] calls on x86-64 (3.7x for decryption), and -//! [`ElectronicCodeBook::encrypt_2blocks`] 1.75x that of two (1.95x for decryption); the crate +//! caller has them, [`BlockPermutation::encrypt_4blocks`] is 3.0x the throughput of four +//! [`BlockPermutation::encrypt_block`] calls on x86-64 (3.7x for decryption), and +//! [`BlockPermutation::encrypt_2blocks`] 1.75x that of two (1.95x for decryption); the crate //! docs have the table and the benches record the numbers: //! //! ``` //! use bouncycastle_aes::hazmat::AES256Internal; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; //! //! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x01; 32], KeyType::SymmetricCipherKey) //! .expect("a 32-byte symmetric cipher key"); diff --git a/crypto/aes/src/hazmat/ecb.rs b/crypto/aes/src/hazmat/ecb.rs index 7904ae3f5..644c52e39 100644 --- a/crypto/aes/src/hazmat/ecb.rs +++ b/crypto/aes/src/hazmat/ecb.rs @@ -177,7 +177,7 @@ use bouncycastle_cipher::modes::hazmat::ecb::Ecb; use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::KeyMaterial; // Imports needed for docs @@ -193,16 +193,14 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti #[allow(non_camel_case_types)] pub type AES_ECB_128<'k, Dir, Pad> =

::Select< PaddedBlockCipherEncryptor< - Ecb<'k, AES128Internal, Encrypting, 16, AES_BLOCK_LEN>, + Ecb<'k, AES128Internal, Encrypting, AES_BLOCK_LEN>, Pad, - 16, 0, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Ecb<'k, AES128Internal, Decrypting, 16, AES_BLOCK_LEN>, + Ecb<'k, AES128Internal, Decrypting, AES_BLOCK_LEN>, Pad, - 16, 0, AES_BLOCK_LEN, >, @@ -212,16 +210,14 @@ pub type AES_ECB_128<'k, Dir, Pad> = ::Select< #[allow(non_camel_case_types)] pub type AES_ECB_192<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Ecb<'k, AES192Internal, Encrypting, 24, AES_BLOCK_LEN>, + Ecb<'k, AES192Internal, Encrypting, AES_BLOCK_LEN>, Pad, - 24, 0, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Ecb<'k, AES192Internal, Decrypting, 24, AES_BLOCK_LEN>, + Ecb<'k, AES192Internal, Decrypting, AES_BLOCK_LEN>, Pad, - 24, 0, AES_BLOCK_LEN, >, @@ -231,16 +227,14 @@ pub type AES_ECB_192<'k, Dir, Pad> = ::Select< #[allow(non_camel_case_types)] pub type AES_ECB_256<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Ecb<'k, AES256Internal, Encrypting, 32, AES_BLOCK_LEN>, + Ecb<'k, AES256Internal, Encrypting, AES_BLOCK_LEN>, Pad, - 32, 0, AES_BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Ecb<'k, AES256Internal, Decrypting, 32, AES_BLOCK_LEN>, + Ecb<'k, AES256Internal, Decrypting, AES_BLOCK_LEN>, Pad, - 32, 0, AES_BLOCK_LEN, >, @@ -250,6 +244,9 @@ impl ElectronicCodeBook<16, AES_BLOCK_LEN> for AES128Internal { fn new(key: &KeyMaterial<16>) -> Result { AES128Internal::new(key) } +} + +impl BlockPermutation for AES128Internal { fn encrypt_block(&self, block: &mut Block) { Self::encrypt_block(self, block) } @@ -274,6 +271,9 @@ impl ElectronicCodeBook<24, AES_BLOCK_LEN> for AES192Internal { fn new(key: &KeyMaterial<24>) -> Result { AES192Internal::new(key) } +} + +impl BlockPermutation for AES192Internal { fn encrypt_block(&self, block: &mut Block) { Self::encrypt_block(self, block) } @@ -298,6 +298,9 @@ impl ElectronicCodeBook<32, AES_BLOCK_LEN> for AES256Internal { fn new(key: &KeyMaterial<32>) -> Result { AES256Internal::new(key) } +} + +impl BlockPermutation for AES256Internal { fn encrypt_block(&self, block: &mut Block) { Self::encrypt_block(self, block) } diff --git a/crypto/aes/tests/cbc_bc-test-data.rs b/crypto/aes/tests/cbc_bc-test-data.rs index ab98a03c2..ca2ed7fb4 100644 --- a/crypto/aes/tests/cbc_bc-test-data.rs +++ b/crypto/aes/tests/cbc_bc-test-data.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 `ElectronicCodeBook::decrypt_2blocks`, so the pair path is +//! puts the multi-block cases through `BlockPermutation::decrypt_2blocks`, so the pair path is //! exercised against real vectors and not only against the toy in //! `crypto/cipher/tests/modes/cbc_tests.rs`. //! diff --git a/crypto/aes/tests/ccm_key_tests.rs b/crypto/aes/tests/ccm_key_tests.rs index 2ea90139a..08ede2f81 100644 --- a/crypto/aes/tests/ccm_key_tests.rs +++ b/crypto/aes/tests/ccm_key_tests.rs @@ -198,10 +198,10 @@ fn worker_sizes_match_the_documented_memory_table() { use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_cipher::modes::{CcmDecryptor, CcmEncryptor}; use core::mem::size_of; - assert_eq!(size_of::>(), 96); - assert_eq!(size_of::>(), 96); - assert_eq!(size_of::>(), 96); - assert_eq!(size_of::>(), 120); - assert_eq!(size_of::>(), 120); - assert_eq!(size_of::>(), 120); + assert_eq!(size_of::>(), 96); + assert_eq!(size_of::>(), 96); + assert_eq!(size_of::>(), 96); + assert_eq!(size_of::>(), 120); + assert_eq!(size_of::>(), 120); + assert_eq!(size_of::>(), 120); } diff --git a/crypto/aes/tests/cfb8_bc-test-data.rs b/crypto/aes/tests/cfb8_bc-test-data.rs index 098d26c86..9dedbd90f 100644 --- a/crypto/aes/tests/cfb8_bc-test-data.rs +++ b/crypto/aes/tests/cfb8_bc-test-data.rs @@ -23,7 +23,7 @@ //! that reach the batch paths. Every case is run **four times**: as one call over the whole //! payload, byte by byte, in 8-byte calls, and in 3-byte calls that never line up with the //! 8-byte batch. Between them those put the multi-byte cases through -//! [`ElectronicCodeBook::encrypt_4blocks`] and [`ElectronicCodeBook::encrypt_2blocks`] -- the +//! [`BlockPermutation::encrypt_4blocks`] and [`BlockPermutation::encrypt_2blocks`] -- the //! *forward* function, even on the decrypt side -- and through the single-byte path, with the //! shift register carried across calls at every alignment. So all of that is exercised against real //! vectors and not only against the toys in `crypto/cipher/tests/modes/cfb8_tests.rs`. diff --git a/crypto/aes/tests/cfb_bc-test-data.rs b/crypto/aes/tests/cfb_bc-test-data.rs index 462d57bcf..a01c027e9 100644 --- a/crypto/aes/tests/cfb_bc-test-data.rs +++ b/crypto/aes/tests/cfb_bc-test-data.rs @@ -25,7 +25,7 @@ //! block, in pairs with a one-block remainder for odd lengths, as one call over the whole payload, //! and in 5-byte calls that never line up with a block. The second and third passes are what put //! the multi-block cases through the pair and four-block paths -- which for CFB are -//! [`ElectronicCodeBook::encrypt_2blocks`] and [`ElectronicCodeBook::encrypt_4blocks`], the +//! [`BlockPermutation::encrypt_2blocks`] and [`BlockPermutation::encrypt_4blocks`], the //! *forward* function, even on the decrypt side -- and the fourth is what puts them through the //! byte path with segments left open between calls. So all of that is exercised against real //! vectors and not only against the toys in `crypto/cipher/tests/modes/cfb_tests.rs`. Every ACVP diff --git a/crypto/aes/tests/cmac_key_tests.rs b/crypto/aes/tests/cmac_key_tests.rs index bf9ff2d37..7d9eaecfb 100644 --- a/crypto/aes/tests/cmac_key_tests.rs +++ b/crypto/aes/tests/cmac_key_tests.rs @@ -42,9 +42,9 @@ fn the_key_object_gives_the_csrc_answer() { #[test] fn the_key_and_the_bare_key_state_agree_at_every_key_length() { - let state_16 = CmacKeyState::new::<16>(AES128Internal::new(&cipher_key::<16>(0x0b)).unwrap()); - let state_24 = CmacKeyState::new::<24>(AES192Internal::new(&cipher_key::<24>(0x0c)).unwrap()); - let state_32 = CmacKeyState::new::<32>(AES256Internal::new(&cipher_key::<32>(0x0d)).unwrap()); + let state_16 = CmacKeyState::new(AES128Internal::new(&cipher_key::<16>(0x0b)).unwrap()); + let state_24 = CmacKeyState::new(AES192Internal::new(&cipher_key::<24>(0x0c)).unwrap()); + let state_32 = CmacKeyState::new(AES256Internal::new(&cipher_key::<32>(0x0d)).unwrap()); for len in [0usize, 1, 15, 16, 17, 43] { let msg = &MSG[..len]; assert_eq!( diff --git a/crypto/aes/tests/ecb_bc-test-data.rs b/crypto/aes/tests/ecb_bc-test-data.rs index e1fae0de4..f46341bd3 100644 --- a/crypto/aes/tests/ecb_bc-test-data.rs +++ b/crypto/aes/tests/ecb_bc-test-data.rs @@ -48,8 +48,8 @@ use bouncycastle_aes::AES_BLOCK_LEN; use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json}; diff --git a/crypto/aes/tests/fips197_tests.rs b/crypto/aes/tests/fips197_tests.rs index df62bc68d..a37f0a23d 100644 --- a/crypto/aes/tests/fips197_tests.rs +++ b/crypto/aes/tests/fips197_tests.rs @@ -15,7 +15,7 @@ //! All values here are transcribed from the published FIPS 197 (Update 1) PDF. use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; diff --git a/crypto/aes/tests/gmac_key_tests.rs b/crypto/aes/tests/gmac_key_tests.rs index ec77684bc..3a0523c8d 100644 --- a/crypto/aes/tests/gmac_key_tests.rs +++ b/crypto/aes/tests/gmac_key_tests.rs @@ -148,6 +148,6 @@ fn sizes() { assert_eq!(size_of::>(), size_of::>()); assert_eq!( size_of::>(), - size_of::>() + size_of::>() ); } diff --git a/crypto/aes/tests/sp800_38a_cfb8_tests.rs b/crypto/aes/tests/sp800_38a_cfb8_tests.rs index 2b19bb0b0..a384fd2ef 100644 --- a/crypto/aes/tests/sp800_38a_cfb8_tests.rs +++ b/crypto/aes/tests/sp800_38a_cfb8_tests.rs @@ -33,7 +33,7 @@ use bouncycastle_aes::hazmat::AES128Internal; use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; use bouncycastle_cipher::modes::CFB8; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{ FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, diff --git a/crypto/aes/tests/sp800_38a_ecb_tests.rs b/crypto/aes/tests/sp800_38a_ecb_tests.rs index 3f49390cc..11a59e12a 100644 --- a/crypto/aes/tests/sp800_38a_ecb_tests.rs +++ b/crypto/aes/tests/sp800_38a_ecb_tests.rs @@ -18,7 +18,7 @@ use bouncycastle_aes::AES_BLOCK_LEN; use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_hex as hex; diff --git a/crypto/aes/tests/sp800_38c_tests.rs b/crypto/aes/tests/sp800_38c_tests.rs index ee7818a6c..210d450bb 100644 --- a/crypto/aes/tests/sp800_38c_tests.rs +++ b/crypto/aes/tests/sp800_38c_tests.rs @@ -970,8 +970,8 @@ fn an_inline_ciphertext_shorter_than_the_tag_is_rejected() { #[test] fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { const DATA_LEN: usize = 32; - type Enc<'k> = CcmPacketEncryptor<'k, AES128Internal, 16, 16, 12, 16, 16, DATA_LEN>; - type Dec<'k> = CcmPacketDecryptor<'k, AES128Internal, 16, 16, 12, 16, 16, DATA_LEN>; + type Enc<'k> = CcmPacketEncryptor<'k, AES128Internal, 16, 12, 16, 16, DATA_LEN>; + type Dec<'k> = CcmPacketDecryptor<'k, AES128Internal, 16, 12, 16, 16, DATA_LEN>; let key = frame_key::<12, 16, 16, DATA_LEN>(); let declared = AES128Key::>::from_key_material(&material::<16>(APPENDIX_C_KEY)) .expect("the appendix key"); @@ -1095,37 +1095,28 @@ fn sizes_match_the_documented_memory_table() { use core::mem::size_of; // The pair holds no payload: a 4 KiB frame costs exactly what a 16-byte one does. - let enc_64 = size_of::>(); - assert_eq!(enc_64, size_of::>()); + let enc_64 = size_of::>(); + assert_eq!(enc_64, size_of::>()); assert_eq!( enc_64, 96 + 64 + 16, "the `Ccm` state, the AAD buffer, its length and the phase flag" ); assert_eq!( - size_of::>() - enc_64, + size_of::>() - enc_64, 4096 - 64, "the value grows by exactly the AAD capacity" ); // Independent of NONCE_LEN and TAG_LEN: the nonce lives inside the counter template and the // tag is assembled at finalization, not held. - assert_eq!(enc_64, size_of::>()); - assert_eq!( - enc_64, - size_of::>() - ); + assert_eq!(enc_64, size_of::>()); + assert_eq!(enc_64, size_of::>()); // The key schedule stays in the key, so the key length makes no difference. - assert_eq!( - size_of::>(), - enc_64 - ); - assert_eq!( - size_of::>(), - enc_64 - ); + assert_eq!(size_of::>(), enc_64); + assert_eq!(size_of::>(), enc_64); // The decryptor adds the tag it holds back and that tag's length. - let dec_64 = size_of::>(); - assert_eq!(dec_64, size_of::>()); + let dec_64 = size_of::>(); + assert_eq!(dec_64, size_of::>()); assert_eq!(dec_64, enc_64 + 16 + 8); } diff --git a/crypto/cipher/src/modes/cmac.rs b/crypto/cipher/src/modes/cmac.rs index 6e6489428..f5cfcd10c 100644 --- a/crypto/cipher/src/modes/cmac.rs +++ b/crypto/cipher/src/modes/cmac.rs @@ -1,6 +1,6 @@ //! CMAC, the block-cipher-based message authentication code of NIST SP 800-38B. //! -//! [`Cmac`] implements [`MAC`] over any [`ElectronicCodeBook`] permutation with a 64- or 128-bit +//! [`Cmac`] implements [`MAC`] over any [`BlockPermutation`] permutation with a 64- or 128-bit //! block (the two block sizes SP 800-38B Sec 5.3 defines `R_b` for). The output is one full block, //! which [`MAC::mac_out`] / [`MAC::do_final_out`] truncate to the caller's buffer, down to //! [`CMAC_MIN_TAG_LEN`] bytes. @@ -23,10 +23,10 @@ //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::traits::MAC; //! -//! type ToyCmac<'k> = Cmac<'k, ToyBlockCipher, 16, 16>; +//! type ToyCmac<'k> = Cmac<'k, ToyBlockCipher, 16>; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let state = CmacKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); +//! let state = CmacKeyState::new(ToyBlockCipher::new(&key).unwrap()); //! let tag = ToyCmac::from_key_state(&state).mac(b"a message"); //! assert_eq!(tag.len(), 16); //! assert!(ToyCmac::from_key_state(&state).verify(b"a message", &tag)); @@ -43,9 +43,9 @@ //! use bouncycastle_core::traits::MAC; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let state = CmacKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); +//! let state = CmacKeyState::new(ToyBlockCipher::new(&key).unwrap()); //! -//! let mut cmac = Cmac::::from_key_state(&state); +//! let mut cmac = Cmac::::from_key_state(&state); //! cmac.do_update(b"a message that arrives "); //! cmac.do_update(b"in pieces"); //! let mut tag = [0u8; 8]; @@ -71,7 +71,8 @@ use super::ModeNames; use alloc::vec::Vec; use bouncycastle_core::errors::{MACError, SuspendableError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, MacMode, Mode}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, MAC, SuspendableKeyed}; use bouncycastle_utils::ct::ct_eq_bytes; @@ -84,10 +85,38 @@ use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; /// a careful analysis of the risks"). pub const CMAC_MIN_TAG_LEN: usize = 8; -/// The CMAC mode, as a key type names it: `AES128Key`. The mode-trait impls over each -/// permutation live with that permutation. +/// The CMAC mode, as a key type names it: `AES128Key`. `BLOCK_LEN` is the permutation's +/// block length, 16 by default and 8 for a 64-bit block cipher; it is a parameter because stable +/// Rust cannot read it off the permutation from inside the mode impl, as for `CTR`. The impls +/// below are generic over any [`BlockPermutation`] of that block length. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CMAC; +pub struct CMAC; + +impl Mode

for CMAC +where + P: BlockPermutation + ModeNames + Clone, +{ + type KeyState = CmacKeyState; + const KEY_TYPE: KeyType = KeyType::MACKey; + + fn key_state(perm: P) -> Self::KeyState { + CmacKeyState::new(perm) + } +} + +impl MacMode

for CMAC +where + P: BlockPermutation + ModeNames + Clone, +{ + type Worker<'k> + = Cmac<'k, P, BLOCK_LEN> + where + P: 'k; + + fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { + Cmac::from_key_state(state) + } +} /// What a CMAC key keeps per key: the keyed permutation and the subkeys `K1` and `K2` /// (Sec 6.1), derived once from `L = CIPH_K(0^b)` rather than per message. @@ -110,9 +139,9 @@ impl CmacKeyState { }; /// Derives the subkeys from an already-keyed permutation (Sec 6.1). - pub fn new(perm: P) -> Self + pub fn new(perm: P) -> Self where - P: ElectronicCodeBook, + P: BlockPermutation, { // Step 1: L = CIPH_K(0^b). Sec 5.3: L "shall also be secret", so it is computed in place // in a `Secret`. @@ -138,11 +167,11 @@ impl CmacKeyState { } } -/// CMAC (SP 800-38B) over the permutation `P`, keyed by a `KEY_LEN`-byte key, with a `BLOCK_LEN` -/// of 8 or 16 bytes. See the module docs. -pub struct Cmac<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize> +/// CMAC (SP 800-38B) over the permutation `P`, with a `BLOCK_LEN` of 8 or 16 bytes. See the +/// module docs. +pub struct Cmac<'k, P, const BLOCK_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { key: &'k CmacKeyState, /// `C_i` of Sec 6.2 step 6 for the blocks absorbed so far; `C_0 = 0^b` (step 5). @@ -154,9 +183,9 @@ where pending_len: usize, } -impl<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize> Cmac<'k, P, KEY_LEN, BLOCK_LEN> +impl<'k, P, const BLOCK_LEN: usize> Cmac<'k, P, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { /// Length of the [`SuspendableKeyed`] state: the three-byte library version, `C_i`, the /// pending block, and its length. @@ -197,18 +226,17 @@ where } } -impl<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize> Algorithm - for Cmac<'k, P, KEY_LEN, BLOCK_LEN> +impl<'k, P, const BLOCK_LEN: usize> Algorithm for Cmac<'k, P, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { const ALG_NAME: &'static str = P::CMAC_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize> MAC for Cmac<'k, P, KEY_LEN, BLOCK_LEN> +impl<'k, P, const BLOCK_LEN: usize> MAC for Cmac<'k, P, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { fn output_len(&self) -> usize { BLOCK_LEN @@ -292,10 +320,10 @@ where /// and silently produces a wrong tag. /// /// `STATE_LEN` must be [`Cmac::SUSPENDED_STATE_LEN`]; any other value fails to compile. -impl<'k, P, const KEY_LEN: usize, const BLOCK_LEN: usize, const STATE_LEN: usize> - SuspendableKeyed<'k, STATE_LEN> for Cmac<'k, P, KEY_LEN, BLOCK_LEN> +impl<'k, P, const BLOCK_LEN: usize, const STATE_LEN: usize> SuspendableKeyed<'k, STATE_LEN> + for Cmac<'k, P, BLOCK_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { type Key = CmacKeyState; diff --git a/crypto/cipher/src/modes/ctr.rs b/crypto/cipher/src/modes/ctr.rs index 2b4a54b6b..13bee8ee3 100644 --- a/crypto/cipher/src/modes/ctr.rs +++ b/crypto/cipher/src/modes/ctr.rs @@ -25,7 +25,7 @@ //! performed in parallel". Counter blocks depend on nothing but the nonce and the index, so unlike //! CBC and CFB there is no feed-forward between blocks at all. //! As such, both directions walk the block-aligned part of the data in fours through -//! [`ElectronicCodeBook::encrypt_4blocks`], then in pairs through [`ElectronicCodeBook::encrypt_2blocks`]. +//! [`BlockPermutation::encrypt_4blocks`], then in pairs through [`BlockPermutation::encrypt_2blocks`]. //! Only a leftover single block, and the keystream block for a short tail at the end, go one block at a time. //! //! Like the rest of CFB and CTR, only the **forward** cipher function is ever used, in both @@ -126,7 +126,7 @@ use crate::stream::StreamCipher; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ - ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, + BlockPermutation, ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, }; use bouncycastle_core::key_material::KeyType; use bouncycastle_core::traits::RNG; @@ -162,7 +162,7 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Encryptor<'k> - = Ctr<'k, P, Encrypting, KEY_LEN, BLOCK_LEN, NONCE_LEN> + = Ctr<'k, P, Encrypting, BLOCK_LEN, NONCE_LEN> where P: 'k; @@ -181,7 +181,7 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Decryptor<'k> - = Ctr<'k, P, Decrypting, KEY_LEN, BLOCK_LEN, NONCE_LEN> + = Ctr<'k, P, Decrypting, BLOCK_LEN, NONCE_LEN> where P: 'k; @@ -224,34 +224,28 @@ where /// // 1-byte counter /// let _ = CTR::<15>::encrypt_init(& as Mode>::key_state(perm), &mut rng).unwrap(); /// ``` -pub type Ctr<'k, P, Dir, const KEY_LEN: usize, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> = - StreamCipher< - CtrKeyStream<'k, P, KEY_LEN, BLOCK_LEN, INIT_DATA_LEN>, - Dir, - KEY_LEN, - INIT_DATA_LEN, - BLOCK_LEN, - >; +pub type Ctr<'k, P, Dir, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> = + StreamCipher, Dir, INIT_DATA_LEN, BLOCK_LEN>; /// XORs `Oj = CIPH_K(Tj)` into `blocks` for the next `blocks.len()` counter values, starting at /// `*next` and advancing it past them, where `Tj = counter_block(j)`. /// /// Shared by [`CtrKeyStream`] and CCM's keystream (SP 800-38C Sec 6.1 steps 5-7), which differ /// only in how a counter block is formatted. Walks the blocks in fours through -/// [`ElectronicCodeBook::encrypt_4blocks`], then pairs, then a single block: the counter blocks +/// [`BlockPermutation::encrypt_4blocks`], then pairs, then a single block: the counter blocks /// depend only on `j`, not on the data or on each other's cipher output, so the forward ciphers in /// a batch are independent. This is the parallelism SP 800-38A Sec 6.5 describes, and it applies /// to both directions. /// /// The keystream scratch is one [`Secret`] per width and per call rather than per batch, so every /// block of `Oj` is zeroized when this returns. -pub(crate) fn apply_counter_blocks( +pub(crate) fn apply_counter_blocks( perm: &P, next: &mut u64, counter_block: impl Fn(u64) -> [u8; BLOCK_LEN], blocks: &mut [[u8; BLOCK_LEN]], ) where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { let (fours, rest) = blocks.as_chunks_mut::<4>(); let mut ks4: Secret<[[u8; BLOCK_LEN]; 4]> = Secret::new(); @@ -274,7 +268,7 @@ pub(crate) fn apply_counter_blocks( +fn apply_batch( perm: &P, next: &mut u64, counter_block: &impl Fn(u64) -> [u8; BLOCK_LEN], @@ -282,7 +276,7 @@ fn apply_batch( keystream: &mut [[u8; BLOCK_LEN]; N], batch: impl Fn(&P, &mut [[u8; BLOCK_LEN]; N]), ) where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation + ModeNames, { for slot in keystream.iter_mut() { *slot = counter_block(*next); diff --git a/crypto/cipher/src/modes/gmac.rs b/crypto/cipher/src/modes/gmac.rs index ca4e789ca..597dbd8b2 100644 --- a/crypto/cipher/src/modes/gmac.rs +++ b/crypto/cipher/src/modes/gmac.rs @@ -31,11 +31,11 @@ //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGmac<'k, Dir> = Gmac<'k, ToyBlockCipher, Dir, 16, 16>; +//! type ToyGmac<'k, Dir> = Gmac<'k, ToyBlockCipher, Dir, 16>; //! //! let mut rng = HashDRBG_SHA512::new_from_os(); //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let state = GcmKeyState::new::<16>(ToyBlockCipher::new(&key).unwrap()); +//! let state = GcmKeyState::new(ToyBlockCipher::new(&key).unwrap()); //! //! let (mut generator, nonce) = ToyGmac::::from_key_state(&state, &mut rng).unwrap(); //! generator.do_update(b"a message "); @@ -61,44 +61,55 @@ use crate::modes::ModeNames; use crate::modes::gcm::{GCM_NONCE_LEN, Gcm, GcmKeyState}; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, Mode}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, RNG}; /// The GMAC mode type: the `M` of a mode-bound key such as `AES128Key`. `GMAC` is GMAC with /// the full 128-bit tag; `GMAC<12>` and so on select a shorter tag, within the `12..=16` byte /// range [`Gcm`] accepts. Its key state is [`GcmKeyState`], so a GMAC key caches `H` as a GCM key -/// does. The [`Mode`] impl and the generator/verifier pair that connect it to a key type are -/// written by each permutation's crate while the cipher traits still carry `KEY_LEN`. -/// -/// [`Mode`]: bouncycastle_core::hazmat::Mode +/// does. The [`Mode`] impl below is generic over any [`BlockPermutation`] with a 128-bit block; +/// the generator/verifier pair is inherent on each key type, since a GMAC tag is bound to a nonce +/// and so fits neither MAC role trait. pub struct GMAC; +impl Mode

for GMAC +where + P: BlockPermutation<16> + ModeNames + Clone, +{ + type KeyState = GcmKeyState

; + const KEY_TYPE: KeyType = KeyType::MACKey; + + fn key_state(perm: P) -> Self::KeyState { + GcmKeyState::new(perm) + } +} + /// GMAC over the permutation `P`, generating (`Dir = Encrypting`) or verifying (`Dir = /// Decrypting`) a `TAG_LEN`-byte tag. See the module docs. -pub struct Gmac<'k, P, Dir, const KEY_LEN: usize, const TAG_LEN: usize> +pub struct Gmac<'k, P, Dir, const TAG_LEN: usize> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { - gcm: Gcm<'k, P, Dir, KEY_LEN, TAG_LEN>, + gcm: Gcm<'k, P, Dir, TAG_LEN>, } /// `Gcm`'s AAD absorption fails only if AAD follows data, which GMAC never supplies, or if the /// running AAD length overflows a `u64`, which needs 2^64 bytes of input. const AAD_INFALLIBLE: &str = "GMAC: GCM rejects AAD only after data or past 2^64 bytes"; -impl<'k, P, Dir, const KEY_LEN: usize, const TAG_LEN: usize> Algorithm - for Gmac<'k, P, Dir, KEY_LEN, TAG_LEN> +impl<'k, P, Dir, const TAG_LEN: usize> Algorithm for Gmac<'k, P, Dir, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { const ALG_NAME: &'static str = P::GMAC_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl<'k, P, const KEY_LEN: usize, const TAG_LEN: usize> Gmac<'k, P, Encrypting, KEY_LEN, TAG_LEN> +impl<'k, P, const TAG_LEN: usize> Gmac<'k, P, Encrypting, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { /// Begins a tag from a key's stored state under a nonce drawn from `rng`: what a mode-bound /// key's `create_generator` calls. @@ -109,7 +120,7 @@ where state: &'k GcmKeyState

, rng: &mut dyn RNG, ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - let (gcm, nonce) = Gcm::::from_key_state(state, rng)?; + let (gcm, nonce) = Gcm::::from_key_state(state, rng)?; Ok((Self { gcm }, nonce)) } @@ -136,15 +147,15 @@ where } } -impl<'k, P, const KEY_LEN: usize, const TAG_LEN: usize> Gmac<'k, P, Decrypting, KEY_LEN, TAG_LEN> +impl<'k, P, const TAG_LEN: usize> Gmac<'k, P, Decrypting, TAG_LEN> where - P: ElectronicCodeBook + ModeNames, + P: BlockPermutation<16> + ModeNames, { /// Begins verifying a tag made under `nonce` from a key's stored state: what a mode-bound /// key's `create_verifier` calls. Infallible: the state is keyed and the nonce length is in /// the type. pub fn from_key_state(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { - Self { gcm: Gcm::::from_key_state(state, nonce) } + Self { gcm: Gcm::::from_key_state(state, nonce) } } /// Absorbs more of the message. diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index 7ec3c8f24..dc326acfa 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -38,9 +38,9 @@ const BLOCK_LEN: usize = 16; const AAD_LEN: usize = 16; const DATA_LEN: usize = 32; type ToyEnc<'k> = - CcmPacketEncryptor<'k, ToyBlockCipher, 16, BLOCK_LEN, NONCE_LEN, 16, AAD_LEN, DATA_LEN>; + CcmPacketEncryptor<'k, ToyBlockCipher, BLOCK_LEN, NONCE_LEN, 16, AAD_LEN, DATA_LEN>; type ToyDec<'k> = - CcmPacketDecryptor<'k, ToyBlockCipher, 16, BLOCK_LEN, NONCE_LEN, 16, AAD_LEN, DATA_LEN>; + CcmPacketDecryptor<'k, ToyBlockCipher, BLOCK_LEN, NONCE_LEN, 16, AAD_LEN, DATA_LEN>; const N: usize = ToyEnc::SUSPENDED_STATE_LEN; const ND: usize = ToyDec::SUSPENDED_STATE_LEN; diff --git a/crypto/cipher/tests/common/toy_block_cipher.rs b/crypto/cipher/tests/common/toy_block_cipher.rs index 36667a1f3..857f3a65b 100644 --- a/crypto/cipher/tests/common/toy_block_cipher.rs +++ b/crypto/cipher/tests/common/toy_block_cipher.rs @@ -16,7 +16,7 @@ use bouncycastle_cipher::modes::ModeNames; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::Algorithm; @@ -70,7 +70,9 @@ impl ElectronicCodeBook for ToyBlockCipher { bytes.copy_from_slice(key.ref_to_bytes()); Ok(Self { key: bytes }) } +} +impl BlockPermutation for ToyBlockCipher { fn encrypt_block(&self, block: &mut [u8; TOY_BLOCK_LEN]) { for (b, k) in block.iter_mut().zip(self.key.iter()) { *b = b.rotate_left(1) ^ *k; diff --git a/crypto/cipher/tests/common/toy_key.rs b/crypto/cipher/tests/common/toy_key.rs index 73b96f060..6ffed649a 100644 --- a/crypto/cipher/tests/common/toy_key.rs +++ b/crypto/cipher/tests/common/toy_key.rs @@ -2,23 +2,17 @@ // written here so the modes can be driven through the key path without a real cipher. The role // impls forward to whichever mode traits `M` implements over `P`, exactly as the AES key's do. // -// GCM, GMAC and CMAC have no `Mode

` impl in this crate (their key state needs the permutation's -// key length, which `Mode

` cannot carry), so the marker types `ToyGCM`, `ToyGMAC` and `ToyCMAC` -// below stand in for them over the toys. -// // Pulled in with `#[path]` from the tests, so every item is `pub` and unused ones are allowed. #![allow(dead_code)] -use bouncycastle_cipher::modes::{ - Cmac, CmacKeyState, GCM_NONCE_LEN, Gcm, GcmKeyState, Gmac, ModeNames, -}; +use bouncycastle_cipher::modes::{GCM_NONCE_LEN, GMAC, Gmac, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, - BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbDecryptionKey, EcbEncryptMode, - EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, ModeKey, SymmetricDecryptMode, - SymmetricEncryptMode, do_hazardous_operations, + BlockDecryptMode, BlockEncryptMode, BlockPermutation, EcbDecryptMode, EcbDecryptionKey, + EcbEncryptMode, EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, ModeKey, + SymmetricDecryptMode, SymmetricEncryptMode, do_hazardous_operations, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::symmetric_key::{ @@ -384,137 +378,31 @@ impl> MacVerificationKey for ToyKey { } } -// ---- the modes whose key state needs the key length: GCM, GMAC and CMAC over a 16-byte toy ---- - -/// GCM over a 16-byte toy, as a mode type. -pub struct ToyGCM; - -impl Mode

for ToyGCM -where - P: ElectronicCodeBook + ModeNames + Clone, -{ - type KeyState = GcmKeyState

; - const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; - - fn key_state(perm: P) -> Self::KeyState { - GcmKeyState::new::(perm) - } -} - -impl AeadEncryptMode - for ToyGCM -where - P: ElectronicCodeBook + ModeNames + Clone, -{ - type Encryptor<'k> - = Gcm<'k, P, Encrypting, TOY_KEY_LEN, TAG_LEN> - where - P: 'k; - - fn encrypt_init<'k>( - state: &'k Self::KeyState, - rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - >::from_key_state(state, rng) - } +// ---- GMAC's pair, inherent on the key as it is on the AES key ---------------------------------- - fn encrypt_init_nonce<'k>( - state: &'k Self::KeyState, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Self::Encryptor<'k> { - >::from_key_state_nonce(state, nonce) - } -} - -impl AeadDecryptMode - for ToyGCM +impl ToyKey> where - P: ElectronicCodeBook + ModeNames + Clone, -{ - type Decryptor<'k> - = Gcm<'k, P, Decrypting, TOY_KEY_LEN, TAG_LEN> - where - P: 'k; - - fn decrypt_init<'k>( - state: &'k Self::KeyState, - nonce: &[u8; GCM_NONCE_LEN], - ) -> Self::Decryptor<'k> { - >::from_key_state(state, nonce) - } -} - -/// GMAC over a 16-byte toy, as a mode type; the generator/verifier pair is inherent on the key, -/// as it is for the AES key, because a GMAC tag is bound to a nonce. -pub struct ToyGMAC; - -impl Mode

for ToyGMAC -where - P: ElectronicCodeBook + ModeNames + Clone, -{ - type KeyState = GcmKeyState

; - const KEY_TYPE: KeyType = KeyType::MACKey; - - fn key_state(perm: P) -> Self::KeyState { - GcmKeyState::new::(perm) - } -} - -impl ToyKey> -where - P: ElectronicCodeBook + ModeNames + Clone, + P: BlockPermutation<16> + ModeNames + Clone, { pub fn create_generator<'k>( &'k self, rng: &mut dyn RNG, - ) -> Result< - (Gmac<'k, P, Encrypting, TOY_KEY_LEN, TAG_LEN>, [u8; GCM_NONCE_LEN]), - SymmetricCipherError, - > { - Gmac::::from_key_state(&self.state, rng) + ) -> Result<(Gmac<'k, P, Encrypting, TAG_LEN>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + Gmac::::from_key_state(&self.state, rng) } pub fn create_generator_nonce<'k>( &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac<'k, P, Encrypting, TOY_KEY_LEN, TAG_LEN> { - Gmac::::from_key_state_nonce(&self.state, nonce) + ) -> Gmac<'k, P, Encrypting, TAG_LEN> { + Gmac::::from_key_state_nonce(&self.state, nonce) } pub fn create_verifier<'k>( &'k self, nonce: &[u8; GCM_NONCE_LEN], - ) -> Gmac<'k, P, Decrypting, TOY_KEY_LEN, TAG_LEN> { - Gmac::::from_key_state(&self.state, nonce) - } -} - -/// CMAC over a 16-byte toy, as a mode type. -pub struct ToyCMAC; - -impl

Mode

for ToyCMAC -where - P: ElectronicCodeBook + ModeNames + Clone, -{ - type KeyState = CmacKeyState; - const KEY_TYPE: KeyType = KeyType::MACKey; - - fn key_state(perm: P) -> Self::KeyState { - CmacKeyState::new::(perm) - } -} - -impl

MacMode

for ToyCMAC -where - P: ElectronicCodeBook + ModeNames + Clone, -{ - type Worker<'k> - = Cmac<'k, P, TOY_KEY_LEN, 16> - where - P: 'k; - - fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k> { - Cmac::from_key_state(state) + ) -> Gmac<'k, P, Decrypting, TAG_LEN> { + Gmac::::from_key_state(&self.state, nonce) } } diff --git a/crypto/cipher/tests/modes/cfb8_tests.rs b/crypto/cipher/tests/modes/cfb8_tests.rs index 999691225..ad2f1922d 100644 --- a/crypto/cipher/tests/modes/cfb8_tests.rs +++ b/crypto/cipher/tests/modes/cfb8_tests.rs @@ -17,7 +17,7 @@ mod toy_key; use bouncycastle_cipher::modes::{CFB, CFB8, Cfb8, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ @@ -30,7 +30,7 @@ use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCiph use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; use toy_key::{ToyKey, os_rng}; -type ToyCfb8<'k, Dir> = Cfb8<'k, Toy, Dir, TOY_LEN, TOY_LEN>; +type ToyCfb8<'k, Dir> = Cfb8<'k, Toy, Dir, TOY_LEN>; type ToyCfb8Key = ToyKey; @@ -130,7 +130,7 @@ fn cfb8_conforms_to_the_stream_cipher_framework() { /// the implementation uses. /// /// This is the independent reference the mode is checked against below. It uses only -/// [`ElectronicCodeBook::encrypt_block`], because that is all the spec calls for. +/// [`BlockPermutation::encrypt_block`], because that is all the spec calls for. fn reference_cfb8(perm: &Toy, iv: [u8; TOY_LEN], input: &[u8], encrypt: bool) -> Vec { let mut chain = iv; // I1 = IV let mut out = Vec::with_capacity(input.len()); diff --git a/crypto/cipher/tests/modes/cfb_tests.rs b/crypto/cipher/tests/modes/cfb_tests.rs index c688074c8..70020468a 100644 --- a/crypto/cipher/tests/modes/cfb_tests.rs +++ b/crypto/cipher/tests/modes/cfb_tests.rs @@ -18,7 +18,7 @@ mod toy_key; use bouncycastle_cipher::modes::{Blocks, CBC, CFB, Cbc, Cfb, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ @@ -33,7 +33,7 @@ use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCiph use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; use toy_key::{ToyKey, os_rng}; -type ToyCfb<'k, Dir> = Cfb<'k, Toy, Dir, TOY_LEN, TOY_LEN>; +type ToyCfb<'k, Dir> = Cfb<'k, Toy, Dir, TOY_LEN>; type ToyCfbKey = ToyKey; @@ -146,7 +146,7 @@ fn cfb_conforms_to_the_stream_cipher_framework() { /// after it. /// /// This is the independent reference the mode is checked against below. It uses only -/// [`ElectronicCodeBook::encrypt_block`], because that is all the spec calls for. +/// [`BlockPermutation::encrypt_block`], because that is all the spec calls for. fn reference_cfb(perm: &Toy, iv: [u8; TOY_LEN], input: &[u8], encrypt: bool) -> Vec { let mut chain = iv; // I1 = IV let mut out = Vec::with_capacity(input.len()); @@ -790,6 +790,6 @@ fn sizes_match_the_documented_memory_table() { // The docs say CFB is one `usize` bigger than CBC. assert_eq!( size_of::>(), - size_of::>() + size_of::() + size_of::>() + size_of::() ); } diff --git a/crypto/cipher/tests/modes/cmac_tests.rs b/crypto/cipher/tests/modes/cmac_tests.rs index 0ba08be7b..b744847a4 100644 --- a/crypto/cipher/tests/modes/cmac_tests.rs +++ b/crypto/cipher/tests/modes/cmac_tests.rs @@ -7,7 +7,7 @@ mod common; #[path = "../common/toy_key.rs"] mod toy_key; -use bouncycastle_cipher::modes::{CMAC_MIN_TAG_LEN, Cmac, CmacKeyState, ModeNames}; +use bouncycastle_cipher::modes::{CMAC, CMAC_MIN_TAG_LEN, Cmac, CmacKeyState, ModeNames}; use bouncycastle_core::errors::{MACError, SuspendableError}; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; @@ -17,10 +17,10 @@ use bouncycastle_core::traits::{MAC, SuspendableKeyed}; use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_core_test_framework::mac::TestFrameworkMAC; use common::{TOY_LEN, Toy, toy_key}; -use toy_key::{ToyCMAC, ToyKey}; +use toy_key::ToyKey; -type ToyCmac<'k> = Cmac<'k, Toy, TOY_LEN, TOY_LEN>; -type ToyCmacKey = ToyKey; +type ToyCmac<'k> = Cmac<'k, Toy, TOY_LEN>; +type ToyCmacKey = ToyKey; fn cmac_key() -> ToyCmacKey { ToyCmacKey::from_key_material(&mac_key()).expect("the toy MAC key") @@ -33,7 +33,7 @@ fn mac_key() -> KeyMaterial { } fn key_state() -> CmacKeyState { - CmacKeyState::new::(Toy::new(&toy_key()).unwrap()) + CmacKeyState::new(Toy::new(&toy_key()).unwrap()) } fn message(len: usize) -> Vec { diff --git a/crypto/cipher/tests/modes/common/mod.rs b/crypto/cipher/tests/modes/common/mod.rs index b195dcfb1..fae2f7476 100644 --- a/crypto/cipher/tests/modes/common/mod.rs +++ b/crypto/cipher/tests/modes/common/mod.rs @@ -20,7 +20,7 @@ use bouncycastle_cipher::modes::ModeNames; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::Algorithm; @@ -79,7 +79,9 @@ impl ElectronicCodeBook for Toy { bytes.copy_from_slice(key.ref_to_bytes()); Ok(Self { key: bytes }) } +} +impl BlockPermutation for Toy { fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { for (b, k) in block.iter_mut().zip(self.key.iter()) { *b = b.rotate_left(1) ^ *k; @@ -152,7 +154,9 @@ impl ElectronicCodeBook for SwappedPairToy { fn new(key: &KeyMaterial) -> Result { Ok(Self { inner: Toy::new(key)? }) } +} +impl BlockPermutation for SwappedPairToy { fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { self.inner.encrypt_block(block); } @@ -228,7 +232,9 @@ impl ElectronicCodeBook for ForwardOnlyToy { fn new(key: &KeyMaterial) -> Result { Ok(Self { inner: Toy::new(key)? }) } +} +impl BlockPermutation for ForwardOnlyToy { fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { self.inner.encrypt_block(block); } @@ -286,7 +292,9 @@ impl ElectronicCodeBook for SwappedFourToy { fn new(key: &KeyMaterial) -> Result { Ok(Self { inner: Toy::new(key)? }) } +} +impl BlockPermutation for SwappedFourToy { fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { self.inner.encrypt_block(block); } diff --git a/crypto/cipher/tests/modes/gmac_tests.rs b/crypto/cipher/tests/modes/gmac_tests.rs index 00d80bfd6..9cbf33b5c 100644 --- a/crypto/cipher/tests/modes/gmac_tests.rs +++ b/crypto/cipher/tests/modes/gmac_tests.rs @@ -7,7 +7,7 @@ mod common; #[path = "../common/toy_key.rs"] mod toy_key; -use bouncycastle_cipher::modes::{GCM_NONCE_LEN, GcmKeyState, Gmac, ModeNames}; +use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN, GMAC, GcmKeyState, Gmac, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; @@ -16,10 +16,10 @@ use bouncycastle_core::symmetric_key::{AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use common::{TOY_LEN, Toy, toy_key}; -use toy_key::{ToyGCM, ToyGMAC, ToyKey}; +use toy_key::ToyKey; -type ToyGmac<'k, Dir, const TAG_LEN: usize = 16> = Gmac<'k, Toy, Dir, TOY_LEN, TAG_LEN>; -type ToyGmacKey = ToyKey>; +type ToyGmac<'k, Dir, const TAG_LEN: usize = 16> = Gmac<'k, Toy, Dir, TAG_LEN>; +type ToyGmacKey = ToyKey>; fn gmac_key() -> ToyGmacKey { ToyGmacKey::from_key_material(&mac_key()).expect("the toy MAC key") @@ -32,7 +32,7 @@ fn mac_key() -> KeyMaterial { } fn key_state() -> GcmKeyState { - GcmKeyState::new::(Toy::new(&toy_key()).unwrap()) + GcmKeyState::new(Toy::new(&toy_key()).unwrap()) } fn message(len: usize) -> Vec { @@ -50,7 +50,7 @@ fn every_route_gives_gcm_s_aad_only_tag() { let state = key_state(); for len in [0, 1, 15, 16, 17, 32, 33, 100] { let msg = message(len); - let (_, gcm_nonce, expected) = ToyKey::::from_key_material(&toy_key()) + let (_, gcm_nonce, expected) = ToyKey::::from_key_material(&toy_key()) .unwrap() .encrypt_detached_out(&mut rng(), &msg, &[], &mut []) .unwrap(); diff --git a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs index 668c26689..935edafd2 100644 --- a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs +++ b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs @@ -34,7 +34,7 @@ use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkSymmetricC use common::{TOY_LEN, Toy, toy_key}; use toy_key::{ToyKey, os_rng}; -type ToyCtr<'k, Dir> = Ctr<'k, Toy, Dir, TOY_LEN, TOY_LEN, 12>; +type ToyCtr<'k, Dir> = Ctr<'k, Toy, Dir, TOY_LEN, 12>; /// The toy key under the stream mode `M`. fn key_for>() -> ToyKey { diff --git a/crypto/core/src/hazmat/electronic_code_book.rs b/crypto/core/src/hazmat/electronic_code_book.rs index 0bcea7b64..b6262bdd2 100644 --- a/crypto/core/src/hazmat/electronic_code_book.rs +++ b/crypto/core/src/hazmat/electronic_code_book.rs @@ -1,4 +1,5 @@ -//! The [`ElectronicCodeBook`] trait: a keyed block permutation. +//! The [`BlockPermutation`] trait, a keyed block permutation, and [`ElectronicCodeBook`], which +//! adds its construction from key material. use crate::errors::SymmetricCipherError; use crate::key_material::KeyMaterial; @@ -9,7 +10,10 @@ use crate::traits::Algorithm; use crate::key_material::KeyType; // end of imports needed for docs -/// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1. +/// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1, already +/// keyed. The modes in `bouncycastle_cipher::modes` bound on this and nothing more, which is what +/// lets a mode's per-key state be generic over the permutation without naming its key length; a +/// key object builds the permutation through [`ElectronicCodeBook::new`]. /// /// # 🚨 Security Considerations 🚨 /// A permutation applied to data block by block is ECB: equal plaintext blocks give equal @@ -25,17 +29,7 @@ use crate::key_material::KeyType; /// 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 [`ElectronicCodeBook::new`] has returned. Only `new` can /// fail, and only because of the key. -pub trait ElectronicCodeBook: - 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; - +pub trait BlockPermutation: Algorithm + Sized { /// The forward cipher function, in place. fn encrypt_block(&self, block: &mut [u8; BLOCK_LEN]); @@ -47,11 +41,11 @@ pub trait ElectronicCodeBook: /// Required, with no default, so that every implementor decides for itself how to run a pair. /// A bit-sliced engine whose natural unit is a pair (see `bouncycastle-aes`) runs both blocks /// in one pass for barely more than the cost of one; an engine with no unit wider than a block - /// makes two [`ElectronicCodeBook::encrypt_block`] calls. A default of two single-block calls + /// makes two [`BlockPermutation::encrypt_block`] calls. A default of two single-block calls /// would be right only for the second kind, and silently wrong -- twice the work, with nothing /// failing -- for a wider engine that forgot to override it. /// - /// Must be indistinguishable from two [`ElectronicCodeBook::encrypt_block`] calls, including + /// Must be indistinguishable from two [`BlockPermutation::encrypt_block`] calls, including /// the order of the two results. `TestFrameworkElectronicCodeBook` pins that. /// /// Modes whose structure is parallel -- CBC decryption, CFB decryption, CTR -- should prefer @@ -60,18 +54,18 @@ pub trait ElectronicCodeBook: fn encrypt_2blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]); /// The inverse cipher function on two *independent* blocks, in place. - /// See [`ElectronicCodeBook::encrypt_2blocks`]. + /// See [`BlockPermutation::encrypt_2blocks`]. fn decrypt_2blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]); /// The forward cipher function on four *independent* blocks, in place. /// - /// Required for the same reason as [`ElectronicCodeBook::encrypt_2blocks`]. An engine whose + /// Required for the same reason as [`BlockPermutation::encrypt_2blocks`]. An engine whose /// natural unit is a pair runs the four as two pair calls; a bit-sliced engine whose S-box /// circuit substitutes four blocks per pass runs them as one full pass rather than two /// half-empty pair calls. Four is the unit because it is the widest any engine in this library /// fills: AES fills a pair, and the `u16`- and `u32`-plane engines (SM4, Camellia, ARIA) fill /// four. - /// Must be indistinguishable from four [`ElectronicCodeBook::encrypt_block`] calls, including + /// Must be indistinguishable from four [`BlockPermutation::encrypt_block`] calls, including /// the order of the four results. `TestFrameworkElectronicCodeBook` pins that. /// /// Modes with parallel structure chunk their data into fours first, then pairs, then single @@ -79,6 +73,20 @@ pub trait ElectronicCodeBook: fn encrypt_4blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 4]); /// The inverse cipher function on four *independent* blocks, in place. - /// See [`ElectronicCodeBook::encrypt_4blocks`]. + /// See [`BlockPermutation::encrypt_4blocks`]. fn decrypt_4blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 4]); } + +/// A [`BlockPermutation`] that can be built from `KEY_LEN` bytes of key material: what a key +/// object does once, when it is made. +pub trait ElectronicCodeBook: + BlockPermutation +{ + /// 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; +} diff --git a/crypto/core/src/hazmat/mod.rs b/crypto/core/src/hazmat/mod.rs index 0c4960c7a..d7c3dcdab 100644 --- a/crypto/core/src/hazmat/mod.rs +++ b/crypto/core/src/hazmat/mod.rs @@ -18,7 +18,7 @@ mod key_stream; mod mode; pub use ecb_key::{EcbDecryptionKey, EcbEncryptionKey}; -pub use electronic_code_book::ElectronicCodeBook; +pub use electronic_code_book::{BlockPermutation, ElectronicCodeBook}; pub use hazardous_operations::do_hazardous_operations; pub use key_stream::KeyStream; pub use mode::{ diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index 4fd356b14..d78ec80f2 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -56,9 +56,11 @@ pub trait ModeKey> { fn state(&self) -> &M::KeyState; } -// `KEY_LEN` is on these traits only while the cipher traits still carry it: the permutation bound -// a mode impl needs, `P: ElectronicCodeBook`, must be tied to the impl's trait. It -// goes when the constructors leave the cipher traits. +// `KEY_LEN` is on these traits as the tie between a key's length and its permutation's: a mode +// impl bounds `P: ElectronicCodeBook`, which only the permutation's own key length +// satisfies, so a key object's role impls, which take `KEY_LEN` from here, exist for that one +// length. The workers themselves never see it; `Mode

` and `MacMode

` bound on +// `BlockPermutation` alone. /// The encryption side of an AEAD mode over `P`: the mode's own constructor for its encryptor. pub trait AeadEncryptMode< diff --git a/mem_usage_benches/src/bench_aes_mem_usage.rs b/mem_usage_benches/src/bench_aes_mem_usage.rs index 40cd2b2e4..ff0bd6d61 100644 --- a/mem_usage_benches/src/bench_aes_mem_usage.rs +++ b/mem_usage_benches/src/bench_aes_mem_usage.rs @@ -47,7 +47,7 @@ #![allow(unused_imports)] use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle::core::key_material::{KeyMaterial, KeyType}; /// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. diff --git a/mem_usage_benches/src/bench_ccm_mem_usage.rs b/mem_usage_benches/src/bench_ccm_mem_usage.rs index d24202b51..22a187913 100644 --- a/mem_usage_benches/src/bench_ccm_mem_usage.rs +++ b/mem_usage_benches/src/bench_ccm_mem_usage.rs @@ -59,9 +59,9 @@ type Aes128CcmMode = CCM; /// The key bound to the fixed frame, whose streams are the pair below. type Aes128CcmPacketKey = AES128Key>; type Aes128CcmEncryptor<'k> = - CcmPacketEncryptor<'k, AES128Internal, 16, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>; + CcmPacketEncryptor<'k, AES128Internal, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>; type Aes128CcmDecryptor<'k> = - CcmPacketDecryptor<'k, AES128Internal, 16, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>; + CcmPacketDecryptor<'k, AES128Internal, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>; fn key() -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&[0x42u8; N], KeyType::SymmetricCipherKey).unwrap() @@ -121,17 +121,16 @@ fn print_struct_sizes() { ); eprintln!( "CcmPacketEncryptor<'_, .., 64, 240> {:>7} B", - size_of::>() + size_of::>() ); eprintln!( "CcmPacketEncryptor<'_, .., 7, 4, ..> {:>7} B", - size_of::>() + size_of::>() ); eprintln!( "CcmPacketEncryptor<'_, AES256Internal> {:>7} B", - size_of::< - CcmPacketEncryptor<'_, AES256Internal, 32, 16, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN>, - >() + size_of::>( + ) ); print!("{}", size_of::()); From c13546e27de0558e2a72684d1ec8b1c482c665ac Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 06:20:47 +1100 Subject: [PATCH 17/54] hmac, ascon, factory: the HMAC worker borrows its key, and resume takes the key object (prototype) `HMAC<'k, H>` holds the inner hash and a reference to the `HmacKey` it came from, whose outer pad state it is finished through, so a worker is one hash state plus a pointer rather than two states: 224 -> 120 bytes for the SHA-256 family, 416 -> 216 for SHA-512, 880 -> 448 for SHA-3. The two halves of the worker are public on the key as `inner_state` and `finish_out`, which is how HKDF's extract phase now runs: a struct cannot own a key and a worker that borrows it, so HKDF keeps the salt key and the inner hash and finishes through the key. Its suspended layout is unchanged. The MAC factory becomes a key enum, `MACFactory`, that hands out a worker enum, `MACFactoryWorker<'k>`, through the MAC role traits; its one-shots are the role traits' and its streaming goes through `create_generator`. Resume takes the key object where there is one: `HMAC`'s `SuspendableKeyed::Key` is `HmacKey`, Ascon-AEAD128's is `AsconAEAD128Key`, with no re-validation on resume since the key object was validated when it was made. HKDF's stays the salt material. No suspended layout changes. quality_stats.sh, ./crypto: unwraps in core code 784 -> 782, Err() 440 unchanged. Assisted-by: Claude:claude-mythos-5-1 --- CLAUDE.md | 12 +- cli/src/mac_cmd.rs | 27 +- crypto/ascon/src/ascon_aead128.rs | 13 +- crypto/ascon/tests/aead128_tests.rs | 28 +- crypto/factory/src/mac_factory.rs | 347 ++++++++----------- crypto/factory/tests/mac_factory_tests.rs | 31 +- crypto/hkdf/src/lib.rs | 44 ++- crypto/hmac/src/lib.rs | 115 +++--- crypto/hmac/tests/hmac_key_tests.rs | 2 +- crypto/hmac/tests/hmac_tests.rs | 134 ++++--- crypto/sha2/src/hmac.rs | 33 +- crypto/sha3/src/hmac.rs | 30 +- crypto/sha3/tests/sp800_185_suspend_tests.rs | 8 +- crypto/sm3/src/hmac.rs | 2 +- 14 files changed, 400 insertions(+), 426 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ab8b4324..2f4f82a33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -235,10 +235,14 @@ What that taught, as mechanics for the next trait that sheds a constructor: function handed `key: &'k K` that names the worker is generic over `'k`; one that builds its key locally bounds `for<'k> K::Encryptor<'k>: ..` with `K: 'static`. A test binds the key before the worker, since `key_fn().create_encryptor(..)` borrows a temporary. -- **Resume takes the key state.** `SuspendableKeyed<'k, N>` and `SuspendableComponent<'k>` take - the key by `&'k`, and a cipher-mode worker's `Key` is the mode's `KeyState`, reached through - `hazmat::ModeKey::state`; the suspended layouts did not change. A worker that owns a copy of its - key (Ascon, HMAC, KMAC) implements the traits for every `'k` and ignores it. +- **Resume takes the key object.** `SuspendableKeyed<'k, N>` and `SuspendableComponent<'k>` take + the key by `&'k`: a cipher-mode worker's `Key` is the mode's `KeyState`, reached through + `hazmat::ModeKey::state`, HMAC's is `HmacKey`, Ascon's is `AsconAEAD128Key`, and HKDF's stays + the salt material, since HKDF is not a key object. The suspended layouts did not change. A + worker that owns a copy of its key (Ascon, KMAC) implements the traits for every `'k` and + ignores it. A struct cannot hold a worker that borrows a key it also owns: HKDF runs its + extract-phase HMAC as the two halves `HmacKey::inner_state` / `finish_out`, and the MAC + factory is a key enum that hands out a worker enum. - **What has no key-path twin is dropped, and said so.** `new_allow_weak_key` on a cipher-based MAC, a wrong-length `KeyMaterial512` into a fixed-length key, GCM's verify-before-decrypt one-shot override: each went with a line in the commit message rather than a shim. diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index 67864ec39..a7386192c 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -53,31 +53,24 @@ pub(crate) fn mac_cmd( // instantiate the MAC object and call do_mac() match hmac_variant { HMACVariant::SHA256 => { - let mac = - HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); - do_mac(mac, verify_val, output_hex); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + do_mac(hmac_key.create_generator(), verify_val, output_hex); } HMACVariant::SHA512 => { - let mac = - HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); - do_mac(mac, verify_val, output_hex); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + do_mac(hmac_key.create_generator(), verify_val, output_hex); } HMACVariant::SHA512_224 => { - let mac = HmacKey::::from_key_material_allow_weak(&key) - .unwrap() - .create_generator(); - do_mac(mac, verify_val, output_hex); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + do_mac(hmac_key.create_generator(), verify_val, output_hex); } HMACVariant::SHA512_256 => { - let mac = HmacKey::::from_key_material_allow_weak(&key) - .unwrap() - .create_generator(); - do_mac(mac, verify_val, output_hex); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + do_mac(hmac_key.create_generator(), verify_val, output_hex); } HMACVariant::SM3 => { - let mac = - HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); - do_mac(mac, verify_val, output_hex); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + do_mac(hmac_key.create_generator(), verify_val, output_hex); } } } diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 8651aca7d..9415ee489 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -728,9 +728,9 @@ const DEC_DIR_BITS: u8 = 4; impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN> for Ascon_AEAD128 { - // The 128-bit key must be re-supplied when resuming; it is never part of the serialized state, - // and is re-validated exactly as construction validates it. - type Key = KeyMaterial; + // The key is never part of the serialized state; the key object the worker was made from is + // re-supplied, already validated. + type Key = AsconAEAD128Key; fn suspend(self) -> [u8; SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN] { self.suspend_as(ENC_DIR_BITS) @@ -750,7 +750,7 @@ impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN> impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN> for Ascon_AEAD128 { - type Key = KeyMaterial; + type Key = AsconAEAD128Key; fn suspend(self) -> [u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN] { let mut out = [0u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN]; @@ -803,7 +803,7 @@ impl

Ascon_AEAD128 { /// direction is not `dir_bits` is refused, so a state never resumes in the other direction. fn resume_as( serialized_state: [u8; CIPHER_STATE_LEN], - key: &KeyMaterial, + key: &AsconAEAD128Key, dir_bits: u8, ) -> Result { // infallible: check_lib_ver returns a slice of exactly CIPHER_STATE_LEN - 3 = 43 bytes. @@ -834,9 +834,8 @@ impl Ascon_AEAD128 { return Err(SuspendableError::InvalidData); } - let key_words = Self::checked_key(key).map_err(|_| SuspendableError::InvalidData)?; let mut key_secret = Secret::<[u64; 2]>::new(); - *key_secret = key_words; + *key_secret = *key.key; Ok(Ascon_AEAD128 { key: key_secret, diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs index 6da33208f..dfbe73ab8 100644 --- a/crypto/ascon/tests/aead128_tests.rs +++ b/crypto/ascon/tests/aead128_tests.rs @@ -608,19 +608,19 @@ fn aead128_suspendable_keyed_state() { let ad = b"suspend-ad"; let ct_ref = enc_oneshot(&KEY, &NONCE, ad, &pt); let key = aead_key(&KEY); - let km = key_material(&KEY); - // Encrypt part of the plaintext, suspend, resume with the re-supplied key, finish, and confirm - // the output matches a one-shot encryption. The key is never part of the serialized state. + // Encrypt part of the plaintext, suspend, resume over the re-supplied key object, finish, and + // confirm the output matches a one-shot encryption. The key is never part of the serialized + // state. let mut e = key.create_encryptor_nonce(&NONCE); e.do_update_aad(ad).unwrap(); let mut out = vec![0u8; pt.len() + 16]; e.do_encrypt_out(&pt[..18], &mut out[..18]).unwrap(); - TestFrameworkSuspendableKeyedState::new().test(&e, &km); + TestFrameworkSuspendableKeyedState::new().test(&e, &key); let serialized = e.clone().suspend(); - let mut resumed = Ascon_AEAD128::::from_suspended(serialized, &km).unwrap(); + let mut resumed = Ascon_AEAD128::::from_suspended(serialized, &key).unwrap(); resumed.do_encrypt_out(&pt[18..], &mut out[18..pt.len()]).unwrap(); let (last, n) = resumed.do_encrypt_final().unwrap(); out[pt.len()..].copy_from_slice(&last[..n]); @@ -630,7 +630,7 @@ fn aead128_suspendable_keyed_state() { let mut busted = serialized; busted[3] ^= 0xFF; assert!(matches!( - Ascon_AEAD128::::from_suspended(busted, &km), + Ascon_AEAD128::::from_suspended(busted, &key), Err(SuspendableError::InvalidData) )); @@ -640,7 +640,7 @@ fn aead128_suspendable_keyed_state() { let mut bad_state = serialized; bad_state[last] = 200; assert!(matches!( - Ascon_AEAD128::::from_suspended(bad_state, &km), + Ascon_AEAD128::::from_suspended(bad_state, &key), Err(SuspendableError::InvalidData) )); @@ -649,7 +649,7 @@ fn aead128_suspendable_keyed_state() { inconsistent[pos_offset] = 3; // pos = 3 inconsistent[last] = 0; // encrypting, Init assert!(matches!( - Ascon_AEAD128::::from_suspended(inconsistent, &km), + Ascon_AEAD128::::from_suspended(inconsistent, &key), Err(SuspendableError::InvalidData) )); @@ -657,7 +657,7 @@ fn aead128_suspendable_keyed_state() { let mut bad_pos = serialized; bad_pos[pos_offset] = 16; assert!(matches!( - Ascon_AEAD128::::from_suspended(bad_pos, &km), + Ascon_AEAD128::::from_suspended(bad_pos, &key), Err(SuspendableError::InvalidData) )); @@ -669,10 +669,10 @@ fn aead128_suspendable_keyed_state() { let n1 = d.do_decrypt_out(&ct_ref[..30], &mut back).unwrap(); assert_eq!(n1, 14); - TestFrameworkSuspendableKeyedState::new().test(&d, &km); + TestFrameworkSuspendableKeyedState::new().test(&d, &key); let d_state = d.clone().suspend(); - let mut d2 = Ascon_AEAD128::::from_suspended(d_state, &km).unwrap(); + let mut d2 = Ascon_AEAD128::::from_suspended(d_state, &key).unwrap(); let n2 = d2.do_decrypt_out(&ct_ref[30..], &mut back[n1..]).unwrap(); let (_, n3) = d2.do_decrypt_final().unwrap(); assert_eq!(n3, 0, "the final releases nothing: what it held was the tag"); @@ -683,7 +683,7 @@ fn aead128_suspendable_keyed_state() { let mut bad_held = d_state; bad_held[SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN - 1] = 17; assert!(matches!( - Ascon_AEAD128::::from_suspended(bad_held, &km), + Ascon_AEAD128::::from_suspended(bad_held, &key), Err(SuspendableError::InvalidData) )); @@ -692,13 +692,13 @@ fn aead128_suspendable_keyed_state() { let mut enc_as_dec = [0u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN]; enc_as_dec[..SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN].copy_from_slice(&serialized); assert!(matches!( - Ascon_AEAD128::::from_suspended(enc_as_dec, &km), + Ascon_AEAD128::::from_suspended(enc_as_dec, &key), Err(SuspendableError::InvalidData) )); let mut dec_as_enc = [0u8; SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN]; dec_as_enc.copy_from_slice(&d_state[..SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN]); assert!(matches!( - Ascon_AEAD128::::from_suspended(dec_as_enc, &km), + Ascon_AEAD128::::from_suspended(dec_as_enc, &key), Err(SuspendableError::InvalidData) )); } diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index 3914b759f..72957af68 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -1,14 +1,16 @@ -//! MAC factory for creating instances of algorithms that implement the [`MAC`] trait. +//! MAC factory for creating MAC keys by algorithm name. //! //! As with all Factory objects, this implements constructions from strings and defaults, and -//! returns a [`MACFactory`] object which itself implements the [`MAC`] trait as a pass-through to the underlying algorithm. +//! returns a [`MACFactory`] object: a key, which implements [`MacGenerationKey`] and +//! [`MacVerificationKey`] as a pass-through to the underlying key type, and whose workers, +//! [`MACFactoryWorker`], implement [`MAC`] the same way. //! //! Example usage: //! Generating and verifying a MAC value for a given piece of data: //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; -//! use bouncycastle_core::traits::MAC; +//! use bouncycastle_core::symmetric_key::{MacGenerationKey, MacVerificationKey}; //! use bouncycastle_hex as hex; //! use bouncycastle_factory::AlgorithmFactory; //! use bouncycastle_factory::mac_factory::MACFactory; @@ -25,8 +27,7 @@ //! // Generate the MAC value //! let mac_value: Vec = hmac.mac(data); //! -//! // Verify the MAC value -//! let hmac = MACFactory::new(bouncycastle_sha3::hmac::HMAC_SHA3_256_NAME, &key).unwrap(); +//! // Verify the MAC value; the key is used for any number of messages //! if hmac.verify(data, &mac_value,) { //! println!("MAC verified successfully!") //! } else { @@ -74,9 +75,9 @@ use crate::{DEFAULT, DEFAULT_128_BIT, DEFAULT_256_BIT, FactoryError}; use bouncycastle_core::errors::MACError; use bouncycastle_core::key_material::KeyMaterialTrait; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::MAC; -use bouncycastle_hmac::HmacKey; +use bouncycastle_hmac::{HMAC, HmacKey}; use bouncycastle_sha2 as sha2; use bouncycastle_sha2::hmac::{ HMAC_SHA224_NAME, HMAC_SHA256_NAME, HMAC_SHA384_NAME, HMAC_SHA512_224_NAME, @@ -100,42 +101,76 @@ pub const DEFAULT_128BIT_MAC_NAME: &str = HMAC_SHA256_NAME; /// pub const DEFAULT_256BIT_MAC_NAME: &str = HMAC_SHA256_NAME; +/// A MAC key selected by name: the key object of whichever algorithm was named, which hands out +/// [`MACFactoryWorker`]s through [`MacGenerationKey`] and [`MacVerificationKey`]. +/// MACFactory deviates from the usual AlgorithmFactory trait because MAC keys do not have a no-arg +/// constructor; instead they have a constructor that takes a [`KeyMaterialTrait`] and can return +/// an error. #[allow(non_camel_case_types)] - -/// Wrapper object for all algorithms that impl [`MAC`]. -/// MACFactory deviates from the usual AlgorithmFactory trait because MAC objects do not have a no-arg constructor; -/// instead they have a constructor that takes a [`KeyMaterialTrait`] and can return an error. #[non_exhaustive] pub enum MACFactory { /// KMAC128 with no customization string and a 32-byte tag (NIST SP 800-185 Sec 4). /// For a customization string or a different output length, construct - /// `bouncycastle_sha3::KMAC128` directly -- the factory selects by name alone and has no + /// `bouncycastle_sha3::KMAC128Key` directly -- the factory selects by name alone and has no /// channel for those parameters. - KMAC128(KMAC128), + KMAC128(KMAC128Key), /// KMAC256 with no customization string and a 64-byte tag. See [`MACFactory::KMAC128`]. + KMAC256(KMAC256Key), + /// + HMAC_SHA224(HmacKey), + /// + HMAC_SHA256(HmacKey), + /// + HMAC_SHA384(HmacKey), + /// + HMAC_SHA512(HmacKey), + /// + HMAC_SHA512_224(HmacKey), + /// + HMAC_SHA512_256(HmacKey), + /// + HMAC_SHA3_224(HmacKey), + /// + HMAC_SHA3_256(HmacKey), + /// + HMAC_SHA3_384(HmacKey), + /// + HMAC_SHA3_512(HmacKey), + /// + HMAC_SM3(HmacKey), +} + +/// The worker a [`MACFactory`] key hands out: the named algorithm's own worker, borrowing the key +/// for `'k`, behind one [`MAC`] impl. +#[allow(non_camel_case_types)] +#[non_exhaustive] +pub enum MACFactoryWorker<'k> { + /// + KMAC128(KMAC128), + /// KMAC256(KMAC256), /// - HMAC_SHA224(sha2::hmac::HMAC_SHA224), + HMAC_SHA224(HMAC<'k, sha2::SHA224>), /// - HMAC_SHA256(sha2::hmac::HMAC_SHA256), + HMAC_SHA256(HMAC<'k, sha2::SHA256>), /// - HMAC_SHA384(sha2::hmac::HMAC_SHA384), + HMAC_SHA384(HMAC<'k, sha2::SHA384>), /// - HMAC_SHA512(sha2::hmac::HMAC_SHA512), + HMAC_SHA512(HMAC<'k, sha2::SHA512>), /// - HMAC_SHA512_224(sha2::hmac::HMAC_SHA512_224), + HMAC_SHA512_224(HMAC<'k, sha2::SHA512_224>), /// - HMAC_SHA512_256(sha2::hmac::HMAC_SHA512_256), + HMAC_SHA512_256(HMAC<'k, sha2::SHA512_256>), /// - HMAC_SHA3_224(sha3::hmac::HMAC_SHA3_224), + HMAC_SHA3_224(HMAC<'k, sha3::SHA3_224>), /// - HMAC_SHA3_256(sha3::hmac::HMAC_SHA3_256), + HMAC_SHA3_256(HMAC<'k, sha3::SHA3_256>), /// - HMAC_SHA3_384(sha3::hmac::HMAC_SHA3_384), + HMAC_SHA3_384(HMAC<'k, sha3::SHA3_384>), /// - HMAC_SHA3_512(sha3::hmac::HMAC_SHA3_512), + HMAC_SHA3_512(HMAC<'k, sha3::SHA3_512>), /// - HMAC_SM3(sm3::hmac::HMAC_SM3), + HMAC_SM3(HMAC<'k, sm3::SM3>), } impl MACFactory { @@ -157,217 +192,125 @@ impl MACFactory { DEFAULT => Self::default(key), DEFAULT_128_BIT => Self::default_128_bit(key), DEFAULT_256_BIT => Self::default_256_bit(key), - KMAC128_NAME => { - Ok(Self::KMAC128(KMAC128Key::from_key_material(key)?.create_generator())) - } - KMAC256_NAME => { - Ok(Self::KMAC256(KMAC256Key::from_key_material(key)?.create_generator())) - } - HMAC_SHA224_NAME => Ok(Self::HMAC_SHA224( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA256_NAME => Ok(Self::HMAC_SHA256( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA384_NAME => Ok(Self::HMAC_SHA384( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA512_NAME => Ok(Self::HMAC_SHA512( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA512_224_NAME => Ok(Self::HMAC_SHA512_224( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA512_256_NAME => Ok(Self::HMAC_SHA512_256( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA3_224_NAME => Ok(Self::HMAC_SHA3_224( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA3_256_NAME => Ok(Self::HMAC_SHA3_256( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA3_384_NAME => Ok(Self::HMAC_SHA3_384( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SHA3_512_NAME => Ok(Self::HMAC_SHA3_512( - HmacKey::::from_key_material(key)?.create_generator(), - )), - HMAC_SM3_NAME => { - Ok(Self::HMAC_SM3(HmacKey::::from_key_material(key)?.create_generator())) - } + KMAC128_NAME => Ok(Self::KMAC128(KMAC128Key::from_key_material(key)?)), + KMAC256_NAME => Ok(Self::KMAC256(KMAC256Key::from_key_material(key)?)), + HMAC_SHA224_NAME => Ok(Self::HMAC_SHA224(HmacKey::from_key_material(key)?)), + HMAC_SHA256_NAME => Ok(Self::HMAC_SHA256(HmacKey::from_key_material(key)?)), + HMAC_SHA384_NAME => Ok(Self::HMAC_SHA384(HmacKey::from_key_material(key)?)), + HMAC_SHA512_NAME => Ok(Self::HMAC_SHA512(HmacKey::from_key_material(key)?)), + HMAC_SHA512_224_NAME => Ok(Self::HMAC_SHA512_224(HmacKey::from_key_material(key)?)), + HMAC_SHA512_256_NAME => Ok(Self::HMAC_SHA512_256(HmacKey::from_key_material(key)?)), + HMAC_SHA3_224_NAME => Ok(Self::HMAC_SHA3_224(HmacKey::from_key_material(key)?)), + HMAC_SHA3_256_NAME => Ok(Self::HMAC_SHA3_256(HmacKey::from_key_material(key)?)), + HMAC_SHA3_384_NAME => Ok(Self::HMAC_SHA3_384(HmacKey::from_key_material(key)?)), + HMAC_SHA3_512_NAME => Ok(Self::HMAC_SHA3_512(HmacKey::from_key_material(key)?)), + HMAC_SM3_NAME => Ok(Self::HMAC_SM3(HmacKey::from_key_material(key)?)), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known MAC", alg_name ))), } } -} -impl MAC for MACFactory { - fn output_len(&self) -> usize { + /// The worker for one message, from whichever key this is. + fn worker(&self) -> MACFactoryWorker<'_> { match self { - Self::KMAC128(h) => h.output_len(), - Self::KMAC256(h) => h.output_len(), - Self::HMAC_SHA224(h) => h.output_len(), - Self::HMAC_SHA256(h) => h.output_len(), - Self::HMAC_SHA384(h) => h.output_len(), - Self::HMAC_SHA512(h) => h.output_len(), - Self::HMAC_SHA512_224(h) => h.output_len(), - Self::HMAC_SHA512_256(h) => h.output_len(), - Self::HMAC_SHA3_224(h) => h.output_len(), - Self::HMAC_SHA3_256(h) => h.output_len(), - Self::HMAC_SHA3_384(h) => h.output_len(), - Self::HMAC_SHA3_512(h) => h.output_len(), - Self::HMAC_SM3(h) => h.output_len(), + Self::KMAC128(k) => MACFactoryWorker::KMAC128(k.create_generator()), + Self::KMAC256(k) => MACFactoryWorker::KMAC256(k.create_generator()), + Self::HMAC_SHA224(k) => MACFactoryWorker::HMAC_SHA224(k.create_generator()), + Self::HMAC_SHA256(k) => MACFactoryWorker::HMAC_SHA256(k.create_generator()), + Self::HMAC_SHA384(k) => MACFactoryWorker::HMAC_SHA384(k.create_generator()), + Self::HMAC_SHA512(k) => MACFactoryWorker::HMAC_SHA512(k.create_generator()), + Self::HMAC_SHA512_224(k) => MACFactoryWorker::HMAC_SHA512_224(k.create_generator()), + Self::HMAC_SHA512_256(k) => MACFactoryWorker::HMAC_SHA512_256(k.create_generator()), + Self::HMAC_SHA3_224(k) => MACFactoryWorker::HMAC_SHA3_224(k.create_generator()), + Self::HMAC_SHA3_256(k) => MACFactoryWorker::HMAC_SHA3_256(k.create_generator()), + Self::HMAC_SHA3_384(k) => MACFactoryWorker::HMAC_SHA3_384(k.create_generator()), + Self::HMAC_SHA3_512(k) => MACFactoryWorker::HMAC_SHA3_512(k.create_generator()), + Self::HMAC_SM3(k) => MACFactoryWorker::HMAC_SM3(k.create_generator()), } } +} - fn mac(self, data: &[u8]) -> Vec { - match self { - Self::KMAC128(h) => h.mac(data), - Self::KMAC256(h) => h.mac(data), - Self::HMAC_SHA224(h) => h.mac(data), - Self::HMAC_SHA256(h) => h.mac(data), - Self::HMAC_SHA384(h) => h.mac(data), - Self::HMAC_SHA512(h) => h.mac(data), - Self::HMAC_SHA512_224(h) => h.mac(data), - Self::HMAC_SHA512_256(h) => h.mac(data), - Self::HMAC_SHA3_224(h) => h.mac(data), - Self::HMAC_SHA3_256(h) => h.mac(data), - Self::HMAC_SHA3_384(h) => h.mac(data), - Self::HMAC_SHA3_512(h) => h.mac(data), - Self::HMAC_SM3(h) => h.mac(data), +impl MacGenerationKey for MACFactory { + type Generator<'k> + = MACFactoryWorker<'k> + where + Self: 'k; + + fn create_generator<'k>(&'k self) -> Self::Generator<'k> { + self.worker() + } +} + +impl MacVerificationKey for MACFactory { + type Verifier<'k> + = MACFactoryWorker<'k> + where + Self: 'k; + + fn create_verifier<'k>(&'k self) -> Self::Verifier<'k> { + self.worker() + } +} + +/// Every method is the named worker's own. +macro_rules! forward { + ($self:expr, $w:ident => $e:expr) => { + match $self { + MACFactoryWorker::KMAC128($w) => $e, + MACFactoryWorker::KMAC256($w) => $e, + MACFactoryWorker::HMAC_SHA224($w) => $e, + MACFactoryWorker::HMAC_SHA256($w) => $e, + MACFactoryWorker::HMAC_SHA384($w) => $e, + MACFactoryWorker::HMAC_SHA512($w) => $e, + MACFactoryWorker::HMAC_SHA512_224($w) => $e, + MACFactoryWorker::HMAC_SHA512_256($w) => $e, + MACFactoryWorker::HMAC_SHA3_224($w) => $e, + MACFactoryWorker::HMAC_SHA3_256($w) => $e, + MACFactoryWorker::HMAC_SHA3_384($w) => $e, + MACFactoryWorker::HMAC_SHA3_512($w) => $e, + MACFactoryWorker::HMAC_SM3($w) => $e, } + }; +} + +impl MAC for MACFactoryWorker<'_> { + fn output_len(&self) -> usize { + forward!(self, h => h.output_len()) + } + + fn mac(self, data: &[u8]) -> Vec { + forward!(self, h => h.mac(data)) } fn mac_out(self, data: &[u8], out: &mut [u8]) -> Result { out.fill(0); - - match self { - Self::KMAC128(h) => h.mac_out(data, out), - Self::KMAC256(h) => h.mac_out(data, out), - Self::HMAC_SHA224(h) => h.mac_out(data, out), - Self::HMAC_SHA256(h) => h.mac_out(data, out), - Self::HMAC_SHA384(h) => h.mac_out(data, out), - Self::HMAC_SHA512(h) => h.mac_out(data, out), - Self::HMAC_SHA512_224(h) => h.mac_out(data, out), - Self::HMAC_SHA512_256(h) => h.mac_out(data, out), - Self::HMAC_SHA3_224(h) => h.mac_out(data, out), - Self::HMAC_SHA3_256(h) => h.mac_out(data, out), - Self::HMAC_SHA3_384(h) => h.mac_out(data, out), - Self::HMAC_SHA3_512(h) => h.mac_out(data, out), - Self::HMAC_SM3(h) => h.mac_out(data, out), - } + forward!(self, h => h.mac_out(data, out)) } fn verify(self, data: &[u8], mac: &[u8]) -> bool { - match self { - Self::KMAC128(h) => h.verify(data, mac), - Self::KMAC256(h) => h.verify(data, mac), - Self::HMAC_SHA224(h) => h.verify(data, mac), - Self::HMAC_SHA256(h) => h.verify(data, mac), - Self::HMAC_SHA384(h) => h.verify(data, mac), - Self::HMAC_SHA512(h) => h.verify(data, mac), - Self::HMAC_SHA512_224(h) => h.verify(data, mac), - Self::HMAC_SHA512_256(h) => h.verify(data, mac), - Self::HMAC_SHA3_224(h) => h.verify(data, mac), - Self::HMAC_SHA3_256(h) => h.verify(data, mac), - Self::HMAC_SHA3_384(h) => h.verify(data, mac), - Self::HMAC_SHA3_512(h) => h.verify(data, mac), - Self::HMAC_SM3(h) => h.verify(data, mac), - } + forward!(self, h => h.verify(data, mac)) } fn do_update(&mut self, data: &[u8]) { - match self { - Self::KMAC128(h) => h.do_update(data), - Self::KMAC256(h) => h.do_update(data), - Self::HMAC_SHA224(h) => h.do_update(data), - Self::HMAC_SHA256(h) => h.do_update(data), - Self::HMAC_SHA384(h) => h.do_update(data), - Self::HMAC_SHA512(h) => h.do_update(data), - Self::HMAC_SHA512_224(h) => h.do_update(data), - Self::HMAC_SHA512_256(h) => h.do_update(data), - Self::HMAC_SHA3_224(h) => h.do_update(data), - Self::HMAC_SHA3_256(h) => h.do_update(data), - Self::HMAC_SHA3_384(h) => h.do_update(data), - Self::HMAC_SHA3_512(h) => h.do_update(data), - Self::HMAC_SM3(h) => h.do_update(data), - } + forward!(self, h => h.do_update(data)) } fn do_final(self) -> Vec { - match self { - Self::KMAC128(h) => h.do_final(), - Self::KMAC256(h) => h.do_final(), - Self::HMAC_SHA224(h) => h.do_final(), - Self::HMAC_SHA256(h) => h.do_final(), - Self::HMAC_SHA384(h) => h.do_final(), - Self::HMAC_SHA512(h) => h.do_final(), - Self::HMAC_SHA512_224(h) => h.do_final(), - Self::HMAC_SHA512_256(h) => h.do_final(), - Self::HMAC_SHA3_224(h) => h.do_final(), - Self::HMAC_SHA3_256(h) => h.do_final(), - Self::HMAC_SHA3_384(h) => h.do_final(), - Self::HMAC_SHA3_512(h) => h.do_final(), - Self::HMAC_SM3(h) => h.do_final(), - } + forward!(self, h => h.do_final()) } - fn do_final_out(self, mut out: &mut [u8]) -> Result { + fn do_final_out(self, out: &mut [u8]) -> Result { out.fill(0); - - match self { - Self::KMAC128(h) => h.do_final_out(&mut out), - Self::KMAC256(h) => h.do_final_out(&mut out), - Self::HMAC_SHA224(h) => h.do_final_out(&mut out), - Self::HMAC_SHA256(h) => h.do_final_out(&mut out), - Self::HMAC_SHA384(h) => h.do_final_out(&mut out), - Self::HMAC_SHA512(h) => h.do_final_out(&mut out), - Self::HMAC_SHA512_224(h) => h.do_final_out(&mut out), - Self::HMAC_SHA512_256(h) => h.do_final_out(&mut out), - Self::HMAC_SHA3_224(h) => h.do_final_out(&mut out), - Self::HMAC_SHA3_256(h) => h.do_final_out(&mut out), - Self::HMAC_SHA3_384(h) => h.do_final_out(&mut out), - Self::HMAC_SHA3_512(h) => h.do_final_out(&mut out), - Self::HMAC_SM3(h) => h.do_final_out(&mut out), - } + forward!(self, h => h.do_final_out(out)) } fn do_verify_final(self, mac: &[u8]) -> bool { - match self { - Self::KMAC128(h) => h.do_verify_final(mac), - Self::KMAC256(h) => h.do_verify_final(mac), - Self::HMAC_SHA224(h) => h.do_verify_final(mac), - Self::HMAC_SHA256(h) => h.do_verify_final(mac), - Self::HMAC_SHA384(h) => h.do_verify_final(mac), - Self::HMAC_SHA512(h) => h.do_verify_final(mac), - Self::HMAC_SHA512_224(h) => h.do_verify_final(mac), - Self::HMAC_SHA512_256(h) => h.do_verify_final(mac), - Self::HMAC_SHA3_224(h) => h.do_verify_final(mac), - Self::HMAC_SHA3_256(h) => h.do_verify_final(mac), - Self::HMAC_SHA3_384(h) => h.do_verify_final(mac), - Self::HMAC_SHA3_512(h) => h.do_verify_final(mac), - Self::HMAC_SM3(h) => h.do_verify_final(mac), - } + forward!(self, h => h.do_verify_final(mac)) } fn max_security_strength(&self) -> SecurityStrength { - match self { - Self::KMAC128(h) => h.max_security_strength(), - Self::KMAC256(h) => h.max_security_strength(), - Self::HMAC_SHA224(h) => h.max_security_strength(), - Self::HMAC_SHA256(h) => h.max_security_strength(), - Self::HMAC_SHA384(h) => h.max_security_strength(), - Self::HMAC_SHA512(h) => h.max_security_strength(), - Self::HMAC_SHA512_224(h) => h.max_security_strength(), - Self::HMAC_SHA512_256(h) => h.max_security_strength(), - Self::HMAC_SHA3_224(h) => h.max_security_strength(), - Self::HMAC_SHA3_256(h) => h.max_security_strength(), - Self::HMAC_SHA3_384(h) => h.max_security_strength(), - Self::HMAC_SHA3_512(h) => h.max_security_strength(), - Self::HMAC_SM3(h) => h.max_security_strength(), - } + forward!(self, h => h.max_security_strength()) } } diff --git a/crypto/factory/tests/mac_factory_tests.rs b/crypto/factory/tests/mac_factory_tests.rs index a71214656..8e3bf0775 100644 --- a/crypto/factory/tests/mac_factory_tests.rs +++ b/crypto/factory/tests/mac_factory_tests.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod hash_factory_tests { use bouncycastle_core::key_material::{KeyMaterial, KeyType}; + use bouncycastle_core::symmetric_key::{MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::MAC; use bouncycastle_factory::mac_factory::MACFactory; use bouncycastle_hex as hex; @@ -31,7 +32,7 @@ mod hash_factory_tests { let msg = hex::decode("2579f5df03e0fccde2b515944d88dc81ca3b4a20517cdc54170559f0d2f889e2f543eacf8a84b34563d0139351ea9a77399d274c5c6c1b0f488063b7255f9df648667fe800151ef288a68d6c8c24d57abd7e4f70eed149752beae4a9763cebf03c").unwrap(); let expected = hex::decode("6e927067f724d4fedc96b310c5115979e8dde8a4").unwrap(); let hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); - assert_eq!(hmac.output_len(), 28); + assert_eq!(hmac.create_generator().output_len(), 28); assert_eq!(&hmac.mac(&msg)[..20], &expected[..]); let hmac = MACFactory::new(bouncycastle_sha2::hmac::HMAC_SHA512_224_NAME, &key).unwrap(); @@ -46,7 +47,7 @@ mod hash_factory_tests { let msg = hex::decode("").unwrap(); let expected = hex::decode("7857d4737760e127f1533185c6ad183ac4e10bd9").unwrap(); let hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); - assert_eq!(hmac.output_len(), 32); + assert_eq!(hmac.create_generator().output_len(), 32); assert_eq!(&hmac.mac(&msg)[..20], &expected[..]); let hmac = MACFactory::new(bouncycastle_sha2::hmac::HMAC_SHA512_256_NAME, &key).unwrap(); @@ -66,7 +67,8 @@ mod hash_factory_tests { &hex::decode("6e927067f724d4fedc96b310c5115979e8dde8a4").unwrap()[..] ); - let mut hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + let hmac_key = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + let mut hmac = hmac_key.create_generator(); for chunk in msg.chunks(7) { hmac.do_update(chunk); } @@ -80,7 +82,8 @@ mod hash_factory_tests { assert_eq!(out, full); let mut out = vec![0xffu8; 28]; - let mut hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + let hmac_key = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + let mut hmac = hmac_key.create_generator(); hmac.do_update(&msg); assert_eq!(hmac.do_final_out(&mut out).unwrap(), 28); assert_eq!(out, full); @@ -89,10 +92,12 @@ mod hash_factory_tests { wrong[0] ^= 1; assert!(MACFactory::new("HMAC-SHA512/224", &key).unwrap().verify(&msg, &full)); assert!(!MACFactory::new("HMAC-SHA512/224", &key).unwrap().verify(&msg, &wrong)); - let mut hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + let hmac_key = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + let mut hmac = hmac_key.create_generator(); hmac.do_update(&msg); assert!(hmac.do_verify_final(&full)); - let mut hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + let hmac_key = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + let mut hmac = hmac_key.create_generator(); hmac.do_update(&msg); assert!(!hmac.do_verify_final(&wrong)); @@ -110,7 +115,8 @@ mod hash_factory_tests { &hex::decode("7857d4737760e127f1533185c6ad183ac4e10bd9").unwrap()[..] ); - let mut hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + let hmac_key = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + let mut hmac = hmac_key.create_generator(); for chunk in msg.chunks(7) { hmac.do_update(chunk); } @@ -124,7 +130,8 @@ mod hash_factory_tests { assert_eq!(out, full); let mut out = vec![0xffu8; 32]; - let mut hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + let hmac_key = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + let mut hmac = hmac_key.create_generator(); hmac.do_update(&msg); assert_eq!(hmac.do_final_out(&mut out).unwrap(), 32); assert_eq!(out, full); @@ -133,10 +140,12 @@ mod hash_factory_tests { wrong[0] ^= 1; assert!(MACFactory::new("HMAC-SHA512/256", &key).unwrap().verify(&msg, &full)); assert!(!MACFactory::new("HMAC-SHA512/256", &key).unwrap().verify(&msg, &wrong)); - let mut hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + let hmac_key = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + let mut hmac = hmac_key.create_generator(); hmac.do_update(&msg); assert!(hmac.do_verify_final(&full)); - let mut hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + let hmac_key = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + let mut hmac = hmac_key.create_generator(); hmac.do_update(&msg); assert!(!hmac.do_verify_final(&wrong)); @@ -154,7 +163,7 @@ mod hash_factory_tests { .unwrap(); for name in ["HMAC-SM3", bouncycastle_sm3::hmac::HMAC_SM3_NAME] { let hmac = MACFactory::new(name, &key).unwrap(); - assert_eq!(hmac.output_len(), 32); + assert_eq!(hmac.create_generator().output_len(), 32); assert!( hmac.verify( b"Hi There", diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index 4aaa8c4a4..d722f0b05 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -114,7 +114,7 @@ use bouncycastle_core::key_material::{ use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; use bouncycastle_core::traits::{Hash, HashAlgParams, KDF, MAC, Suspendable, SuspendableKeyed}; -use bouncycastle_hmac::{HMAC, HmacKey}; +use bouncycastle_hmac::HmacKey; use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_utils::{max, min}; use std::marker::PhantomData; @@ -163,14 +163,22 @@ pub struct HKDF< const HASH_STATE_LEN: usize, const HKDF_STATE_LEN: usize, > { - // Optional because an HMAC cannot be constructed until a key is provided - // to initialize it with. + // Optional because the extract-phase HMAC cannot be keyed until the salt is provided. // None must correspond to a state of Uninitialized. - hmac: Option>, + hmac: Option>, entropy: HkdfEntropyTracker, state: HkdfStates, } +/// The extract phase's HMAC, held as its two halves: the salt key and the inner hash absorbing +/// the IKM. An `HMAC` worker borrows its key, and a struct cannot borrow from itself, so the +/// phase is run through [`HmacKey::inner_state`] and [`HmacKey::finish_out`] instead. +#[derive(Clone)] +struct HkdfExtract { + key: HmacKey, + inner: H, +} + // Note: does not need to impl Drop because HKDF itself does not hold any sensitive state data. #[derive(Clone, Debug, PartialOrd, PartialEq)] @@ -494,7 +502,9 @@ impl::from_key_material_allow_weak(salt)?.create_generator()); + let key = HmacKey::::from_key_material_allow_weak(salt)?; + let inner = key.inner_state(); + self.hmac = Some(HkdfExtract { key, inner }); let additional_entropy = self.entropy.credit_entropy(salt); self.state = HkdfStates::Initialized; @@ -527,9 +537,7 @@ impl = self.hmac.as_mut().unwrap(); - hmac_ref.do_update(ikm.ref_to_bytes()); - // self.hmac.as_mut().unwrap().do_update(ikm.ref_to_bytes()); + self.hmac.as_mut().unwrap().inner.do_update(ikm.ref_to_bytes()); Ok(additional_entropy) } @@ -551,7 +559,7 @@ impl None, // infallible: the sub-slice is exactly HASH_STATE_LEN bytes by const construction. - 1 => Some(HMAC::::from_suspended( - state[4..4 + HASH_STATE_LEN].try_into().unwrap(), - salt, - )?), + 1 => Some(HkdfExtract { + key: HmacKey::::from_key_material_allow_weak(salt) + .map_err(|_| SuspendableError::InvalidData)?, + inner: H::from_suspended(state[4..4 + HASH_STATE_LEN].try_into().unwrap())?, + }), _ => return Err(SuspendableError::InvalidData), }; diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index aa1425f33..a855b6985 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -3,7 +3,7 @@ //! //! This is a utility crate and is not intended to be used directly. It provides [`HmacKey`] -- the //! key object, which holds the two precomputed pad states RFC 2104 Section 4 describes and hands -//! out workers -- and [`HMAC`] -- the worker, one per message. Both are generic over any struct +//! out workers -- and [`HMAC`] -- the worker, one per message, which borrows the key it came from. Both are generic over any struct //! that implements [`Hash`] and [`HMACParams`], the extension point through which a hash declares //! the metadata from which the HMAC instance is built. The library provides the following concrete //! instantiations of HMAC: @@ -61,15 +61,15 @@ //! 3. Publish a type alias for the instantiation, `HMAC`. //! 4. Optionally publish the suspended-state length as a constant. [`SuspendableKeyed`] is //! implemented automatically for any hash that implements [`Suspendable`], and HMAC's suspended -//! state is exactly the inner hash's -- the key is deliberately excluded -- so the constant is -//! just an alias for the hash's own. +//! state is exactly the inner hash's -- the key is deliberately excluded, and resume takes the +//! [`HmacKey`] again -- so the constant is just an alias for the hash's own. //! //! ## Worked example //! //! As an example, the `bouncycastle-sha2` crate follows exactly the recipe above; its entry for SHA-256 reduces to: //! //! ```rust,ignore -//! pub type HMAC_SHA256 = HMAC; +//! pub type HMAC_SHA256<'k> = HMAC<'k, SHA256>; //! //! impl HMACParams for SHA256 { //! type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; @@ -108,9 +108,9 @@ //! FIPS 140-2 IG A.8 / NIST SP 800-107-r1 Section 5.3.3. It is a floor, not a recommendation: //! RFC 2104 Section 5 recommends that the output length "be not less than half the length of the //! hash output ... and not less than 80 bits". -//! * The key is deliberately excluded from the suspended state and must be re-supplied on resume. -//! Resuming with the wrong key cannot be detected and silently produces a wrong MAC, computed with -//! different keys in the inner and outer pad. +//! * The key is deliberately excluded from the suspended state and the [`HmacKey`] must be +//! re-supplied on resume. Resuming with the wrong key cannot be detected and silently produces a +//! wrong MAC, computed with different keys in the inner and outer pad. //! * The two pad states are key-equivalent, as RFC 2104 Section 4 says ("the stored intermediate //! values need to be treated and protected the same as secret keys"). [`HmacKey`] and the worker //! hold them as hash states, which the hash crates zeroize on drop; neither has a `Debug` that @@ -170,12 +170,12 @@ pub trait HMACParams: Hash + HashAlgParams + Default { const HMAC_OID_DER: &'static [u8]; } -impl Algorithm for HMAC { +impl Algorithm for HMAC<'_, HASH> { const ALG_NAME: &'static str = HASH::HMAC_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = HASH::HMAC_MAX_SECURITY_STRENGTH; } -impl AlgorithmOID for HMAC { +impl AlgorithmOID for HMAC<'_, HASH> { const OID: &'static [u32] = HASH::HMAC_OID; const OID_DER: &'static [u8] = HASH::HMAC_OID_DER; } @@ -195,10 +195,12 @@ const LARGEST_HASHER_OUTPUT_LEN: usize = 64; /// An HMAC key: the two intermediate hash states of RFC 2104 Section 4, `H` after absorbing /// `K XOR ipad` and after absorbing `K XOR opad`, computed once from key material of any length. /// -/// Every worker the key hands out starts from clones of the two states, so the per-message cost -/// of the key is a clone and no hashing. The key holds no key bytes: a caller who needs the -/// material again keeps the `KeyMaterial`. Construction is [`FromAnyKeyMaterial`]; the workers -/// come from [`MacGenerationKey`] and [`MacVerificationKey`]. +/// Every worker the key hands out starts from a clone of the inner state and borrows the key for +/// the outer one, so the per-message cost of the key is one clone and no hashing. The key holds +/// no key bytes: a caller who needs the material again keeps the `KeyMaterial`. Construction is +/// [`FromAnyKeyMaterial`]; the workers come from [`MacGenerationKey`] and [`MacVerificationKey`], +/// and a construction that keeps the inner hash itself (HKDF's extract phase) uses +/// [`inner_state`](Self::inner_state) and [`finish_out`](Self::finish_out) directly. pub struct HmacKey { /// `H` after absorbing `K XOR ipad`: the start of RFC 2104 Section 2 step (4). inner: HASH, @@ -314,20 +316,48 @@ impl FromAnyKeyMaterial for HmacKey { } } +impl HmacKey { + /// `H` after absorbing `K XOR ipad`, ready for the text: the start of RFC 2104 Section 2 + /// step (4). What a worker's updates feed; [`finish_out`](Self::finish_out) is its other half. + pub fn inner_state(&self) -> HASH { + self.inner.clone() + } + + /// RFC 2104 Section 2 steps (4), (6) and (7) over an inner hash that has absorbed the text: + /// finishes it, feeds the result to the outer pad state, and finishes that into `out`, which + /// is zeroized first. Returns the number of bytes written. + /// + /// # Errors + /// [`MACError::InvalidLength`] if `out` is shorter than [`MIN_FIPS_DIGEST_LEN`]. + pub fn finish_out(&self, inner: HASH, out: &mut [u8]) -> Result { + out.fill(0); + if out.len() < MIN_FIPS_DIGEST_LEN { + return Err(MACError::InvalidLength( + "HMAC truncation too short for FIPS 140-2 guidelines", + )); + } + let mut ihash = vec![0u8; inner.output_len()]; + inner.do_final_out(&mut ihash); // step (4) + let mut outer = self.outer.clone(); + outer.do_update(&ihash); // step (6) + Ok(outer.do_final_out(out)) // step (7) + } +} + impl MacGenerationKey for HmacKey { type Generator<'k> - = HMAC + = HMAC<'k, HASH> where Self: 'k; fn create_generator<'k>(&'k self) -> Self::Generator<'k> { - HMAC { hasher: self.inner.clone(), outer: self.outer.clone() } + HMAC { hasher: self.inner.clone(), key: self } } } impl MacVerificationKey for HmacKey { type Verifier<'k> - = HMAC + = HMAC<'k, HASH> where Self: 'k; @@ -338,23 +368,24 @@ impl MacVerificationKey for HmacKey { /*** Worker ***/ -/// The HMAC worker for one message: the inner hash, absorbing the message, and the outer pad state -/// it is finished through. Obtained from an [`HmacKey`]. +/// The HMAC worker for one message: the inner hash, absorbing the message, and the key it was made +/// from, borrowed for `'k`, whose outer pad state it is finished through. Obtained from an +/// [`HmacKey`]. #[derive(Clone)] -pub struct HMAC { +pub struct HMAC<'k, HASH: Hash + Default> { /// RFC 2104 Section 2 steps (3)-(4): `H(K XOR ipad, text)` in progress. hasher: HASH, - /// `H` after absorbing `K XOR opad`, for step (7). - outer: HASH, + /// The key, for `H` after absorbing `K XOR opad` at step (7). + key: &'k HmacKey, } -impl Debug for HMAC { +impl Debug for HMAC<'_, HASH> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "HMAC-{} instance", HASH::ALG_NAME,) } } -impl Display for HMAC { +impl Display for HMAC<'_, HASH> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "HMAC-{} instance", HASH::ALG_NAME,) } @@ -376,26 +407,14 @@ const OPAD_BYTE: u8 = 0x5C; /// = 32 bits / 8 = 4 bytes; pub const MIN_FIPS_DIGEST_LEN: usize = 4; -impl HMAC { - /// RFC 2104 Section 2 steps (4), (6) and (7): finish the inner hash, feed its result to the - /// outer pad state, and finish that into `out`. +impl HMAC<'_, HASH> { + /// RFC 2104 Section 2 steps (4), (6) and (7), through the key: [`HmacKey::finish_out`]. fn do_final_internal_out(self, out: &mut [u8]) -> Result { - if out.len() < MIN_FIPS_DIGEST_LEN { - return Err(MACError::InvalidLength( - "HMAC truncation too short for FIPS 140-2 guidelines", - )); - } - out.fill(0); - - let Self { hasher, mut outer } = self; - let mut ihash = vec![0u8; hasher.output_len()]; - hasher.do_final_out(&mut ihash); // step (4) - outer.do_update(&ihash); // step (6) - Ok(outer.do_final_out(out)) // step (7) + self.key.finish_out(self.hasher, out) } } -impl MAC for HMAC { +impl MAC for HMAC<'_, HASH> { fn output_len(&self) -> usize { self.hasher.output_len() } @@ -450,14 +469,14 @@ impl MAC for HMAC { /// Suspend / resume for `HMAC`. /// -/// The suspended state is exactly the inner hash's: the outer pad state is a function of the key -/// alone and is rebuilt from the re-supplied key on resume, so the state carries one secret fewer. -/// The key is *not* authenticated against the state: resuming with the wrong key cannot be -/// detected and produces a wrong MAC. +/// The suspended state is exactly the inner hash's: the outer pad state is the key's, which the +/// worker borrows, so resume takes the [`HmacKey`] the worker was made from and the state carries +/// one secret fewer. The key is *not* authenticated against the state: resuming with the wrong key +/// cannot be detected and produces a wrong MAC. impl<'k, const HASH_STATE_LEN: usize, HASH: Hash + Default + Suspendable> - SuspendableKeyed<'k, HASH_STATE_LEN> for HMAC + SuspendableKeyed<'k, HASH_STATE_LEN> for HMAC<'k, HASH> { - type Key = dyn KeyMaterialTrait; + type Key = HmacKey; fn suspend(self) -> [u8; HASH_STATE_LEN] { self.hasher.suspend() @@ -467,15 +486,13 @@ impl<'k, const HASH_STATE_LEN: usize, HASH: Hash + Default + Suspendable Result { - let hasher = HASH::from_suspended(state)?; - let HmacKey { outer, .. } = HmacKey::::from_key_bytes(key.ref_to_bytes()); - Ok(HMAC { hasher, outer }) + Ok(HMAC { hasher: HASH::from_suspended(state)?, key }) } } /* KeyGen functions */ -impl HMAC { +impl HMAC<'_, HASH> { /// Generates a fresh key for this HMAC from `rng`: [`HMACParams::MACKey`] bytes (the hash's /// output length), tagged [`KeyType::MACKey`]. [`FromAnyKeyMaterial::generate`] on /// [`HmacKey`] draws the same key and builds the key object from it in one step. diff --git a/crypto/hmac/tests/hmac_key_tests.rs b/crypto/hmac/tests/hmac_key_tests.rs index 394e65f27..4a42e0413 100644 --- a/crypto/hmac/tests/hmac_key_tests.rs +++ b/crypto/hmac/tests/hmac_key_tests.rs @@ -169,7 +169,7 @@ fn a_worker_from_the_key_suspends_and_resumes() { let mut w = key.create_generator(); w.do_update(&MSG[..7]); let state: [u8; SUSPENDED_SHA256_STATE_LEN] = w.suspend(); - let mut resumed = HMAC::::from_suspended(state, &km as &dyn KeyMaterialTrait).unwrap(); + let mut resumed = HMAC::::from_suspended(state, &key).unwrap(); resumed.do_update(&MSG[7..]); assert_eq!(resumed.do_final(), key.mac(MSG)); } diff --git a/crypto/hmac/tests/hmac_tests.rs b/crypto/hmac/tests/hmac_tests.rs index 64ab8562a..91f400e26 100644 --- a/crypto/hmac/tests/hmac_tests.rs +++ b/crypto/hmac/tests/hmac_tests.rs @@ -34,9 +34,9 @@ mod hmac_tests { assert_eq!(zero_length_key.key_len(), 0); assert_eq!(zero_length_key.key_type(), KeyType::MACKey); - let mut mac = HmacKey::::from_key_material_allow_weak(&zero_length_key) - .unwrap() - .create_generator(); + let mac_key = HmacKey::::from_key_material_allow_weak(&zero_length_key).unwrap(); + + let mut mac = mac_key.create_generator(); mac.do_update("Hi There".as_bytes()); let output = mac.do_final(); assert_eq!(output, b"\xe4\x84\x11\x26\x27\x15\xc8\x37\x0c\xd5\xe7\xbf\x8e\x82\xbe\xf5\x3b\xd5\x37\x12\xd0\x07\xf3\x42\x93\x51\x84\x3b\x77\xc7\xbb\x9b"); @@ -47,7 +47,8 @@ mod hmac_tests { KeyType::MACKey, ) .unwrap(); - let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); mac.do_update(b"Hi There"); let output = mac.do_final(); assert_eq!(output, b"\x89\x6f\xb1\x12\x8a\xbb\xdf\x19\x68\x32\x10\x7c\xd4\x9d\xf3\x3f\x47\xb4\xb1\x16\x99\x12\xba\x4f\x53\x68\x4b\x22"); @@ -58,7 +59,8 @@ mod hmac_tests { KeyType::MACKey, ) .unwrap(); - let mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mac = mac_key.create_generator(); // mac.do_update(b"").unwrap(); let output = mac.do_final(); assert_eq!( @@ -168,14 +170,16 @@ mod hmac_tests { // SHA-512 has a 128-byte block, so use a 127-byte key let key = KeyMaterial::<200>::from_bytes_as_type(&[0x0B; 127], KeyType::MACKey).unwrap(); - let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); mac.do_update(b"Hi There"); let tag = mac.do_final(); assert!(HmacKey::::from_key_material(&key).unwrap().verify(b"Hi There", &tag)); // SHA3-224 has the largest block (144 bytes); a 143-byte key exercises the top of the range. let key = KeyMaterial::<200>::from_bytes_as_type(&[0x0B; 143], KeyType::MACKey).unwrap(); - let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); mac.do_update(b"Hi There"); let tag = mac.do_final(); assert!(HmacKey::::from_key_material(&key).unwrap().verify(b"Hi There", &tag)); @@ -212,8 +216,8 @@ mod hmac_tests { } } // It passes through the weak form - let mut hmac = - HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + let mut hmac = hmac_key.create_generator(); hmac.do_update(b"Hi There"); hmac.do_final(); @@ -249,12 +253,14 @@ mod hmac_tests { let out = HmacKey::::from_key_material(&key).unwrap().mac(b"Hi There"); // test output that's the wrong length, should simply return False - let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); mac.do_update(b"Hi There"); assert!(!mac.do_verify_final(&out[..out.len() - 1])); // test output that's the right length but wrong value -- do_verify - let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); mac.do_update(b"Hi There"); assert!(!mac.do_verify_final(&[0x01_u8; 28])); @@ -266,7 +272,8 @@ mod hmac_tests { ); // error case: test that it'll refuse to truncate below MIN_FIPS_DIGEST_LEN - let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); mac.do_update(b"Hi There"); let mut out = vec![0u8; MIN_FIPS_DIGEST_LEN - 1]; match mac.do_final_out(&mut out) { @@ -280,7 +287,8 @@ mod hmac_tests { } // success case: ... but it will truncate to exactly MIN_FIPS_DIGEST_LEN - let mut mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); mac.do_update(b"Hi There"); let mut out = vec![0u8; MIN_FIPS_DIGEST_LEN]; let bytes_written = mac.do_final_out(&mut out).unwrap(); @@ -288,17 +296,21 @@ mod hmac_tests { assert_eq!(&out, b"\x89\x6f\xb1\x12"); // fail case: mac value is correct but truncated - let mac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mac = mac_key.create_generator(); let mut mac_val = mac.mac(b"Polly want a cracker?"); - let verifier = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let verifier_key = HmacKey::::from_key_material(&key).unwrap(); + let verifier = verifier_key.create_generator(); assert!(verifier.verify(b"Polly want a cracker?", &mac_val)); // truncation of the mac value is considered a fail - let verifier = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let verifier_key = HmacKey::::from_key_material(&key).unwrap(); + let verifier = verifier_key.create_generator(); assert!(!verifier.verify(b"Polly want a cracker?", &mac_val[..mac_val.len() - 1])); // .. as is some extra bytes at the end - let verifier = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let verifier_key = HmacKey::::from_key_material(&key).unwrap(); + let verifier = verifier_key.create_generator(); mac_val.extend_from_slice(&[0u8; 4]); assert!(!verifier.verify(b"Polly want a cracker?", &mac_val)); } @@ -346,8 +358,8 @@ mod hmac_tests { assert_eq!(full.len(), 28); assert_eq!(&full[..20], &expected[..]); // the same vector through the streaming API in uneven chunks - let mut mac = - HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); for chunk in msg.chunks(13) { mac.do_update(chunk); } @@ -365,8 +377,8 @@ mod hmac_tests { assert_eq!(full.len(), 28); assert_eq!(&full[..20], &expected[..]); // the same vector through the streaming API in uneven chunks - let mut mac = - HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); for chunk in msg.chunks(13) { mac.do_update(chunk); } @@ -392,8 +404,8 @@ mod hmac_tests { assert_eq!(full.len(), 32); assert_eq!(&full[..20], &expected[..]); // the same vector through the streaming API in uneven chunks - let mut mac = - HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); for chunk in msg.chunks(13) { mac.do_update(chunk); } @@ -412,8 +424,8 @@ mod hmac_tests { assert_eq!(full.len(), 32); assert_eq!(&full[..20], &expected[..]); // the same vector through the streaming API in uneven chunks - let mut mac = - HmacKey::::from_key_material(&key).unwrap().create_generator(); + let mac_key = HmacKey::::from_key_material(&key).unwrap(); + let mut mac = mac_key.create_generator(); for chunk in msg.chunks(13) { mac.do_update(chunk); } @@ -648,8 +660,8 @@ mod hmac_tests { .unwrap(); let mut out = [0u8; 128 / 8]; // Key is shorter than HMAC security strength, so it needs the weak form - let hmac = - HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + let hmac = hmac_key.create_generator(); hmac.mac_out(b"Test With Truncation", &mut out).unwrap(); assert_eq!(&Vec::from(out), &hex::decode("3abf34c3503b2a23a46efc619baef897").unwrap()); @@ -725,8 +737,8 @@ mod hmac_tests { .unwrap(); let mut out = [0u8; 128 / 8]; // Key is shorter than HMAC security strength, so it needs the weak form - let hmac = - HmacKey::::from_key_material_allow_weak(&key).unwrap().create_generator(); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); + let hmac = hmac_key.create_generator(); hmac.mac_out(b"Test With Truncation", &mut out).unwrap(); assert_eq!(&Vec::from(out), &hex::decode("415fad6271580a531d4179bc891d87a6").unwrap()); @@ -783,9 +795,8 @@ mod hmac_tests { // zero-length key (weak; needs the weak form) let mut zero_length_key = KeyMaterial256::default(); do_hazardous_operations(&mut zero_length_key, |k| k.set_key_type(KeyType::MACKey)).unwrap(); - let mut mac = HmacKey::::from_key_material_allow_weak(&zero_length_key) - .unwrap() - .create_generator(); + let mac_key = HmacKey::::from_key_material_allow_weak(&zero_length_key).unwrap(); + let mut mac = mac_key.create_generator(); mac.do_update(b"abc"); assert_eq!( mac.do_final(), @@ -815,17 +826,14 @@ mod hmac_tests { let msg = b"Colorless green ideas sleep furiously"; // A helper that exercises the full round-trip for one HMAC variant. HMAC is keyed, so the - // key is NOT in the serialized state -- it is re-supplied (by reference) to - // from_serialized_state. - // The `+ 'static` on the trait object matches the associated type `type Key = dyn - // KeyMaterialTrait` (a bare `dyn` in an associated type defaults to `'static`). The concrete - // key types are owned, so they satisfy it. + // key is NOT in the serialized state -- the key object is re-supplied (by reference) to + // from_suspended. fn round_trip<'k, const N: usize, H>( mut hmac: H, - key: &'k (dyn KeyMaterialTrait + 'static), + key: &'k >::Key, input: &[u8], ) where - H: MAC + Clone + SuspendableKeyed<'k, N, Key = dyn KeyMaterialTrait + 'static>, + H: MAC + Clone + SuspendableKeyed<'k, N>, { hmac.do_update(&input[..10]); @@ -857,41 +865,24 @@ mod hmac_tests { } } - round_trip( - HmacKey::::from_key_material(&key).unwrap().create_generator(), - &key, - msg, - ); - round_trip( - HmacKey::::from_key_material(&key).unwrap().create_generator(), - &key, - msg, - ); - round_trip( - HmacKey::::from_key_material(&key).unwrap().create_generator(), - &key, - msg, - ); - round_trip( - HmacKey::::from_key_material(&key).unwrap().create_generator(), - &key, - msg, - ); - round_trip( - HmacKey::::from_key_material(&key).unwrap().create_generator(), - &key, - msg, - ); - round_trip(HmacKey::::from_key_material(&key).unwrap().create_generator(), &key, msg); + let hmac_key = HmacKey::::from_key_material(&key).unwrap(); + round_trip(hmac_key.create_generator(), &hmac_key, msg); + let hmac_key = HmacKey::::from_key_material(&key).unwrap(); + round_trip(hmac_key.create_generator(), &hmac_key, msg); + let hmac_key = HmacKey::::from_key_material(&key).unwrap(); + round_trip(hmac_key.create_generator(), &hmac_key, msg); + let hmac_key = HmacKey::::from_key_material(&key).unwrap(); + round_trip(hmac_key.create_generator(), &hmac_key, msg); + let hmac_key = HmacKey::::from_key_material(&key).unwrap(); + round_trip(hmac_key.create_generator(), &hmac_key, msg); + let hmac_key = HmacKey::::from_key_material(&key).unwrap(); + round_trip(hmac_key.create_generator(), &hmac_key, msg); // test suspend / resume with a key larger than block size let long_key = KeyMaterial::<200>::from_bytes_as_type(&DUMMY_SEED[..200], KeyType::MACKey).unwrap(); - round_trip( - HmacKey::::from_key_material(&long_key).unwrap().create_generator(), - &long_key, - msg, - ); + let hmac_key = HmacKey::::from_key_material(&long_key).unwrap(); + round_trip(hmac_key.create_generator(), &hmac_key, msg); } /// Tests that no private data is displayed @@ -902,7 +893,8 @@ mod hmac_tests { KeyType::MACKey, ) .unwrap(); - let hmac = HmacKey::::from_key_material(&key).unwrap().create_generator(); + let hmac_key = HmacKey::::from_key_material(&key).unwrap(); + let hmac = hmac_key.create_generator(); // test fmt let fmt_str = format!("{}", &hmac); diff --git a/crypto/sha2/src/hmac.rs b/crypto/sha2/src/hmac.rs index 94dc95e47..4e6d76ee0 100644 --- a/crypto/sha2/src/hmac.rs +++ b/crypto/sha2/src/hmac.rs @@ -186,7 +186,8 @@ //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); //! -//! let mut hmac = HmacKey::::from_key_material(&key).unwrap().create_generator(); +//! let key = HmacKey::::from_key_material(&key).unwrap(); +//! let mut hmac = key.create_generator(); //! hmac.do_update(msg_part1); //! //! // suspend the in-progress mac (the key is NOT included in the serialized state) @@ -197,7 +198,7 @@ //! // ... //! //! // ... later, possibly on another host: resume from the serialized state by re-supplying -//! // the same key (make sure you store it securely!). +//! // the same key object (make sure you store the material securely!). //! let mut hmac_resumed = HMAC_SHA256::from_suspended(serialized_state, &key).unwrap(); //! hmac_resumed.do_update(msg_part2); //! let h: Vec = hmac_resumed.do_final(); @@ -210,16 +211,18 @@ //! //! | Object | Size (bytes) | //! |-----------------------------------------------------------|--------------| -//! | `HMAC_SHA224`, `HMAC_SHA256`, and their `HmacKey` | 224 | -//! | `HMAC_SHA384`, `HMAC_SHA512`, and their `HmacKey` | 416 | +//! | `HmacKey` for `HMAC_SHA224`, `HMAC_SHA256` | 224 | +//! | `HmacKey` for `HMAC_SHA384`, `HMAC_SHA512` | 416 | +//! | `HMAC_SHA224`, `HMAC_SHA256` worker | 120 | +//! | `HMAC_SHA384`, `HMAC_SHA512` worker | 216 | //! | Suspended `HMAC_SHA224`/`HMAC_SHA256` state | 108 | //! | Suspended `HMAC_SHA384`/`HMAC_SHA512` state | 204 | //! -//! The worker and the key are each two of the underlying hash object (see the crate-level Memory -//! Usage section): the inner hash and the outer pad state, 2 x 112 = 224 for the SHA-256 family, -//! 2 x 208 = 416 for the SHA-512 family. The suspended state is exactly the inner hash's suspended -//! state -- the key is deliberately excluded -- so it matches the corresponding row for the bare -//! hash. +//! The key is two of the underlying hash object (see the crate-level Memory Usage section): the +//! inner and the outer pad state, 2 x 112 = 224 for the SHA-256 family, 2 x 208 = 416 for the +//! SHA-512 family. The worker is the inner hash plus a reference to the key it borrows. The +//! suspended state is exactly the inner hash's suspended state -- the key is deliberately +//! excluded -- so it matches the corresponding row for the bare hash. //! //! # 🚨 Security Considerations 🚨 //! @@ -273,7 +276,7 @@ pub const HMAC_SHA512_256_NAME: &str = "HMAC-SHA512/256"; /*** Type aliases ***/ /// Public type for HMAC using SHA224. #[allow(non_camel_case_types)] -pub type HMAC_SHA224 = HMAC; +pub type HMAC_SHA224<'k> = HMAC<'k, SHA224>; impl HMACParams for SHA224 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA224_NAME; @@ -286,7 +289,7 @@ impl HMACParams for SHA224 { /// Public type for HMAC using SHA256. #[allow(non_camel_case_types)] -pub type HMAC_SHA256 = HMAC; +pub type HMAC_SHA256<'k> = HMAC<'k, SHA256>; impl HMACParams for SHA256 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA256_NAME; @@ -299,7 +302,7 @@ impl HMACParams for SHA256 { /// Public type for HMAC using SHA384. #[allow(non_camel_case_types)] -pub type HMAC_SHA384 = HMAC; +pub type HMAC_SHA384<'k> = HMAC<'k, SHA384>; impl HMACParams for SHA384 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA384_NAME; @@ -312,7 +315,7 @@ impl HMACParams for SHA384 { /// Public type for HMAC using SHA512. #[allow(non_camel_case_types)] -pub type HMAC_SHA512 = HMAC; +pub type HMAC_SHA512<'k> = HMAC<'k, SHA512>; impl HMACParams for SHA512 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA512_NAME; @@ -325,7 +328,7 @@ impl HMACParams for SHA512 { /// Public type for HMAC using SHA512/224. #[allow(non_camel_case_types)] -pub type HMAC_SHA512_224 = HMAC; +pub type HMAC_SHA512_224<'k> = HMAC<'k, SHA512_224>; impl HMACParams for SHA512_224 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA512_224_NAME; @@ -338,7 +341,7 @@ impl HMACParams for SHA512_224 { /// Public type for HMAC using SHA512/256. #[allow(non_camel_case_types)] -pub type HMAC_SHA512_256 = HMAC; +pub type HMAC_SHA512_256<'k> = HMAC<'k, SHA512_256>; impl HMACParams for SHA512_256 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA512_256_NAME; diff --git a/crypto/sha3/src/hmac.rs b/crypto/sha3/src/hmac.rs index 7c2d0dca1..61af8efec 100644 --- a/crypto/sha3/src/hmac.rs +++ b/crypto/sha3/src/hmac.rs @@ -191,7 +191,8 @@ //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::MACKey).unwrap(); //! -//! let mut hmac = HmacKey::::from_key_material(&key).unwrap().create_generator(); +//! let key = HmacKey::::from_key_material(&key).unwrap(); +//! let mut hmac = key.create_generator(); //! hmac.do_update(msg_part1); //! //! // suspend the in-progress mac (the key is NOT included in the serialized state) @@ -202,7 +203,7 @@ //! // ... //! //! // ... later, possibly on another host: resume from the serialized state by re-supplying -//! // the same key (make sure you store it securely!). +//! // the same key object (make sure you store the material securely!). //! let mut hmac_resumed = HMAC_SHA3_256::from_suspended(serialized_state, &key).unwrap(); //! hmac_resumed.do_update(msg_part2); //! let h: Vec = hmac_resumed.do_final(); @@ -213,15 +214,16 @@ //! No heap memory is used by the algorithm itself; the `Vec`-returning convenience methods //! allocate only the output buffer, and the `*_out` variants allocate nothing. //! -//! | Object | Size (bytes) | -//! |--------------------------------------------------|--------------| -//! | `HMAC_SHA3_224` .. `HMAC_SHA3_512`, and their `HmacKey` | 880 | -//! | Suspended state, all four ([`SuspendableKeyed`]) | 415 | +//! | Object | Size (bytes) | +//! |---------------------------------------------------------|--------------| +//! | `HmacKey` for `HMAC_SHA3_224` .. `HMAC_SHA3_512` | 880 | +//! | `HMAC_SHA3_224` .. `HMAC_SHA3_512` worker | 448 | +//! | Suspended state, all four ([`SuspendableKeyed`]) | 415 | //! -//! The worker and the key are each two Keccak-f\[1600\] sponges (440 bytes each, see the -//! crate-level Memory Usage section): the inner hash and the outer pad state. The suspended state -//! is exactly the inner hash's suspended state -- the key is deliberately excluded -- so all four -//! share the sponge's single value. +//! The key is two Keccak-f\[1600\] sponges (440 bytes each, see the crate-level Memory Usage +//! section): the inner and the outer pad state. The worker is the inner sponge plus a reference +//! to the key it borrows. The suspended state is exactly the inner hash's suspended state -- the +//! key is deliberately excluded -- so all four share the sponge's single value. //! //! # 🚨 Security Considerations 🚨 //! @@ -274,7 +276,7 @@ pub const HMAC_SHA3_512_NAME: &str = "HMAC-SHA3-512"; /*** Type aliases ***/ /// Public type for HMAC using SHA3_224. #[allow(non_camel_case_types)] -pub type HMAC_SHA3_224 = HMAC; +pub type HMAC_SHA3_224<'k> = HMAC<'k, SHA3_224>; impl HMACParams for SHA3_224 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA3_224_NAME; @@ -287,7 +289,7 @@ impl HMACParams for SHA3_224 { /// Public type for HMAC using SHA3_256. #[allow(non_camel_case_types)] -pub type HMAC_SHA3_256 = HMAC; +pub type HMAC_SHA3_256<'k> = HMAC<'k, SHA3_256>; impl HMACParams for SHA3_256 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA3_256_NAME; @@ -300,7 +302,7 @@ impl HMACParams for SHA3_256 { /// Public type for HMAC using SHA3_384. #[allow(non_camel_case_types)] -pub type HMAC_SHA3_384 = HMAC; +pub type HMAC_SHA3_384<'k> = HMAC<'k, SHA3_384>; impl HMACParams for SHA3_384 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA3_384_NAME; @@ -313,7 +315,7 @@ impl HMACParams for SHA3_384 { /// Public type for HMAC using SHA3_512. #[allow(non_camel_case_types)] -pub type HMAC_SHA3_512 = HMAC; +pub type HMAC_SHA3_512<'k> = HMAC<'k, SHA3_512>; impl HMACParams for SHA3_512 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SHA3_512_NAME; diff --git a/crypto/sha3/tests/sp800_185_suspend_tests.rs b/crypto/sha3/tests/sp800_185_suspend_tests.rs index d925b7529..76c516a36 100644 --- a/crypto/sha3/tests/sp800_185_suspend_tests.rs +++ b/crypto/sha3/tests/sp800_185_suspend_tests.rs @@ -157,7 +157,9 @@ fn each_type_rejects_the_others() { rejected::<_, KMACXOF128>(squeezer.clone().suspend(), "an unbound squeezer in KMACXOF128"); rejected::<_, CSHAKE128>(squeezer.suspend(), "an unbound squeezer in cSHAKE128"); - let mut k = KMAC128Key::from_key_material(&key()).unwrap().create_generator(); + let k_key = KMAC128Key::from_key_material(&key()).unwrap(); + + let mut k = k_key.create_generator(); k.do_update(PART1); rejected::<_, TupleHash128>(k.clone().suspend(), "a KMAC128 state in TupleHash128"); rejected::<_, KMAC256>(k.suspend(), "a KMAC128 state in KMAC256"); @@ -192,7 +194,9 @@ fn corrupt_fields_are_rejected() { bad[SQUEEZING_FLAG] = 1; rejected::<_, CSHAKE128>(bad, "a squeezing sponge in an absorbing cSHAKE"); - let mut k = KMAC128Key::from_key_material(&key()).unwrap().create_generator(); + let k_key = KMAC128Key::from_key_material(&key()).unwrap(); + + let mut k = k_key.create_generator(); k.do_update(PART1); let mut bad = k.suspend(); bad[CUSTOMIZED] = 0; diff --git a/crypto/sm3/src/hmac.rs b/crypto/sm3/src/hmac.rs index 559ebacba..3694a88a6 100644 --- a/crypto/sm3/src/hmac.rs +++ b/crypto/sm3/src/hmac.rs @@ -63,7 +63,7 @@ pub const HMAC_SM3_NAME: &str = "HMAC-SM3"; /*** Type aliases ***/ /// Public type for HMAC using SM3. #[allow(non_camel_case_types)] -pub type HMAC_SM3 = HMAC; +pub type HMAC_SM3<'k> = HMAC<'k, SM3>; impl HMACParams for SM3 { type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; const HMAC_ALG_NAME: &'static str = HMAC_SM3_NAME; From e389082c90270ba7c4d123b485dace325d66b0ed Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 16:58:09 +1100 Subject: [PATCH 18/54] hmac: HmacKey::inner_state and finish_out are doc(hidden) (prototype) They exist for HKDF's extract phase and are not part of the key's documented API; the worker is the way onto HMAC. Assisted-by: Claude:claude-mythos-5-1 --- crypto/hmac/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index a855b6985..68c057c4f 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -198,9 +198,7 @@ const LARGEST_HASHER_OUTPUT_LEN: usize = 64; /// Every worker the key hands out starts from a clone of the inner state and borrows the key for /// the outer one, so the per-message cost of the key is one clone and no hashing. The key holds /// no key bytes: a caller who needs the material again keeps the `KeyMaterial`. Construction is -/// [`FromAnyKeyMaterial`]; the workers come from [`MacGenerationKey`] and [`MacVerificationKey`], -/// and a construction that keeps the inner hash itself (HKDF's extract phase) uses -/// [`inner_state`](Self::inner_state) and [`finish_out`](Self::finish_out) directly. +/// [`FromAnyKeyMaterial`]; the workers come from [`MacGenerationKey`] and [`MacVerificationKey`]. pub struct HmacKey { /// `H` after absorbing `K XOR ipad`: the start of RFC 2104 Section 2 step (4). inner: HASH, @@ -318,7 +316,10 @@ impl FromAnyKeyMaterial for HmacKey { impl HmacKey { /// `H` after absorbing `K XOR ipad`, ready for the text: the start of RFC 2104 Section 2 - /// step (4). What a worker's updates feed; [`finish_out`](Self::finish_out) is its other half. + /// step (4). What a worker's updates feed; `finish_out` is its other half. Hidden: the two + /// exist for HKDF's extract phase, which cannot hold a worker that borrows a key it owns, and + /// are not part of the key's documented API. + #[doc(hidden)] pub fn inner_state(&self) -> HASH { self.inner.clone() } @@ -329,6 +330,7 @@ impl HmacKey { /// /// # Errors /// [`MACError::InvalidLength`] if `out` is shorter than [`MIN_FIPS_DIGEST_LEN`]. + #[doc(hidden)] pub fn finish_out(&self, inner: HASH, out: &mut [u8]) -> Result { out.fill(0); if out.len() < MIN_FIPS_DIGEST_LEN { From 6c5e22341f3895fce37a4c24207613bf598bbc6b Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 20 Oct 2026 17:03:14 +1100 Subject: [PATCH 19/54] ascon: the stack-usage table re-measured on the key path (prototype) Every row of the Ascon-AEAD128 and sponge tables is re-read from the frame remarks and the assembly of the release build, with the same method as before, under one compiler (rustc 1.97.0-nightly): the key check, the constructors and the one-shots, which had been withdrawn when the key objects arrived, get their figures, and the rows that had stood since the direction moved into the type are brought up to date. The sponge rows come out 8 bytes lower than the old figures under this compiler. Assisted-by: Claude:claude-mythos-5-1 --- crypto/ascon/src/lib.rs | 30 +++++++++++++++--------------- crypto/cipher/src/modes/ctr.rs | 33 +++++++++++++++++---------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs index 014c044e7..92680a1f6 100644 --- a/crypto/ascon/src/lib.rs +++ b/crypto/ascon/src/lib.rs @@ -153,29 +153,29 @@ //! //! Per-call stack usage is separate from that and does not depend on the data length either. //! Measured as the deepest frame chain below each entry point in the release build (x86-64 Linux, -//! rustc 1.95.0, return addresses included): +//! rustc 1.97.0-nightly, return addresses included; `bench_ascon_mem_usage` has the harness and +//! the method): //! //! | Ascon-AEAD128 entry point | Encrypting | Decrypting | //! |---|---|---| -//! | `AsconAEAD128Key::from_key_material` (the key check, once per key) | - | - | -//! | `create_encryptor` / `create_decryptor` | - | - | -//! | `do_update_aad` | 104 B | 104 B | +//! | `AsconAEAD128Key::from_key_material` (the key check, once per key) | 24 B | 24 B | +//! | `create_encryptor` / `create_decryptor` | 120 B | 48 B | +//! | `do_update_aad` | 48 B | 48 B | //! | `do_encrypt_out` / `do_decrypt_out` | 176 B | 208 B | -//! | `do_encrypt_final` / `do_decrypt_final` (tag inline) | 176 B | 208 B | -//! | `do_encrypt_final_detached_out` / `do_decrypt_final_detached_out` | 176 B | 272 B | -//! | `encrypt_with_aad_out` / `decrypt_with_aad_out` (one-shot) | - | - | +//! | `do_encrypt_final` / `do_decrypt_final` (tag inline) | 112 B | 256 B | +//! | `do_encrypt_final_detached_out` / `do_decrypt_final_detached_out` | 112 B | 288 B | +//! | `encrypt_with_aad_out` / `decrypt_with_aad_out` (one-shot) | 520 B | 640 B | //! -//! The key path's rows have not been measured yet: the key check moved out of the per-message -//! init into `from_key_material`, and the one-shots are now the role traits' provided bodies, -//! so the old figures for those entry points no longer apply. `bench_ascon_mem_usage` has the -//! harness. The encrypting figures do not include the caller's RNG. +//! The encrypting `create_encryptor` figure includes the call into the caller's RNG but not the +//! RNG's own stack; the one-shots are the role traits' provided bodies, which copy the message +//! through a 64-byte chunk on the stack, and the decrypting one holds the tag back as well. //! //! | Sponge entry point | Ascon-Hash256 | Ascon-XOF128 | Ascon-CXOF128 | //! |---|---|---|---| -//! | `with_customization` | | | 200 B | -//! | `do_update` | 72 B | 72 B | 72 B | -//! | `do_final_out` / `do_output_out` | 152 B | 104 B | 104 B | -//! | `hash_out` / `xof_out` (one-shot) | 152 B | 168 B | 168 B | +//! | `with_customization` | | | 192 B | +//! | `do_update` | 64 B | 64 B | 64 B | +//! | `do_final_out` / `do_output_out` | 144 B | 96 B | 96 B | +//! | `hash_out` / `xof_out` (one-shot) | 144 B | 160 B | 160 B | //! //! The sponges' `new` copies a precomputed state and runs no permutation. //! diff --git a/crypto/cipher/src/modes/ctr.rs b/crypto/cipher/src/modes/ctr.rs index 13bee8ee3..da2c639e9 100644 --- a/crypto/cipher/src/modes/ctr.rs +++ b/crypto/cipher/src/modes/ctr.rs @@ -41,24 +41,26 @@ //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; +//! use bouncycastle_core::hazmat::{ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode}; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; -//! use bouncycastle_cipher::modes::Ctr; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! // A 12-byte nonce, leaving a 4-byte counter. -//! type ToyCtr = Ctr; +//! use bouncycastle_cipher::modes::CTR; +//! use bouncycastle_rng::HashDRBG_SHA512; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! // `CTR`'s defaults: a 12-byte nonce, leaving a 4-byte counter on a 16-byte block. +//! let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); //! //! let plaintext = b"the quick brown fox!!"; //! let mut data = *plaintext; //! -//! let (bytes_written, nonce) = ToyCtr::::encrypt_inplace(&key, &mut data).expect("encryption"); +//! let (mut encryptor, nonce) = +//! CTR::<12, 16>::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()).expect("nonce draw"); +//! let bytes_written = encryptor.do_encrypt_inplace(&mut data).expect("encryption"); //! assert_eq!(bytes_written, plaintext.len()); //! -//! ToyCtr::::decrypt_inplace(&key, &nonce, &mut data).expect("decryption"); +//! CTR::<12, 16>::decrypt_init(&state, &nonce).do_decrypt_inplace(&mut data).expect("decryption"); //! assert_eq!(data, *plaintext); //! ``` //! @@ -67,20 +69,19 @@ //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } //! # use toy::ToyBlockCipher; +//! use bouncycastle_core::hazmat::{ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode}; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::{ -//! StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor -//! }; -//! use bouncycastle_cipher::modes::Ctr; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! -//! type ToyCtr = Ctr; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_cipher::modes::CTR; +//! use bouncycastle_rng::HashDRBG_SHA512; //! //! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let state = as Mode>::key_state(ToyBlockCipher::new(&key).unwrap()); //! let mut data = [0x5Au8; 40]; //! -//! let (mut encryptor, nonce) = ToyCtr::::do_encrypt_init(&key).expect("init"); +//! let (mut encryptor, nonce) = +//! CTR::<12, 16>::encrypt_init(&state, &mut HashDRBG_SHA512::new_from_os()).expect("nonce draw"); //! //! // Just to prove that this can handle arbitrary sizes, we'll feed in //! // 7 bytes, then 33: neither is a whole block. @@ -91,7 +92,7 @@ //! assert_eq!(bytes_written, 33); //! //! // Decrypting in a different chunking must also agree. -//! let mut decryptor = ToyCtr::::do_decrypt_init(&key, &nonce).expect("init"); +//! let mut decryptor = CTR::<12, 16>::decrypt_init(&state, &nonce); //! decryptor.do_decrypt_inplace(&mut data[..19]).expect("first chunk"); //! decryptor.do_decrypt_inplace(&mut data[19..]).expect("the rest"); //! assert_eq!(data, [0x5Au8; 40]); From 5607ba9808a6287b98684f121f74f5fd40272485 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 25 Oct 2026 18:29:15 +1100 Subject: [PATCH 20/54] core, cipher, aes, ascon: key objects compare in constant time (prototype) Lifted from #167, whose SymmetricCipherKey bound every key on PartialEq + Eq: a caller that holds two keys can ask whether they are the same key without reaching the material, which the key objects never hand back. Equality is over the expanded state and decided in constant time at every layer. AESInternal compares its schedule through Secret's byte-wise equality, which inspects every byte (round key 0 is the key itself, so two schedules are equal exactly when the keys are); GcmKeyState and CmacKeyState compare the permutation and their derived values joined with `&` rather than `&&`, so the time taken does not say which field differed; AESKey is equal when its mode state is, bounded on the state rather than on Mode::KeyState so that a permutation without the impl still has keys; AsconAEAD128Key compares its Secret words. Mode::KeyState's docs now say that a state implementing PartialEq must compare in constant time. Tests: equality follows the material, for a clone, the same bytes and a single differing byte, through the bare schedule (CTR), GCM at 128 and 256 bits, CMAC and Ascon. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/src/hazmat/aes_internal.rs | 11 ++++++++++ crypto/aes/src/key.rs | 14 ++++++++++++ crypto/aes/tests/cmac_key_tests.rs | 10 +++++++++ crypto/aes/tests/key_tests.rs | 31 ++++++++++++++++++++++++++- crypto/ascon/src/ascon_aead128.rs | 10 +++++++++ crypto/ascon/tests/aead128_tests.rs | 12 +++++++++++ crypto/cipher/src/modes/cmac.rs | 10 +++++++++ crypto/cipher/src/modes/gcm.rs | 11 ++++++++++ crypto/core/src/hazmat/mode.rs | 3 ++- 9 files changed, 110 insertions(+), 2 deletions(-) diff --git a/crypto/aes/src/hazmat/aes_internal.rs b/crypto/aes/src/hazmat/aes_internal.rs index cc8abc276..4437b4f10 100644 --- a/crypto/aes/src/hazmat/aes_internal.rs +++ b/crypto/aes/src/hazmat/aes_internal.rs @@ -101,6 +101,17 @@ pub struct AESInternal { schedule: Secret, } +// Written out rather than derived so that `P`, a marker type, need not be `PartialEq`. Round key +// 0 is the key itself (FIPS 197 Sec 5.2), so two schedules are equal exactly when the keys are; +// `Secret`'s equality inspects every byte, so the time taken does not say where they differ. +impl PartialEq for AESInternal

{ + fn eq(&self, other: &Self) -> bool { + self.schedule == other.schedule + } +} + +impl Eq for AESInternal

{} + /// AES-128: 16-byte key, 10 rounds (FIPS 197 Table 3). #[allow(non_camel_case_types)] pub type AES128Internal = AESInternal; diff --git a/crypto/aes/src/key.rs b/crypto/aes/src/key.rs index 04c84043d..d0360f028 100644 --- a/crypto/aes/src/key.rs +++ b/crypto/aes/src/key.rs @@ -113,6 +113,20 @@ impl>> Clone for AESKey { } } +// Likewise for equality: two keys are equal when their mode states are, which every state over +// `AESInternal` decides in constant time, so the comparison follows the material without +// exposing it. +impl>> PartialEq for AESKey +where + M::KeyState: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.state == other.state + } +} + +impl>> Eq for AESKey where M::KeyState: Eq {} + /// The key-type check the mode asks for (`Mode::KEY_TYPE`), ahead of the permutation's own /// validation. fn check_key_type( diff --git a/crypto/aes/tests/cmac_key_tests.rs b/crypto/aes/tests/cmac_key_tests.rs index 7d9eaecfb..f832bdeaa 100644 --- a/crypto/aes/tests/cmac_key_tests.rs +++ b/crypto/aes/tests/cmac_key_tests.rs @@ -139,3 +139,13 @@ fn sizes() { [48, 48, 48] ); } + +/// Equality follows the material through the CMAC state, subkeys included: the same bytes +/// compare equal, as does a clone, and different bytes do not. +#[test] +fn cmac_keys_compare_equal_exactly_when_their_material_does() { + let key = AES128Key::::from_key_material(&mac_key::<16>(0x2b)).unwrap(); + assert!(key == key.clone()); + assert!(key == AES128Key::::from_key_material(&mac_key::<16>(0x2b)).unwrap()); + assert!(key != AES128Key::::from_key_material(&mac_key::<16>(0x2c)).unwrap()); +} diff --git a/crypto/aes/tests/key_tests.rs b/crypto/aes/tests/key_tests.rs index d733f555a..128ab1621 100644 --- a/crypto/aes/tests/key_tests.rs +++ b/crypto/aes/tests/key_tests.rs @@ -4,7 +4,7 @@ use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_aes::{AES_GCM_128, AES128Key, AES192Key, AES256Key}; -use bouncycastle_cipher::modes::{GCM, Gcm, GcmKeyState, ModeNames}; +use bouncycastle_cipher::modes::{CTR, GCM, Gcm, GcmKeyState, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::ElectronicCodeBook; @@ -305,6 +305,35 @@ fn a_cloned_key_is_the_same_key() { assert_eq!(encrypt_via_key::<16, _>(©), (nonce, ct, tag)); } +/// Equality is over the expanded state, so it follows the material: the same bytes under the same +/// mode compare equal, as does a clone, and different bytes do not. Both shapes of state are +/// covered: the bare schedule (`CTR`) and GCM's, which adds `H`. Keys have no `Debug`, hence +/// `assert!` over `==` rather than `assert_eq!`. +#[test] +fn keys_compare_equal_exactly_when_their_material_does() { + let other = KeyMaterial::<16>::from_bytes_as_type(&[0x55; 16], KeyType::SymmetricCipherKey) + .expect("a valid key"); + + let key = AES128Key::::from_key_material(&material::<16>()).unwrap(); + assert!(key == key.clone()); + assert!(key == AES128Key::::from_key_material(&material::<16>()).unwrap()); + assert!(key != AES128Key::::from_key_material(&other).unwrap()); + + let key = AES128Key::::from_key_material(&material::<16>()).unwrap(); + assert!(key == key.clone()); + assert!(key != AES128Key::::from_key_material(&other).unwrap()); + + // The longer schedules are a different plane width; the comparison is over all of it. + let key = AES256Key::::from_key_material(&material::<32>()).unwrap(); + assert!(key == AES256Key::::from_key_material(&material::<32>()).unwrap()); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(material::<32>().ref_to_bytes()); + bytes[31] ^= 1; + let last_byte_differs = + KeyMaterial::<32>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); + assert!(key != AES256Key::::from_key_material(&last_byte_differs).unwrap()); +} + /// Lengths either side of the 64-byte chunk the provided in-place bodies copy through, plus the /// empty message and a long one. const IN_PLACE_LENS: [usize; 9] = [0, 1, 15, 16, 63, 64, 65, 200, 1000]; diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 9415ee489..16a21f5a0 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -636,6 +636,16 @@ impl Clone for AsconAEAD128Key { } } +// `Secret`'s equality: every byte is inspected, so the time taken does not say where two keys +// differ. +impl PartialEq for AsconAEAD128Key { + fn eq(&self, other: &Self) -> bool { + self.key == other.key + } +} + +impl Eq for AsconAEAD128Key {} + impl FromKeyMaterial for AsconAEAD128Key { const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs index dfbe73ab8..63e3bdf22 100644 --- a/crypto/ascon/tests/aead128_tests.rs +++ b/crypto/ascon/tests/aead128_tests.rs @@ -89,6 +89,18 @@ fn aead_key(key: &[u8; 16]) -> AsconAEAD128Key { AsconAEAD128Key::from_key_material(&key_material(key)).expect("a valid key") } +/// Equality follows the material: the same bytes compare equal, as does a clone, and different +/// bytes do not. The key has no `Debug`, hence `assert!` over `==`. +#[test] +fn keys_compare_equal_exactly_when_their_material_does() { + let key = aead_key(&[0x42; 16]); + assert!(key == key.clone()); + assert!(key == aead_key(&[0x42; 16])); + let mut last_byte_differs = [0x42; 16]; + last_byte_differs[15] ^= 1; + assert!(key != aead_key(&last_byte_differs)); +} + fn enc_oneshot(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8]) -> Vec { let km = aead_key(key); let mut out = vec![0u8; Ascon_AEAD128::::encrypt_out_len(pt.len())]; diff --git a/crypto/cipher/src/modes/cmac.rs b/crypto/cipher/src/modes/cmac.rs index f5cfcd10c..6558c8a63 100644 --- a/crypto/cipher/src/modes/cmac.rs +++ b/crypto/cipher/src/modes/cmac.rs @@ -129,6 +129,16 @@ pub struct CmacKeyState { k2: Secret<[u8; BLOCK_LEN]>, } +// All three fields are compared, joined with `&` rather than `&&` so that the time taken does not +// say which of them differed; see `GcmKeyState` for the expectation on `P`. +impl PartialEq for CmacKeyState { + fn eq(&self, other: &Self) -> bool { + (self.perm == other.perm) & (self.k1 == other.k1) & (self.k2 == other.k2) + } +} + +impl Eq for CmacKeyState {} + impl CmacKeyState { /// The last byte of `R_b` (Sec 5.3): `R_128 = 0^120 || 10000111` and `R_64 = 0^59 || 11011`, /// every other byte zero. Any other block length fails to compile. diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index de85bff6d..a67fd9e44 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -292,6 +292,17 @@ pub struct GcmKeyState

{ h: Secret<[u8; 16]>, } +// Both fields are compared, joined with `&` rather than `&&` so that the time taken does not say +// which of them differed. `P`'s equality is expected to be constant-time, as a keyed permutation's +// must be; `H`'s is `Secret`'s. +impl PartialEq for GcmKeyState

{ + fn eq(&self, other: &Self) -> bool { + (self.perm == other.perm) & (self.h == other.h) + } +} + +impl Eq for GcmKeyState

{} + impl

GcmKeyState

{ /// Derives `H` from `perm`. pub fn new(perm: P) -> Self diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index d78ec80f2..d2400731b 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -36,7 +36,8 @@ use crate::traits::{ pub trait Mode

{ /// What the mode keeps per key: the permutation plus anything derived from it once. A mode /// with nothing to derive sets `type KeyState = P`. Sensitive, and held so that it is zeroized - /// on drop as the schedule it contains is. + /// on drop as the schedule it contains is. A state that implements `PartialEq`, which gives the + /// key object its equality, must compare in constant time for the same reason. type KeyState: Clone; /// The [`KeyType`] a key for this mode must carry: [`KeyType::SymmetricCipherKey`] for a From 9c343a53815f7f15cef624269db94131cc4226d1 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 25 Oct 2026 18:29:46 +1100 Subject: [PATCH 21/54] core, cli: the two rustdoc lints the rebase left, so docs build with warnings denied again (prototype) BlockEncryptionKey's one-shot doc linked BlockCipherEncryptor::do_encrypt_inplace, which left that trait with the rebase; it now names do_encrypt_blocks_inplace. The AEAD helper module's link to SymmetricCipherDecryptor carried an explicit target the label already resolves to, which rustdoc now reports as redundant. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/helpers/aead_cipher_helpers.rs | 2 +- crypto/core/src/symmetric_key.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/helpers/aead_cipher_helpers.rs b/cli/src/helpers/aead_cipher_helpers.rs index c4bc5769c..3574f6aa4 100644 --- a/cli/src/helpers/aead_cipher_helpers.rs +++ b/cli/src/helpers/aead_cipher_helpers.rs @@ -13,7 +13,7 @@ //! rest of stdin through the inline decryptor. With `--iv`, the IV is the caller's and is //! neither written nor read: the format is `ciphertext || tag` (see //! [`crate::helpers::block_mode_helpers`] for the flag). The decryptor, per -//! [`SymmetricCipherDecryptor`](bouncycastle::core::traits::SymmetricCipherDecryptor)'s contract, +//! [`SymmetricCipherDecryptor`]'s contract, //! holds back the last 16 bytes it has seen because they might be the tag -- and checks the tag on //! `do_decrypt_final`. //! diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index 65274eb4c..9ae7a7f59 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -1066,7 +1066,7 @@ pub trait BlockEncryptionKey< /// One-shot: encrypts `LEN` bytes, a whole number of blocks, in place under init data drawn /// from `rng`, and returns the number of bytes written with the init data. `LEN % BLOCK_LEN - /// == 0` is checked at compile time, as for [`BlockCipherEncryptor::do_encrypt_inplace`]. + /// == 0` is checked at compile time, as for [`BlockCipherEncryptor::do_encrypt_blocks_inplace`]. /// /// # Errors /// As [`create_encryptor`](Self::create_encryptor), or the encryptor's own refusals. From d9a7ddda82b37e755d1c18b71f7fc27041b4cdb4 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 28 Oct 2026 01:49:15 +1100 Subject: [PATCH 22/54] cipher: a GCM state resumes only in the direction it was suspended in The call-state byte held only the phase, and the SuspendableKeyed impl is one impl generic over `Dir`, so a state suspended while encrypting resumed as a decryptor and the reverse. `Direction` gains `STATE_BITS` (0x10 encrypting, 0x20 decrypting), GCM writes them above the phase nibble and refuses a state whose bits are the other direction's. The layout and length are unchanged. Found by the PR #157 review follow-up; Ascon already refuses the other direction. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/lib.rs | 7 ++++ crypto/cipher/src/modes/gcm.rs | 29 +++++++++++----- crypto/cipher/tests/modes/gcm_tests.rs | 47 ++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/crypto/cipher/src/lib.rs b/crypto/cipher/src/lib.rs index 351a1dee1..ec8ea7c13 100644 --- a/crypto/cipher/src/lib.rs +++ b/crypto/cipher/src/lib.rs @@ -119,12 +119,19 @@ mod sealed { pub trait Direction: sealed::Sealed { /// `Enc` for [`Encrypting`], `Dec` for [`Decrypting`]. type Select; + + /// The bits that mark this direction in a suspended call-state byte, above the low nibble a + /// mode keeps for its phase: `0x10` encrypting, `0x20` decrypting. A state resumed under the + /// other marker is refused, so an encryptor's state never continues as a decryptor. + const STATE_BITS: u8; } impl Direction for Encrypting { type Select = Enc; + const STATE_BITS: u8 = 0x10; } impl Direction for Decrypting { type Select = Dec; + const STATE_BITS: u8 = 0x20; } diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index a67fd9e44..e9ce4a05f 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -151,8 +151,9 @@ //! //! [`Gcm`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte //! array and resumed later over the re-supplied key state. The state is the CTR half, the running -//! GHASH, the byte counts and whatever a decryptor is holding back as a possible tag; `H` is the -//! key state's and the tag mask is re-derived from it. The array length is `Gcm::SUSPENDED_STATE_LEN`; see [the crate +//! GHASH, the byte counts, the phase with the direction it was suspended in, and whatever a +//! decryptor is holding back as a possible tag; `H` is the key state's and the tag mask is +//! re-derived from it. A state resumes only in its own direction: the other type refuses it. The array length is `Gcm::SUSPENDED_STATE_LEN`; see [the crate //! docs](crate#suspending-and-resuming-execution) for an example. //! //! # 🚨 Security Considerations 🚨 @@ -197,7 +198,7 @@ use crate::modes::Ctr; use crate::modes::ModeNames; use crate::modes::hazmat::ctr_key_stream::CtrKeyStream; -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ AeadDecryptMode, AeadEncryptMode, BlockPermutation, ElectronicCodeBook, Mode, @@ -377,8 +378,12 @@ where /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the version header, the CTR state, /// the GHASH state, the two byte counts, the phase, the held-back tail and its length. See /// [`bouncycastle_utils::suspendable_state`]. - pub const SUSPENDED_STATE_LEN: usize = - LIB_VERSION_LEN + >::STATE_LEN; + pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN + Self::COMPONENT_STATE_LEN; + + /// The component's share, spelled out here rather than read through the trait so that it is + /// nameable for any `Dir`; the trait impl's `STATE_LEN` is this constant. + const COMPONENT_STATE_LEN: usize = + Self::CTR_STATE_LEN + GHASH_STATE_LEN + 8 + 8 + 1 + TAG_LEN + 8; /// The CTR half's share of the suspended state. const CTR_STATE_LEN: usize = @@ -770,8 +775,9 @@ where impl<'k, P, Dir, const TAG_LEN: usize> SuspendableComponent<'k> for Gcm<'k, P, Dir, TAG_LEN> where P: BlockPermutation<16> + ModeNames, + Dir: Direction, { - const STATE_LEN: usize = Self::CTR_STATE_LEN + GHASH_STATE_LEN + 8 + 8 + 1 + TAG_LEN + 8; + const STATE_LEN: usize = Self::COMPONENT_STATE_LEN; type Key = GcmKeyState

; fn write_state(&self, out: &mut [u8]) { @@ -782,7 +788,9 @@ where let mut w = CursorMut::new(rest); w.u64(self.aad_len); w.u64(self.data_len); - w.u8(self.phase as u8); + // The phase in the low nibble, the direction above it, so that a state suspended while + // encrypting cannot be resumed as a decryptor or the reverse. + w.u8(Dir::STATE_BITS | self.phase as u8); w.bytes(&*self.tail); w.u64(self.tail_len as u64); debug_assert!(w.is_done()); @@ -806,7 +814,11 @@ where let mut r = Cursor::new(rest); gcm.aad_len = r.u64(); gcm.data_len = r.u64(); - gcm.phase = Phase::from_u8(r.u8())?; + let call_state = r.u8(); + if call_state & 0xF0 != Dir::STATE_BITS { + return Err(SuspendableError::InvalidData); + } + gcm.phase = Phase::from_u8(call_state & 0x0F)?; (*gcm.tail).copy_from_slice(r.bytes(TAG_LEN)); gcm.tail_len = bounded_usize(r.u64(), TAG_LEN)?; debug_assert!(r.is_done()); @@ -819,6 +831,7 @@ impl<'k, P, Dir, const TAG_LEN: usize, const N: usize> SuspendableKeyed<'k, N> for Gcm<'k, P, Dir, TAG_LEN> where P: BlockPermutation<16> + ModeNames, + Dir: Direction, { type Key = GcmKeyState

; diff --git a/crypto/cipher/tests/modes/gcm_tests.rs b/crypto/cipher/tests/modes/gcm_tests.rs index efe264bea..9b2563c85 100644 --- a/crypto/cipher/tests/modes/gcm_tests.rs +++ b/crypto/cipher/tests/modes/gcm_tests.rs @@ -10,12 +10,14 @@ mod common; #[path = "../common/toy_key.rs"] mod toy_key; -use bouncycastle_cipher::modes::{GCM, ModeNames}; -use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_cipher::modes::{GCM, Gcm, ModeNames}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; use bouncycastle_core::traits::{ - AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, + AEADCipherDecryptor, AEADCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; @@ -331,3 +333,42 @@ fn the_key_one_shots_release_nothing_on_forgery() { )); assert_eq!(out, [0u8; 32], "decrypt_detached_out must zeroize on a failed tag check"); } + +/// A suspended state carries the direction it was suspended in, and the other direction refuses +/// it: an encryptor's state cannot continue as a decryptor, whose hold-back and tag check it +/// never went through, nor the reverse. +#[test] +fn a_state_resumes_only_in_its_own_direction() { + const N: usize = Gcm::<'static, Toy, Encrypting, 16>::SUSPENDED_STATE_LEN; + let key = gcm_key::<16>(); + + let (mut enc, nonce) = key.create_encryptor(&mut os_rng()).unwrap(); + enc.do_update_aad(b"header").unwrap(); + let state: [u8; N] = enc.suspend(); + assert!(matches!( + as SuspendableKeyed<'_, N>>::from_suspended( + state, + key.state() + ), + Err(SuspendableError::InvalidData) + )); + let enc = as SuspendableKeyed<'_, N>>::from_suspended( + state, + key.state(), + ) + .expect("its own direction"); + let _ = enc.do_encrypt_final_detached().unwrap(); + + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(b"header").unwrap(); + let state: [u8; N] = dec.suspend(); + assert!(matches!( + as SuspendableKeyed<'_, N>>::from_suspended( + state, + key.state() + ), + Err(SuspendableError::InvalidData) + )); + as SuspendableKeyed<'_, N>>::from_suspended(state, key.state()) + .expect("its own direction"); +} From 985a16a60955e667cb208c2af5ace30f167a9105 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 28 Oct 2026 01:49:15 +1100 Subject: [PATCH 23/54] ascon: the key constructor's doc no longer cites the dropped algorithm binding Assisted-by: Claude:claude-mythos-5-1 --- crypto/ascon/src/ascon_aead128.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 16a21f5a0..772aaeb36 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -650,8 +650,7 @@ impl FromKeyMaterial for AsconAEAD128Key { const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; /// # Errors - /// * [`KeyMaterialError::InvalidKeyType`] if the key is not a `SymmetricCipherKey`, or is - /// bound to another algorithm (`KeyMaterialTrait::set_algorithm`). + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not a `SymmetricCipherKey`. /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. fn from_key_material(key: &KeyMaterial) -> Result { From 4e5224063eff2ab9432ae4cd4e072fbb3b8665e0 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 28 Oct 2026 01:49:15 +1100 Subject: [PATCH 24/54] cli: the CTR doc admits --iv; the MAC commands report a bad key or tag instead of panicking A non-hex `--key`, a missing `--key-file` or a non-hex `-v` value each printed a panic from an unwrap; they now exit with the same messages the cipher commands use. Key-file bytes are still taken raw, as before. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/aes_ctr_cmd.rs | 5 +++-- cli/src/mac_cmd.rs | 36 ++++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/cli/src/aes_ctr_cmd.rs b/cli/src/aes_ctr_cmd.rs index 138eab6e5..66ac5be35 100644 --- a/cli/src/aes_ctr_cmd.rs +++ b/cli/src/aes_ctr_cmd.rs @@ -30,8 +30,9 @@ //! //! A repeated nonce is fatal here rather than merely unwise: the same nonce under the same key //! gives the same keystream, and two messages XORed with the same keystream leak their XOR. The -//! nonce is drawn from the OS-backed DRBG for exactly that reason, and there is no way to supply -//! one. +//! nonce is drawn from the OS-backed DRBG for exactly that reason. `--iv` / `--iv-file` exist for +//! reproducing published vectors and for formats that carry the nonce themselves; with them, its +//! uniqueness under the key is the caller's to keep. use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index a7386192c..66e6d3b3c 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -1,7 +1,8 @@ +use std::io; use std::io::Read; use std::process::exit; -use std::{fs, io}; +use crate::helpers::read_from_file_raw; use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; @@ -23,10 +24,13 @@ pub(crate) enum HMACVariant { /// Loads a MAC key from `--key` (hex) or `--key-file` (raw), tagged as a MAC key. fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial512 { - let key_bytes: Vec = if key.is_some() { - hex::decode(key.as_ref().unwrap()).unwrap() - } else if key_file.is_some() { - fs::read(key_file.as_ref().unwrap()).unwrap() + let key_bytes: Vec = if let Some(key) = key { + hex::decode(key).unwrap_or_else(|_| { + eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes."); + exit(-1); + }) + } else if let Some(key_file) = key_file { + read_from_file_raw(key_file) } else { eprintln!("Error: either `key` or `key-file` must be supplied."); exit(-1) @@ -36,8 +40,16 @@ fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial5 eprintln!("Error: CLI only supports MAC keys 64 bytes."); exit(-1); } - let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap(); - do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap(); + let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }); + do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap_or_else( + |e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }, + ); key } @@ -126,10 +138,10 @@ fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { crate::helpers::write_stdout(b"\n"); } else { // verify a MAC - if mac.do_verify_final(&hex::decode(verify_val.as_ref().unwrap()).unwrap()) { - exit(0) - } else { - exit(-1) - } + let expected = hex::decode(verify_val.as_ref().unwrap()).unwrap_or_else(|_| { + eprintln!("Error: the MAC to verify must be hex."); + exit(-1); + }); + if mac.do_verify_final(&expected) { exit(0) } else { exit(-1) } } } From 5ad4643044d37580e47371da5a40b080a9c8a0d3 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 28 Oct 2026 01:49:15 +1100 Subject: [PATCH 25/54] CLAUDE.md: the CI section lists the five workflows Assisted-by: Claude:claude-mythos-5-1 --- CLAUDE.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2f4f82a33..ef2b020d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -293,4 +293,16 @@ external vector suites — is specified in QUALITY_AND_STYLE.md and CONTRIBUTING ## CI -The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job — local `cargo test --workspace` is the gate, and nothing but a developer running it stands between a broken test and `main`. \ No newline at end of file +Five workflows under `.github/workflows/`, all triggered by pull requests: + +- `rust-build.yml`: `cargo build --workspace --all-targets --all-features` on stable. +- `rust-test.yml`: `cargo test --all` on stable. +- `rust-style.yml`: `cargo fmt --all --check` on **nightly** rustfmt, so format before every commit. +- `rust-docs.yml`: `cargo doc --all` on stable. +- `publish_doc_benches_to_ghpages.yaml`: builds rustdoc and runs `quality_stats.sh`; on `main` it also + publishes docs and code stats to GitHub Pages (`https://bcgit.github.io/bc-rust/`). Its bench step is + commented out because the benches are too slow on the hosted runner, so benchmark figures come from a + developer's machine, not CI. + +`cargo test --all` is the workspace run, so a failing test does block a PR; what CI does not run is +`cargo mutants`, the CLI shell tests under `cli/tests/`, or rustdoc with warnings denied. \ No newline at end of file From f25797d7ea57055a8066fe3920eb11380abe4fa6 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:32:31 +1100 Subject: [PATCH 26/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: cipher: CCM's any-length workers refuse before they copy, and the docs the rework left behind From the review of PR #172 (R1, S3, S4, S6). CcmEncryptor::do_encrypt_out and CcmDecryptor::do_decrypt_out zeroed nothing and copied the input into the caller's buffer before Ccm::do_encrypt_inplace could refuse it for AAD still owed, so a refused call left plaintext behind. Both now zero the buffer first and ask the new Ccm::accepts_payload, the check take_owed makes, before the copy; the packet pair already did. ccm_tests pins it on both sides, with the over-length case, an accepted update's zeroed tail and the empty update that must stay a no-op (the scoped mutants run on ccm.rs had the new check's mutants missed from this crate's own tests until then). The resume-direction decision d9a7ddda made for GCM is recorded on the other modes' from_suspended: their state carries no direction, so the other direction's state is accepted and gives wrong output, not an error. Docs the key-object rework made false: Ecb is a reference, not the permutation; GCM's one-shots zeroize on a failed tag check rather than verifying first (the override went in 16c34972) and GcmKeyState::new is public; CFB, CFB8 and CCM reach the permutation through the re-supplied key rather than rebuilding it; the Ccm::new / Self::new citations name a constructor that no longer exists; two orphaned "forwarded while those still exist" comments go. ecb_tests' padded round trip runs under a FixedSeedRNG with no bytes, which pins that the padded ECB path draws nothing from the RNG, the pin the should_panic tests used to give. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/cbc.rs | 3 ++ crypto/cipher/src/modes/ccm.rs | 46 ++++++++++++++---------- crypto/cipher/src/modes/cfb.rs | 8 +++-- crypto/cipher/src/modes/cfb8.rs | 5 ++- crypto/cipher/src/modes/gcm.rs | 20 +++++------ crypto/cipher/src/modes/hazmat/ecb.rs | 7 ++-- crypto/cipher/src/stream.rs | 3 ++ crypto/cipher/tests/modes/ccm_tests.rs | 49 ++++++++++++++++++++++++++ crypto/cipher/tests/modes/ecb_tests.rs | 11 +++--- 9 files changed, 113 insertions(+), 39 deletions(-) diff --git a/crypto/cipher/src/modes/cbc.rs b/crypto/cipher/src/modes/cbc.rs index f5f10e459..f9931ffc8 100644 --- a/crypto/cipher/src/modes/cbc.rs +++ b/crypto/cipher/src/modes/cbc.rs @@ -426,6 +426,9 @@ where suspend_component(&self) } + /// The state carries no direction: one suspended by the other direction's worker is accepted + /// and resumes over the same chaining value, which gives wrong output rather than an error. + /// Only GCM, whose state means different things per direction, refuses the other one. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 7e05acc31..0367cedf1 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -79,7 +79,7 @@ //! [`CcmPacketEncryptor`] and [`CcmPacketDecryptor`] implement [`SuspendableKeyed`], so a message in progress //! can be suspended to a byte array and resumed later with the re-supplied key. The state is the //! CTR half, the CBC-MAC chaining value, the payload still owed and the AAD held back so far; the -//! permutation is rebuilt from the key. The array length is each type's `SUSPENDED_STATE_LEN`; +//! permutation is reached through the re-supplied key. The array length is each type's `SUSPENDED_STATE_LEN`; //! see [the crate docs](crate#suspending-and-resuming-execution) for an example. //! //! # 🚨 Security Considerations 🚨 @@ -268,7 +268,7 @@ where /// # Errors /// [`SymmetricCipherError::StateError`] if `aad` would take the total past the declared AAD /// length -- which includes any non-empty AAD once the declared amount is complete, and so any - /// after [`Self::new`], which declares exactly the AAD it is given. Nothing is absorbed in that + /// after construction, which declares exactly the AAD it is given. Nothing is absorbed in that /// case. pub(crate) fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { if aad.len() > self.aad_owed { @@ -475,10 +475,12 @@ where } } - /// Debits `len` bytes from the payload length declared to [`Self::new`], refusing any payload - /// while declared AAD is still outstanding. + /// Whether `len` bytes of payload would be accepted now: refused while declared AAD is still + /// outstanding, or past the payload length declared at construction. Checked by the + /// `_out` workers before they copy anything into the caller's buffer, and again by + /// [`take_owed`](Self::take_owed) when the bytes are debited. #[inline] - fn take_owed(&mut self, len: usize) -> Result<(), SymmetricCipherError> { + pub(crate) fn accepts_payload(&self, len: usize) -> Result<(), SymmetricCipherError> { if len > 0 && self.aad_owed != 0 { return Err(SymmetricCipherError::StateError( "CCM was given payload before all of the declared AAD; A.2.3 puts the payload \ @@ -490,6 +492,14 @@ where "CCM was given more payload than the declared payload length, which B0 commits to", )); } + Ok(()) + } + + /// Debits `len` bytes from the payload length declared at construction, refusing any payload + /// while declared AAD is still outstanding. + #[inline] + fn take_owed(&mut self, len: usize) -> Result<(), SymmetricCipherError> { + self.accepts_payload(len)?; self.owed -= len; Ok(()) } @@ -550,7 +560,7 @@ where /// /// # Errors /// [`SymmetricCipherError::StateError`] if less payload was supplied than the length declared - /// to [`Self::new`] -- `B0` commits to that length, so a short message would produce a tag no + /// at construction -- `B0` commits to that length, so a short message would produce a tag no /// verifier could reproduce -- or less AAD than declared, for the same reason. pub(crate) fn do_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError> { if self.aad_owed != 0 { @@ -607,7 +617,7 @@ where /// # Errors /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify, and /// [`SymmetricCipherError::StateError`] if less ciphertext was supplied than the length declared - /// to [`Self::new`], or less AAD than declared. + /// at construction, or less AAD than declared. pub(crate) fn do_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { if self.aad_owed != 0 { return Err(SymmetricCipherError::StateError( @@ -698,7 +708,7 @@ where /// Table 2) and the counter field of `Ctr_i` (A.3, Table 3), which occupy the same octets. /// /// `Q_LEN <= 8`, so the low `Q_LEN` bytes of a big-endian `u64` are exactly `[x]_8q`. Nothing - /// is ever truncated in a way that matters: `Ccm::new` refuses a payload above + /// is ever truncated in a way that matters: `Ccm`'s constructors refuse a payload above /// `Ccm::MAX_PAYLOAD_LEN`, and the counter cannot pass that either, since there is one /// counter block per `BLOCK_LEN` payload bytes. #[inline] @@ -733,7 +743,7 @@ where P: BlockPermutation + ModeNames, { /// Every counter value from `next_ctr` to `2^8q - 1`. Never the binding limit in practice: - /// `Ccm::new` caps the payload at `2^8q - 1` bytes, far fewer than that many blocks. + /// `Ccm`'s constructors cap the payload at `2^8q - 1` bytes, far fewer than that many blocks. fn remaining_blocks(&self) -> u64 { Self::MAX_COUNTER - self.next_ctr + 1 } @@ -810,7 +820,7 @@ where fn check_shape() { const { // `B0` could not carry it (A.1's `p < 2^8q`), and this is the one place the length is - // known at compile time, so it is a compile error rather than `Ccm::new`'s `Err`. + // known at compile time, so it is a compile error rather than the constructor's `Err`. assert!( DATA_LEN as u64 <= Ccm::::MAX_PAYLOAD_LEN, "CCM: DATA_LEN exceeds the payload limit 2^8q - 1 that NONCE_LEN implies (A.1)" @@ -1880,14 +1890,14 @@ where plaintext: &[u8], ciphertext: &mut [u8], ) -> Result { - if plaintext.len() > self.0.owed { - return Err(SymmetricCipherError::StateError( - "CCM was given more payload than the declared payload length, which B0 commits to", - )); - } + ciphertext.fill(0); if ciphertext.len() < plaintext.len() { return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); } + // Before the copy: `Ccm::do_encrypt_inplace` would refuse this too, but only after the + // plaintext had been copied into the caller's buffer, and a refused call must not leave + // plaintext there. + self.0.accepts_payload(plaintext.len())?; let out = &mut ciphertext[..plaintext.len()]; out.copy_from_slice(plaintext); self.0.do_encrypt_inplace(out)?; @@ -1949,6 +1959,7 @@ where ciphertext: &[u8], plaintext: &mut [u8], ) -> Result { + plaintext.fill(0); let take = ciphertext.len().min(self.ccm.owed); let rest = ciphertext.len() - take; if self.tag_len + rest > TAG_LEN { @@ -1959,6 +1970,8 @@ where if plaintext.len() < take { return Err(SymmetricCipherError::OutputBufferTooSmall(take)); } + // Before the copy, as for the encryptor: a refused call must leave nothing behind. + self.ccm.accepts_payload(take)?; let out = &mut plaintext[..take]; out.copy_from_slice(&ciphertext[..take]); self.ccm.do_decrypt_inplace(out)?; @@ -2193,9 +2206,6 @@ where } } -// The key-produced worker traits for the fixed-frame pair, forwarded to the cipher traits' own -// methods while those still exist; named by path as `gcm.rs` names them. - // ---- suspending the pair ----------------------------------------------------------------- /// The adapter's suspended state is the `Ccm` state's, then the held-back AAD, its length and the diff --git a/crypto/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs index 5c05d0061..348e1d954 100644 --- a/crypto/cipher/src/modes/cfb.rs +++ b/crypto/cipher/src/modes/cfb.rs @@ -93,13 +93,14 @@ //! //! [`Cfb`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte //! array and resumed later with the re-supplied key. The state is the open segment and how much of -//! it is used; the permutation is rebuilt from the key. The array length is +//! it is used; the permutation is reached through the re-supplied key. The array length is //! `Cfb::SUSPENDED_STATE_LEN`; see [the crate docs](crate#suspending-and-resuming-execution) for an //! example. //! //! # Memory Usage //! -//! The state consists of the underlying permutation struct, one block, `buf`, and a byte count, `used`. +//! The state consists of a reference to the permutation, one block, `buf`, and a byte count, +//! `used`. //! //! # 🚨 Security Considerations 🚨 //! @@ -604,6 +605,9 @@ where suspend_component(&self) } + /// The state carries no direction: one suspended by the other direction's worker is accepted + /// and resumes over the same chaining value, which gives wrong output rather than an error. + /// Only GCM, whose state means different things per direction, refuses the other one. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } diff --git a/crypto/cipher/src/modes/cfb8.rs b/crypto/cipher/src/modes/cfb8.rs index 36517dd7c..b1797ff8e 100644 --- a/crypto/cipher/src/modes/cfb8.rs +++ b/crypto/cipher/src/modes/cfb8.rs @@ -27,7 +27,7 @@ //! //! [`Cfb8`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte //! array and resumed later with the re-supplied key. The state is the shift register; the -//! permutation is rebuilt from the key. The array length is `Cfb8::SUSPENDED_STATE_LEN`; see [the +//! permutation is reached through the re-supplied key. The array length is `Cfb8::SUSPENDED_STATE_LEN`; see [the //! crate docs](crate#suspending-and-resuming-execution) for an example. //! //! # 🚨 Security Considerations 🚨 @@ -399,6 +399,9 @@ where suspend_component(&self) } + /// The state carries no direction: one suspended by the other direction's worker is accepted + /// and resumes over the same chaining value, which gives wrong output rather than an error. + /// Only GCM, whose state means different things per direction, refuses the other one. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index e9ce4a05f..1354230b1 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -189,8 +189,8 @@ //! the end of the ciphertext has been reached, and the `do_decrypt_final` / //! `do_decrypt_final_detached` succeeds. //! -//! The one-shots (`decrypt_out`, `decrypt_detached_out`, `decrypt_with_aad_out`) verify the -//! tag first and release nothing on failure, making them more robust. +//! The one-shots (`decrypt_out`, `decrypt_detached_out`, `decrypt_with_aad_out`) zeroize their +//! output buffer when the tag check fails, so nothing is released on failure. //! //! * **GMAC is GCM with no plaintext** (Sec 5.2): [`Gmac`](crate::modes::gmac) is this type fed //! only AAD, with the plaintext-free API. @@ -285,8 +285,8 @@ where /// What a GCM key keeps per key: the keyed permutation and the hash subkey `H = CIPH_K(0^128)` /// (SP 800-38D Algorithm 4 step 1), derived once here rather than once per message. It is the -/// `KeyState` of [`GCM`] over any permutation, and is only built and consumed through the mode -/// impls. +/// `KeyState` of [`GCM`] over any permutation: built by [`new`](Self::new) or through the mode +/// impls, and consumed through them. #[derive(Clone)] pub struct GcmKeyState

{ perm: P, @@ -737,8 +737,8 @@ where } /// The AEAD view: [`AEADCipherDecryptor`] over the [`SymmetricCipherDecryptor`] impl above, with -/// `FINAL_LEN = TAG_LEN`. The one-shots are overridden, as `decrypt_out` is, to check the tag -/// before any plaintext is written. +/// `FINAL_LEN = TAG_LEN`. The one-shots are the key's provided ones, which zeroize the output on +/// a failed tag check. impl<'k, P, const TAG_LEN: usize> AEADCipherDecryptor for Gcm<'k, P, Decrypting, TAG_LEN> where @@ -844,9 +844,6 @@ where } } -// The key-produced worker traits, forwarded to the cipher traits' own methods while those still -// exist. Named by path rather than imported, since the method names coincide and an import would -// make every call in this file ambiguous. mod ghash { //! GHASH: the universal hash function GCM builds its authentication on (NIST SP 800-38D Sec 6.3, //! 6.4), and the GF(2^128) multiplication it is defined over. @@ -1108,9 +1105,8 @@ mod ghash { /// Appends `[aad_bits]_64 || [data_bits]_64` (Algorithm 4 step 5's final block) and writes /// `Y_m`, i.e. `S`, to `out` -- a `Secret`, since `S` is the tag with its mask removed. /// - /// Takes `&mut self` rather than `self` -- `Gcm`'s verify-before-decrypt one-shot needs the rest - /// of its own state (the `Ctr` field) after computing the tag, so consuming `Ghash` here would - /// force that caller to reconstruct it. Nothing asserts a "was padded" flag: the caller is + /// Takes `&mut self` rather than `self`, so that `Gcm` keeps the rest of its state around + /// the call. Nothing asserts a "was padded" flag: the caller is /// expected to have called [`pad_to_block`](Self::pad_to_block) for both the AAD and the data /// phase already (the `0^v` and `0^u` of step 5), so by the time `finish` runs there is nothing /// pending except this one final length block, and no caller should call `update` or diff --git a/crypto/cipher/src/modes/hazmat/ecb.rs b/crypto/cipher/src/modes/hazmat/ecb.rs index 08b4cd010..5e6bc1430 100644 --- a/crypto/cipher/src/modes/hazmat/ecb.rs +++ b/crypto/cipher/src/modes/hazmat/ecb.rs @@ -196,9 +196,9 @@ where /// runtime check. /// /// # State -/// Only the permutation, which owns the key schedule and is responsible for keeping it in a +/// Only a reference to the permutation, which owns the key schedule and keeps it in a /// zeroize-on-drop wrapper. Nothing chains from one block to the next, so unlike `Cbc` and `Cfb` -/// there is no block of chaining value: `size_of::>() == size_of::

()`. +/// there is no block of chaining value: an `Ecb` is the reference and nothing else. #[derive(Clone)] pub struct Ecb<'k, P, Dir, const BLOCK_LEN: usize> where @@ -325,6 +325,9 @@ where suspend_component(&self) } + /// The state carries no direction: one suspended by the other direction's worker is accepted + /// and resumes over the same chaining value, which gives wrong output rather than an error. + /// Only GCM, whose state means different things per direction, refuses the other one. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } diff --git a/crypto/cipher/src/stream.rs b/crypto/cipher/src/stream.rs index d17110979..6c102501b 100644 --- a/crypto/cipher/src/stream.rs +++ b/crypto/cipher/src/stream.rs @@ -340,6 +340,9 @@ where suspend_component(&self) } + /// The state carries no direction: one suspended by the other direction's worker is accepted + /// and resumes over the same chaining value, which gives wrong output rather than an error. + /// Only GCM, whose state means different things per direction, refuses the other one. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { resume_component(&state, key) } diff --git a/crypto/cipher/tests/modes/ccm_tests.rs b/crypto/cipher/tests/modes/ccm_tests.rs index 878f294e2..98945dc57 100644 --- a/crypto/cipher/tests/modes/ccm_tests.rs +++ b/crypto/cipher/tests/modes/ccm_tests.rs @@ -526,3 +526,52 @@ fn the_keys_enforce_the_material_policy() { framework.test::>(Toy::CCM_ALG_NAME, SecurityStrength::_128bit); framework.test::>(Toy::CCM_ALG_NAME, SecurityStrength::_128bit); } + +/// A refused `do_encrypt_out` / `do_decrypt_out` on the any-length workers leaves nothing of the +/// input in the caller's buffer, and zeroes the buffer first as every `_out` method must: payload +/// offered while declared AAD is still owed, and payload past the declared length, on both sides. +/// The refusal must come before the copy; the inner `Ccm` refuses the same calls, but only after +/// the bytes have been copied. +#[test] +fn a_refused_update_leaves_no_payload_in_the_output_buffer() { + let key = declared_key::(); + let nonce = pinned_nonce(); + let payload = [0x5Au8; 16]; + + // AAD still owed: the four bytes declared have not been supplied. An empty update is still + // a no-op, not a refusal. + let mut enc = key.create_encryptor_nonce(&nonce, 4, 16).unwrap(); + let mut out = [0xEEu8; 32]; + assert_eq!(enc.do_encrypt_out(&[], &mut out).unwrap(), 0, "an empty update is accepted"); + let mut out = [0xEEu8; 32]; + assert!(matches!( + enc.do_encrypt_out(&payload, &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + assert_eq!(out, [0u8; 32], "a refused encrypt must leave only zeros behind"); + + // Past the declared payload length. + let mut enc = key.create_encryptor_nonce(&nonce, 0, 8).unwrap(); + let mut out = [0xEEu8; 32]; + assert!(matches!( + enc.do_encrypt_out(&payload, &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + assert_eq!(out, [0u8; 32], "an over-length encrypt must leave only zeros behind"); + + // The decryptor, with AAD still owed: the sixteen bytes are within the declared payload, so + // they are not held back as a tag, and must not be copied either. + let mut dec = key.create_decryptor(&nonce, 4, 16).unwrap(); + let mut out = [0xEEu8; 32]; + assert!(matches!( + dec.do_decrypt_out(&payload, &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + assert_eq!(out, [0u8; 32], "a refused decrypt must leave only zeros behind"); + + // An accepted update still zeroes the buffer past what it wrote. + let mut enc = key.create_encryptor_nonce(&nonce, 0, 16).unwrap(); + let mut out = [0xEEu8; 32]; + assert_eq!(enc.do_encrypt_out(&payload, &mut out).unwrap(), 16); + assert_eq!(out[16..], [0u8; 16], "the bytes past the ciphertext must be zeroed"); +} diff --git a/crypto/cipher/tests/modes/ecb_tests.rs b/crypto/cipher/tests/modes/ecb_tests.rs index 9c426192d..eff627534 100644 --- a/crypto/cipher/tests/modes/ecb_tests.rs +++ b/crypto/cipher/tests/modes/ecb_tests.rs @@ -33,7 +33,7 @@ use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use common::{SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; -use toy_key::{ToyKey, os_rng}; +use toy_key::ToyKey; type ToyEcb<'k, Dir> = Ecb<'k, Toy, Dir, TOY_LEN>; type ToyEcbKey = ToyKey>; @@ -358,7 +358,9 @@ fn the_keys_enforce_the_material_policy() { /// ECB is block-aligned by contract, so arbitrary-length data goes through `bouncycastle_cipher::padding` /// like the other modes: `ECB` is the symmetric key path over the adapters, with its -/// `INIT_DATA_LEN` of 0 flowing through as an empty array and the RNG unused. +/// `INIT_DATA_LEN` of 0 flowing through as an empty array and the RNG unused. The RNG is a +/// `FixedSeedRNG` with no bytes at all, which refuses any draw, so this also pins that the padded +/// path draws nothing. #[test] fn the_padding_layer_round_trips_every_length() { type Key = ToyKey>; @@ -369,8 +371,9 @@ fn the_padding_layer_round_trips_every_length() { for len in 0..=(3 * TOY_LEN + 1) { let plaintext: Vec = (0..len).map(|i| (i * 5 + 3) as u8).collect(); let mut ciphertext = vec![0u8; Enc::encrypt_out_len(len)]; - let (written, init) = - key.encrypt_out(&mut os_rng(), &plaintext, &mut ciphertext).expect("padded encryption"); + let (written, init) = key + .encrypt_out(&mut FixedSeedRNG::<0>::new([]), &plaintext, &mut ciphertext) + .expect("padded encryption draws nothing from the RNG"); assert_eq!(init, [0u8; 0]); assert_eq!(written, ciphertext.len(), "len {len}"); let mut recovered = vec![0u8; Dec::decrypt_out_len(written)]; From e024c7a8c3c7dc45f78f31c9e07091f27407a41d Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:32:31 +1100 Subject: [PATCH 27/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: core-test-framework: the AEAD suite reaches the finals and the streaming contract the symmetric suite used to From the review of PR #172 (R2, S7). On the base, aead.rs ran the whole symmetric-cipher suite over the AEAD pair and called the array-returning detached finals; after the key-object rework an AEAD key has no symmetric role, so neither ran, and the provided do_encrypt_final_detached / do_decrypt_final_detached and the inline do_*_final_out finals were exercised by nothing in the framework. test_key now checks the array-returning detached finals against the _out ones (round trip and the wrong-tag refusal) and runs the no-AAD inline layout as the symmetric contract over the key's workers: streaming through do_*_final_out in every chunking, a refused short streaming buffer, the per-call output counts at the cuts that catch a running total, and a corrupted inline ciphertext failing at every position. A fixed-length pair runs it at its one length, zero included. The three rationale comments the rewrite dropped are back: why the ciphertext-differs check waits for eight bytes (a 1-in-256 flake otherwise), why the inline one-shot is probed into a roomy buffer and an exact one, and why the streaming cuts are placed where they are. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core-test-framework/src/aead.rs | 200 +++++++++++++++++- .../src/symmetric_ciphers.rs | 5 + 2 files changed, 204 insertions(+), 1 deletion(-) diff --git a/crypto/core-test-framework/src/aead.rs b/crypto/core-test-framework/src/aead.rs index e5b2aae48..6c9d78223 100644 --- a/crypto/core-test-framework/src/aead.rs +++ b/crypto/core-test-framework/src/aead.rs @@ -48,6 +48,11 @@ impl TestFrameworkAEADCipher { /// A key whose workers take one message length ([`fixed_message_len`](Self::fixed_message_len)) /// takes it through the one-shots too, since those are written over the workers; every other /// length is checked to be refused. + /// + /// The array-returning detached finals are checked against the `_out` ones, and the no-AAD + /// inline layout is run as the symmetric-cipher contract (the inline `do_*_final_out` finals, + /// the per-call output counts, a short streaming buffer, a corrupted ciphertext), since an + /// AEAD key cannot be handed to `TestFrameworkSymmetricCipher`. pub fn test_key< const KEY_LEN: usize, const NONCE_LEN: usize, @@ -117,6 +122,8 @@ impl TestFrameworkAEADCipher { let (ct_len, nonce, tag) = key.encrypt_detached_out(rng, aad, msg, &mut ct).unwrap(); ct.truncate(ct_len); assert_ne!(tag, [0u8; TAG_LEN], "len {len}: the tag must not be all zeros"); + // Only assert the ciphertext differs from the plaintext once there is enough of it for + // an accidental match to be negligible rather than a 1-in-256 flake. if len >= 8 { assert_ne!(&ct[..], msg, "len {len}: the ciphertext must not be the plaintext"); } @@ -157,7 +164,10 @@ impl TestFrameworkAEADCipher { .unwrap(); assert_eq!(&pt4[..pt4_len], msg, "tagged one-shot round trip, len {len}"); - // the pinned inline one-shot, into a roomy buffer and an exact one + // the pinned inline one-shot, into a roomy buffer and an exact one. The roomy buffer + // is deliberate: an implementation that sizes its work by the buffer rather than the + // message would write past the ciphertext, and the exact one catches the off-by-one + // in the other direction. let mut inline_rng = vec![0u8; encrypt_out_len(len) + 3]; let (rng_len, rng_nonce) = key.encrypt_with_aad_out(&mut fixed_rng(), aad, msg, &mut inline_rng).unwrap(); @@ -382,6 +392,41 @@ impl TestFrameworkAEADCipher { ); assert!(last.iter().all(|&b| b == 0), "a failed detached final must leave zeros"); + // the array-returning detached finals agree with the `_out` ones the chunked loop used + let (mut enc, nonce) = key.create_encryptor(&mut fixed_rng()).unwrap(); + enc.do_update_aad(aad).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(msg.len())]; + let n = enc.do_encrypt_out(msg, &mut ct).unwrap(); + ct.truncate(n); + let (last, last_len, tag) = enc.do_encrypt_final_detached().unwrap(); + ct.extend_from_slice(&last[..last_len]); + assert_eq!(ct, ct_ref, "do_encrypt_final_detached must give the one-shot ciphertext"); + assert_eq!(tag, tag_ref, "do_encrypt_final_detached must give the one-shot tag"); + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(aad).unwrap(); + let mut pt = vec![0u8; dec.do_decrypt_out_len(ct.len())]; + let n = dec.do_decrypt_out(&ct, &mut pt).unwrap(); + pt.truncate(n); + let (last, data_len) = dec.do_decrypt_final_detached(&tag).unwrap(); + pt.extend_from_slice(&last[..data_len]); + assert_eq!(pt, msg, "do_decrypt_final_detached must round trip"); + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(aad).unwrap(); + let mut pt = vec![0u8; dec.do_decrypt_out_len(ct.len())]; + dec.do_decrypt_out(&ct, &mut pt).unwrap(); + assert!( + matches!( + dec.do_decrypt_final_detached(&wrong_tag), + Err(SymmetricCipherError::AEADTagCheckFailed) + ), + "do_decrypt_final_detached must check the tag" + ); + + // With no AAD and the tag inline, an AEAD pair is a symmetric-cipher pair, and the + // provided `do_*_final_out` finals and the streaming output-buffer contract are reached + // only this way, since an AEAD key does not carry the symmetric role. + self.symmetric_contract_over_the_pair(key, rng, &encrypt_out_len, &decrypt_out_len); + // an empty AAD is a no-op: it must give exactly what absorbing no AAD at all gives, and // the inline layout with an empty AAD is the no-AAD symmetric contract let mut with_empty = vec![0u8; encrypt_detached_out_len(msg.len())]; @@ -607,6 +652,159 @@ impl TestFrameworkAEADCipher { "do_decrypt_out and do_decrypt_final_detached_out must report only their own bytes" ); } + + /// The parts of the symmetric-cipher suite an AEAD pair cannot reach through + /// `TestFrameworkSymmetricCipher::test_key` (whose bound is the symmetric role): no AAD, the + /// tag inline, over the key's `create_*`. Streaming through the inline `do_*_final_out` + /// finals in every chunking, a refused short streaming buffer, the per-call output counts + /// at the cuts that catch a running total, and a corrupted inline ciphertext failing at + /// every position. + fn symmetric_contract_over_the_pair< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + K: AeadEncryptionKey + + AeadDecryptionKey, + >( + &self, + key: &K, + rng: &mut dyn RNG, + encrypt_out_len: &dyn Fn(usize) -> usize, + decrypt_out_len: &dyn Fn(usize) -> usize, + ) { + // the pair's one length if it has one, which may be zero; chunk sizes stay positive + let len = self.fixed_message_len.unwrap_or(3 * TAG_LEN.max(1) + 5); + let msg = &DUMMY_SEED[..len]; + + // streaming in every chunking, finished through the inline `_out` finals, round trips + // and agrees with the one-shot's length + let chunkings: [usize; 6] = + [1, 3, TAG_LEN.max(1), TAG_LEN + 1, 2 * TAG_LEN + 3, len.max(1)]; + for chunk in chunkings { + let (mut enc, nonce) = key.create_encryptor(rng).unwrap(); + let mut ct = Vec::new(); + for piece in msg.chunks(chunk) { + let expect = enc.do_encrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}: do_encrypt_out_len must be exact"); + ct.extend_from_slice(&buf[..n]); + } + let mut last = [0u8; FINAL_LEN]; + let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); + assert!( + last_len <= FINAL_LEN, + "do_encrypt_final_out must not claim more than FINAL_LEN" + ); + ct.extend_from_slice(&last[..last_len]); + assert_eq!(ct.len(), encrypt_out_len(len), "chunk {chunk}: streaming total"); + + let mut dec = key.create_decryptor(&nonce); + let mut rec = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}: do_decrypt_out_len must be exact"); + rec.extend_from_slice(&buf[..n]); + } + let mut block = [0u8; FINAL_LEN]; + let data_len = dec.do_decrypt_final_out(&mut block).unwrap(); + rec.extend_from_slice(&block[..data_len]); + assert_eq!(rec, msg, "chunk {chunk}: streamed round trip through do_decrypt_final_out"); + } + + // a too-short streaming buffer is refused with the required length, before any work + let (mut enc, _) = key.create_encryptor(rng).unwrap(); + let need = enc.do_encrypt_out_len(len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match enc.do_encrypt_out(msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("do_encrypt_out into a short buffer: {other:?}"), + } + } + + // Streaming in pieces of 1 byte, FINAL_LEN + 1 bytes and the rest: a cipher that holds + // data back then releases nothing on some calls after earlier calls released data, which is + // where a running total and a per-call count differ. Each call must report its own + // `do_*_out_len`, and the per-call counts must add up to exactly the whole stream's + // length, which a running total would overshoot. The sentinel past the count must be + // zeroed by every call. + const SENTINEL: u8 = 0xA5; + const EXTRA: usize = 7; + let assert_tail_zeroed = |buf: &[u8], n: usize, what: &str| { + assert!( + n <= buf.len(), + "{what}: claims {n} bytes written to a {}-byte buffer", + buf.len() + ); + assert!( + buf[n..].iter().all(|&b| b == 0), + "{what}: the bytes past the {n} written must be zeroed" + ); + }; + let cuts = |total: usize| [0, 1.min(total), (FINAL_LEN + 2).min(total), total]; + let c = cuts(len); + let (mut enc, nonce) = key.create_encryptor(rng).unwrap(); + let mut streamed = Vec::new(); + for w in c.windows(2) { + let piece = &msg[w[0]..w[1]]; + let expect = enc.do_encrypt_out_len(piece.len()); + let mut buf = vec![SENTINEL; expect + EXTRA]; + let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "do_encrypt_out must report the bytes this call wrote"); + assert_tail_zeroed(&buf, n, "do_encrypt_out"); + streamed.extend_from_slice(&buf[..n]); + } + let mut last = [SENTINEL; FINAL_LEN]; + let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); + assert_tail_zeroed(&last, last_len, "do_encrypt_final_out"); + streamed.extend_from_slice(&last[..last_len]); + assert_eq!( + streamed.len(), + encrypt_out_len(len), + "the per-call counts must sum to encrypt_out_len, not overshoot it as running totals" + ); + let c = cuts(streamed.len()); + let mut dec = key.create_decryptor(&nonce); + let mut rec = Vec::new(); + for w in c.windows(2) { + let piece = &streamed[w[0]..w[1]]; + let expect = dec.do_decrypt_out_len(piece.len()); + let mut buf = vec![SENTINEL; expect + EXTRA]; + let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "do_decrypt_out must report the bytes this call wrote"); + assert_tail_zeroed(&buf, n, "do_decrypt_out"); + rec.extend_from_slice(&buf[..n]); + } + let mut last = [SENTINEL; FINAL_LEN]; + let data_len = dec.do_decrypt_final_out(&mut last).unwrap(); + assert_tail_zeroed(&last, data_len, "do_decrypt_final_out"); + rec.extend_from_slice(&last[..data_len]); + assert_eq!(rec, msg, "the per-call counts must sum to the plaintext"); + + // a corrupted inline ciphertext fails the tag check at every position, through the + // no-AAD one-shot + let ct_len = streamed.len(); + for flip in [0usize, ct_len / 2, ct_len.saturating_sub(1)] { + if ct_len == 0 { + break; + } + let mut bad = streamed.clone(); + bad[flip] ^= 0x80; + let mut pt = vec![0xEEu8; decrypt_out_len(ct_len)]; + match key.decrypt_with_aad_out(&nonce, &[], &bad, &mut pt) { + Err(SymmetricCipherError::AEADTagCheckFailed) => {} + other => panic!("corrupted byte {flip} must fail the tag check, got {other:?}"), + } + assert!( + pt.iter().all(|&b| b == 0), + "corrupted byte {flip}: the buffer must be zeroized" + ); + } + } } /// Instance of the test framework. diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 335c49d76..a46fccb92 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -340,6 +340,11 @@ impl TestFrameworkSymmetricCipher { assert_eq!(&pt[..n], msg, "decrypt_out into an oversized buffer"); assert_tail_zeroed(&pt, n, "decrypt_out"); + // Streaming, in pieces of 1 byte, FINAL_LEN + 1 bytes and the rest: a cipher that holds + // data back then releases nothing on some calls after earlier calls released data, which is + // where a running total and a per-call count differ. Each call must report its own + // `do_*_out_len`, and the per-call counts must add up to exactly the whole stream's + // length, which a running total would overshoot. let cuts = |total: usize| [0, 1.min(total), (FINAL_LEN + 2).min(total), total]; let c = cuts(len); let (mut enc, init_data) = key.create_encryptor(rng).unwrap(); From 8a82d76ae2ca806340a3f35d4fce0634bddf64ef Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:32:31 +1100 Subject: [PATCH 28/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: core: the block one-shots say what they check at run time, from_rng's strength cap is tested, KeyStream's docs follow the key objects From the review of PR #172 (S1, S2, S4). BlockEncryptionKey::encrypt_inplace, BlockDecryptionKey::decrypt_inplace and the two ECB one-shots said the block alignment is checked at compile time; the parameter is a slice and the bodies return DataNotBlockAligned, which their Errors sections now name. The cap cb8188f8 put on KeyMaterial::from_rng -- the RNG's strength, at most what the key length carries -- had no test asserting the strength; one does now at 16, 32 and 64 bytes from the 256-bit default DRBG. KeyStream's security note described a constructor that took the init data and implementors that own the key; a keystream is started by the key object and borrows it. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core/src/hazmat/ecb_key.rs | 14 ++++++++------ crypto/core/src/hazmat/key_stream.rs | 16 ++++++++-------- crypto/core/src/symmetric_key.rs | 16 +++++++++------- crypto/core/tests/key_material_tests.rs | 19 +++++++++++++++++++ 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/crypto/core/src/hazmat/ecb_key.rs b/crypto/core/src/hazmat/ecb_key.rs index 5a3b5b2d2..060f40a0d 100644 --- a/crypto/core/src/hazmat/ecb_key.rs +++ b/crypto/core/src/hazmat/ecb_key.rs @@ -20,11 +20,12 @@ pub trait EcbEncryptionKey { /// Begins an encryption. fn create_encryptor<'k>(&'k self) -> Self::Encryptor<'k>; - /// One-shot: encrypts `LEN` bytes, a whole number of blocks, in place, and returns the number - /// of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. + /// One-shot: encrypts `data`, which must be a whole number of blocks, in place, and returns + /// the number of bytes written. /// /// # Errors - /// The encryptor's own refusals; ECB has none. + /// [`SymmetricCipherError::DataNotBlockAligned`] if `data.len()` is not a multiple of + /// `BLOCK_LEN`; ECB itself refuses nothing. fn encrypt_inplace(&self, data: &mut [u8]) -> Result { let (blocks, []) = data.as_chunks_mut::() else { return Err(SymmetricCipherError::DataNotBlockAligned); @@ -43,11 +44,12 @@ pub trait EcbDecryptionKey { /// Begins a decryption. fn create_decryptor<'k>(&'k self) -> Self::Decryptor<'k>; - /// One-shot: decrypts `LEN` bytes, a whole number of blocks, in place, and returns the number - /// of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. + /// One-shot: decrypts `data`, which must be a whole number of blocks, in place, and returns + /// the number of bytes written. /// /// # Errors - /// The decryptor's own refusals; ECB has none. + /// [`SymmetricCipherError::DataNotBlockAligned`] if `data.len()` is not a multiple of + /// `BLOCK_LEN`; ECB itself refuses nothing. fn decrypt_inplace(&self, data: &mut [u8]) -> Result { let (blocks, []) = data.as_chunks_mut::() else { return Err(SymmetricCipherError::DataNotBlockAligned); diff --git a/crypto/core/src/hazmat/key_stream.rs b/crypto/core/src/hazmat/key_stream.rs index 3566bb0d3..e689868e5 100644 --- a/crypto/core/src/hazmat/key_stream.rs +++ b/crypto/core/src/hazmat/key_stream.rs @@ -25,15 +25,15 @@ use crate::traits::{BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEnc /// keystream block is the encryption of the last ciphertext block. /// /// # 🚨 Security Considerations 🚨 -/// A keystream's constructor takes the init data from the caller, so nothing stops a caller -/// reusing a nonce under a key -- which repeats the keystream and reveals the XOR of the two plaintexts -- -/// and nothing stops it running past [`KeyStream::remaining_blocks`]. `StreamCipher` generates -/// the init data and enforces the limit; use it. See the [module docs](crate::hazmat) for the -/// supported uses of the raw trait. +/// A keystream is started at init data the caller chose, so nothing here stops a caller reusing +/// a nonce under a key -- which repeats the keystream and reveals the XOR of the two plaintexts +/// -- and nothing stops it running past [`KeyStream::remaining_blocks`]. A key object's +/// `create_encryptor` draws the init data and `StreamCipher` enforces the limit; use those. See +/// the [module docs](crate::hazmat) for the supported uses of the raw trait. /// -/// Implementors hold the key in a zeroize-on-drop wrapper, as for [`ElectronicCodeBook`]. Any -/// keystream they produce into scratch space of their own is live key material until it has been -/// XORed in, and gets the same treatment. +/// Implementors borrow the key, which keeps its schedule in a zeroize-on-drop wrapper as for +/// [`ElectronicCodeBook`]. Any keystream they produce into scratch space of their own is live +/// key material until it has been XORed in, and gets the same treatment. pub trait KeyStream: Algorithm + Sized { /// How many more keystream blocks this value can produce before its keystream would repeat. /// A keystream with no practical limit returns `u64::MAX`. diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index 9ae7a7f59..af4236752 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -1064,12 +1064,13 @@ pub trait BlockEncryptionKey< rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// One-shot: encrypts `LEN` bytes, a whole number of blocks, in place under init data drawn - /// from `rng`, and returns the number of bytes written with the init data. `LEN % BLOCK_LEN - /// == 0` is checked at compile time, as for [`BlockCipherEncryptor::do_encrypt_blocks_inplace`]. + /// One-shot: encrypts `data`, which must be a whole number of blocks, in place under init + /// data drawn from `rng`, and returns the number of bytes written with the init data. /// /// # Errors - /// As [`create_encryptor`](Self::create_encryptor), or the encryptor's own refusals. + /// [`SymmetricCipherError::DataNotBlockAligned`] if `data.len()` is not a multiple of + /// `BLOCK_LEN`; otherwise as [`create_encryptor`](Self::create_encryptor), or the encryptor's + /// own refusals. fn encrypt_inplace( &self, rng: &mut dyn RNG, @@ -1102,11 +1103,12 @@ pub trait BlockDecryptionKey< /// Begins the decryption of a message whose encryptor returned `init_data`. fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k>; - /// One-shot: decrypts `LEN` bytes, a whole number of blocks, in place under `init_data`, and - /// returns the number of bytes written. `LEN % BLOCK_LEN == 0` is checked at compile time. + /// One-shot: decrypts `data`, which must be a whole number of blocks, in place under + /// `init_data`, and returns the number of bytes written. /// /// # Errors - /// The decryptor's own refusals. + /// [`SymmetricCipherError::DataNotBlockAligned`] if `data.len()` is not a multiple of + /// `BLOCK_LEN`; otherwise the decryptor's own refusals. fn decrypt_inplace( &self, init_data: &[u8; INIT_DATA_LEN], diff --git a/crypto/core/tests/key_material_tests.rs b/crypto/core/tests/key_material_tests.rs index bca2f318d..dea104962 100644 --- a/crypto/core/tests/key_material_tests.rs +++ b/crypto/core/tests/key_material_tests.rs @@ -7,6 +7,7 @@ mod test_key_material { KeyMaterialTrait, KeyType, }; use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::RNG; const DUMMY_KEY: &[u8; 64] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\ \x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\ @@ -214,6 +215,24 @@ mod test_key_material { assert_eq!(key.key_type(), KeyType::CryptographicRandom); } + /// The material carries the RNG's strength, capped at what the key length can carry: the + /// default DRBG is 256-bit, so 32 bytes keep that and 16 bytes are a 128-bit key. + #[test] + fn from_rng_caps_the_strength_at_the_key_length() { + use bouncycastle_rng as rng; + + let mut drbg = rng::DefaultRNG::default(); + let rng_strength = drbg.security_strength(); + assert!(rng_strength >= SecurityStrength::_256bit, "the default DRBG is 256-bit"); + + let key = KeyMaterial256::from_rng(&mut drbg).unwrap(); + assert_eq!(key.security_strength(), rng_strength, "32 bytes carry the RNG's strength"); + let key = KeyMaterial128::from_rng(&mut drbg).unwrap(); + assert_eq!(key.security_strength(), SecurityStrength::_128bit, "16 bytes cap it"); + let key = KeyMaterial512::from_rng(&mut drbg).unwrap(); + assert_eq!(key.security_strength(), rng_strength, "64 bytes keep the RNG's strength"); + } + #[test] fn zeroize() { let mut key = KeyMaterial256::from_bytes(&DUMMY_KEY[..32]).unwrap(); From 84e0400c5660f6faa127531a394273a2651ceeb8 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:32:31 +1100 Subject: [PATCH 29/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: hmac, hkdf: the two halves of an HMAC computation move under hmac::hazmat From the review of PR #172 (R4, S2). HmacKey::inner_state and finish_out were pub behind doc(hidden), which is not a visibility; a primitive whose safe use depends on the caller composing it correctly belongs under hazmat. They are crate-private on the key now and reached through bouncycastle_hmac::hazmat's inner_state and finish_out, with the security note saying what the pair does not check. HKDF's extract phase, their one caller, uses those. HmacKey::generate's doc says the drawn material's strength is capped at what the output length carries, and the key test pins that rule on the material it is shared with (a key object has no strength accessor, so the cap is not observable through the key itself). Assisted-by: Claude:claude-mythos-5-1 --- crypto/hkdf/src/lib.rs | 8 +++--- crypto/hmac/src/hazmat.rs | 39 +++++++++++++++++++++++++++++ crypto/hmac/src/lib.rs | 16 ++++++------ crypto/hmac/tests/hmac_key_tests.rs | 18 ++++++++++++- 4 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 crypto/hmac/src/hazmat.rs diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index d722f0b05..017b5bf45 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -115,6 +115,7 @@ use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; use bouncycastle_core::traits::{Hash, HashAlgParams, KDF, MAC, Suspendable, SuspendableKeyed}; use bouncycastle_hmac::HmacKey; +use bouncycastle_hmac::hazmat as hmac_hazmat; use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_utils::{max, min}; use std::marker::PhantomData; @@ -172,7 +173,7 @@ pub struct HKDF< /// The extract phase's HMAC, held as its two halves: the salt key and the inner hash absorbing /// the IKM. An `HMAC` worker borrows its key, and a struct cannot borrow from itself, so the -/// phase is run through [`HmacKey::inner_state`] and [`HmacKey::finish_out`] instead. +/// phase is run through `bouncycastle_hmac::hazmat`'s `inner_state` and `finish_out` instead. #[derive(Clone)] struct HkdfExtract { key: HmacKey, @@ -503,7 +504,7 @@ impl::from_key_material_allow_weak(salt)?; - let inner = key.inner_state(); + let inner = hmac_hazmat::inner_state(&key); self.hmac = Some(HkdfExtract { key, inner }); let additional_entropy = self.entropy.credit_entropy(salt); @@ -590,8 +591,7 @@ impl(key: &HmacKey) -> HASH { + key.inner_state() +} + +/// RFC 2104 Section 2 steps (4), (6) and (7) over an inner hash that has absorbed the text: +/// finishes it, feeds the result to the key's outer pad state, and finishes that into `out`, +/// which is zeroized first. Returns the number of bytes written. +/// +/// # Errors +/// [`MACError::InvalidLength`] if `out` is shorter than [`MIN_FIPS_DIGEST_LEN`](crate::MIN_FIPS_DIGEST_LEN). +pub fn finish_out( + key: &HmacKey, + inner: HASH, + out: &mut [u8], +) -> Result { + key.finish_out(inner, out) +} diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index 68c057c4f..85275690e 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -122,6 +122,8 @@ #![forbid(unsafe_code)] #![forbid(missing_docs)] +pub mod hazmat; + use bouncycastle_core::errors::{KeyMaterialError, MACError, RNGError, SuspendableError}; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; @@ -291,7 +293,8 @@ impl FromAnyKeyMaterial for HmacKey { } /// Draws a key of the hash's output length (RFC 2104 Section 3's recommended minimum) from - /// `rng`, tagged [`KeyType::MACKey`] at the RNG's strength, and builds the key from it. + /// `rng`, tagged [`KeyType::MACKey`] at the RNG's strength capped at what that length can + /// carry, and builds the key from it. /// /// # Errors /// [`MACError::KeyMaterialError`] if the RNG fails, or with @@ -316,11 +319,9 @@ impl FromAnyKeyMaterial for HmacKey { impl HmacKey { /// `H` after absorbing `K XOR ipad`, ready for the text: the start of RFC 2104 Section 2 - /// step (4). What a worker's updates feed; `finish_out` is its other half. Hidden: the two - /// exist for HKDF's extract phase, which cannot hold a worker that borrows a key it owns, and - /// are not part of the key's documented API. - #[doc(hidden)] - pub fn inner_state(&self) -> HASH { + /// step (4). What a worker's updates feed; `finish_out` is its other half. The pair is + /// reachable outside the crate only through [`hazmat`]. + pub(crate) fn inner_state(&self) -> HASH { self.inner.clone() } @@ -330,8 +331,7 @@ impl HmacKey { /// /// # Errors /// [`MACError::InvalidLength`] if `out` is shorter than [`MIN_FIPS_DIGEST_LEN`]. - #[doc(hidden)] - pub fn finish_out(&self, inner: HASH, out: &mut [u8]) -> Result { + pub(crate) fn finish_out(&self, inner: HASH, out: &mut [u8]) -> Result { out.fill(0); if out.len() < MIN_FIPS_DIGEST_LEN { return Err(MACError::InvalidLength( diff --git a/crypto/hmac/tests/hmac_key_tests.rs b/crypto/hmac/tests/hmac_key_tests.rs index 4a42e0413..c654c34ba 100644 --- a/crypto/hmac/tests/hmac_key_tests.rs +++ b/crypto/hmac/tests/hmac_key_tests.rs @@ -5,7 +5,7 @@ use bouncycastle_core::errors::{KeyMaterialError, MACError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; -use bouncycastle_core::traits::{Hash, MAC, SuspendableKeyed}; +use bouncycastle_core::traits::{Hash, MAC, RNG, SuspendableKeyed}; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hmac::{HMAC, HmacKey}; use bouncycastle_sha2::{SHA256, SHA512, SUSPENDED_SHA256_STATE_LEN}; @@ -152,6 +152,22 @@ fn generate_draws_a_key_of_the_hash_output_length_at_the_rng_strength() { km.set_key_type(KeyType::MACKey).unwrap(); assert_eq!(HmacKey::::from_key_material(&km).unwrap().mac(MSG), tag); + // The drawn material's strength is the RNG's, capped at what the hash's output length can + // carry, by the rule `KeyMaterial::from_rng` applies: a key object has no strength accessor, + // so the rule is pinned on the material. 32 bytes from a 256-bit RNG keep 256 bits; 28 + // bytes, HMAC-SHA224's draw, carry 224 bits, which the strength scale rounds down to 192. + let mut rng3 = FixedSeedRNG::<32>::new([0x5Au8; 32]); + assert_eq!(rng3.security_strength(), SecurityStrength::_256bit); + assert_eq!( + KeyMaterial::<32>::from_rng(&mut rng3).unwrap().security_strength(), + SecurityStrength::_256bit + ); + let mut rng4 = FixedSeedRNG::<28>::new([0x5Au8; 28]); + assert_eq!( + KeyMaterial::<28>::from_rng(&mut rng4).unwrap().security_strength(), + SecurityStrength::_192bit + ); + // An RNG below the HMAC's strength is refused, through the material's strength tag. let mut weak_rng = FixedSeedRNG::<32>::new([0x5Au8; 32]); weak_rng.set_security_strength(SecurityStrength::_128bit); From 3a040e9cf5e7f527a94d5d0b738182a89bda197b Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:32:31 +1100 Subject: [PATCH 30/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: sha2: the HKDF Memory Usage table is measured after the rework From the review of PR #172 (R3). HKDF holds the extract phase as an HmacKey, both pad states, plus the inner hash, so the objects grew: size_of gives 368, 656 and 656 bytes for HKDF_SHA256, HKDF_SHA384 and HKDF_SHA512 where the table still said 256, 448 and 448. Assisted-by: Claude:claude-mythos-5-1 --- crypto/sha2/src/hkdf.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crypto/sha2/src/hkdf.rs b/crypto/sha2/src/hkdf.rs index 7d73b596a..8d9e13236 100644 --- a/crypto/sha2/src/hkdf.rs +++ b/crypto/sha2/src/hkdf.rs @@ -186,9 +186,9 @@ //! //! | Object | Size (bytes) | //! |---------------------------------------------------|--------------| -//! | `HKDF_SHA256` | 256 | -//! | `HKDF_SHA384` | 448 | -//! | `HKDF_SHA512` | 448 | +//! | `HKDF_SHA256` | 368 | +//! | `HKDF_SHA384` | 656 | +//! | `HKDF_SHA512` | 656 | //! | Suspended `HKDF_SHA256` state | 122 | //! | Suspended `HKDF_SHA384` state | 218 | //! | Suspended `HKDF_SHA512` state | 218 | From 88557011d97f0819a255f4844cce0de3372d1a7b Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:32:31 +1100 Subject: [PATCH 31/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: sha3: KMAC's generate doc says the drawn strength is capped at the key length From the review of PR #172 (S2): the doc still said "at the RNG's strength". Assisted-by: Claude:claude-mythos-5-1 --- crypto/sha3/src/kmac.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index 2420eb710..9e0e12a2d 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -203,8 +203,9 @@ impl FromAnyKeyMaterial for KMACKeyInternal { } /// Draws a key of the security strength's length (Sec 8.4.1: 16 bytes for KMAC128, 32 for - /// KMAC256) from `rng`, tagged [`KeyType::MACKey`] at the RNG's strength, and builds the key - /// from it with no customization and the nominal output length. + /// KMAC256) from `rng`, tagged [`KeyType::MACKey`] at the RNG's strength capped at what that + /// length can carry, and builds the key from it with no customization and the nominal + /// output length. /// /// # Errors /// [`MACError::KeyMaterialError`] if the RNG fails, or with From b360f07b45d0162404255f14374907eb9b2755ca Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:32:31 +1100 Subject: [PATCH 32/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: factory: the MAC worker enum's forwarding is written out, not a macro From the review of PR #172 (R5). c13546e2 introduced a forward! macro for the nine MAC methods of MACFactoryWorker with no justification against the Macros rule; the base was unrolled, as HashFactory still is. The thirteen-arm matches are written out per method. Assisted-by: Claude:claude-mythos-5-1 --- crypto/factory/src/mac_factory.rs | 165 ++++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 30 deletions(-) diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index 4e5c84a8e..47261bab5 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -255,63 +255,168 @@ impl MacVerificationKey for MACFactory { } } -/// Every method is the named worker's own. -macro_rules! forward { - ($self:expr, $w:ident => $e:expr) => { - match $self { - MACFactoryWorker::KMAC128($w) => $e, - MACFactoryWorker::KMAC256($w) => $e, - MACFactoryWorker::HMAC_SHA224($w) => $e, - MACFactoryWorker::HMAC_SHA256($w) => $e, - MACFactoryWorker::HMAC_SHA384($w) => $e, - MACFactoryWorker::HMAC_SHA512($w) => $e, - MACFactoryWorker::HMAC_SHA512_224($w) => $e, - MACFactoryWorker::HMAC_SHA512_256($w) => $e, - MACFactoryWorker::HMAC_SHA3_224($w) => $e, - MACFactoryWorker::HMAC_SHA3_256($w) => $e, - MACFactoryWorker::HMAC_SHA3_384($w) => $e, - MACFactoryWorker::HMAC_SHA3_512($w) => $e, - MACFactoryWorker::HMAC_SM3($w) => $e, - } - }; -} - impl MAC for MACFactoryWorker<'_> { fn output_len(&self) -> usize { - forward!(self, h => h.output_len()) + match self { + Self::KMAC128(h) => h.output_len(), + Self::KMAC256(h) => h.output_len(), + Self::HMAC_SHA224(h) => h.output_len(), + Self::HMAC_SHA256(h) => h.output_len(), + Self::HMAC_SHA384(h) => h.output_len(), + Self::HMAC_SHA512(h) => h.output_len(), + Self::HMAC_SHA512_224(h) => h.output_len(), + Self::HMAC_SHA512_256(h) => h.output_len(), + Self::HMAC_SHA3_224(h) => h.output_len(), + Self::HMAC_SHA3_256(h) => h.output_len(), + Self::HMAC_SHA3_384(h) => h.output_len(), + Self::HMAC_SHA3_512(h) => h.output_len(), + Self::HMAC_SM3(h) => h.output_len(), + } } fn mac(self, data: &[u8]) -> Vec { - forward!(self, h => h.mac(data)) + match self { + Self::KMAC128(h) => h.mac(data), + Self::KMAC256(h) => h.mac(data), + Self::HMAC_SHA224(h) => h.mac(data), + Self::HMAC_SHA256(h) => h.mac(data), + Self::HMAC_SHA384(h) => h.mac(data), + Self::HMAC_SHA512(h) => h.mac(data), + Self::HMAC_SHA512_224(h) => h.mac(data), + Self::HMAC_SHA512_256(h) => h.mac(data), + Self::HMAC_SHA3_224(h) => h.mac(data), + Self::HMAC_SHA3_256(h) => h.mac(data), + Self::HMAC_SHA3_384(h) => h.mac(data), + Self::HMAC_SHA3_512(h) => h.mac(data), + Self::HMAC_SM3(h) => h.mac(data), + } } fn mac_out(self, data: &[u8], out: &mut [u8]) -> Result { out.fill(0); - forward!(self, h => h.mac_out(data, out)) + match self { + Self::KMAC128(h) => h.mac_out(data, out), + Self::KMAC256(h) => h.mac_out(data, out), + Self::HMAC_SHA224(h) => h.mac_out(data, out), + Self::HMAC_SHA256(h) => h.mac_out(data, out), + Self::HMAC_SHA384(h) => h.mac_out(data, out), + Self::HMAC_SHA512(h) => h.mac_out(data, out), + Self::HMAC_SHA512_224(h) => h.mac_out(data, out), + Self::HMAC_SHA512_256(h) => h.mac_out(data, out), + Self::HMAC_SHA3_224(h) => h.mac_out(data, out), + Self::HMAC_SHA3_256(h) => h.mac_out(data, out), + Self::HMAC_SHA3_384(h) => h.mac_out(data, out), + Self::HMAC_SHA3_512(h) => h.mac_out(data, out), + Self::HMAC_SM3(h) => h.mac_out(data, out), + } } fn verify(self, data: &[u8], mac: &[u8]) -> bool { - forward!(self, h => h.verify(data, mac)) + match self { + Self::KMAC128(h) => h.verify(data, mac), + Self::KMAC256(h) => h.verify(data, mac), + Self::HMAC_SHA224(h) => h.verify(data, mac), + Self::HMAC_SHA256(h) => h.verify(data, mac), + Self::HMAC_SHA384(h) => h.verify(data, mac), + Self::HMAC_SHA512(h) => h.verify(data, mac), + Self::HMAC_SHA512_224(h) => h.verify(data, mac), + Self::HMAC_SHA512_256(h) => h.verify(data, mac), + Self::HMAC_SHA3_224(h) => h.verify(data, mac), + Self::HMAC_SHA3_256(h) => h.verify(data, mac), + Self::HMAC_SHA3_384(h) => h.verify(data, mac), + Self::HMAC_SHA3_512(h) => h.verify(data, mac), + Self::HMAC_SM3(h) => h.verify(data, mac), + } } fn do_update(&mut self, data: &[u8]) { - forward!(self, h => h.do_update(data)) + match self { + Self::KMAC128(h) => h.do_update(data), + Self::KMAC256(h) => h.do_update(data), + Self::HMAC_SHA224(h) => h.do_update(data), + Self::HMAC_SHA256(h) => h.do_update(data), + Self::HMAC_SHA384(h) => h.do_update(data), + Self::HMAC_SHA512(h) => h.do_update(data), + Self::HMAC_SHA512_224(h) => h.do_update(data), + Self::HMAC_SHA512_256(h) => h.do_update(data), + Self::HMAC_SHA3_224(h) => h.do_update(data), + Self::HMAC_SHA3_256(h) => h.do_update(data), + Self::HMAC_SHA3_384(h) => h.do_update(data), + Self::HMAC_SHA3_512(h) => h.do_update(data), + Self::HMAC_SM3(h) => h.do_update(data), + } } fn do_final(self) -> Vec { - forward!(self, h => h.do_final()) + match self { + Self::KMAC128(h) => h.do_final(), + Self::KMAC256(h) => h.do_final(), + Self::HMAC_SHA224(h) => h.do_final(), + Self::HMAC_SHA256(h) => h.do_final(), + Self::HMAC_SHA384(h) => h.do_final(), + Self::HMAC_SHA512(h) => h.do_final(), + Self::HMAC_SHA512_224(h) => h.do_final(), + Self::HMAC_SHA512_256(h) => h.do_final(), + Self::HMAC_SHA3_224(h) => h.do_final(), + Self::HMAC_SHA3_256(h) => h.do_final(), + Self::HMAC_SHA3_384(h) => h.do_final(), + Self::HMAC_SHA3_512(h) => h.do_final(), + Self::HMAC_SM3(h) => h.do_final(), + } } fn do_final_out(self, out: &mut [u8]) -> Result { out.fill(0); - forward!(self, h => h.do_final_out(out)) + match self { + Self::KMAC128(h) => h.do_final_out(out), + Self::KMAC256(h) => h.do_final_out(out), + Self::HMAC_SHA224(h) => h.do_final_out(out), + Self::HMAC_SHA256(h) => h.do_final_out(out), + Self::HMAC_SHA384(h) => h.do_final_out(out), + Self::HMAC_SHA512(h) => h.do_final_out(out), + Self::HMAC_SHA512_224(h) => h.do_final_out(out), + Self::HMAC_SHA512_256(h) => h.do_final_out(out), + Self::HMAC_SHA3_224(h) => h.do_final_out(out), + Self::HMAC_SHA3_256(h) => h.do_final_out(out), + Self::HMAC_SHA3_384(h) => h.do_final_out(out), + Self::HMAC_SHA3_512(h) => h.do_final_out(out), + Self::HMAC_SM3(h) => h.do_final_out(out), + } } fn do_verify_final(self, mac: &[u8]) -> bool { - forward!(self, h => h.do_verify_final(mac)) + match self { + Self::KMAC128(h) => h.do_verify_final(mac), + Self::KMAC256(h) => h.do_verify_final(mac), + Self::HMAC_SHA224(h) => h.do_verify_final(mac), + Self::HMAC_SHA256(h) => h.do_verify_final(mac), + Self::HMAC_SHA384(h) => h.do_verify_final(mac), + Self::HMAC_SHA512(h) => h.do_verify_final(mac), + Self::HMAC_SHA512_224(h) => h.do_verify_final(mac), + Self::HMAC_SHA512_256(h) => h.do_verify_final(mac), + Self::HMAC_SHA3_224(h) => h.do_verify_final(mac), + Self::HMAC_SHA3_256(h) => h.do_verify_final(mac), + Self::HMAC_SHA3_384(h) => h.do_verify_final(mac), + Self::HMAC_SHA3_512(h) => h.do_verify_final(mac), + Self::HMAC_SM3(h) => h.do_verify_final(mac), + } } fn max_security_strength(&self) -> SecurityStrength { - forward!(self, h => h.max_security_strength()) + match self { + Self::KMAC128(h) => h.max_security_strength(), + Self::KMAC256(h) => h.max_security_strength(), + Self::HMAC_SHA224(h) => h.max_security_strength(), + Self::HMAC_SHA256(h) => h.max_security_strength(), + Self::HMAC_SHA384(h) => h.max_security_strength(), + Self::HMAC_SHA512(h) => h.max_security_strength(), + Self::HMAC_SHA512_224(h) => h.max_security_strength(), + Self::HMAC_SHA512_256(h) => h.max_security_strength(), + Self::HMAC_SHA3_224(h) => h.max_security_strength(), + Self::HMAC_SHA3_256(h) => h.max_security_strength(), + Self::HMAC_SHA3_384(h) => h.max_security_strength(), + Self::HMAC_SHA3_512(h) => h.max_security_strength(), + Self::HMAC_SM3(h) => h.max_security_strength(), + } } } From 437b53ba2c54233620d447e3df2596d283703df2 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:32:32 +1100 Subject: [PATCH 33/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: aes: the CCM refusal pin, ECB through the framework, and the modes bench says what it measures From the review of PR #172 (R1, S5, S6). ccm_key_tests pins that a refused do_encrypt_out / do_decrypt_out on the any-length CCM workers leaves only zeros in the caller's buffer, over AES as well as the cipher crate's toy. mode_key_tests runs the symmetric suite over ECB at both key lengths, which reached no framework run. The modes bench named create_encryptor "(schedule clone + IV draw)" and create_decryptor "(schedule clone)" when nothing is cloned any more: the names and the bench_init doc say borrow; the decrypt comment names decrypt_4blocks, which is what the N=8 bench uses; two stray "controlled comparison" lines go; and the four single-element for loops become the bench_function calls they wrapped. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/benches/aes_modes_benches.rs | 125 +++++++++++------------- crypto/aes/tests/ccm_key_tests.rs | 40 ++++++++ crypto/aes/tests/mode_key_tests.rs | 2 + 3 files changed, 97 insertions(+), 70 deletions(-) diff --git a/crypto/aes/benches/aes_modes_benches.rs b/crypto/aes/benches/aes_modes_benches.rs index 099ef1d6f..86abc1220 100644 --- a/crypto/aes/benches/aes_modes_benches.rs +++ b/crypto/aes/benches/aes_modes_benches.rs @@ -117,7 +117,7 @@ fn bench_aes128cbc(c: &mut Criterion) { ) }); - // ---- decryption: parallel, uses decrypt_2blocks for every pair ---- + // ---- decryption: parallel, uses decrypt_4blocks for every four ---- let (mut enc, iv) = key.create_encryptor(&mut rng).unwrap(); let mut ciphertext = blocks.clone(); for chunk in ciphertext.chunks_exact_mut(8) { @@ -138,8 +138,6 @@ fn bench_aes128cbc(c: &mut Criterion) { ) }); - // The controlled comparison: identical N, identical cipher, bit-sliced pair vs single-block loops. - // This pair of numbers -- and only this pair -- measures what `decrypt_2blocks` buys. group.finish(); } @@ -178,20 +176,17 @@ fn bench_cfb_aes128(c: &mut Criterion) { group.throughput(Throughput::Bytes(DATA_LEN as u64)); // ---- encryption: serial. Oj+1 = CIPH_K(Cj), and Cj is the previous call's output ---- - for (name, call_len) in [("16KiB encrypt -- N=8", 8 * BLOCK_LEN)] { - group.bench_function(name, |b| { - b.iter_batched( - || flat.clone(), - |mut scratch| { - let (enc, _): (Aes128Cfb, _) = - key.create_encryptor(&mut rng).unwrap(); - stream_encrypt_in_calls(enc, &mut scratch, call_len); - black_box(&scratch); - }, - BatchSize::LargeInput, - ) - }); - } + group.bench_function("16KiB encrypt -- N=8", |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + let (enc, _): (Aes128Cfb, _) = key.create_encryptor(&mut rng).unwrap(); + stream_encrypt_in_calls(enc, &mut scratch, 8 * BLOCK_LEN); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); // ---- decryption: parallel, and uses `encrypt_4blocks` / `encrypt_2blocks` -- the FORWARD // batch methods ---- @@ -199,19 +194,17 @@ fn bench_cfb_aes128(c: &mut Criterion) { let mut ciphertext = flat.clone(); enc.do_encrypt_inplace(&mut ciphertext).unwrap(); - for (name, call_len) in [("16KiB decrypt -- N=8 (all fours)", 8 * BLOCK_LEN)] { - group.bench_function(name, |b| { - b.iter_batched( - || ciphertext.clone(), - |mut scratch| { - let dec: Aes128Cfb = key.create_decryptor(&iv); - stream_decrypt_in_calls(dec, &mut scratch, call_len); - black_box(&scratch); - }, - BatchSize::LargeInput, - ) - }); - } + group.bench_function("16KiB decrypt -- N=8 (all fours)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let dec: Aes128Cfb = key.create_decryptor(&iv); + stream_decrypt_in_calls(dec, &mut scratch, 8 * BLOCK_LEN); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); group.finish(); } @@ -246,22 +239,18 @@ fn bench_cfb8_aes128(c: &mut Criterion) { let mut ciphertext = flat.clone(); enc.do_encrypt_inplace(&mut ciphertext).unwrap(); - for (name, call_len) in [ - // One call: fours, then pairs, then the tail. This is the batched path. - ("16KiB decrypt -- whole message in one call (batched)", DATA_LEN), - ] { - group.bench_function(name, |b| { - b.iter_batched( - || ciphertext.clone(), - |mut scratch| { - let dec: Aes128Cfb8 = key.create_decryptor(&iv); - stream_decrypt_in_calls(dec, &mut scratch, call_len); - black_box(&scratch); - }, - BatchSize::LargeInput, - ) - }); - } + // One call: fours, then pairs, then the tail. This is the batched path. + group.bench_function("16KiB decrypt -- whole message in one call (batched)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let dec: Aes128Cfb8 = key.create_decryptor(&iv); + stream_decrypt_in_calls(dec, &mut scratch, DATA_LEN); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); group.finish(); } @@ -277,20 +266,17 @@ fn bench_ctr_aes128(c: &mut Criterion) { let mut group = c.benchmark_group("modes::ctr::AES_128"); group.throughput(Throughput::Bytes(DATA_LEN as u64)); - for (name, call_len) in [("16KiB encrypt -- N=8 (two fours per call)", 8 * BLOCK_LEN)] { - group.bench_function(name, |b| { - b.iter_batched( - || flat.clone(), - |mut scratch| { - let (enc, _): (Aes128Ctr, _) = - key.create_encryptor(&mut rng).unwrap(); - stream_encrypt_in_calls(enc, &mut scratch, call_len); - black_box(&scratch); - }, - BatchSize::LargeInput, - ) - }); - } + group.bench_function("16KiB encrypt -- N=8 (two fours per call)", |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + let (enc, _): (Aes128Ctr, _) = key.create_encryptor(&mut rng).unwrap(); + stream_encrypt_in_calls(enc, &mut scratch, 8 * BLOCK_LEN); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); group.finish(); } @@ -318,15 +304,14 @@ fn bench_ecb_aes128(c: &mut Criterion) { ) }); - // The controlled comparison: identical N, identical cipher, bit-sliced pair vs single-block loops. group.finish(); } /// What a message pays before its first byte, split as the key path splits it: `from_key_material` -/// is the key expansion, paid once per key; `create_encryptor` is a clone of the stored schedule -/// plus an IV draw, and `create_decryptor` the clone alone, paid per message. The IV comes from -/// the cheap deterministic RNG, so the draw's cost is the copy and not a DRBG. Worth its own -/// measurement, because for short messages construction dominates. +/// is the key expansion, paid once per key; `create_encryptor` borrows the key and draws an IV, +/// and `create_decryptor` only borrows it, paid per message. The IV comes from the cheap +/// deterministic RNG, so the draw's cost is the copy and not a DRBG. Worth its own measurement, +/// because for short messages construction dominates. fn bench_init(c: &mut Criterion) { let k128 = key::<16>(); let k256 = key::<32>(); @@ -343,23 +328,23 @@ fn bench_init(c: &mut Criterion) { group.bench_function("AES_256 from_key_material, CBC (key schedule)", |b| { b.iter(|| black_box(AES256Key::>::from_key_material(black_box(&k256)).unwrap())) }); - group.bench_function("AES_128 create_encryptor, CBC (schedule clone + IV draw)", |b| { + group.bench_function("AES_128 create_encryptor, CBC (borrow + IV draw)", |b| { b.iter(|| black_box(black_box(&cbc128).create_encryptor(&mut rng).unwrap().1)) }); - group.bench_function("AES_128 create_decryptor, CBC (schedule clone)", |b| { + group.bench_function("AES_128 create_decryptor, CBC (borrow)", |b| { b.iter(|| black_box(black_box(&cbc128).create_decryptor(&iv))) }); - // CFB does exactly the same work here -- one key expansion per key, one clone plus an IV + // CFB does exactly the same work here -- one key expansion per key, one borrow plus an IV // draw per encryptor -- so these should match the CBC numbers. A divergence would mean one // mode is doing something extra at construction time. group.bench_function("AES_128 from_key_material, CFB (key schedule)", |b| { b.iter(|| black_box(AES128Key::::from_key_material(black_box(&k128)).unwrap())) }); - group.bench_function("AES_128 create_encryptor, CFB (schedule clone + IV draw)", |b| { + group.bench_function("AES_128 create_encryptor, CFB (borrow + IV draw)", |b| { b.iter(|| black_box(black_box(&cfb128).create_encryptor(&mut rng).unwrap().1)) }); - group.bench_function("AES_128 create_decryptor, CFB (schedule clone)", |b| { + group.bench_function("AES_128 create_decryptor, CFB (borrow)", |b| { b.iter(|| black_box(black_box(&cfb128).create_decryptor(&iv))) }); diff --git a/crypto/aes/tests/ccm_key_tests.rs b/crypto/aes/tests/ccm_key_tests.rs index 08ede2f81..868408407 100644 --- a/crypto/aes/tests/ccm_key_tests.rs +++ b/crypto/aes/tests/ccm_key_tests.rs @@ -205,3 +205,43 @@ fn worker_sizes_match_the_documented_memory_table() { assert_eq!(size_of::>(), 120); assert_eq!(size_of::>(), 120); } + +/// A refused `do_encrypt_out` / `do_decrypt_out` leaves nothing of the input in the caller's +/// buffer, and zeroes the buffer first as every `_out` method must: payload offered while declared +/// AAD is still owed, and payload past the declared length, on both sides. +#[test] +fn a_refused_update_leaves_no_payload_in_the_output_buffer() { + use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; + + let key = AES128Key::>::from_key_material(&material::<16>()).unwrap(); + let nonce = [0x11u8; 12]; + let payload = [0x5Au8; 16]; + + // AAD still owed: the four bytes declared have not been supplied. + let mut enc = key.create_encryptor_nonce(&nonce, 4, 16).unwrap(); + let mut out = [0xEEu8; 32]; + assert!(matches!( + enc.do_encrypt_out(&payload, &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + assert_eq!(out, [0u8; 32], "a refused encrypt must leave only zeros behind"); + + // Past the declared payload length. + let mut enc = key.create_encryptor_nonce(&nonce, 0, 8).unwrap(); + let mut out = [0xEEu8; 32]; + assert!(matches!( + enc.do_encrypt_out(&payload, &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + assert_eq!(out, [0u8; 32], "an over-length encrypt must leave only zeros behind"); + + // The decryptor, with AAD still owed: the sixteen bytes are within the declared payload, so + // they are not held back as a tag, and must not be copied either. + let mut dec = key.create_decryptor(&nonce, 4, 16).unwrap(); + let mut out = [0xEEu8; 32]; + assert!(matches!( + dec.do_decrypt_out(&payload, &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + assert_eq!(out, [0u8; 32], "a refused decrypt must leave only zeros behind"); +} diff --git a/crypto/aes/tests/mode_key_tests.rs b/crypto/aes/tests/mode_key_tests.rs index 99ea2f9c5..c3dd8749e 100644 --- a/crypto/aes/tests/mode_key_tests.rs +++ b/crypto/aes/tests/mode_key_tests.rs @@ -105,6 +105,8 @@ fn no_padding_keys_pass_the_symmetric_cipher_framework() { framework.required_alignment = 16; framework.test_key::<16, 16, 16, _>(&key128::>(), &mut rng()); framework.test_key::<32, 16, 16, _>(&key256::>(), &mut rng()); + framework.test_key::<16, 0, 16, _>(&key128::>(), &mut rng()); + framework.test_key::<32, 0, 16, _>(&key256::>(), &mut rng()); } #[test] From 5444ea32b52d6ad3fc12c3635b5e69176c256616 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 34/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: core: the GAT docs say the borrow once, and the decrypt one-shots say when the plaintext is zeroized again The eighteen `Encryptor<'k>` / `Decryptor<'k>` / `Generator<'k>` doc comments in `symmetric_key` and `hazmat::mode` each restated the module-level borrow-not-copy statement; they now say "borrowing the key for `'k`" and leave the rest to the module docs. `ElectronicCodeBook` loses the paragraph of rationale its own module docs already give. The AEAD and symmetric decrypt one-shots said the output buffer is zeroized "again before an error is returned"; the provided bodies zeroize the released prefix when the final fails, not on every error, and the docs now say that. The `KeyStream` security note is rewrapped after the earlier reword. Assisted-by: Claude:claude-mythos-5-1 --- .../core/src/hazmat/electronic_code_book.rs | 9 ++-- crypto/core/src/hazmat/key_stream.rs | 9 ++-- crypto/core/src/hazmat/mode.rs | 40 +++++------------ crypto/core/src/symmetric_key.rs | 43 ++++++------------- 4 files changed, 33 insertions(+), 68 deletions(-) diff --git a/crypto/core/src/hazmat/electronic_code_book.rs b/crypto/core/src/hazmat/electronic_code_book.rs index b6262bdd2..e64384772 100644 --- a/crypto/core/src/hazmat/electronic_code_book.rs +++ b/crypto/core/src/hazmat/electronic_code_book.rs @@ -11,9 +11,8 @@ use crate::key_material::KeyType; // end of imports needed for docs /// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1, already -/// keyed. The modes in `bouncycastle_cipher::modes` bound on this and nothing more, which is what -/// lets a mode's per-key state be generic over the permutation without naming its key length; a -/// key object builds the permutation through [`ElectronicCodeBook::new`]. +/// keyed. A key object builds it through [`ElectronicCodeBook::new`]; the modes in +/// `bouncycastle_cipher::modes` bound on this trait alone. /// /// # 🚨 Security Considerations 🚨 /// A permutation applied to data block by block is ECB: equal plaintext blocks give equal @@ -27,8 +26,8 @@ use crate::key_material::KeyType; /// # 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 [`ElectronicCodeBook::new`] has returned. Only `new` can -/// fail, and only because of the key. +/// is nothing a caller can get wrong once [`ElectronicCodeBook::new`] has returned; that +/// constructor is the one fallible step, and only because of the key. pub trait BlockPermutation: Algorithm + Sized { /// The forward cipher function, in place. fn encrypt_block(&self, block: &mut [u8; BLOCK_LEN]); diff --git a/crypto/core/src/hazmat/key_stream.rs b/crypto/core/src/hazmat/key_stream.rs index e689868e5..b830196e3 100644 --- a/crypto/core/src/hazmat/key_stream.rs +++ b/crypto/core/src/hazmat/key_stream.rs @@ -16,10 +16,11 @@ use crate::traits::{BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEnc /// [`ElectronicCodeBook`] is the raw primitive under a block cipher mode. /// /// It is positioned by its init data over an already-keyed permutation it borrows, and XORs -/// successive keystream blocks into whatever it is handed. It has no direction and no init-data policy: generating the nonce, buffering a -/// partly-used block between calls, and refusing a call that would run past the end of the -/// keystream all belong to `bouncycastle_cipher::stream::StreamCipher`, which turns any -/// `KeyStream` into a [`StreamCipherEncryptor`] / [`StreamCipherDecryptor`] pair. +/// successive keystream blocks into whatever it is handed. It has no direction and no init-data +/// policy: generating the nonce, buffering a partly-used block between calls, and refusing a call +/// that would run past the end of the keystream all belong to +/// `bouncycastle_cipher::stream::StreamCipher`, which turns any `KeyStream` into a +/// [`StreamCipherEncryptor`] / [`StreamCipherDecryptor`] pair. /// /// Only a keystream that is independent of the data fits: CTR does, CFB does not, since its next /// keystream block is the encryption of the last ciphertext block. diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index d2400731b..faa4540ab 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -72,9 +72,7 @@ pub trait AeadEncryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming encryptor this mode produces over `P`, borrowing the key state for `'k`. type Encryptor<'k>: AEADCipherEncryptor where P: 'k; @@ -139,9 +137,7 @@ pub trait AeadDecryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming decryptor this mode produces over `P`, borrowing the key state for `'k`. type Decryptor<'k>: AEADCipherDecryptor where P: 'k; @@ -180,9 +176,7 @@ pub trait AeadDeclaredEncryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming encryptor this mode produces over `P`, borrowing the key state for `'k`. type Encryptor<'k>: AEADCipherEncryptor where P: 'k; @@ -256,9 +250,7 @@ pub trait AeadDeclaredDecryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming decryptor this mode produces over `P`, borrowing the key state for `'k`. type Decryptor<'k>: AEADCipherDecryptor where P: 'k; @@ -303,9 +295,7 @@ pub trait SymmetricEncryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming encryptor this mode produces over `P`, borrowing the key state for `'k`. type Encryptor<'k>: SymmetricCipherEncryptor where P: 'k; @@ -328,9 +318,7 @@ pub trait SymmetricDecryptMode< const FINAL_LEN: usize, >: Mode

{ - /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming decryptor this mode produces over `P`, borrowing the key state for `'k`. type Decryptor<'k>: SymmetricCipherDecryptor where P: 'k; @@ -352,9 +340,7 @@ pub trait BlockEncryptMode< const BLOCK_LEN: usize, >: Mode

{ - /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming encryptor this mode produces over `P`, borrowing the key state for `'k`. type Encryptor<'k>: BlockCipherEncryptor where P: 'k; @@ -377,9 +363,7 @@ pub trait BlockDecryptMode< const BLOCK_LEN: usize, >: Mode

{ - /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming decryptor this mode produces over `P`, borrowing the key state for `'k`. type Decryptor<'k>: BlockCipherDecryptor where P: 'k; @@ -395,9 +379,7 @@ pub trait BlockDecryptMode< /// takes no RNG and cannot fail. Its own pair rather than [`BlockEncryptMode`] with /// `INIT_DATA_LEN = 0`, so that nothing is handed an RNG it has no use for. pub trait EcbEncryptMode: Mode

{ - /// The streaming encryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming encryptor this mode produces over `P`, borrowing the key state for `'k`. type Encryptor<'k>: BlockCipherEncryptor<0, BLOCK_LEN> where P: 'k; @@ -408,9 +390,7 @@ pub trait EcbEncryptMode: Mode< /// The decryption side of ECB over `P`; see [`EcbEncryptMode`]. pub trait EcbDecryptMode: Mode

{ - /// The streaming decryptor this mode produces over `P`. It borrows the key state it was made - /// from for `'k`: the schedule is read through the reference, never copied into the worker, - /// and the worker cannot outlive the state. + /// The streaming decryptor this mode produces over `P`, borrowing the key state for `'k`. type Decryptor<'k>: BlockCipherDecryptor<0, BLOCK_LEN> where P: 'k; diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index af4236752..401b5b319 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -139,9 +139,7 @@ pub trait AeadEncryptionKey< const FINAL_LEN: usize, > { - /// The streaming encryptor this key produces. It borrows the key for `'k`: the key's per-key - /// state is read through the reference rather than copied into the worker, and the worker - /// cannot outlive the key. + /// The streaming encryptor this key produces, borrowing the key for `'k`. type Encryptor<'k>: AEADCipherEncryptor where Self: 'k; @@ -381,9 +379,7 @@ pub trait AeadDecryptionKey< const FINAL_LEN: usize, > { - /// The streaming decryptor this key produces. It borrows the key for `'k`: the key's per-key - /// state is read through the reference rather than copied into the worker, and the worker - /// cannot outlive the key. + /// The streaming decryptor this key produces, borrowing the key for `'k`. type Decryptor<'k>: AEADCipherDecryptor where Self: 'k; @@ -394,8 +390,9 @@ pub trait AeadDecryptionKey< /// One-shot with the tag inline: decrypts `ciphertext` under `nonce` and `aad` into /// `plaintext`, which needs `decrypt_out_len(ciphertext.len())` bytes of the decryptor type, /// and returns the number of plaintext bytes written. The entire output buffer is zeroized - /// before the plaintext is written, and again before an error is returned, so a caller who - /// ignores the `Result` is left with zeros rather than unauthenticated plaintext. + /// before anything is written, and the plaintext released before a failed tag check is + /// zeroized again, so a caller who ignores the `Result` is left with zeros rather than + /// unauthenticated plaintext. /// /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, @@ -511,9 +508,7 @@ pub trait AeadDeclaredEncryptionKey< const FINAL_LEN: usize, > { - /// The streaming encryptor this key produces. It borrows the key for `'k`: the key's per-key - /// state is read through the reference rather than copied into the worker, and the worker - /// cannot outlive the key. + /// The streaming encryptor this key produces, borrowing the key for `'k`. type Encryptor<'k>: AEADCipherEncryptor where Self: 'k; @@ -745,9 +740,7 @@ pub trait AeadDeclaredDecryptionKey< const FINAL_LEN: usize, > { - /// The streaming decryptor this key produces. It borrows the key for `'k`: the key's per-key - /// state is read through the reference rather than copied into the worker, and the worker - /// cannot outlive the key. + /// The streaming decryptor this key produces, borrowing the key for `'k`. type Decryptor<'k>: AEADCipherDecryptor where Self: 'k; @@ -877,9 +870,7 @@ pub trait SymmetricEncryptionKey< const FINAL_LEN: usize, > { - /// The streaming encryptor this key produces. It borrows the key for `'k`: the key's per-key - /// state is read through the reference rather than copied into the worker, and the worker - /// cannot outlive the key. + /// The streaming encryptor this key produces, borrowing the key for `'k`. type Encryptor<'k>: SymmetricCipherEncryptor where Self: 'k; @@ -965,9 +956,7 @@ pub trait SymmetricDecryptionKey< const FINAL_LEN: usize, > { - /// The streaming decryptor this key produces. It borrows the key for `'k`: the key's per-key - /// state is read through the reference rather than copied into the worker, and the worker - /// cannot outlive the key. + /// The streaming decryptor this key produces, borrowing the key for `'k`. type Decryptor<'k>: SymmetricCipherDecryptor where Self: 'k; @@ -977,8 +966,8 @@ pub trait SymmetricDecryptionKey< /// One-shot: decrypts `ciphertext` under `init_data` into `plaintext`, which needs /// `decrypt_out_len(ciphertext.len())` bytes of the decryptor type, and returns the number - /// of plaintext bytes written. The entire output buffer is zeroized before the plaintext is - /// written, and again before an error is returned. + /// of plaintext bytes written. The entire output buffer is zeroized before anything is + /// written, and the plaintext released before a failed final is zeroized again. /// /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, or any error @@ -1046,9 +1035,7 @@ pub trait BlockEncryptionKey< const BLOCK_LEN: usize, > { - /// The streaming encryptor this key produces. It borrows the key for `'k`: the key's per-key - /// state is read through the reference rather than copied into the worker, and the worker - /// cannot outlive the key. + /// The streaming encryptor this key produces, borrowing the key for `'k`. type Encryptor<'k>: BlockCipherEncryptor where Self: 'k; @@ -1093,9 +1080,7 @@ pub trait BlockDecryptionKey< const BLOCK_LEN: usize, > { - /// The streaming decryptor this key produces. It borrows the key for `'k`: the key's per-key - /// state is read through the reference rather than copied into the worker, and the worker - /// cannot outlive the key. + /// The streaming decryptor this key produces, borrowing the key for `'k`. type Decryptor<'k>: BlockCipherDecryptor where Self: 'k; @@ -1124,7 +1109,7 @@ pub trait BlockDecryptionKey< // The MAC worker is bounded on `MAC` as a whole for now: splitting its finals into `MACGenerator` // and `MACVerifier` while `MAC` still carries them would put the same method names on two traits, // which is ambiguous at every call site that has both in scope (`do_update` is on `MAC`, `do_final` -// would be on both). The split happens when `MAC` loses its constructors and finals. +// would be on both). The split waits on `MAC` losing its finals; its constructors are already gone. /// A key that can generate MACs. Each call to [`create_generator`](Self::create_generator) starts /// a fresh worker over the key's per-key state, so any number of generators can be in flight from From 2ceabe3d16acd195ca73ac37e658a1bcdc2bd633 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 35/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: core-test-framework: the comments name the contract, not a test that no longer exists `block_cipher.rs` and `symmetric_ciphers.rs` pointed at `test` and `test_encryptor_decryptor`, which went with the constructors; the comments now state the contract they introduce. `symmetric_key.rs`'s note on the MAC split said the constructors were still to go. `from_key_material.rs` says what `alg_name` is used for and that material of the right type is accepted. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core-test-framework/src/block_cipher.rs | 2 +- crypto/core-test-framework/src/from_key_material.rs | 6 +++--- crypto/core-test-framework/src/symmetric_ciphers.rs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crypto/core-test-framework/src/block_cipher.rs b/crypto/core-test-framework/src/block_cipher.rs index 814ef530c..3ba082f2e 100644 --- a/crypto/core-test-framework/src/block_cipher.rs +++ b/crypto/core-test-framework/src/block_cipher.rs @@ -40,7 +40,7 @@ impl TestFrameworkBlockCipher { let mut decryptor = key.create_decryptor(&iv); Self::round_trip_pairs(&mut encryptor, &mut decryptor); - // one-shot API: a block-aligned byte array, in place; see `test`. + // one-shot API: a block-aligned byte array, in place let one_block: &[u8; BLOCK_LEN] = &DUMMY_SEED.as_chunks::().0[0]; let mut buf = *one_block; let (n, iv) = key.encrypt_inplace(rng, &mut buf).unwrap(); diff --git a/crypto/core-test-framework/src/from_key_material.rs b/crypto/core-test-framework/src/from_key_material.rs index 1d3f33aea..4ff9e7ff2 100644 --- a/crypto/core-test-framework/src/from_key_material.rs +++ b/crypto/core-test-framework/src/from_key_material.rs @@ -27,11 +27,11 @@ impl TestFrameworkFromKeyMaterial { Self {} } - /// Exercises the [`FromKeyMaterial`] contract for one key type, whose workers report - /// `alg_name` and whose strength is `max_strength`. + /// Exercises the [`FromKeyMaterial`] contract for one key type, named `alg_name` in the + /// failure messages, whose strength is `max_strength`. /// /// Checks, in order: - /// * material of the right type, unbound or bound to `alg_name`, is accepted; + /// * material of the right type is accepted; /// * material of the wrong [`KeyType`] is refused with [`KeyMaterialError::InvalidKeyType`]; /// * the security-strength policy matches `max_strength`; /// * [`FromKeyMaterial::generate`] draws a key from an RNG of sufficient strength, tagged diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index a46fccb92..1cf50135d 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -313,8 +313,9 @@ impl TestFrameworkSymmetricCipher { } } - // Output-buffer contract, for every method that writes into a caller's buffer: see - // `test_encryptor_decryptor`. + // Output-buffer contract, for every method that writes into a caller's buffer: a buffer + // larger than needed is accepted, the returned count is what that call wrote, and every + // byte past it is zeroed. const SENTINEL: u8 = 0xA5; const EXTRA: usize = 7; let assert_tail_zeroed = |buf: &[u8], n: usize, what: &str| { From e6e8914c6c8b7230f86db214a3a69dbcc95dc3df Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 36/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: cipher: the CCM unit tests move to the end of the file The `#[cfg(test)] mod tests` block sat between the mode's helpers and the public types; it is now the last item in `ccm.rs`, where the other modes keep theirs. One over-wide doc line on `CCM` is rewrapped. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/ccm.rs | 778 ++++++++++++++++----------------- 1 file changed, 389 insertions(+), 389 deletions(-) diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 0367cedf1..4b08bac6f 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -1543,415 +1543,131 @@ where } } -#[cfg(test)] -mod tests { - //! Tests for the private formatting helpers, which are what a reviewer with SP 800-38C open - //! most needs to check and which no public API exposes directly. - //! - //! The expected values are the `B` and `Ctr_i` strings printed in the spec's own Appendix C - //! examples, transcribed from the errata-updated PDF. Appendix C gives the formatted block - //! string for each example, so these pin the flags octet, the placement of `N` and `Q`, and - //! the AAD length encoding against the document rather than against this implementation. +/// The CCM mode type: the `M` of a mode-bound key such as `AES128Key>`. +/// +/// CCM must commit to the AAD and payload lengths before its first byte (`B0` carries the +/// payload length, A.2.2 length-prefixes the AAD), so the key's constructors take them: `CCM` +/// implements the declared-length mode traits, and its workers, [`CcmEncryptor`] / +/// [`CcmDecryptor`], then take exactly those amounts in any chunking and hold nothing +/// back. The generic-trait pair with the frame in its type, [`CcmPacketEncryptor`] / +/// [`CcmPacketDecryptor`], is the other way onto the same mode. +pub struct CCM; - use super::*; - use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +impl CCM { + /// The largest payload this nonce length can carry, from A.1's "by definition, p<2^8q" with + /// `q = 15 - n`. At `n = 7`, `2^8q` is `2^64`, which does not fit a `u64`; there the bound is + /// `u64::MAX`, no bound at all on a `usize` length. + pub const MAX_PAYLOAD_LEN: u64 = + if 15 - NONCE_LEN >= 8 { u64::MAX } else { (1u64 << (8 * (15 - NONCE_LEN))) - 1 }; +} - /// A stand-in permutation: the identity. `B0` and `Ctr_i` are formatted *before* any cipher - /// call, so the identity is enough to read them back out of the state, and it keeps these - /// tests about the formatting function rather than about AES. - struct Identity; +/// The encrypting worker a `CCM` key produces: Sec 6.1 over the lengths declared at +/// construction, released as it is processed. The direction is in the type. +pub struct CcmEncryptor<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize>( + Ccm<'k, P, Encrypting, 16, NONCE_LEN, TAG_LEN>, +) +where + P: BlockPermutation<16> + ModeNames; - impl Algorithm for Identity { - const ALG_NAME: &'static str = "identity"; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; - } +/// The decrypting worker a `CCM` key produces: Sec 6.2 over the lengths declared at +/// construction. The payload is released as it is recovered, up to the declared length; what +/// follows can only be the inline tag, and is held back for the final. +pub struct CcmDecryptor<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> +where + P: BlockPermutation<16> + ModeNames, +{ + ccm: Ccm<'k, P, Decrypting, 16, NONCE_LEN, TAG_LEN>, + // Public either way (the tag travels in the clear), so not wrapped. + tag: [u8; TAG_LEN], + tag_len: usize, +} - impl ModeNames for Identity { - const CBC_ALG_NAME: &'static str = "identity_CBC"; - const CCM_ALG_NAME: &'static str = "identity_CCM"; - const CFB_ALG_NAME: &'static str = "identity_CFB"; - const CFB8_ALG_NAME: &'static str = "identity_CFB8"; - const CMAC_ALG_NAME: &'static str = "identity_CMAC"; - const CTR_ALG_NAME: &'static str = "identity_CTR"; - const ECB_ALG_NAME: &'static str = "identity_ECB"; - const GCM_ALG_NAME: &'static str = "identity_GCM"; - const GMAC_ALG_NAME: &'static str = "identity_GMAC"; - } +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> Algorithm + for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ + const ALG_NAME: &'static str = P::CCM_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} - impl ElectronicCodeBook<16, 16> for Identity { - fn new(_key: &KeyMaterial<16>) -> Result { - Ok(Identity) - } +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> + SymmetricCipherEncryptor for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ + /// The identity: nothing is held back, since `B0` is already committed to the length. + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + input_len } - impl BlockPermutation<16> for Identity { - fn encrypt_block(&self, _block: &mut [u8; 16]) {} - fn decrypt_block(&self, _block: &mut [u8; 16]) {} - fn encrypt_2blocks(&self, _blocks: &mut [[u8; 16]; 2]) {} - fn decrypt_2blocks(&self, _blocks: &mut [[u8; 16]; 2]) {} - fn encrypt_4blocks(&self, _blocks: &mut [[u8; 16]; 4]) {} - fn decrypt_4blocks(&self, _blocks: &mut [[u8; 16]; 4]) {} + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); + } + // Before the copy: `Ccm::do_encrypt_inplace` would refuse this too, but only after the + // plaintext had been copied into the caller's buffer, and a refused call must not leave + // plaintext there. + self.0.accepts_payload(plaintext.len())?; + let out = &mut ciphertext[..plaintext.len()]; + out.copy_from_slice(plaintext); + self.0.do_encrypt_inplace(out)?; + Ok(plaintext.len()) } - fn key() -> KeyMaterial<16> { - KeyMaterial::<16>::from_bytes_as_type( - &[ - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, - 0x4e, 0x4f, - ], - KeyType::SymmetricCipherKey, - ) - .expect("Appendix C's 128-bit key") + fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + Ok((self.0.do_encrypt_final()?, TAG_LEN)) } - /// Appendix C.1: `Tlen=32, Nlen=56, Alen=64, Plen=32`, so `t = 4`, `n = 7`, `q = 8`. - /// - /// The spec prints `B` as - /// `4f101112 13141516 00000000 00000004 | 00080001 02030405 06070000 00000000 | ...`, - /// so `B0` is `4f` then the 7-byte nonce then `[4]_64`, and `B1` is `[8]_16` then the 8-byte - /// AAD then six zero bytes of pad. - /// - /// C.1's AAD is 8 bytes, so its Adata bit is set. - #[test] - fn c1_b0_matches_the_spec() { - let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; - assert_eq!( - Ccm::::format_b0(&nonce, true, 4), - [0x4f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0, 0, 0, 0, 0, 0, 0, 4], - "C.1 B0: flags 0x4f = Adata 1 | [(4-2)/2]_3 = 001 | [8-1]_3 = 111, then Q = [4]_64" - ); + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + TAG_LEN } +} - /// A.2.2: the Adata bit is "'0' if a=0 and '1' if a>0", and it is bit 6 -- so clearing it must - /// take C.1's `0x4f` to `0x0f` and change nothing else in the block. - #[test] - fn adata_flag_is_bit_6_of_the_flags_octet() { - let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; - let with = Ccm::::format_b0(&nonce, true, 4); - let without = Ccm::::format_b0(&nonce, false, 4); - assert_eq!(without[0], 0x0f, "a = 0 clears bit 6, leaving the t and q fields alone"); - assert_eq!(with[0] ^ without[0], 1 << 6, "Adata is bit 6 and nothing else"); - assert_eq!(with[1..], without[1..], "the flag must not disturb N or Q"); +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> + AEADCipherEncryptor for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.0.do_update_aad(aad) } - /// The constructor really does absorb the `B0` that [`Ccm::format_b0`] built. With the identity - /// permutation the CBC-MAC chaining value after one block is that block itself, so a - /// no-AAD, no-payload construction leaves `B0` sitting in `y`. - /// - /// Without this, `format_b0` could be correct and unused. - #[test] - fn the_constructor_absorbs_b0() { - let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; - let identity = Identity::new(&key()).unwrap(); - let ccm = - Ccm::::from_perm_with_lengths(&identity, &nonce, 0, 4) - .unwrap(); - assert_eq!(*ccm.y, Ccm::::format_b0(&nonce, false, 4)); - assert_eq!(ccm.mac_pos, 0, "a whole block was absorbed, so nothing is part-filled"); + fn do_encrypt_final_detached_out( + self, + ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + Ok((0, self.0.do_encrypt_final()?)) } - /// Appendix C.4: `Tlen=112, Nlen=104, Plen=256`, so `t = 14`, `n = 13`, `q = 2`; the spec - /// prints `B0` as `71101112 13141516 1718191a 1b1c0020`. - /// - /// This is the other end of the `q` range from C.1, so between them the two tests pin the - /// `[q-1]_3` encoding and the fact that `Q` is `q` octets wide, not a fixed width. - #[test] - fn c4_b0_matches_the_spec() { - let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; - assert_eq!( - Ccm::::format_b0(&nonce, true, 32), - [ - 0x71, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, - 0x00, 0x20 - ], - "C.4 B0: flags 0x71 = Adata 1 | [(14-2)/2]_3 = 110 | [2-1]_3 = 001, then Q = [32]_16" - ); + fn encrypt_detached_out_len(plaintext_len: usize) -> usize { + plaintext_len } +} - /// Appendix C.1 prints `Ctr0` as `07101112 13141516 00000000 00000000` and `Ctr1` as the same - /// with a trailing `01`; C.4's are `01101112 ... 1b1c0000` and `... 1b1c0001`. - /// - /// Table 4 makes the counter flags `[q-1]_3` alone, with every other bit zero -- which is what - /// keeps them distinct from `B0`, whose `t` field cannot be zero. - #[test] - fn counter_blocks_match_the_spec() { - let nonce_c1 = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; - let mut ks = CcmKeyStream::::from_perm(&Identity, &nonce_c1); - // `Ctr0` is the template with a zero counter field. - let ctr0 = CcmKeyStream::::counter_block(&ks.ctr_template, 0); - assert_eq!( - ctr0, - [0x07, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0, 0, 0, 0, 0, 0, 0, 0], - "C.1 Ctr0" - ); - // The first payload keystream block is `S1`, so the first block the keystream XORs in must - // be `Ctr1`: under the identity permutation, XORed into zeros, that is `Ctr1` itself. - let mut s1 = [[0u8; 16]]; - ks.apply_blocks(&mut s1); - let mut ctr1 = ctr0; - ctr1[15] = 1; - assert_eq!(s1[0], ctr1, "C.1 Ctr1 (the identity permutation leaves S1 = Ctr1)"); +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> Algorithm + for CcmDecryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ + const ALG_NAME: &'static str = P::CCM_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} - let nonce_c4 = - [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; - let ks4 = CcmKeyStream::::from_perm(&Identity, &nonce_c4); - assert_eq!( - CcmKeyStream::::counter_block(&ks4.ctr_template, 0), - [ - 0x01, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, - 0x00, 0x00 - ], - "C.4 Ctr0" - ); - } - - /// A fresh keystream has every counter value but `Ctr0` left: step 7's `S1 || S2 || ...` runs - /// from `j = 1` to the largest `q`-octet counter, `2^8q - 1`, and `S0` is the tag mask. Pinned - /// absolutely because the limit can never bind through the public API -- A.1 caps the payload - /// at `2^8q - 1` bytes, far fewer than that many blocks -- so nothing else would notice an - /// off-by-one here. - #[test] - fn a_fresh_keystream_has_every_counter_but_ctr0_left() { - // n = 13, so q = 2: counters 1 ..= 65535. - let ks = CcmKeyStream::::from_perm(&Identity, &[0u8; 13]); - assert_eq!(ks.remaining_blocks(), 65535); - // n = 7, so q = 8: counters 1 ..= 2^64 - 1, which is `u64::MAX` of them. - let ks = CcmKeyStream::::from_perm(&Identity, &[0u8; 7]); - assert_eq!(ks.remaining_blocks(), u64::MAX); - } - - /// CCM's keystream against the shared [`KeyStream`] conformance suite. A unit test rather than - /// an integration test because `CcmKeyStream` is crate-private. Over the keyed toy rather than - /// the identity, which ignores the key and so could not pass the key-policy checks. - #[test] - fn ccm_keystream_conforms_to_the_key_stream_framework() { - mod toy { - include!("../../tests/common/toy_block_cipher.rs"); - } - use bouncycastle_core_test_framework::key_stream::TestFrameworkKeyStream; - use toy::ToyBlockCipher; - let perm = ToyBlockCipher::new(&key()).unwrap(); - let framework = TestFrameworkKeyStream::new(); - framework.test::<7, 16, _>(|nonce| { - CcmKeyStream::::from_perm(&perm, nonce) - }); - framework.test::<13, 16, _>(|nonce| { - CcmKeyStream::::from_perm(&perm, nonce) - }); - } - - /// A.2.2's three AAD length encodings, at and around both boundaries. - /// - /// Two of these values come from the spec itself: C.1's `a = 8` is printed as `0008`, and - /// C.4's `a = 65536` (`Alen = 524288` bits) is printed as - /// `11111111 11111110 00000000 00000001 00000000 00000000`, i.e. `ff fe 00 01 00 00`. - /// - /// The rest pin the boundaries, which is the part no end-to-end test can reach: the first is - /// `2^16 - 2^8` = 65280 rather than the obvious-but-wrong `2^16`, and the second is `2^32`, - /// which through the public API would need a 4 GiB AAD. - #[test] - fn aad_length_encoding_matches_a_2_2() { - type Mode<'k> = Ccm<'k, Identity, Encrypting, 16, 7, 4>; - - // Case 1: 0 < a < 2^16 - 2^8, two octets, `[a]_16`. - assert_eq!( - Mode::encode_aad_len(8), - ([0x00, 0x08, 0, 0, 0, 0, 0, 0, 0, 0], 2), - "C.1's a = 8" - ); - assert_eq!(Mode::encode_aad_len(1).1, 2); - // 65279 = 2^16 - 2^8 - 1 is the largest value still in the first case. - assert_eq!( - Mode::encode_aad_len(65279), - ([0xfe, 0xff, 0, 0, 0, 0, 0, 0, 0, 0], 2), - "65279 is still [a]_16" - ); - - // Case 2: 2^16 - 2^8 <= a < 2^32, six octets, `0xff || 0xfe || [a]_32`. 65280 is the first. - assert_eq!( - Mode::encode_aad_len(65280), - ([0xff, 0xfe, 0x00, 0x00, 0xff, 0x00, 0, 0, 0, 0], 6), - "65280 crosses into the six-octet case; a two-octet 0xff00 would be ambiguous" - ); - assert_eq!( - Mode::encode_aad_len(65536), - ([0xff, 0xfe, 0x00, 0x01, 0x00, 0x00, 0, 0, 0, 0], 6), - "C.4's a = 65536" - ); - // 2^32 - 1 is the largest value still in the second case. - assert_eq!( - Mode::encode_aad_len(u32::MAX as u64), - ([0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0], 6), - "2^32 - 1 is still the six-octet case" - ); - - // Case 3: 2^32 <= a < 2^64, ten octets, `0xff || 0xff || [a]_64`. - assert_eq!( - Mode::encode_aad_len(1u64 << 32), - ([0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00], 10), - "2^32 is the first ten-octet case" - ); - assert_eq!( - Mode::encode_aad_len(u64::MAX), - ([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 10) - ); - - // A.2.2's whole point: the three cases are distinguishable by their leading octets, so no - // two distinct lengths can encode to the same prefix. The first octet is 0xff only in the - // second and third cases, and the second octet separates those. - for a in [1u64, 8, 65279] { - assert_ne!(Mode::encode_aad_len(a).0[0], 0xff, "case 1 must not lead with 0xff"); - } - } - - /// The constructor really uses `Ccm::encode_aad_len`, and puts it *before* the AAD. - /// - /// With the identity permutation the CBC-MAC is `y = B0 ^ B1 ^ ... ^ Br`, so with a one-block - /// all-zero AAD the only nonzero contributions are `B0` and the length encoding. That makes the - /// encoding readable back out, which is what pins the ordering rather than just the value. - #[test] - fn the_constructor_prefixes_the_aad_with_its_length() { - type Mode<'k> = Ccm<'k, Identity, Encrypting, 16, 7, 4>; - let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; - // 14 zero bytes of AAD: the 2-byte length plus 14 bytes is exactly one 16-byte block, so - // there is no padding to reason about. - let identity = Identity::new(&key()).unwrap(); - let mut ccm = Mode::from_perm_with_lengths(&identity, &nonce, 14, 0).unwrap(); - ccm.absorb_aad(&[0u8; 14]); - - let b0 = Mode::format_b0(&nonce, true, 0); - let mut b1 = [0u8; 16]; - b1[..2].copy_from_slice(&14u16.to_be_bytes()); - let expected: [u8; 16] = core::array::from_fn(|i| b0[i] ^ b1[i]); - assert_eq!(*ccm.y, expected, "y must be B0 ^ B1, with B1 starting with [14]_16"); - } -} - -/// The CCM mode type: the `M` of a mode-bound key such as `AES128Key>`. -/// -/// CCM must commit to the AAD and payload lengths before its first byte (`B0` carries the -/// payload length, A.2.2 length-prefixes the AAD), so the key's constructors take them: `CCM` -/// implements the declared-length mode traits, and its workers, [`CcmEncryptor`] / -/// [`CcmDecryptor`], then take exactly those amounts in any chunking and hold nothing -/// back. The generic-trait pair with the frame in its type, [`CcmPacketEncryptor`] / [`CcmPacketDecryptor`], -/// is the other way onto the same mode. -pub struct CCM; - -impl CCM { - /// The largest payload this nonce length can carry, from A.1's "by definition, p<2^8q" with - /// `q = 15 - n`. At `n = 7`, `2^8q` is `2^64`, which does not fit a `u64`; there the bound is - /// `u64::MAX`, no bound at all on a `usize` length. - pub const MAX_PAYLOAD_LEN: u64 = - if 15 - NONCE_LEN >= 8 { u64::MAX } else { (1u64 << (8 * (15 - NONCE_LEN))) - 1 }; -} - -/// The encrypting worker a `CCM` key produces: Sec 6.1 over the lengths declared at -/// construction, released as it is processed. The direction is in the type. -pub struct CcmEncryptor<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize>( - Ccm<'k, P, Encrypting, 16, NONCE_LEN, TAG_LEN>, -) -where - P: BlockPermutation<16> + ModeNames; - -/// The decrypting worker a `CCM` key produces: Sec 6.2 over the lengths declared at -/// construction. The payload is released as it is recovered, up to the declared length; what -/// follows can only be the inline tag, and is held back for the final. -pub struct CcmDecryptor<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> -where - P: BlockPermutation<16> + ModeNames, -{ - ccm: Ccm<'k, P, Decrypting, 16, NONCE_LEN, TAG_LEN>, - // Public either way (the tag travels in the clear), so not wrapped. - tag: [u8; TAG_LEN], - tag_len: usize, -} - -impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> Algorithm - for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> -where - P: BlockPermutation<16> + ModeNames, -{ - const ALG_NAME: &'static str = P::CCM_ALG_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; -} - -impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> - SymmetricCipherEncryptor for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> -where - P: BlockPermutation<16> + ModeNames, -{ - /// The identity: nothing is held back, since `B0` is already committed to the length. - fn do_encrypt_out_len(&self, input_len: usize) -> usize { - input_len - } - - fn do_encrypt_out( - &mut self, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result { - ciphertext.fill(0); - if ciphertext.len() < plaintext.len() { - return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); - } - // Before the copy: `Ccm::do_encrypt_inplace` would refuse this too, but only after the - // plaintext had been copied into the caller's buffer, and a refused call must not leave - // plaintext there. - self.0.accepts_payload(plaintext.len())?; - let out = &mut ciphertext[..plaintext.len()]; - out.copy_from_slice(plaintext); - self.0.do_encrypt_inplace(out)?; - Ok(plaintext.len()) - } - - fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - Ok((self.0.do_encrypt_final()?, TAG_LEN)) - } - - fn encrypt_out_len(plaintext_len: usize) -> usize { - plaintext_len + TAG_LEN - } -} - -impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> - AEADCipherEncryptor for CcmEncryptor<'k, P, NONCE_LEN, TAG_LEN> -where - P: BlockPermutation<16> + ModeNames, -{ - fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { - self.0.do_update_aad(aad) - } - - fn do_encrypt_final_detached_out( - self, - ciphertext: &mut [u8; TAG_LEN], - ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { - ciphertext.fill(0); - Ok((0, self.0.do_encrypt_final()?)) - } - - fn encrypt_detached_out_len(plaintext_len: usize) -> usize { - plaintext_len - } -} - -impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> Algorithm - for CcmDecryptor<'k, P, NONCE_LEN, TAG_LEN> -where - P: BlockPermutation<16> + ModeNames, -{ - const ALG_NAME: &'static str = P::CCM_ALG_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; -} - -impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> - SymmetricCipherDecryptor for CcmDecryptor<'k, P, NONCE_LEN, TAG_LEN> -where - P: BlockPermutation<16> + ModeNames, -{ - /// Every byte still inside the declared payload; the rest can only be the tag, and is held. - fn do_decrypt_out_len(&self, input_len: usize) -> usize { - input_len.min(self.ccm.owed) +impl<'k, P, const NONCE_LEN: usize, const TAG_LEN: usize> + SymmetricCipherDecryptor for CcmDecryptor<'k, P, NONCE_LEN, TAG_LEN> +where + P: BlockPermutation<16> + ModeNames, +{ + /// Every byte still inside the declared payload; the rest can only be the tag, and is held. + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + input_len.min(self.ccm.owed) } fn do_decrypt_out( @@ -2460,3 +2176,287 @@ where resume_component(&state, key) } } + +#[cfg(test)] +mod tests { + //! Tests for the private formatting helpers, which are what a reviewer with SP 800-38C open + //! most needs to check and which no public API exposes directly. + //! + //! The expected values are the `B` and `Ctr_i` strings printed in the spec's own Appendix C + //! examples, transcribed from the errata-updated PDF. Appendix C gives the formatted block + //! string for each example, so these pin the flags octet, the placement of `N` and `Q`, and + //! the AAD length encoding against the document rather than against this implementation. + + use super::*; + use bouncycastle_core::key_material::{KeyMaterial, KeyType}; + + /// A stand-in permutation: the identity. `B0` and `Ctr_i` are formatted *before* any cipher + /// call, so the identity is enough to read them back out of the state, and it keeps these + /// tests about the formatting function rather than about AES. + struct Identity; + + impl Algorithm for Identity { + const ALG_NAME: &'static str = "identity"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + } + + impl ModeNames for Identity { + const CBC_ALG_NAME: &'static str = "identity_CBC"; + const CCM_ALG_NAME: &'static str = "identity_CCM"; + const CFB_ALG_NAME: &'static str = "identity_CFB"; + const CFB8_ALG_NAME: &'static str = "identity_CFB8"; + const CMAC_ALG_NAME: &'static str = "identity_CMAC"; + const CTR_ALG_NAME: &'static str = "identity_CTR"; + const ECB_ALG_NAME: &'static str = "identity_ECB"; + const GCM_ALG_NAME: &'static str = "identity_GCM"; + const GMAC_ALG_NAME: &'static str = "identity_GMAC"; + } + + impl ElectronicCodeBook<16, 16> for Identity { + fn new(_key: &KeyMaterial<16>) -> Result { + Ok(Identity) + } + } + + impl BlockPermutation<16> for Identity { + fn encrypt_block(&self, _block: &mut [u8; 16]) {} + fn decrypt_block(&self, _block: &mut [u8; 16]) {} + fn encrypt_2blocks(&self, _blocks: &mut [[u8; 16]; 2]) {} + fn decrypt_2blocks(&self, _blocks: &mut [[u8; 16]; 2]) {} + fn encrypt_4blocks(&self, _blocks: &mut [[u8; 16]; 4]) {} + fn decrypt_4blocks(&self, _blocks: &mut [[u8; 16]; 4]) {} + } + + fn key() -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type( + &[ + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, + 0x4e, 0x4f, + ], + KeyType::SymmetricCipherKey, + ) + .expect("Appendix C's 128-bit key") + } + + /// Appendix C.1: `Tlen=32, Nlen=56, Alen=64, Plen=32`, so `t = 4`, `n = 7`, `q = 8`. + /// + /// The spec prints `B` as + /// `4f101112 13141516 00000000 00000004 | 00080001 02030405 06070000 00000000 | ...`, + /// so `B0` is `4f` then the 7-byte nonce then `[4]_64`, and `B1` is `[8]_16` then the 8-byte + /// AAD then six zero bytes of pad. + /// + /// C.1's AAD is 8 bytes, so its Adata bit is set. + #[test] + fn c1_b0_matches_the_spec() { + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + assert_eq!( + Ccm::::format_b0(&nonce, true, 4), + [0x4f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0, 0, 0, 0, 0, 0, 0, 4], + "C.1 B0: flags 0x4f = Adata 1 | [(4-2)/2]_3 = 001 | [8-1]_3 = 111, then Q = [4]_64" + ); + } + + /// A.2.2: the Adata bit is "'0' if a=0 and '1' if a>0", and it is bit 6 -- so clearing it must + /// take C.1's `0x4f` to `0x0f` and change nothing else in the block. + #[test] + fn adata_flag_is_bit_6_of_the_flags_octet() { + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + let with = Ccm::::format_b0(&nonce, true, 4); + let without = Ccm::::format_b0(&nonce, false, 4); + assert_eq!(without[0], 0x0f, "a = 0 clears bit 6, leaving the t and q fields alone"); + assert_eq!(with[0] ^ without[0], 1 << 6, "Adata is bit 6 and nothing else"); + assert_eq!(with[1..], without[1..], "the flag must not disturb N or Q"); + } + + /// The constructor really does absorb the `B0` that [`Ccm::format_b0`] built. With the identity + /// permutation the CBC-MAC chaining value after one block is that block itself, so a + /// no-AAD, no-payload construction leaves `B0` sitting in `y`. + /// + /// Without this, `format_b0` could be correct and unused. + #[test] + fn the_constructor_absorbs_b0() { + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + let identity = Identity::new(&key()).unwrap(); + let ccm = + Ccm::::from_perm_with_lengths(&identity, &nonce, 0, 4) + .unwrap(); + assert_eq!(*ccm.y, Ccm::::format_b0(&nonce, false, 4)); + assert_eq!(ccm.mac_pos, 0, "a whole block was absorbed, so nothing is part-filled"); + } + + /// Appendix C.4: `Tlen=112, Nlen=104, Plen=256`, so `t = 14`, `n = 13`, `q = 2`; the spec + /// prints `B0` as `71101112 13141516 1718191a 1b1c0020`. + /// + /// This is the other end of the `q` range from C.1, so between them the two tests pin the + /// `[q-1]_3` encoding and the fact that `Q` is `q` octets wide, not a fixed width. + #[test] + fn c4_b0_matches_the_spec() { + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; + assert_eq!( + Ccm::::format_b0(&nonce, true, 32), + [ + 0x71, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x00, 0x20 + ], + "C.4 B0: flags 0x71 = Adata 1 | [(14-2)/2]_3 = 110 | [2-1]_3 = 001, then Q = [32]_16" + ); + } + + /// Appendix C.1 prints `Ctr0` as `07101112 13141516 00000000 00000000` and `Ctr1` as the same + /// with a trailing `01`; C.4's are `01101112 ... 1b1c0000` and `... 1b1c0001`. + /// + /// Table 4 makes the counter flags `[q-1]_3` alone, with every other bit zero -- which is what + /// keeps them distinct from `B0`, whose `t` field cannot be zero. + #[test] + fn counter_blocks_match_the_spec() { + let nonce_c1 = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + let mut ks = CcmKeyStream::::from_perm(&Identity, &nonce_c1); + // `Ctr0` is the template with a zero counter field. + let ctr0 = CcmKeyStream::::counter_block(&ks.ctr_template, 0); + assert_eq!( + ctr0, + [0x07, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0, 0, 0, 0, 0, 0, 0, 0], + "C.1 Ctr0" + ); + // The first payload keystream block is `S1`, so the first block the keystream XORs in must + // be `Ctr1`: under the identity permutation, XORed into zeros, that is `Ctr1` itself. + let mut s1 = [[0u8; 16]]; + ks.apply_blocks(&mut s1); + let mut ctr1 = ctr0; + ctr1[15] = 1; + assert_eq!(s1[0], ctr1, "C.1 Ctr1 (the identity permutation leaves S1 = Ctr1)"); + + let nonce_c4 = + [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; + let ks4 = CcmKeyStream::::from_perm(&Identity, &nonce_c4); + assert_eq!( + CcmKeyStream::::counter_block(&ks4.ctr_template, 0), + [ + 0x01, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x00, 0x00 + ], + "C.4 Ctr0" + ); + } + + /// A fresh keystream has every counter value but `Ctr0` left: step 7's `S1 || S2 || ...` runs + /// from `j = 1` to the largest `q`-octet counter, `2^8q - 1`, and `S0` is the tag mask. Pinned + /// absolutely because the limit can never bind through the public API -- A.1 caps the payload + /// at `2^8q - 1` bytes, far fewer than that many blocks -- so nothing else would notice an + /// off-by-one here. + #[test] + fn a_fresh_keystream_has_every_counter_but_ctr0_left() { + // n = 13, so q = 2: counters 1 ..= 65535. + let ks = CcmKeyStream::::from_perm(&Identity, &[0u8; 13]); + assert_eq!(ks.remaining_blocks(), 65535); + // n = 7, so q = 8: counters 1 ..= 2^64 - 1, which is `u64::MAX` of them. + let ks = CcmKeyStream::::from_perm(&Identity, &[0u8; 7]); + assert_eq!(ks.remaining_blocks(), u64::MAX); + } + + /// CCM's keystream against the shared [`KeyStream`] conformance suite. A unit test rather than + /// an integration test because `CcmKeyStream` is crate-private. Over the keyed toy rather than + /// the identity, which ignores the key and so could not pass the key-policy checks. + #[test] + fn ccm_keystream_conforms_to_the_key_stream_framework() { + mod toy { + include!("../../tests/common/toy_block_cipher.rs"); + } + use bouncycastle_core_test_framework::key_stream::TestFrameworkKeyStream; + use toy::ToyBlockCipher; + let perm = ToyBlockCipher::new(&key()).unwrap(); + let framework = TestFrameworkKeyStream::new(); + framework.test::<7, 16, _>(|nonce| { + CcmKeyStream::::from_perm(&perm, nonce) + }); + framework.test::<13, 16, _>(|nonce| { + CcmKeyStream::::from_perm(&perm, nonce) + }); + } + + /// A.2.2's three AAD length encodings, at and around both boundaries. + /// + /// Two of these values come from the spec itself: C.1's `a = 8` is printed as `0008`, and + /// C.4's `a = 65536` (`Alen = 524288` bits) is printed as + /// `11111111 11111110 00000000 00000001 00000000 00000000`, i.e. `ff fe 00 01 00 00`. + /// + /// The rest pin the boundaries, which is the part no end-to-end test can reach: the first is + /// `2^16 - 2^8` = 65280 rather than the obvious-but-wrong `2^16`, and the second is `2^32`, + /// which through the public API would need a 4 GiB AAD. + #[test] + fn aad_length_encoding_matches_a_2_2() { + type Mode<'k> = Ccm<'k, Identity, Encrypting, 16, 7, 4>; + + // Case 1: 0 < a < 2^16 - 2^8, two octets, `[a]_16`. + assert_eq!( + Mode::encode_aad_len(8), + ([0x00, 0x08, 0, 0, 0, 0, 0, 0, 0, 0], 2), + "C.1's a = 8" + ); + assert_eq!(Mode::encode_aad_len(1).1, 2); + // 65279 = 2^16 - 2^8 - 1 is the largest value still in the first case. + assert_eq!( + Mode::encode_aad_len(65279), + ([0xfe, 0xff, 0, 0, 0, 0, 0, 0, 0, 0], 2), + "65279 is still [a]_16" + ); + + // Case 2: 2^16 - 2^8 <= a < 2^32, six octets, `0xff || 0xfe || [a]_32`. 65280 is the first. + assert_eq!( + Mode::encode_aad_len(65280), + ([0xff, 0xfe, 0x00, 0x00, 0xff, 0x00, 0, 0, 0, 0], 6), + "65280 crosses into the six-octet case; a two-octet 0xff00 would be ambiguous" + ); + assert_eq!( + Mode::encode_aad_len(65536), + ([0xff, 0xfe, 0x00, 0x01, 0x00, 0x00, 0, 0, 0, 0], 6), + "C.4's a = 65536" + ); + // 2^32 - 1 is the largest value still in the second case. + assert_eq!( + Mode::encode_aad_len(u32::MAX as u64), + ([0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0], 6), + "2^32 - 1 is still the six-octet case" + ); + + // Case 3: 2^32 <= a < 2^64, ten octets, `0xff || 0xff || [a]_64`. + assert_eq!( + Mode::encode_aad_len(1u64 << 32), + ([0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00], 10), + "2^32 is the first ten-octet case" + ); + assert_eq!( + Mode::encode_aad_len(u64::MAX), + ([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 10) + ); + + // A.2.2's whole point: the three cases are distinguishable by their leading octets, so no + // two distinct lengths can encode to the same prefix. The first octet is 0xff only in the + // second and third cases, and the second octet separates those. + for a in [1u64, 8, 65279] { + assert_ne!(Mode::encode_aad_len(a).0[0], 0xff, "case 1 must not lead with 0xff"); + } + } + + /// The constructor really uses `Ccm::encode_aad_len`, and puts it *before* the AAD. + /// + /// With the identity permutation the CBC-MAC is `y = B0 ^ B1 ^ ... ^ Br`, so with a one-block + /// all-zero AAD the only nonzero contributions are `B0` and the length encoding. That makes the + /// encoding readable back out, which is what pins the ordering rather than just the value. + #[test] + fn the_constructor_prefixes_the_aad_with_its_length() { + type Mode<'k> = Ccm<'k, Identity, Encrypting, 16, 7, 4>; + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + // 14 zero bytes of AAD: the 2-byte length plus 14 bytes is exactly one 16-byte block, so + // there is no padding to reason about. + let identity = Identity::new(&key()).unwrap(); + let mut ccm = Mode::from_perm_with_lengths(&identity, &nonce, 14, 0).unwrap(); + ccm.absorb_aad(&[0u8; 14]); + + let b0 = Mode::format_b0(&nonce, true, 0); + let mut b1 = [0u8; 16]; + b1[..2].copy_from_slice(&14u16.to_be_bytes()); + let expected: [u8; 16] = core::array::from_fn(|i| b0[i] ^ b1[i]); + assert_eq!(*ccm.y, expected, "y must be B0 ^ B1, with B1 starting with [14]_16"); + } +} From 62ad79ef8372cc72c4a8dbe22f081479aa3253bb Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 37/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: aes: the doc examples say what they expect, and the key module docs are rewrapped The CBC, CFB8 and ECB usage examples unwrapped key construction and the cipher calls with a bare `.unwrap()`; each now carries an `.expect` message naming the thing that cannot fail. The `key` module's opening paragraph is rewrapped at the column limit after the earlier borrow-not-copy edit. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/src/cbc.rs | 14 +++++++++----- crypto/aes/src/cfb8.rs | 11 ++++++----- crypto/aes/src/hazmat/ecb.rs | 20 +++++++++++++------- crypto/aes/src/key.rs | 16 ++++++++-------- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/crypto/aes/src/cbc.rs b/crypto/aes/src/cbc.rs index d21b99c65..a682b54d2 100644 --- a/crypto/aes/src/cbc.rs +++ b/crypto/aes/src/cbc.rs @@ -99,8 +99,10 @@ //! use bouncycastle_rng::HashDRBG_SHA512; //! //! let mut rng = HashDRBG_SHA512::new_from_os(); -//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let key = AES128Key::>::from_key_material(&material).unwrap(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::>::from_key_material(&material) +//! .expect("a valid AES-128 key"); //! //! // A whole block is fine, and comes out the same length. //! let mut out = [0u8; 16]; @@ -124,11 +126,13 @@ //! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let key = AES128Key::>::from_key_material(&material).unwrap(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::>::from_key_material(&material) +//! .expect("a valid AES-128 key"); //! //! // Built from a NoPadding key, annotated as PKCS7: mismatched types. -//! let (enc, _iv) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); +//! let (enc, _iv) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).expect("an IV"); //! let _mismatched: AES_CBC_128 = enc; //! ``` //! diff --git a/crypto/aes/src/cfb8.rs b/crypto/aes/src/cfb8.rs index 397ca600d..eb55d952e 100644 --- a/crypto/aes/src/cfb8.rs +++ b/crypto/aes/src/cfb8.rs @@ -91,16 +91,17 @@ //! use bouncycastle_rng::HashDRBG_SHA512; //! //! let mut rng = HashDRBG_SHA512::new_from_os(); -//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let cfb8_key = AES128Key::::from_key_material(&material).unwrap(); -//! let cfb_key = AES128Key::::from_key_material(&material).unwrap(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let cfb8_key = AES128Key::::from_key_material(&material).expect("a valid AES-128 key"); +//! let cfb_key = AES128Key::::from_key_material(&material).expect("a valid AES-128 key"); //! let plaintext = *b"hello"; //! //! let mut data = plaintext; -//! let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut data).unwrap(); +//! let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut data).expect("encryption"); //! //! // Decrypting CFB8 output as CFB128 does not recover the plaintext. -//! cfb_key.decrypt_inplace(&iv, &mut data).unwrap(); +//! cfb_key.decrypt_inplace(&iv, &mut data).expect("decryption"); //! assert_ne!(data, plaintext); //! ``` //! diff --git a/crypto/aes/src/hazmat/ecb.rs b/crypto/aes/src/hazmat/ecb.rs index 644c52e39..6d910ed04 100644 --- a/crypto/aes/src/hazmat/ecb.rs +++ b/crypto/aes/src/hazmat/ecb.rs @@ -109,8 +109,10 @@ //! use bouncycastle_rng::HashDRBG_SHA512; //! //! let mut rng = HashDRBG_SHA512::new_from_os(); -//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let key = AES128Key::>::from_key_material(&material).unwrap(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::>::from_key_material(&material) +//! .expect("a valid AES-128 key"); //! //! // A whole block is fine, and comes out the same length. //! let mut out = [0u8; 16]; @@ -135,11 +137,14 @@ //! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let key = AES128Key::>::from_key_material(&material).unwrap(); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::>::from_key_material(&material) +//! .expect("a valid AES-128 key"); //! //! // Built from a NoPadding key, annotated as PKCS7: mismatched types. -//! let (enc, _no_iv) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); +//! let (enc, _no_iv) = +//! key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).expect("no IV to draw"); //! let _mismatched: AES_ECB_128 = enc; //! ``` //! @@ -158,8 +163,9 @@ //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::symmetric_key::FromKeyMaterial; //! -//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let key = AES128Key::>::from_key_material(&material).unwrap(); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key = AES128Key::>::from_key_material(&material).expect("a valid AES-128 key"); //! //! // Two identical blocks in... //! let mut blocks = [0x5Au8; 32]; diff --git a/crypto/aes/src/key.rs b/crypto/aes/src/key.rs index d0360f028..4a867e10c 100644 --- a/crypto/aes/src/key.rs +++ b/crypto/aes/src/key.rs @@ -5,14 +5,14 @@ //! is bound to its AES variant and its mode by the Rust type rather than by the length of a byte //! buffer and the call site. Validation and key expansion happen once, in the constructor, and //! every message under the key starts from the stored schedule (and, for GCM, the stored hash -//! subkey, or for CMAC the stored subkeys), which the workers borrow rather than copy. Using one key under two modes is two -//! `from_key_material` calls on two key types; the accidental version does not compile. A MAC -//! mode's key takes `KeyType::MACKey` material, a cipher mode's `KeyType::SymmetricCipherKey`. -//! Which workers a key hands out is decided by its mode: the symmetric pair for the stream modes -//! and the padded block modes (`CFB`, `CFB8`, `CTR`, `CBC`, `ECB`), the block pair -//! for `CBC`, the `hazmat` ECB pair for `ECB`, the AEAD pairs for `GCM`, `CCM` -//! and `CCM_Packet`, the MAC pair for `CMAC`, and for `GMAC`, whose tag is nonce-bound, an inherent -//! pair (see [`crate::gmac`]). +//! subkey, or for CMAC the stored subkeys), which the workers borrow rather than copy. Using one +//! key under two modes is two `from_key_material` calls on two key types; the accidental version +//! does not compile. A MAC mode's key takes `KeyType::MACKey` material, a cipher mode's +//! `KeyType::SymmetricCipherKey`. Which workers a key hands out is decided by its mode: the +//! symmetric pair for the stream modes and the padded block modes (`CFB`, `CFB8`, `CTR`, +//! `CBC`, `ECB`), the block pair for `CBC`, the `hazmat` ECB pair for +//! `ECB`, the AEAD pairs for `GCM`, `CCM` and `CCM_Packet`, the MAC pair for `CMAC`, and +//! for `GMAC`, whose tag is nonce-bound, an inherent pair (see [`crate::gmac`]). //! //! # Usage Examples //! From 5a73ea5b238d1e3640e95e312168aebe32d7e82e Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 38/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: ascon: AsconAEAD128Key does not need the non_camel_case_types allow The attribute was carried over from `Ascon_AEAD128

`, whose name has the underscore; the key type's does not. Assisted-by: Claude:claude-mythos-5-1 --- crypto/ascon/src/ascon_aead128.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 772aaeb36..69e019a0e 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -625,7 +625,6 @@ impl Ascon_AEAD128 { /// let plaintext = key.decrypt_detached(&nonce, b"header", &ciphertext, &tag).unwrap(); /// assert_eq!(plaintext, b"attack at dawn"); /// ``` -#[allow(non_camel_case_types)] pub struct AsconAEAD128Key { key: Secret<[u64; 2]>, } From 8d30219a3b90330d5e12644f497ba801d3195120 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 39/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: factory: the KMAC128 variant doc names the key type `MACFactory::KMAC128` told a caller wanting a customization string to construct `KMAC128` directly, which is the worker; the key object `bouncycastle_sha3::kmac::KMAC128Key` is what takes the parameters. Assisted-by: Claude:claude-mythos-5-1 --- crypto/factory/src/mac_factory.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index 47261bab5..a14f6596d 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -112,8 +112,8 @@ pub const DEFAULT_256BIT_MAC_NAME: &str = HMAC_SHA256_NAME; pub enum MACFactory { /// KMAC128 with no customization string and a 32-byte tag (NIST SP 800-185 Sec 4). /// For a customization string or a different output length, construct - /// `bouncycastle_sha3::KMAC128Key` directly -- the factory selects by name alone and has no - /// channel for those parameters. + /// `bouncycastle_sha3::kmac::KMAC128Key` directly -- the factory selects by name alone and has + /// no channel for those parameters. KMAC128(KMAC128Key), /// KMAC256 with no customization string and a 64-byte tag. See [`MACFactory::KMAC128`]. KMAC256(KMAC256Key), From f6f30973329d3a9cb97ca42502b5076b4306c867 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 40/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: hmac: the crate docs are rewrapped after the key-object edit Assisted-by: Claude:claude-mythos-5-1 --- crypto/hmac/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index 85275690e..096604382 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -3,10 +3,10 @@ //! //! This is a utility crate and is not intended to be used directly. It provides [`HmacKey`] -- the //! key object, which holds the two precomputed pad states RFC 2104 Section 4 describes and hands -//! out workers -- and [`HMAC`] -- the worker, one per message, which borrows the key it came from. Both are generic over any struct -//! that implements [`Hash`] and [`HMACParams`], the extension point through which a hash declares -//! the metadata from which the HMAC instance is built. The library provides the following concrete -//! instantiations of HMAC: +//! out workers -- and [`HMAC`] -- the worker, one per message, which borrows the key it came from. +//! Both are generic over any struct that implements [`Hash`] and [`HMACParams`], the extension +//! point through which a hash declares the metadata from which the HMAC instance is built. The +//! library provides the following concrete instantiations of HMAC: //! //! | Hash family | Instantiations | //! |-------------|------------------------------------------------------------------------------| From 5f630750d8b46fda0526a6f221aa9949a212b452 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 41/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: mem_usage_benches: the Ascon FixedNonce doc is rewrapped Assisted-by: Claude:claude-mythos-5-1 --- mem_usage_benches/src/bench_ascon_mem_usage.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mem_usage_benches/src/bench_ascon_mem_usage.rs b/mem_usage_benches/src/bench_ascon_mem_usage.rs index b8912b5b4..2b21d0185 100644 --- a/mem_usage_benches/src/bench_ascon_mem_usage.rs +++ b/mem_usage_benches/src/bench_ascon_mem_usage.rs @@ -94,7 +94,8 @@ const NONCE: [u8; NONCE_LEN] = [ /// Stands in for the caller's RNG and hands back [`NONCE`], so that the encrypting init runs /// Ascon's initialization and nothing else: a DRBG's generate would be the deepest part of that -/// chain, and a DRBG's own stack is outside what the frame remarks can see. Not a random number generator; it exists only for this harness. +/// chain, and a DRBG's own stack is outside what the frame remarks can see. Not a random number +/// generator; it exists only for this harness. struct FixedNonce; impl RNG for FixedNonce { From 2e5a18306979ff35e914f819ca7e7a8a81f4670e Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 42/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: cli: aes-ccm's module doc says "declared-length" once Assisted-by: Claude:claude-mythos-5-1 --- cli/src/aes_ccm_cmd.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/aes_ccm_cmd.rs b/cli/src/aes_ccm_cmd.rs index 6e4394ffd..d2592dc08 100644 --- a/cli/src/aes_ccm_cmd.rs +++ b/cli/src/aes_ccm_cmd.rs @@ -20,8 +20,8 @@ //! `ascon-aead128`, are the streaming alternatives. //! //! The AAD is read whole too: the key's one-shot takes it as a slice. (A.2.2 needs its length -//! before its first byte, so streaming it would need the declared-length constructor the declared-length -//! type keeps for itself.) +//! before its first byte, so streaming it would need the declared-length constructor the type +//! keeps for itself.) //! //! # The nonce is supplied, not generated //! From 234baabb0eebd3d39d711c23e4c842516708eacc Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 30 Oct 2026 23:47:44 +1100 Subject: [PATCH 43/54] From review REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md: CLAUDE.md: MACFactory is a key enum, not a MAC The factory section said every factory enum implements the trait it fronts; `MACFactory` implements the MAC role traits and hands out a `MACFactoryWorker`, which is the `MAC`. Assisted-by: Claude:claude-mythos-5-1 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index ef2b020d6..17c5b7bdf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,7 @@ The workspace has four top-level kinds of member: - `crypto/core` defines the abstract traits (`Hash`, `KDF`, `MAC`, `KEM`, `Signature`, `RNG`, `Algorithm`, `HashAlgParams`, …), error enums (`HashError`, `KDFError`, `KEMError`, `MACError`, `RNGError`, `SignatureError`), and the `KeyMaterial` / `KeyType` wrapper that all sensitive byte buffers are required to use (see `Secret` super-trait requirement in QUALITY_AND_STYLE.md). - `crypto/core-test-framework` contains the shared per-trait test suite (`hash.rs`, `kdf.rs`, `kem.rs`, `mac.rs`, `signature.rs`). New implementations of a core trait must be exercised through this framework — it's how trait conformance and error-condition coverage stay consistent across implementations. -- `crypto/factory` provides enum-based string-name factories (`HashFactory`, `KDFFactory`, `MACFactory`, `RNGFactory`, `XOFFactory`). Each factory enum impls the underlying trait so it can be used transparently as that primitive, and each impls `AlgorithmFactory` (`new(name)`, `default_128_bit()`, `default_256_bit()`). When adding a new primitive that fits an existing trait, register it in the corresponding factory. +- `crypto/factory` provides enum-based string-name factories (`HashFactory`, `KDFFactory`, `MACFactory`, `RNGFactory`, `XOFFactory`). Each factory enum impls the underlying trait so it can be used transparently as that primitive, except `MACFactory`, which is a key enum implementing the MAC role traits and handing out a `MACFactoryWorker` that impls `MAC`; and each impls `AlgorithmFactory` (`new(name)`, `default_128_bit()`, `default_256_bit()`). When adding a new primitive that fits an existing trait, register it in the corresponding factory. Trait/factory/CLI is the standard layering: a new algorithm typically requires (a) the primitive crate, (b) implementing the relevant `core` trait, (c) wiring it into the matching factory, (d) a CLI subcommand in `cli/src/*_cmd.rs` registered in `cli/src/main.rs`. From 404d016fa092e6e61988cc3567f370dc2a8a6ce3 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 00:10:36 +1100 Subject: [PATCH 44/54] From review REVIEW-2026-10-30-PR172-correctness-mythos.md: sha3: KMAC's output length has SP 800-185's floor and a cap, at construction and on resume C1 and C3. with_params and with_params_allow_weak accepted any output length: at 0 a zero-length tag verified every message, since do_verify_final compares only against the stored length, and above 2^61 the right_encode argument 8 * L overflowed its u64 at the final. Both constructors now refuse a length below KMAC_MIN_OUTPUT_LEN, 4 bytes, SP 800-185 Sec 8.4.2's "shall not select an output length L that is less than 32 bits", and above KMAC_MAX_OUTPUT_LEN, u64::MAX / 8, with MACError::InvalidLength. The check sits on the two public MAC constructors, not the shared key binding: KMACXOF is the L = 0 form of Sec 4.3.1 and builds its key through the same binding. A suspended KMAC state claiming a length outside the bounds is refused on resume, where it was previously read up to usize::MAX. kmac_tests pins the refusals on both constructors, that the floor is the shortest tag and never verifies an empty one, and both ends of the cap; sp800_185_suspend_tests pins the resume bounds and both edges. A scoped mutants run on kmac.rs: 142 tested, 78 caught, 57 unviable, 6 missed, all of them pre-existing (the generate strength comparison, the provided mac and verify bodies). Assisted-by: Claude:claude-mythos-5-1 --- crypto/sha3/src/kmac.rs | 37 +++++++++++++++++-- crypto/sha3/tests/kmac_tests.rs | 38 +++++++++++++++++++- crypto/sha3/tests/sp800_185_suspend_tests.rs | 20 ++++++++++- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs index 9e0e12a2d..726740555 100644 --- a/crypto/sha3/src/kmac.rs +++ b/crypto/sha3/src/kmac.rs @@ -78,6 +78,13 @@ pub const KMACXOF128_NAME: &str = "KMACXOF128"; /// The name of the KMACXOF256 algorithm (NIST SP 800-185 Sec 4.3.1). pub const KMACXOF256_NAME: &str = "KMACXOF256"; +/// The shortest KMAC output, in bytes. SP 800-185 Sec 8.4.2: applications "shall not select an +/// output length L that is less than 32 bits". +pub const KMAC_MIN_OUTPUT_LEN: usize = 4; +/// The longest KMAC output, in bytes: `8 * n` is what `right_encode` absorbs (Sec 4.3 step 1), +/// and it is a `u64` here. +pub const KMAC_MAX_OUTPUT_LEN: usize = (u64::MAX / 8) as usize; + /// KMAC128: the Keccak MAC of NIST SP 800-185 Sec 4, at a 128-bit security strength. /// /// The worker a [`KMAC128Key`] hands out: `from_key_material` gives the common case -- no @@ -132,14 +139,16 @@ impl KMACKeyInternal { const NOMINAL_OUTPUT_LEN: usize = (PARAMS::SIZE as usize) / 4; /// A key for KMAC under a customization string and an output length of the caller's - /// choosing. `output_len` is `L` in bytes and is bound into the computation, so it must be - /// the length the verifier will use; `customization` may be empty. + /// choosing. `output_len` is `L` in bytes, from [`KMAC_MIN_OUTPUT_LEN`] to + /// [`KMAC_MAX_OUTPUT_LEN`], and is bound into the computation, so it must be the length the + /// verifier will use; `customization` may be empty. /// /// Sec 8.4.1 requires the key to be at least as long as the security strength for approved /// use; that is enforced through the key's [`SecurityStrength`] tag, exactly as HMAC does, /// and [`Self::with_params_allow_weak`] is the escape hatch. /// /// # Errors + /// [`MACError::InvalidLength`] if `output_len` is outside the bounds above; /// [`MACError::KeyMaterialError`] if the key is not tagged as a MAC key, or is tagged below /// this KMAC's security strength. pub fn with_params( @@ -147,21 +156,38 @@ impl KMACKeyInternal { customization: &[u8], output_len: usize, ) -> Result { + Self::check_output_len(output_len)?; Self::with_params_checked(key, customization, output_len, false) } /// As [`with_params`](Self::with_params) without the strength check. /// /// # Errors + /// [`MACError::InvalidLength`] if `output_len` is outside the bounds; /// [`MACError::KeyMaterialError`] if the key is not tagged as a MAC key. pub fn with_params_allow_weak( key: &dyn KeyMaterialTrait, customization: &[u8], output_len: usize, ) -> Result { + Self::check_output_len(output_len)?; Self::with_params_checked(key, customization, output_len, true) } + /// The MAC's bounds on `L`. KMACXOF is the `L = 0` form (Sec 4.3.1) and does not pass here. + fn check_output_len(output_len: usize) -> Result<(), MACError> { + if output_len < KMAC_MIN_OUTPUT_LEN { + return Err(MACError::InvalidLength( + "KMAC output length is below the 32-bit floor of SP 800-185 Sec 8.4.2", + )); + } + if output_len > KMAC_MAX_OUTPUT_LEN { + return Err(MACError::InvalidLength("KMAC output length in bits does not fit a u64")); + } + Ok(()) + } + + /// The key binding shared with KMACXOF, which passes `output_len == 0`. fn with_params_checked( key: &dyn KeyMaterialTrait, customization: &[u8], @@ -310,7 +336,12 @@ impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for KMACInternal let (cshake, rest) = state.split_at(CSHAKE_COMPONENT_LEN); let cshake = CSHAKEInternal::read_tagged_customized(cshake, PARAMS::KMAC_STATE_TAG)?; let mut r = Cursor::new(rest); - let output_len = bounded_usize(r.u64(), usize::MAX)?; + // The bounds `with_params` applies: a state claiming a length outside them was never + // written by this type. + let output_len = bounded_usize(r.u64(), KMAC_MAX_OUTPUT_LEN)?; + if output_len < KMAC_MIN_OUTPUT_LEN { + return Err(SuspendableError::InvalidData); + } debug_assert!(r.is_done()); // The strength is fixed by the parameter set (see `with_params`), not stored. Ok(Self { diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs index 281e8b6a6..808815528 100644 --- a/crypto/sha3/tests/kmac_tests.rs +++ b/crypto/sha3/tests/kmac_tests.rs @@ -5,7 +5,10 @@ use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromAnyKeyMaterial; -use bouncycastle_sha3::kmac::{KMAC128, KMAC128Key, KMAC256, KMAC256Key, KMACXOF128, KMACXOF256}; +use bouncycastle_sha3::kmac::{ + KMAC_MAX_OUTPUT_LEN, KMAC_MIN_OUTPUT_LEN, KMAC128, KMAC128Key, KMAC256, KMAC256Key, KMACXOF128, + KMACXOF256, +}; /// A 32-byte MAC key carries a 256-bit strength, so it satisfies both KMAC128 and KMAC256 without /// the weak-key escape hatch. @@ -254,3 +257,36 @@ fn kmacxof_hash_view_partial_bits() { Err(bouncycastle_core::errors::HashError::InvalidLength(_)) )); } + +/// SP 800-185 Sec 8.4.2's floor and the `right_encode` cap, on both constructors: below the floor +/// a zero-length tag would verify every message, above the cap `8 * L` does not fit the `u64`. +#[test] +fn output_length_is_bounded() { + let key = key_material(&[0x42; 32]); + assert_eq!(KMAC_MIN_OUTPUT_LEN, 4); + for n in 0..KMAC_MIN_OUTPUT_LEN { + assert!( + matches!(KMAC128Key::with_params(&key, b"", n), Err(MACError::InvalidLength(_))), + "an output length of {n} must be refused" + ); + assert!( + matches!( + KMAC256Key::with_params_allow_weak(&key, b"", n), + Err(MACError::InvalidLength(_)) + ), + "an output length of {n} must be refused by the weak-key form too" + ); + } + let shortest = KMAC128Key::with_params(&key, b"", KMAC_MIN_OUTPUT_LEN).unwrap(); + assert_eq!(shortest.mac(b"abc").len(), KMAC_MIN_OUTPUT_LEN); + assert!(!shortest.create_verifier().verify(b"abc", &[]), "an empty tag never verifies"); + + // Construction only: the length is absorbed at the final, nothing is allocated for it here. + assert!(KMAC128Key::with_params(&key, b"", KMAC_MAX_OUTPUT_LEN).is_ok()); + if let Some(over) = KMAC_MAX_OUTPUT_LEN.checked_add(1) { + assert!(matches!( + KMAC128Key::with_params(&key, b"", over), + Err(MACError::InvalidLength(_)) + )); + } +} diff --git a/crypto/sha3/tests/sp800_185_suspend_tests.rs b/crypto/sha3/tests/sp800_185_suspend_tests.rs index 76c516a36..3b8c79c57 100644 --- a/crypto/sha3/tests/sp800_185_suspend_tests.rs +++ b/crypto/sha3/tests/sp800_185_suspend_tests.rs @@ -198,9 +198,27 @@ fn corrupt_fields_are_rejected() { let mut k = k_key.create_generator(); k.do_update(PART1); - let mut bad = k.suspend(); + let good = k.suspend(); + let mut bad = good; bad[CUSTOMIZED] = 0; rejected::<_, KMAC128>(bad, "a KMAC claiming an empty function name"); + // The output length is the trailing u64, bounded as `with_params` bounds it. + let output_len = good.len() - 8; + let mut bad = good; + bad[output_len..].copy_from_slice(&0u64.to_le_bytes()); + rejected::<_, KMAC128>(bad, "a KMAC claiming a zero output length"); + let mut bad = good; + bad[output_len..].copy_from_slice(&3u64.to_le_bytes()); + rejected::<_, KMAC128>(bad, "a KMAC claiming an output length below the 32-bit floor"); + let mut bad = good; + bad[output_len..].copy_from_slice(&(u64::MAX / 4).to_le_bytes()); + rejected::<_, KMAC128>(bad, "a KMAC whose output length in bits overflows a u64"); + let mut edge = good; + edge[output_len..].copy_from_slice(&(KMAC_MIN_OUTPUT_LEN as u64).to_le_bytes()); + assert!(KMAC128::from_suspended(edge).is_ok(), "the floor itself is a valid output length"); + let mut edge = good; + edge[output_len..].copy_from_slice(&(KMAC_MAX_OUTPUT_LEN as u64).to_le_bytes()); + assert!(KMAC128::from_suspended(edge).is_ok(), "the cap itself is a valid output length"); let mut k = KMACXOF128::new(&key(), b"", false).unwrap(); k.do_update(PART1); From 496e3ad4e0c84e7b7103e251633a2887eca9bafd Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 00:10:36 +1100 Subject: [PATCH 45/54] From review REVIEW-2026-10-30-PR172-correctness-mythos.md: hkdf: the entropy counter saturates, and resume refuses a value usize cannot hold C5. A resumed state carried its entropy counter straight into HkdfEntropyTracker, and the next credit_entropy did a plain add, which overflows on a counter set to u64::MAX. The counter is bookkeeping, so the add is now saturating, and read_state reads it through bounded_usize, which refuses a value a narrower usize would have truncated. hkdf_tests pins the saturating resume. Scoped mutants on lib.rs: 136 tested, 99 caught, 37 unviable, none missed. Assisted-by: Claude:claude-mythos-5-1 --- crypto/hkdf/src/lib.rs | 15 +++++++++------ crypto/hkdf/tests/hkdf_tests.rs | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index 017b5bf45..5f59212d0 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -116,7 +116,7 @@ use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; use bouncycastle_core::traits::{Hash, HashAlgParams, KDF, MAC, Suspendable, SuspendableKeyed}; use bouncycastle_hmac::HmacKey; use bouncycastle_hmac::hazmat as hmac_hazmat; -use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_utils::suspendable_state::{add_lib_ver, bounded_usize, check_lib_ver}; use bouncycastle_utils::{max, min}; use std::marker::PhantomData; // Imports needed only for docs @@ -230,7 +230,8 @@ impl HkdfEntropyTracker { /// Returns the amount of entropy credited. fn credit_entropy(&mut self, key: &impl KeyMaterialTrait) -> usize { let additional_entropy = if key.is_full_entropy() { key.key_len() } else { 0 }; - self.entropy += additional_entropy; + // Saturating: the counter is bookkeeping, and a resumed state may carry any value. + self.entropy = self.entropy.saturating_add(additional_entropy); self.security_strength = max(&self.security_strength, &key.security_strength()).clone(); self.security_strength = min(&self.security_strength, &SecurityStrength::from_bytes(H::OUTPUT_LEN / 2)).clone(); @@ -817,10 +818,12 @@ where return Err(SuspendableError::InvalidData); } - // infallible: the sub-slice is exactly 8 bytes by const construction. - let entropy = - u64::from_le_bytes(state[5 + HASH_STATE_LEN..13 + HASH_STATE_LEN].try_into().unwrap()) - as usize; + // infallible: the sub-slice is exactly 8 bytes by const construction. The counter is + // refused rather than truncated where `usize` is narrower than the encoding. + let entropy = bounded_usize( + u64::from_le_bytes(state[5 + HASH_STATE_LEN..13 + HASH_STATE_LEN].try_into().unwrap()), + usize::MAX, + )?; let security_strength = SecurityStrength::try_from(state[13 + HASH_STATE_LEN])?; Ok(HKDF { diff --git a/crypto/hkdf/tests/hkdf_tests.rs b/crypto/hkdf/tests/hkdf_tests.rs index 82c46db2f..e36c0767b 100644 --- a/crypto/hkdf/tests/hkdf_tests.rs +++ b/crypto/hkdf/tests/hkdf_tests.rs @@ -828,6 +828,30 @@ mod hkdf_tests { )); } + /// The entropy counter is bookkeeping a resumed state may set to anything: the next credit + /// saturates rather than overflowing. + #[test] + fn a_resumed_entropy_counter_saturates() { + use bouncycastle_core::traits::SuspendableKeyed; + use bouncycastle_sha2::hkdf::SUSPENDED_HKDF_SHA256_STATE_LEN; + + let salt = KeyMaterial128::from_bytes_as_type(&DUMMY_SEED[..16], KeyType::MACKey).unwrap(); + let full_entropy = + KeyMaterial128::from_bytes_as_type(&DUMMY_SEED[16..32], KeyType::CryptographicRandom) + .unwrap(); + let mut hkdf = HKDF_SHA256::new(); + hkdf.do_extract_init(&salt).unwrap(); + let mut state = hkdf.suspend(); + // Layout: [version(3) | present flag | hmac blob | state | entropy(8) | strength]. + let entropy_idx = SUSPENDED_HKDF_SHA256_STATE_LEN - 9; + state[entropy_idx..entropy_idx + 8].copy_from_slice(&u64::MAX.to_le_bytes()); + + let mut resumed = HKDF_SHA256::from_suspended(state, &salt).unwrap(); + assert_eq!(resumed.get_entropy(), usize::MAX); + assert_eq!(resumed.do_extract_update_key(&full_entropy).unwrap(), 16); + assert_eq!(resumed.get_entropy(), usize::MAX); + } + /// RFC 5869 Sec 2.3: "OKM = first L octets of T", for any L up to 255 * HashLen, so a shorter /// output is a prefix of a longer one. Every L from 0 to 200 covers each value of L mod HashLen /// for both hashes: the last block used to be produced as an HMAC truncated to the remaining From ee608a00c3b24a12f09ea9953dba164b09e1805f Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 00:10:36 +1100 Subject: [PATCH 46/54] From review REVIEW-2026-10-30-PR172-correctness-mythos.md: cipher: CFB's open segment is a Secret, and the GCM nonce and CCM_Packet one-shot docs say what the code does C6. Cfb kept the unused tail of O_j, keystream that gives away the next plaintext bytes against their ciphertext, in a plain array, with a comment saying it never held key material; it is now a Secret, read straight into on resume, and the comment says why. C7. The GCM module doc said the nonce is never accepted from the caller; create_encryptor_nonce, the _nonce one-shots and from_key_state_nonce take one, and the doc now names them. The CcmPacketEncryptor and CcmPacketDecryptor docs said the one-shots were Ccm's own and took any length; a key bound to CCM_Packet provides them over the packet workers, so they carry the frame, which CCM_Packet's own doc already said. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/ccm.rs | 19 +++++++++---------- crypto/cipher/src/modes/cfb.rs | 29 +++++++++++++++++------------ crypto/cipher/src/modes/gcm.rs | 5 +++-- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 4b08bac6f..2a4e7d57b 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -896,10 +896,9 @@ where /// `FINAL_LEN` is `TAG_LEN`, exactly as for GCM. The price is that they accept exactly /// `DATA_LEN` bytes of payload -- the fixed frame of SP 800-38C Sec 3's "packet environment" -- /// and refuse any other amount, more at the update that would exceed it and less at the final. -/// The one-shots are not bound by either parameter: with the whole message in hand there is -/// nothing to declare up front and nothing to hold back, so they are `Ccm`'s own one-shots -/// under a generated nonce, for a payload and AAD of any length, and the AAD buffer is never -/// built. `DATA_LEN` and `AAD_LEN` govern the streaming methods only. +/// The one-shots a key bound to [`CCM_Packet`] provides are written over this worker, so they +/// carry the same frame: a payload of exactly `DATA_LEN` bytes and up to `AAD_LEN` of AAD. A +/// message of another length goes through [`CCM`], whose constructors take the lengths. /// /// `AAD_LEN` is a capacity, not an exact length: the trait's AAD is optional and open-ended, and /// the inherited [`SymmetricCipherEncryptor`] methods are this AEAD with none at all. Up to @@ -1252,8 +1251,8 @@ where /// ciphertext and are decrypted and released by the call that brings them; anything after them /// can only be an inline tag (Sec 6.2 step 6's `LSB_Tlen(C)`), and only those bytes -- at most /// `TAG_LEN` -- are held back for the final to check. A `C` of any other length than `DATA_LEN` -/// (detached) or `DATA_LEN + TAG_LEN` (inline) is refused as malformed. The one-shots are -/// `Ccm`'s own and take a `C` of any length; see [`CcmPacketEncryptor`]. +/// (detached) or `DATA_LEN + TAG_LEN` (inline) is refused as malformed, by the one-shots of a key +/// bound to [`CCM_Packet`] as much as by the streaming methods; see [`CcmPacketEncryptor`]. /// /// # 🚨 Security Considerations 🚨 /// @@ -1415,15 +1414,15 @@ where Ok(([0u8; TAG_LEN], 0)) } - /// `C` less its inline tag, as for any AEAD: the one-shots take a `C` of any length. The - /// streaming side's own bound is [`do_decrypt_out_len`](Self::do_decrypt_out_len). + /// `C` less its inline tag, as for any AEAD; the frame's own bound is + /// [`do_decrypt_out_len`](Self::do_decrypt_out_len). fn decrypt_out_len(ciphertext_len: usize) -> usize { ciphertext_len.saturating_sub(TAG_LEN) } } -/// The AEAD view, with `FINAL_LEN = TAG_LEN`. The one-shots are `Ccm`'s own, for a `C` and -/// AAD of any length; see [`CcmPacketEncryptor`]. +/// The AEAD view, with `FINAL_LEN = TAG_LEN`; the one-shots over it carry the frame, see +/// [`CcmPacketEncryptor`]. impl< 'k, P, diff --git a/crypto/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs index 348e1d954..36213f44c 100644 --- a/crypto/cipher/src/modes/cfb.rs +++ b/crypto/cipher/src/modes/cfb.rs @@ -156,6 +156,7 @@ use bouncycastle_core::traits::{ Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; +use bouncycastle_utils::secret::Secret; use bouncycastle_utils::suspendable_state::{ Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, suspend_component, @@ -251,9 +252,9 @@ where // into `O_{j+1}`. So the same 16 bytes are the input block, then the output block, then the // next input block, and no copy is ever made. // - // Since `buf` holds either plaintext or ciphertext, but never any keymaterial, it is not - // necessary to tag it as `Secret<>`. - buf: [u8; BLOCK_LEN], + // `buf[used..]` is keystream, which gives away the next `BLOCK_LEN - used` plaintext bytes + // against their ciphertext, so the buffer is a `Secret`. + buf: Secret<[u8; BLOCK_LEN]>, /// Bytes of the current segment already processed, `0..=BLOCK_LEN`. used: usize, _dir: PhantomData, @@ -270,7 +271,9 @@ where /// `I1 = IV`, with no segment open: the first byte in either direction will compute `O1`. #[inline] fn start(perm: &'k P, iv: [u8; BLOCK_LEN]) -> Self { - Self { perm, buf: iv, used: BLOCK_LEN, _dir: PhantomData } + let mut buf: Secret<[u8; BLOCK_LEN]> = Secret::new(); + *buf = iv; + Self { perm, buf, used: BLOCK_LEN, _dir: PhantomData } } /// Makes the next keystream byte available: if the current segment is complete, `buf` is the @@ -329,7 +332,7 @@ where *b ^= *o; } // I_{j+1} = Cj. Serial: this is the input to the next cipher call. - self.buf = *block; + *self.buf = *block; } /// Decrypts one whole block at a segment boundary. `Cj` is overwritten by `Pj`, so it is copied @@ -342,7 +345,7 @@ where for (b, o) in block.iter_mut().zip(self.buf.iter()) { *b ^= *o; } - self.buf = cj; + *self.buf = cj; } /// Decrypts two consecutive blocks with one [`BlockPermutation::encrypt_2blocks`] call. @@ -366,11 +369,11 @@ where fn decrypt_pair(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { debug_assert_eq!(self.used, BLOCK_LEN, "the block path needs a segment boundary"); // The two input blocks, constructed in series: Ij (already held) and Ij+1 (= Cj). - let mut o = [self.buf, blocks[0]]; + let mut o = [*self.buf, blocks[0]]; self.perm.encrypt_2blocks(&mut o); // I_{j+2} = Cj+1, read before the XOR below turns it into Pj+1. - self.buf = blocks[1]; + *self.buf = blocks[1]; for (block, o) in blocks.iter_mut().zip(o.iter()) { for (b, o) in block.iter_mut().zip(o.iter()) { @@ -388,9 +391,9 @@ where #[inline] fn decrypt_four(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 4]) { debug_assert_eq!(self.used, BLOCK_LEN, "the block path needs a segment boundary"); - let mut o = [self.buf, blocks[0], blocks[1], blocks[2]]; + let mut o = [*self.buf, blocks[0], blocks[1], blocks[2]]; self.perm.encrypt_4blocks(&mut o); - self.buf = blocks[3]; + *self.buf = blocks[3]; for (block, o) in blocks.iter_mut().zip(o.iter()) { for (b, o) in block.iter_mut().zip(o.iter()) { *b ^= *o; @@ -577,7 +580,7 @@ where fn write_state(&self, out: &mut [u8]) { let mut w = CursorMut::new(out); - w.bytes(&self.buf); + w.bytes(&*self.buf); w.u64(self.used as u64); debug_assert!(w.is_done()); } @@ -585,7 +588,9 @@ where fn read_state(state: &[u8], key: &'k Self::Key) -> Result { let perm = key; let mut r = Cursor::new(state); - let buf = r.array::(); + // Read straight into the `Secret`, so no copy of the keystream tail sits on the stack. + let mut buf: Secret<[u8; BLOCK_LEN]> = Secret::new(); + (*buf).copy_from_slice(r.bytes(BLOCK_LEN)); // `used` is `0..=BLOCK_LEN`; anything past the buffer would index out of it. let used = bounded_usize(r.u64(), BLOCK_LEN)?; debug_assert!(r.is_done()); diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index 1354230b1..e3449dabc 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -161,8 +161,9 @@ //! ## Nonce uniqueness //! //! As with all symmetric cipher modes, repeated key and nonce for multiple messages is catastrophic -//! for security, which is why the nonce is always drawn from the library's default RNG or a provided -//! RNG and never accepted from the caller. +//! for security, which is why the key's `create_encryptor` draws the nonce from the caller's RNG. +//! The forms that take a supplied nonce, `create_encryptor_nonce` and the `_nonce` one-shots on +//! the key and [`Gcm::from_key_state_nonce`] here, leave uniqueness to the caller. //! //! ## Invocation limit //! From 778382d5303a5c24f5e59b03a87dc9c7a016d40c Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 00:10:36 +1100 Subject: [PATCH 47/54] From review REVIEW-2026-10-30-PR172-correctness-mythos.md: cli: the KMAC tag length help gives the floor, and the construction failure names the length Assisted-by: Claude:claude-mythos-5-1 --- cli/src/mac_cmd.rs | 4 ++-- cli/src/main.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index 66e6d3b3c..112cebd8d 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -105,14 +105,14 @@ pub(crate) fn kmac_cmd( match bit_len { 128 => do_mac( KMAC128Key::with_params_allow_weak(&key, s, length) - .expect("a valid MAC key") + .expect("a valid MAC key and tag length") .create_generator(), verify_val, output_hex, ), 256 => do_mac( KMAC256Key::with_params_allow_weak(&key, s, length) - .expect("a valid MAC key") + .expect("a valid MAC key and tag length") .create_generator(), verify_val, output_hex, diff --git a/cli/src/main.rs b/cli/src/main.rs index 1377e4b33..548c4143e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1346,7 +1346,7 @@ enum Subcommands { /// The tag length and customization string are bound into the computation, so the verifier /// must use the same values. KMAC128 { - /// Length of the tag in bytes. + /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). length: usize, #[arg(short = 's', long)] @@ -1373,7 +1373,7 @@ enum Subcommands { /// Compute or verify a KMAC256 (NIST SP 800-185 Sec 4) over the content provided on stdin. /// See kmac128. KMAC256 { - /// Length of the tag in bytes. + /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). length: usize, #[arg(short = 's', long)] From 8b71109dcb7e79c26b303ffcfd909aeffb2df917 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 14:37:01 +1100 Subject: [PATCH 48/54] From review REVIEW-2026-10-31-PR172-fix-verification-mythos.md: cli: a refused KMAC tag length is reported, not panicked on The kmac128 and kmac256 commands built their key with an expect, so a tag length the key refuses, below the 32-bit floor or too large for a u64 of bits, exited through a panic trace rather than the error route the hex and key-file errors take. The construction now reports the refusal with its reason and exits as those do. test_kmac.sh covers the round trip for both widths, the customization string and tag length being bound into the tag, and the two refused lengths on compute and on verify. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/mac_cmd.rs | 32 ++++++++++-------- cli/tests/test_kmac.sh | 76 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 14 deletions(-) create mode 100755 cli/tests/test_kmac.sh diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index 112cebd8d..819f83e9c 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -3,6 +3,7 @@ use std::io::Read; use std::process::exit; use crate::helpers::read_from_file_raw; +use bouncycastle::core::errors::MACError; use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; @@ -103,24 +104,27 @@ pub(crate) fn kmac_cmd( // The allow-weak form, as the HMAC commands use: a CLI is used for test vectors and // scripting, where a short or all-zero key is a legitimate thing to want. match bit_len { - 128 => do_mac( - KMAC128Key::with_params_allow_weak(&key, s, length) - .expect("a valid MAC key and tag length") - .create_generator(), - verify_val, - output_hex, - ), - 256 => do_mac( - KMAC256Key::with_params_allow_weak(&key, s, length) - .expect("a valid MAC key and tag length") - .create_generator(), - verify_val, - output_hex, - ), + 128 => { + let kmac_key = KMAC128Key::with_params_allow_weak(&key, s, length) + .unwrap_or_else(|e| kmac_key_error(e)); + do_mac(kmac_key.create_generator(), verify_val, output_hex) + } + 256 => { + let kmac_key = KMAC256Key::with_params_allow_weak(&key, s, length) + .unwrap_or_else(|e| kmac_key_error(e)); + do_mac(kmac_key.create_generator(), verify_val, output_hex) + } _ => panic!("Unsupported algorithm: KMAC-{bit_len}"), } } +/// The key is already a MAC key, so what fails here is the tag length, which came from the +/// command line: report it the way the key errors are reported rather than through a panic. +fn kmac_key_error(e: MACError) -> ! { + eprintln!("Error: couldn't build the KMAC key: {e:?}"); + exit(-1) +} + fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { // read the content to be MAC'd from stdin let mut buf: [u8; 1024] = [0u8; 1024]; diff --git a/cli/tests/test_kmac.sh b/cli/tests/test_kmac.sh new file mode 100755 index 000000000..4c95e26c9 --- /dev/null +++ b/cli/tests/test_kmac.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# The kmac128 / kmac256 subcommands, end to end through the binary: a MAC computed over random +# data under a random key must verify with `-v`, must stop verifying when the tag, the message, +# the key, the customization string or the tag length changes, and a tag length the key refuses +# must be reported as an error rather than through a panic. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# `kmac_round_trip SUBCOMMAND` runs the round trip for one variant at a 32-byte tag. +kmac_round_trip() { + local sub=$1 tag + rng 32 >"$TMP/key" + rng 32 >"$TMP/other_key" + rng 100 >"$TMP/msg" + cp "$TMP/msg" "$TMP/other_msg" + flip_byte "$TMP/other_msg" 50 + + tag=$(hex_out "$BC_RUST" "$sub" 32 --key-file "$TMP/key" -x <"$TMP/msg") + assert_eq "${#tag}" 64 "$sub tag length" + + expect_ok "the tag must verify" "$BC_RUST" "$sub" 32 --key-file "$TMP/key" -v "$tag" <"$TMP/msg" + expect_fail "a modified tag must not verify" \ + "$BC_RUST" "$sub" 32 --key-file "$TMP/key" -v "$(flip_hex "$tag" 0 1)" <"$TMP/msg" + expect_fail "a modified message must not verify" \ + "$BC_RUST" "$sub" 32 --key-file "$TMP/key" -v "$tag" <"$TMP/other_msg" + expect_fail "another key must not verify" \ + "$BC_RUST" "$sub" 32 --key-file "$TMP/other_key" -v "$tag" <"$TMP/msg" + expect_fail "another customization string must not verify" \ + "$BC_RUST" "$sub" 32 -s other --key-file "$TMP/key" -v "$tag" <"$TMP/msg" + expect_fail "another tag length must not verify" \ + "$BC_RUST" "$sub" 16 --key-file "$TMP/key" -v "${tag:0:32}" <"$TMP/msg" +} + +# `kmac_refused_length SUBCOMMAND LENGTH` checks that LENGTH is refused with an error message, +# not a panic trace, whether computing or verifying. +kmac_refused_length() { + local sub=$1 length=$2 + rng 32 >"$TMP/key" + rng 100 >"$TMP/msg" + + expect_fail "tag length $length must be refused" \ + "$BC_RUST" "$sub" "$length" --key-file "$TMP/key" -x <"$TMP/msg" + assert_stderr_has "Error: couldn't build the KMAC key" + ! grep -q panicked "$LAST_STDERR" || fail "a refused tag length must not panic" + + expect_fail "tag length $length must be refused on verify" \ + "$BC_RUST" "$sub" "$length" --key-file "$TMP/key" -v "" <"$TMP/msg" + assert_stderr_has "Error: couldn't build the KMAC key" + ! grep -q panicked "$LAST_STDERR" || fail "a refused tag length must not panic" +} + +test_kmac128_round_trip() { + kmac_round_trip kmac128 +} + +test_kmac256_round_trip() { + kmac_round_trip kmac256 +} + +test_kmac128_refuses_a_short_tag() { + kmac_refused_length kmac128 0 +} + +test_kmac256_refuses_a_short_tag() { + kmac_refused_length kmac256 3 +} + +test_kmac128_refuses_a_tag_that_does_not_fit() { + kmac_refused_length kmac128 9223372036854775808 +} + +test_kmac256_refuses_a_tag_that_does_not_fit() { + kmac_refused_length kmac256 9223372036854775808 +} + +run_all From 096bf63f89894fc68eebdf98540c227705c28448 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 14:54:44 +1100 Subject: [PATCH 49/54] From review REVIEW-2026-10-31-PR170-correctness-mythos.md: cipher: a resumed CCM packet state owes no AAD P7. CcmAdapter::read_state bounded the payload still owed by the frame but left aad_owed at whatever the state carried, once the header was formatted. The packet pair declares and absorbs its AAD in one step in begin_data, so a real state never owes any, before the header or after it; a resumed state with a non-zero count is now refused in both directions. The Ccm component's own read is unchanged, since the any-length workers it also serves do not suspend. ccm_suspend_tests pins the formatted case alongside the unformatted one it already had. Scoped mutants on the adapter's read_state: 11 caught, none missed. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/ccm.rs | 7 ++++--- crypto/cipher/tests/ccm_suspend_tests.rs | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 2a4e7d57b..13e5f7ffa 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -1974,9 +1974,10 @@ where _ => return Err(SuspendableError::InvalidData), }; debug_assert!(r.is_done()); - // `B0` commits to `DATA_LEN`, so no more than that can still be owed; and until the - // header is formatted the CBC-MAC has absorbed nothing and no AAD is owed to it. - if ccm.owed > DATA_LEN || (!formatted && (ccm.mac_pos != 0 || ccm.aad_owed != 0)) { + // `B0` commits to `DATA_LEN`, so no more than that can still be owed. No AAD is ever + // owed: until the header is formatted the CBC-MAC has absorbed nothing, and `begin_data` + // declares the AAD and absorbs all of it in one step. + if ccm.owed > DATA_LEN || ccm.aad_owed != 0 || (!formatted && ccm.mac_pos != 0) { return Err(SuspendableError::InvalidData); } Ok(Self { ccm, aad, aad_len, formatted }) diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index dc326acfa..7e1cb0afb 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -4,8 +4,8 @@ //! The round trips in `suspend_tests.rs` show that a faithful state resumes. These show that an //! unfaithful one does not, which is the other half of the contract and the half that pins each //! check individually: with the flags octet, the counter field, the counter index, the CBC-MAC -//! position, the owed payload, the AAD length and the phase flag all validated, a test that -//! corrupts several at once would still pass if any one check were dropped. +//! position, the owed payload, the owed AAD, the AAD length and the phase flag all validated, a +//! test that corrupts several at once would still pass if any one check were dropped. //! //! The offsets are those of the layout the `SuspendableComponent` impls write, in order: the //! library version, the `Ccm` state (the keystream's counter template and counter index, the @@ -185,6 +185,17 @@ fn an_unformatted_state_has_absorbed_nothing() { assert_eq!(mid_payload()[FORMATTED], 1); } +/// `aad_owed` is the AAD still to come after the header declared it, and `begin_data` declares +/// and absorbs the AAD in one step, so a packet worker never holds a non-zero count: not before +/// the header (the test above) and not after it either. +#[test] +fn no_aad_is_owed_once_the_header_is_formatted() { + let state = mid_payload(); + assert_eq!(state[FORMATTED], 1); + assert_eq!(resumes(with_u64(state, AAD_OWED, 1)), Err(SuspendableError::InvalidData)); + assert_eq!(resumes(with_u64(state, AAD_OWED, u64::MAX)), Err(SuspendableError::InvalidData)); +} + /// The held-back AAD can be no longer than the buffer that holds it. #[test] fn the_aad_length_is_held_to_its_capacity() { @@ -211,6 +222,7 @@ fn the_decrypting_direction_checks_the_same_bounds() { assert!(resumes(state).is_ok()); assert!(resumes(with_u64(state, OWED, DATA_LEN as u64 + 1)).is_err()); + assert!(resumes(with_u64(state, AAD_OWED, 1)).is_err()); assert!(resumes(with_u64(state, NEXT_CTR, 0)).is_err()); assert!(resumes(with_u64(state, TAG_LEN_AT, 16)).is_ok()); assert!(resumes(with_u64(state, TAG_LEN_AT, 17)).is_err()); From 0142fac5d52698409f6b3516a06b5517048ede49 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 14:54:44 +1100 Subject: [PATCH 50/54] From review REVIEW-2026-10-31-PR170-correctness-mythos.md: cipher: GCM holds its byte counts to the spec limits and refuses before it copies P3. A resumed GCM state carried its AAD and data byte counts unbounded, and the length block multiplied each by 8 unchecked, on the argument that the live path's checked_add could never get there; a crafted state could, and the final overflowed. The two counts are now held to SP 800-38D Sec 5.2.1.1's limits, 2^64 - 1 bits of AAD and 2^39 - 256 bits of plaintext, on every absorption and on resume, which is what makes the multiplication safe. The plaintext limit is where the keystream runs out anyway, so the live path reports it as DataLimitExceeded, as the counter did; the AAD limit stays a StateError. P4. The encryptor copied the plaintext into the output before the keystream check refused the call, so a refused do_encrypt_out handed the caller's plaintext back in the ciphertext buffer; the decryptor had absorbed the bytes into GHASH before its keystream refused. Both directions now ask whether the bytes fit, in the limit and in the keystream, before they copy or absorb anything, through a check the stream cipher exposes beside its own. gcm_tests pins the four edges and the zeroed output in both directions. Scoped mutants on the changed lines of gcm.rs and stream.rs: 29 caught, none missed. The crate-local run also reports GCM's length block (`* 8`, unchanged here) as missed, since only the AES known answers see the tag, and three equivalent zero-length guards in do_decrypt_out. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/gcm.rs | 62 ++++++++++--- crypto/cipher/src/stream.rs | 28 ++++-- crypto/cipher/tests/modes/gcm_tests.rs | 124 ++++++++++++++++++++++++- 3 files changed, 189 insertions(+), 25 deletions(-) diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index e3449dabc..dee29e051 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -222,6 +222,13 @@ use ghash::{GHASH_STATE_LEN, Ghash}; /// The nonce (IV) length this type uses: 96 bits, SP 800-38D Sec 5.2.1.1's recommended length. pub const GCM_NONCE_LEN: usize = 12; +/// Sec 5.2.1.1's `len(A) <= 2^64 - 1` bits, in bytes. +const MAX_AAD_LEN: u64 = (1 << 61) - 1; + +/// Sec 5.2.1.1's `len(P) <= 2^39 - 256` bits, in bytes: the `2^32 - 2` counter blocks left once +/// `J0` and `inc32(J0)` are taken, which is also where the keystream runs out. +const MAX_DATA_LEN: u64 = (1 << 36) - 32; + /// The GCM mode type: the `M` of a mode-bound key such as `AES128Key`. `GCM` is GCM with /// the full 128-bit tag; `GCM<12>` and so on select a shorter tag, within the `12..=16` byte range /// [`Gcm`] accepts. The [`Mode`] and AEAD mode-trait impls below are generic over any @@ -451,14 +458,36 @@ where ciphertext (SP 800-38D Algorithm 4 absorbs A before C in one GHASH pass)", )); } + self.aad_len = match self.aad_len.checked_add(aad.len() as u64) { + Some(total) if total <= MAX_AAD_LEN => total, + _ => { + return Err(SymmetricCipherError::StateError( + "GCM: additional authenticated data longer than SP 800-38D Sec 5.2.1.1's \ + 2^64 - 1 bits", + )); + } + }; self.ghash.update(aad); - self.aad_len = - self.aad_len.checked_add(aad.len() as u64).ok_or(SymmetricCipherError::StateError( - "GCM: additional authenticated data length exceeds the supported range", - ))?; Ok(()) } + /// `data_len` after `len` more bytes, if that is still within Sec 5.2.1.1's plaintext limit. + fn data_len_after(&self, len: usize) -> Result { + match self.data_len.checked_add(len as u64) { + Some(total) if total <= MAX_DATA_LEN => Ok(total), + _ => Err(SymmetricCipherError::DataLimitExceeded), + } + } + + /// Whether `len` more data bytes can be taken: within Sec 5.2.1.1's plaintext limit and the + /// keystream left under this nonce. Both directions ask before they copy anything into an + /// output buffer, so a refused call leaves the buffer zeroed rather than holding the + /// caller's plaintext, and before they absorb anything, so it leaves the state untouched. + fn accepts_data(&self, len: usize) -> Result<(), SymmetricCipherError> { + self.data_len_after(len)?; + self.ctr.accepts(len) + } + /// The AAD-to-data transition: pads the AAD to a block boundary (the `0^v` of step 5) the /// first time data arrives. A no-op on every later call. fn begin_data_if_needed(&mut self) { @@ -473,11 +502,9 @@ where /// plaintext into ciphertext in place) and the decryptor (which calls this *before* GCTR turns /// the ciphertext back into plaintext): either way GHASH must see ciphertext, never plaintext. fn absorb_data(&mut self, data: &[u8]) -> Result<(), SymmetricCipherError> { + self.data_len = self.data_len_after(data.len())?; self.begin_data_if_needed(); self.ghash.update(data); - self.data_len = self.data_len.checked_add(data.len() as u64).ok_or( - SymmetricCipherError::StateError("GCM: data length exceeds the supported range"), - )?; Ok(()) } @@ -487,10 +514,8 @@ where /// because on the decrypting side it is the expected tag `T'`, which forges the rejected /// ciphertext if it survives a failed comparison. /// - /// The byte-to-bit multiplication (`* 8`) is not checked for overflow: `aad_len` and `data_len` - /// are accumulated with `checked_add` at every absorption (`absorb_aad`, `absorb_data`), so - /// reaching a count whose `* 8` could overflow `u64` would already require far more calls than - /// are physically possible to make. + /// The byte-to-bit multiplication (`* 8`) cannot overflow: `aad_len` and `data_len` are held + /// under [`MAX_AAD_LEN`] and [`MAX_DATA_LEN`] at every absorption and on resume. fn tag_block(&mut self, out: &mut Secret<[u8; 16]>) { self.ghash.pad_to_block(); let aad_bits = self.aad_len * 8; @@ -540,11 +565,11 @@ where /// into GHASH (step 5). Nothing is held back. /// /// # Errors - /// [`SymmetricCipherError::DataLimitExceeded`] if the underlying `Ctr` counter would be - /// exhausted -- the SP 800-38D Sec 5.2.1.1 bound `len(P) <= 2^39 - 256` bits -- or - /// [`SymmetricCipherError::StateError`] if the AAD/data length bookkeeping would overflow. - /// Nothing is consumed in either case. + /// [`SymmetricCipherError::DataLimitExceeded`] if `data` would take the message past the + /// SP 800-38D Sec 5.2.1.1 bound `len(P) <= 2^39 - 256` bits, which is where the `Ctr` + /// counter would be exhausted. Nothing is consumed in that case. fn encrypt_in_place(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.accepts_data(data.len())?; self.ctr.do_encrypt_inplace(data)?; self.absorb_data(data) } @@ -581,6 +606,7 @@ where if ciphertext.len() < plaintext.len() { return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); } + self.accepts_data(plaintext.len())?; ciphertext[..plaintext.len()].copy_from_slice(plaintext); self.encrypt_in_place(&mut ciphertext[..plaintext.len()])?; Ok(plaintext.len()) @@ -639,6 +665,7 @@ where /// # Errors /// As `encrypt_in_place`. fn decrypt_in_place(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.accepts_data(data.len())?; self.absorb_data(data)?; self.ctr.do_decrypt_inplace(data)?; Ok(()) @@ -685,6 +712,7 @@ where if plaintext.len() < release { return Err(SymmetricCipherError::OutputBufferTooSmall(release)); } + self.accepts_data(release)?; // Data has started even if every byte is still held back as a possible tag, so the AAD // phase ends here rather than at the first byte released: otherwise a `do_update_aad` // after a first call shorter than `TAG_LEN` would be accepted, and absorbed as if it came @@ -759,6 +787,7 @@ where ) -> Result { plaintext.fill(0); let n = self.tail_len; + self.accepts_data(n)?; plaintext[..n].copy_from_slice(&self.tail[..n]); self.decrypt_in_place(&mut plaintext[..n])?; if let Err(e) = self.finish(tag) { @@ -815,6 +844,9 @@ where let mut r = Cursor::new(rest); gcm.aad_len = r.u64(); gcm.data_len = r.u64(); + if gcm.aad_len > MAX_AAD_LEN || gcm.data_len > MAX_DATA_LEN { + return Err(SuspendableError::InvalidData); + } let call_state = r.u8(); if call_state & 0xF0 != Dir::STATE_BITS { return Err(SuspendableError::InvalidData); diff --git a/crypto/cipher/src/stream.rs b/crypto/cipher/src/stream.rs index 6c102501b..bf6addd76 100644 --- a/crypto/cipher/src/stream.rs +++ b/crypto/cipher/src/stream.rs @@ -107,6 +107,24 @@ where }; } + /// Whether `len` more bytes fit in the keystream this init data has left: the check every + /// call makes before it consumes anything, on its own for a construction that must know + /// before it copies the caller's input into an output buffer (GCM). + /// + /// # Errors + /// [`SymmetricCipherError::DataLimitExceeded`] if the call would need more keystream than + /// remains for this init data, and continuing would repeat keystream. + pub(crate) fn accepts(&self, len: usize) -> Result<(), SymmetricCipherError> { + let pending_len = BLOCK_LEN - self.used; + // Saturating: a keystream with no practical limit reports `u64::MAX` blocks. + let capacity = (pending_len as u64) + .saturating_add(self.keystream.remaining_blocks().saturating_mul(BLOCK_LEN as u64)); + if len as u64 > capacity { + return Err(SymmetricCipherError::DataLimitExceeded); + } + Ok(()) + } + /// The whole data path, shared by both directions: a keystream cipher's encryption and /// decryption are the same XOR, so there is one implementation and the direction is only a /// type. @@ -120,16 +138,8 @@ where /// [`SymmetricCipherError::DataLimitExceeded`] if the keystream cannot cover the call; nothing /// is consumed in that case. fn apply(&mut self, data: &mut [u8]) -> Result { + self.accepts(data.len())?; let pending_len = BLOCK_LEN - self.used; - // Saturating: a keystream with no practical limit reports `u64::MAX` blocks. - let capacity = (pending_len as u64) - .saturating_add(self.keystream.remaining_blocks().saturating_mul(BLOCK_LEN as u64)); - if data.len() as u64 > capacity { - // Keystream exhausted: this call would need more keystream than remains for this init - // data, and continuing would repeat keystream. - return Err(SymmetricCipherError::DataLimitExceeded); - } - let head_len = core::cmp::min(pending_len, data.len()); let (head, rest) = data.split_at_mut(head_len); for (b, k) in head.iter_mut().zip(self.pending[self.used..].iter()) { diff --git a/crypto/cipher/tests/modes/gcm_tests.rs b/crypto/cipher/tests/modes/gcm_tests.rs index 9b2563c85..869711b00 100644 --- a/crypto/cipher/tests/modes/gcm_tests.rs +++ b/crypto/cipher/tests/modes/gcm_tests.rs @@ -10,7 +10,7 @@ mod common; #[path = "../common/toy_key.rs"] mod toy_key; -use bouncycastle_cipher::modes::{GCM, Gcm, ModeNames}; +use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN, Gcm, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::security_strength::SecurityStrength; @@ -21,6 +21,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_utils::suspendable_state::LIB_VERSION_LEN; use common::{ForwardOnlyToy, TOY_LEN, Toy, toy_key}; use toy_key::{ToyKey, os_rng}; @@ -372,3 +373,124 @@ fn a_state_resumes_only_in_its_own_direction() { as SuspendableKeyed<'_, N>>::from_suspended(state, key.state()) .expect("its own direction"); } + +// The suspended layout, in order: the library version, the CTR half (the nonce, the next counter +// value, the pending keystream block and its used count), the GHASH state, the AAD and data byte +// counts, the call-state byte, the held-back tail and its length. Every integer is a little-endian +// `u64`. The offsets below are the ones the tests patch. +const GCM_N: usize = Gcm::<'static, Toy, Encrypting, 16>::SUSPENDED_STATE_LEN; +const NEXT_COUNTER_AT: usize = LIB_VERSION_LEN + 12; +const DATA_LEN_AT: usize = GCM_N - 8 - 16 - 1 - 8; +const AAD_LEN_AT: usize = DATA_LEN_AT - 8; + +/// SP 800-38D Sec 5.2.1.1's `len(A) <= 2^64 - 1` and `len(P) <= 2^39 - 256` bits, in bytes. +const MAX_AAD_LEN: u64 = (1 << 61) - 1; +const MAX_DATA_LEN: u64 = (1 << 36) - 32; + +fn with_u64(mut state: [u8; GCM_N], at: usize, value: u64) -> [u8; GCM_N] { + state[at..at + 8].copy_from_slice(&value.to_le_bytes()); + state +} + +/// An encryptor that has absorbed AAD and nothing else, so its AAD phase is still open and its +/// counter is at `inc32(J0)`, with the key it borrows. +fn aad_only_encryptor(key: &ToyGcmKey) -> (Gcm<'_, Toy, Encrypting, 16>, [u8; GCM_NONCE_LEN]) { + let (mut enc, nonce) = key.create_encryptor(&mut os_rng()).unwrap(); + enc.do_update_aad(b"header").unwrap(); + (enc, nonce) +} + +/// The two byte counts resume only within Sec 5.2.1.1's limits, where the length block's +/// multiplication by 8 cannot overflow, and the live path holds the same bounds: at the limit the +/// final completes and one more byte is refused. +#[test] +fn the_length_counts_are_held_to_the_spec_limits_on_resume_and_live() { + let key = gcm_key::<16>(); + let (enc, _) = aad_only_encryptor(&key); + let state: [u8; GCM_N] = enc.suspend(); + let resume = |state| { + as SuspendableKeyed<'_, GCM_N>>::from_suspended( + state, + key.state(), + ) + }; + + assert!(matches!( + resume(with_u64(state, AAD_LEN_AT, MAX_AAD_LEN + 1)), + Err(SuspendableError::InvalidData) + )); + let mut at_limit = resume(with_u64(state, AAD_LEN_AT, MAX_AAD_LEN)).expect("the limit itself"); + assert!(at_limit.do_update_aad(b"").is_ok()); + assert!(matches!(at_limit.do_update_aad(b"x"), Err(SymmetricCipherError::StateError(_)))); + let _ = at_limit.do_encrypt_final().expect("the length block is formed without overflow"); + + assert!(matches!( + resume(with_u64(state, DATA_LEN_AT, MAX_DATA_LEN + 1)), + Err(SuspendableError::InvalidData) + )); + let mut at_limit = + resume(with_u64(state, DATA_LEN_AT, MAX_DATA_LEN)).expect("the limit itself"); + let mut out = [0xEEu8; 1]; + assert!(matches!( + at_limit.do_encrypt_out(&[0x41], &mut out), + Err(SymmetricCipherError::DataLimitExceeded) + )); + assert_eq!(out, [0], "a refused call leaves the output zeroed"); + let _ = at_limit.do_encrypt_final().expect("the length block is formed without overflow"); +} + +/// A call the keystream cannot cover is refused before the caller's input is copied into the +/// output, so the buffer holds zeros rather than the plaintext, and the state is untouched: the +/// bytes that do fit still go through afterwards. +#[test] +fn a_call_past_the_keystream_limit_leaves_no_plaintext_in_the_output() { + let key = gcm_key::<16>(); + let (enc, nonce) = aad_only_encryptor(&key); + // One counter block left under this nonce: 16 bytes of keystream. + let state = with_u64(enc.suspend(), NEXT_COUNTER_AT, (1 << 32) - 1); + let mut enc = as SuspendableKeyed<'_, GCM_N>>::from_suspended( + state, + key.state(), + ) + .expect("one block left is a state the counter can be in"); + + let plaintext = [0x41u8; 32]; + let mut ciphertext = [0xEEu8; 32]; + assert!(matches!( + enc.do_encrypt_out(&plaintext, &mut ciphertext), + Err(SymmetricCipherError::DataLimitExceeded) + )); + assert_eq!(ciphertext, [0u8; 32]); + let mut last_block = [0u8; 16]; + assert_eq!(enc.do_encrypt_out(&plaintext[..16], &mut last_block).unwrap(), 16); + assert_ne!(last_block, plaintext[..16]); + assert!(matches!( + enc.do_encrypt_out(&plaintext[..1], &mut [0u8; 1]), + Err(SymmetricCipherError::DataLimitExceeded) + )); + let (tag, _) = enc.do_encrypt_final().unwrap(); + + // The decryptor at the same counter: a call that would release more than the keystream + // covers is refused with the output zeroed, and the block that fits then verifies. + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(b"header").unwrap(); + let state = with_u64(dec.suspend(), NEXT_COUNTER_AT, (1 << 32) - 1); + let mut dec = as SuspendableKeyed<'_, GCM_N>>::from_suspended( + state, + key.state(), + ) + .unwrap(); + let mut stream = last_block.to_vec(); + stream.extend_from_slice(&last_block); + stream.extend_from_slice(&tag); + let mut out = [0xEEu8; 32]; + assert!(matches!( + dec.do_decrypt_out(&stream, &mut out), + Err(SymmetricCipherError::DataLimitExceeded) + )); + assert_eq!(out, [0u8; 32]); + let mut out = [0u8; 16]; + assert_eq!(dec.do_decrypt_out(&stream[16..], &mut out).unwrap(), 16); + assert_eq!(out, plaintext[..16]); + assert!(dec.do_decrypt_final().is_ok()); +} From bdd45b8a773f8546788f81e0f03f976458d1d663 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 15:43:18 +1100 Subject: [PATCH 51/54] From review REVIEW-2026-10-31-PR170-correctness-mythos.md: core: a nonce-based MAC mode trait, which GMAC implements and the keys forward to P13, first step. GMAC's generator/verifier pair on AESKey named Gmac's constructors directly, from the aes crate, which is what kept those constructors public. The hazmat mode traits gain NonceMacMode, the shape of an AEAD mode's constructors with no ciphertext: a generator under a drawn or supplied nonce, a verifier under the tag's nonce. GMAC implements it in cipher; the AES key's inherent pair and the test toy key forward to it, as the role impls forward to the other mode traits. No role trait is added, GMAC being the only nonce-based MAC. Public API otherwise unchanged. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/src/gmac.rs | 11 +++---- crypto/cipher/src/modes/gmac.rs | 43 ++++++++++++++++++++++++--- crypto/cipher/tests/common/toy_key.rs | 8 ++--- crypto/core/src/hazmat/mod.rs | 2 +- crypto/core/src/hazmat/mode.rs | 42 +++++++++++++++++++++++++- 5 files changed, 91 insertions(+), 15 deletions(-) diff --git a/crypto/aes/src/gmac.rs b/crypto/aes/src/gmac.rs index a8002b126..feb60a4cb 100644 --- a/crypto/aes/src/gmac.rs +++ b/crypto/aes/src/gmac.rs @@ -46,7 +46,7 @@ use crate::{AESKey, AESParams}; use bouncycastle_cipher::modes::{GCM_NONCE_LEN, GMAC, Gmac, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::BlockPermutation; +use bouncycastle_core::hazmat::{BlockPermutation, NonceMacMode}; use bouncycastle_core::traits::RNG; // Imports needed for docs @@ -68,7 +68,8 @@ pub type AES_GMAC_192<'k, Dir> = Gmac<'k, AES192Internal, Dir, 16>; pub type AES_GMAC_256<'k, Dir> = Gmac<'k, AES256Internal, Dir, 16>; // The pair is inherent on the key rather than a role trait, since a GMAC tag is bound to a nonce; -// one impl over every AES key length. +// one impl over every AES key length, forwarding to the mode's `NonceMacMode` impl as the role +// impls forward to theirs. impl AESKey> where P: AESParams, @@ -85,7 +86,7 @@ where (Gmac<'k, AESInternal

, Encrypting, TAG_LEN>, [u8; GCM_NONCE_LEN]), SymmetricCipherError, > { - Gmac::, Encrypting, TAG_LEN>::from_key_state(&self.state, rng) + GMAC::::generate_init(&self.state, rng) } /// Begins a tag under a nonce the caller supplies, who takes on the obligation that it never @@ -94,7 +95,7 @@ where &'k self, nonce: &[u8; GCM_NONCE_LEN], ) -> Gmac<'k, AESInternal

, Encrypting, TAG_LEN> { - Gmac::, Encrypting, TAG_LEN>::from_key_state_nonce(&self.state, nonce) + GMAC::::generate_init_nonce(&self.state, nonce) } /// Begins the verification of a tag made under `nonce`. Infallible. @@ -102,6 +103,6 @@ where &'k self, nonce: &[u8; GCM_NONCE_LEN], ) -> Gmac<'k, AESInternal

, Decrypting, TAG_LEN> { - Gmac::, Decrypting, TAG_LEN>::from_key_state(&self.state, nonce) + GMAC::::verify_init(&self.state, nonce) } } diff --git a/crypto/cipher/src/modes/gmac.rs b/crypto/cipher/src/modes/gmac.rs index 597dbd8b2..4c91574e9 100644 --- a/crypto/cipher/src/modes/gmac.rs +++ b/crypto/cipher/src/modes/gmac.rs @@ -61,7 +61,7 @@ use crate::modes::ModeNames; use crate::modes::gcm::{GCM_NONCE_LEN, Gcm, GcmKeyState}; use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::{BlockPermutation, Mode}; +use bouncycastle_core::hazmat::{BlockPermutation, Mode, NonceMacMode}; use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, RNG}; @@ -69,9 +69,9 @@ use bouncycastle_core::traits::{Algorithm, RNG}; /// The GMAC mode type: the `M` of a mode-bound key such as `AES128Key`. `GMAC` is GMAC with /// the full 128-bit tag; `GMAC<12>` and so on select a shorter tag, within the `12..=16` byte /// range [`Gcm`] accepts. Its key state is [`GcmKeyState`], so a GMAC key caches `H` as a GCM key -/// does. The [`Mode`] impl below is generic over any [`BlockPermutation`] with a 128-bit block; -/// the generator/verifier pair is inherent on each key type, since a GMAC tag is bound to a nonce -/// and so fits neither MAC role trait. +/// does. The [`Mode`] and [`NonceMacMode`] impls below are generic over any [`BlockPermutation`] +/// with a 128-bit block; the generator/verifier pair is inherent on each key type and forwards +/// to the latter, since a GMAC tag is bound to a nonce and so fits neither MAC role trait. pub struct GMAC; impl Mode

for GMAC @@ -86,6 +86,41 @@ where } } +impl NonceMacMode for GMAC +where + P: BlockPermutation<16> + ModeNames + Clone, +{ + type Generator<'k> + = Gmac<'k, P, Encrypting, TAG_LEN> + where + P: 'k; + type Verifier<'k> + = Gmac<'k, P, Decrypting, TAG_LEN> + where + P: 'k; + + fn generate_init<'k>( + state: &'k Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Generator<'k>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + Gmac::::from_key_state(state, rng) + } + + fn generate_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Generator<'k> { + Gmac::::from_key_state_nonce(state, nonce) + } + + fn verify_init<'k>( + state: &'k Self::KeyState, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self::Verifier<'k> { + Gmac::::from_key_state(state, nonce) + } +} + /// GMAC over the permutation `P`, generating (`Dir = Encrypting`) or verifying (`Dir = /// Decrypting`) a `TAG_LEN`-byte tag. See the module docs. pub struct Gmac<'k, P, Dir, const TAG_LEN: usize> diff --git a/crypto/cipher/tests/common/toy_key.rs b/crypto/cipher/tests/common/toy_key.rs index 6ffed649a..563fa8b8f 100644 --- a/crypto/cipher/tests/common/toy_key.rs +++ b/crypto/cipher/tests/common/toy_key.rs @@ -11,7 +11,7 @@ use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, BlockDecryptMode, BlockEncryptMode, BlockPermutation, EcbDecryptMode, EcbDecryptionKey, - EcbEncryptMode, EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, ModeKey, + EcbEncryptMode, EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, ModeKey, NonceMacMode, SymmetricDecryptMode, SymmetricEncryptMode, do_hazardous_operations, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; @@ -388,21 +388,21 @@ where &'k self, rng: &mut dyn RNG, ) -> Result<(Gmac<'k, P, Encrypting, TAG_LEN>, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { - Gmac::::from_key_state(&self.state, rng) + GMAC::::generate_init(&self.state, rng) } pub fn create_generator_nonce<'k>( &'k self, nonce: &[u8; GCM_NONCE_LEN], ) -> Gmac<'k, P, Encrypting, TAG_LEN> { - Gmac::::from_key_state_nonce(&self.state, nonce) + GMAC::::generate_init_nonce(&self.state, nonce) } pub fn create_verifier<'k>( &'k self, nonce: &[u8; GCM_NONCE_LEN], ) -> Gmac<'k, P, Decrypting, TAG_LEN> { - Gmac::::from_key_state(&self.state, nonce) + GMAC::::verify_init(&self.state, nonce) } } diff --git a/crypto/core/src/hazmat/mod.rs b/crypto/core/src/hazmat/mod.rs index d7c3dcdab..ea55088bf 100644 --- a/crypto/core/src/hazmat/mod.rs +++ b/crypto/core/src/hazmat/mod.rs @@ -24,5 +24,5 @@ pub use key_stream::KeyStream; pub use mode::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbEncryptMode, MacMode, Mode, ModeKey, - SymmetricDecryptMode, SymmetricEncryptMode, + NonceMacMode, SymmetricDecryptMode, SymmetricEncryptMode, }; diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index faa4540ab..2d91084ef 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -13,7 +13,9 @@ //! data of any length (padded CBC, CFB, CFB8, CTR), [`BlockEncryptMode`] / [`BlockDecryptMode`] //! for one that takes whole blocks (CBC with no padding layer), and [`EcbEncryptMode`] / //! [`EcbDecryptMode`] for ECB, which has no init data and so takes no RNG. A MAC mode -- CMAC -- -//! implements [`MacMode`], whose worker takes no nonce and serves both roles. +//! implements [`MacMode`], whose worker takes no nonce and serves both roles; a nonce-based MAC +//! -- GMAC -- implements [`NonceMacMode`], whose generator and verifier are made as an AEAD +//! mode's workers are. //! //! Every worker borrows the key state it is made from, for the lifetime `'k` on the mode's //! associated worker type: the schedule stays in the key and is read through the reference, so @@ -414,3 +416,41 @@ pub trait MacMode

: Mode

{ /// nothing to draw. fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k>; } + +/// A nonce-based MAC mode over `P` -- GMAC -- whose tag is bound to a nonce, so its two sides are +/// made as an AEAD mode's workers are: the generator under a nonce drawn from an RNG or supplied +/// by the caller, the verifier under the nonce the tag was made under. There is no role trait +/// for it in [`crate::symmetric_key`], GMAC being the only such MAC in the tree; a key type's +/// inherent generator/verifier pair forwards here. +pub trait NonceMacMode: Mode

{ + /// The streaming generator this mode produces over `P`, borrowing the key state for `'k`. + type Generator<'k> + where + P: 'k; + + /// The streaming verifier this mode produces over `P`, borrowing the key state for `'k`. + type Verifier<'k> + where + P: 'k; + + /// Begins a tag from `state` under a nonce drawn from `rng`, which is returned with the + /// generator. + /// + /// # Errors + /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. + fn generate_init<'k>( + state: &'k Self::KeyState, + rng: &mut dyn RNG, + ) -> Result<(Self::Generator<'k>, [u8; NONCE_LEN]), SymmetricCipherError>; + + /// Begins a tag from `state` under a nonce the caller supplies, who takes on the obligation + /// that it never repeat under this key. + fn generate_init_nonce<'k>( + state: &'k Self::KeyState, + nonce: &[u8; NONCE_LEN], + ) -> Self::Generator<'k>; + + /// Begins the verification of a tag made under `nonce` from `state`. Infallible: the state + /// is keyed and the nonce length is in the type. + fn verify_init<'k>(state: &'k Self::KeyState, nonce: &[u8; NONCE_LEN]) -> Self::Verifier<'k>; +} From 534d182058dd75883aca1d645f229f9bebcfc17d Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 15:50:26 +1100 Subject: [PATCH 52/54] From review REVIEW-2026-10-31-PR170-correctness-mythos.md: cipher: the bare key-state and permutation constructors leave the public API P13. GcmKeyState::new, CmacKeyState::new, Gcm's and Gmac's from_key_state constructors, Cmac::from_key_state and the CCM workers' from_perm forms were pub and re-exported from modes, so a caller holding a keyed permutation could build any worker without the key object's type, strength and nonce handling, and nothing on that path said hazmat, which QUALITY_AND_STYLE's rule requires. Every mode-trait impl that calls them lives in this crate, so they are now pub(crate): the key object is the route, and a key reaches the state only to resume a suspended worker, through hazmat::ModeKey. The doc examples build their key as the test suite does, through the toy key over the toy permutation, which is now include!d beside it, and so no longer carry an inner attribute. The cipher and aes tests that compared the key route against the bare one compare it against the hazmat mode traits the key forwards to, which is the same route under its notice. Public surface otherwise unchanged; the key-state types stay nameable. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/tests/cmac_key_tests.rs | 22 +++-- crypto/aes/tests/key_tests.rs | 20 ++--- crypto/cipher/src/modes/ccm.rs | 6 +- crypto/cipher/src/modes/cmac.rs | 42 ++++----- crypto/cipher/src/modes/gcm.rs | 88 +++++++++---------- crypto/cipher/src/modes/gmac.rs | 29 +++--- crypto/cipher/tests/ccm_suspend_tests.rs | 1 + crypto/cipher/tests/common/toy_key.rs | 4 +- crypto/cipher/tests/modes/cbc_tests.rs | 1 + crypto/cipher/tests/modes/ccm_tests.rs | 1 + crypto/cipher/tests/modes/cfb8_tests.rs | 1 + crypto/cipher/tests/modes/cfb_tests.rs | 1 + crypto/cipher/tests/modes/cmac_tests.rs | 57 ++++++------ crypto/cipher/tests/modes/ctr_tests.rs | 1 + crypto/cipher/tests/modes/ecb_tests.rs | 1 + crypto/cipher/tests/modes/gcm_tests.rs | 1 + crypto/cipher/tests/modes/gmac_tests.rs | 65 ++++++++------ .../tests/modes/symmetric_cipher_api_tests.rs | 1 + crypto/cipher/tests/padding/padded_tests.rs | 1 + crypto/cipher/tests/suspend_tests.rs | 1 + 20 files changed, 190 insertions(+), 154 deletions(-) diff --git a/crypto/aes/tests/cmac_key_tests.rs b/crypto/aes/tests/cmac_key_tests.rs index f832bdeaa..4fbcaaad6 100644 --- a/crypto/aes/tests/cmac_key_tests.rs +++ b/crypto/aes/tests/cmac_key_tests.rs @@ -1,11 +1,11 @@ //! `AES*Key`: the key object holding the subkeys, and the workers it hands out producing -//! exactly what `Cmac::from_key_state` produces over a `CmacKeyState` built from the same bytes. +//! exactly what the mode's own route produces over a `CmacKeyState` built from the same bytes. use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_aes::{AES_CMAC_128, AES_CMAC_192, AES_CMAC_256, AES128Key, AES192Key, AES256Key}; use bouncycastle_cipher::modes::{CMAC, CmacKeyState}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{ElectronicCodeBook, MacMode, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; @@ -42,22 +42,28 @@ fn the_key_object_gives_the_csrc_answer() { #[test] fn the_key_and_the_bare_key_state_agree_at_every_key_length() { - let state_16 = CmacKeyState::new(AES128Internal::new(&cipher_key::<16>(0x0b)).unwrap()); - let state_24 = CmacKeyState::new(AES192Internal::new(&cipher_key::<24>(0x0c)).unwrap()); - let state_32 = CmacKeyState::new(AES256Internal::new(&cipher_key::<32>(0x0d)).unwrap()); + let state_16 = >::key_state( + AES128Internal::new(&cipher_key::<16>(0x0b)).unwrap(), + ); + let state_24 = >::key_state( + AES192Internal::new(&cipher_key::<24>(0x0c)).unwrap(), + ); + let state_32 = >::key_state( + AES256Internal::new(&cipher_key::<32>(0x0d)).unwrap(), + ); for len in [0usize, 1, 15, 16, 17, 43] { let msg = &MSG[..len]; assert_eq!( AES128Key::::from_key_material(&mac_key::<16>(0x0b)).unwrap().mac(msg), - AES_CMAC_128::from_key_state(&state_16).mac(msg) + >::mac_init(&state_16).mac(msg) ); assert_eq!( AES192Key::::from_key_material(&mac_key::<24>(0x0c)).unwrap().mac(msg), - AES_CMAC_192::from_key_state(&state_24).mac(msg) + >::mac_init(&state_24).mac(msg) ); assert_eq!( AES256Key::::from_key_material(&mac_key::<32>(0x0d)).unwrap().mac(msg), - AES_CMAC_256::from_key_state(&state_32).mac(msg) + >::mac_init(&state_32).mac(msg) ); } } diff --git a/crypto/aes/tests/key_tests.rs b/crypto/aes/tests/key_tests.rs index 128ab1621..52a8188de 100644 --- a/crypto/aes/tests/key_tests.rs +++ b/crypto/aes/tests/key_tests.rs @@ -1,13 +1,13 @@ //! The mode-bound AES key objects: validation at construction, and `key.create_encryptor(rng)` -//! producing exactly what the mode's worker built from the bare key state produces from the same material -//! and RNG. +//! producing exactly what the mode's own route -- the hazmat mode traits the key forwards to -- +//! produces from the same material and RNG. use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_aes::{AES_GCM_128, AES128Key, AES192Key, AES256Key}; -use bouncycastle_cipher::modes::{CTR, GCM, Gcm, GcmKeyState, ModeNames}; +use bouncycastle_cipher::modes::{CTR, GCM, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{AeadDecryptMode, AeadEncryptMode, ElectronicCodeBook, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; @@ -89,15 +89,15 @@ fn the_key_type_a_mode_requires_is_the_constructor_constant() { /// The key-side encryptor is the mode's own type built from the same material, so with the same /// RNG it must produce the same nonce, ciphertext and tag as the worker built from the bare key -/// state (the `from_key_state` route a mode impl uses). +/// state through the mode trait the key forwards to. #[test] fn create_encryptor_matches_the_key_state_route() { let km = material::<32>(); let key = AES256Key::::from_key_material(&km).expect("a valid key"); - let state = GcmKeyState::new(AES256Internal::new(&km).unwrap()); + let state = >::key_state(AES256Internal::new(&km).unwrap()); let (mut enc_state, nonce_state) = - Gcm::::from_key_state(&state, &mut rng()) + >::encrypt_init(&state, &mut rng()) .expect("key-state init"); let (mut enc_key, nonce_key) = key.create_encryptor(&mut rng()).expect("key-side init"); assert_eq!(nonce_state, nonce_key); @@ -194,10 +194,10 @@ fn the_key_and_the_key_state_route_interoperate_at_every_key_length() { P: ElectronicCodeBook + ModeNames + Clone, K: AeadEncryptionKey + AeadDecryptionKey, { - let state = GcmKeyState::new(P::new(&material::()).unwrap()); + let state = >::key_state(P::new(&material::()).unwrap()); let (mut enc, nonce) = - Gcm::::from_key_state(&state, &mut rng()).unwrap(); + >::encrypt_init(&state, &mut rng()).unwrap(); enc.do_update_aad(AAD).unwrap(); let mut ct = vec![0u8; MSG.len()]; enc.do_encrypt_out(MSG, &mut ct).unwrap(); @@ -206,7 +206,7 @@ fn the_key_and_the_key_state_route_interoperate_at_every_key_length() { assert_eq!(pt, MSG); let (nonce, ct, tag) = encrypt_via_key::(key); - let mut dec = Gcm::::from_key_state(&state, &nonce); + let mut dec = >::decrypt_init(&state, &nonce); dec.do_update_aad(AAD).unwrap(); let mut pt = vec![0u8; ct.len()]; let n = dec.do_decrypt_out(&ct, &mut pt).unwrap(); diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 13e5f7ffa..2ad71174c 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -1104,7 +1104,7 @@ where /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. - pub fn from_perm( + pub(crate) fn from_perm( perm: &'k P, rng: &mut dyn RNG, ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { @@ -1115,7 +1115,7 @@ where /// As [`Self::from_perm`] under a nonce the caller supplies, at any A.1 length: the /// constructor a mode-bound key's `encrypt_init_nonce` uses. - pub fn from_perm_nonce(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { + pub(crate) fn from_perm_nonce(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { Self(CcmAdapter::new(perm, nonce)) } @@ -1316,7 +1316,7 @@ where /// mode-bound key's `decrypt_init` uses. Infallible, as [`AeadDecryptMode::decrypt_init`] is. /// /// [`AeadDecryptMode::decrypt_init`]: bouncycastle_core::hazmat::AeadDecryptMode::decrypt_init - pub fn from_perm(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { + pub(crate) fn from_perm(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { Self { inner: CcmAdapter::new(perm, nonce), tag: [0u8; TAG_LEN], tag_len: 0 } } diff --git a/crypto/cipher/src/modes/cmac.rs b/crypto/cipher/src/modes/cmac.rs index 6558c8a63..7698b6434 100644 --- a/crypto/cipher/src/modes/cmac.rs +++ b/crypto/cipher/src/modes/cmac.rs @@ -12,40 +12,42 @@ //! //! # Usage Examples //! -//! One-shot, and verifying, from a key state -- what a mode-bound key holds -- over -//! `ToyBlockCipher` (see the [module docs](super) for why): +//! One-shot, and verifying, from a mode-bound key over `ToyBlockCipher` (see the +//! [module docs](super) for why); over the toy permutation the key is the test suite's `ToyKey`, +//! the shape of the AES one: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_cipher::modes::{Cmac, CmacKeyState}; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! # use toy_key::ToyKey; +//! use bouncycastle_cipher::modes::CMAC; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; -//! use bouncycastle_core::traits::MAC; -//! -//! type ToyCmac<'k> = Cmac<'k, ToyBlockCipher, 16>; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let state = CmacKeyState::new(ToyBlockCipher::new(&key).unwrap()); -//! let tag = ToyCmac::from_key_state(&state).mac(b"a message"); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::MACKey).unwrap(); +//! let key = ToyKey::::from_key_material(&material).unwrap(); +//! let tag = key.mac(b"a message"); //! assert_eq!(tag.len(), 16); -//! assert!(ToyCmac::from_key_state(&state).verify(b"a message", &tag)); +//! assert!(key.verify(b"a message", &tag)); //! ``` //! //! Streaming, with a tag truncated to 8 bytes: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_cipher::modes::{Cmac, CmacKeyState}; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! # use toy_key::ToyKey; +//! use bouncycastle_cipher::modes::CMAC; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey}; //! use bouncycastle_core::traits::MAC; //! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let state = CmacKeyState::new(ToyBlockCipher::new(&key).unwrap()); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::MACKey).unwrap(); +//! let key = ToyKey::::from_key_material(&material).unwrap(); //! -//! let mut cmac = Cmac::::from_key_state(&state); +//! let mut cmac = key.create_generator(); //! cmac.do_update(b"a message that arrives "); //! cmac.do_update(b"in pieces"); //! let mut tag = [0u8; 8]; @@ -148,8 +150,8 @@ impl CmacKeyState { _ => panic!("CMAC is defined for 64- and 128-bit block ciphers only (SP 800-38B Sec 5.3)"), }; - /// Derives the subkeys from an already-keyed permutation (Sec 6.1). - pub fn new(perm: P) -> Self + /// Derives the subkeys from an already-keyed permutation (Sec 6.1): [`CMAC`]'s `Mode` impl. + pub(crate) fn new(perm: P) -> Self where P: BlockPermutation, { @@ -202,8 +204,8 @@ where pub const SUSPENDED_STATE_LEN: usize = 3 + 2 * BLOCK_LEN + 1; /// Begins a MAC over a new message over a key state, which the worker borrows: the - /// permutation and the subkeys stay in the key. - pub fn from_key_state(key: &'k CmacKeyState) -> Self { + /// permutation and the subkeys stay in the key. [`CMAC`]'s `MacMode` impl. + pub(crate) fn from_key_state(key: &'k CmacKeyState) -> Self { Self { key, c: Secret::new(), pending: Secret::new(), pending_len: 0 } } diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index dee29e051..9a2f6823c 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -18,38 +18,38 @@ //! `bouncycastle_aes::AES128Key`, whose AEAD role traits carry the one-shots -- and it //! implements [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] with `FINAL_LEN = TAG_LEN`, and the //! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] traits they extend. The key holds a -//! [`GcmKeyState`] and starts each message from it, which is the route over the toy permutation. +//! [`GcmKeyState`] and starts each message from it; over the toy permutation the key is the test +//! suite's `ToyKey`, the shape of the AES one. //! //! Through the SymmetricCipher methods there is no additional associated data (aad), and the tag //! is inlined into the ciphertext as `ciphertext || tag`: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; +//! # use toy_key::ToyKey; //! use bouncycastle_core::errors::SymmetricCipherError; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; //! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_cipher::modes::{Gcm, GcmKeyState}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_cipher::modes::GCM; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGcm<'k, Dir> = Gcm<'k, ToyBlockCipher, Dir, 16>; -//! -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let state = GcmKeyState::new(ToyBlockCipher::new(&key).unwrap()); +//! let key = ToyKey::::from_key_material(&material).expect("a valid key"); //! let plaintext: [u8; 16] = *b"attack at dawn!!"; //! -//! let (mut enc, nonce) = ToyGcm::::from_key_state(&state, &mut HashDRBG_SHA512::new_from_os()) -//! .expect("nonce draw"); +//! let (mut enc, nonce) = +//! key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).expect("nonce draw"); //! let mut ciphertext = vec![0u8; 16 + 16]; //! let n = enc.do_encrypt_out(&plaintext, &mut ciphertext).expect("encrypt"); //! let (tag, tag_len) = enc.do_encrypt_final().expect("the tag"); //! ciphertext[n..].copy_from_slice(&tag[..tag_len]); //! //! let open = |ct: &[u8]| -> Result, SymmetricCipherError> { -//! let mut dec = ToyGcm::::from_key_state(&state, &nonce); +//! let mut dec = key.create_decryptor(&nonce); //! let mut pt = vec![0u8; ct.len()]; //! let n = dec.do_decrypt_out(ct, &mut pt)?; //! let (last, last_len) = dec.do_decrypt_final()?; @@ -72,32 +72,30 @@ //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! # use toy_key::ToyKey; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{ //! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, //! }; -//! use bouncycastle_cipher::modes::{Gcm, GcmKeyState}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_cipher::modes::GCM; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGcm<'k, Dir> = Gcm<'k, ToyBlockCipher, Dir, 16>; -//! -//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let state = GcmKeyState::new(ToyBlockCipher::new(&key).unwrap()); +//! let key = ToyKey::::from_key_material(&material).expect("a valid key"); //! let aad = b"some associated data"; //! let message = b"a message that streams in over more than one call"; //! -//! let (mut enc, nonce) = -//! ToyGcm::::from_key_state(&state, &mut HashDRBG_SHA512::new_from_os()).unwrap(); +//! let (mut enc, nonce) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); //! enc.do_update_aad(aad).unwrap(); //! let mut ct = vec![0u8; message.len()]; //! enc.do_encrypt_out(message, &mut ct).unwrap(); //! let (_, tag) = enc.do_encrypt_final_detached_out(&mut [0u8; 16]).unwrap(); //! -//! let mut dec = ToyGcm::::from_key_state(&state, &nonce); +//! let mut dec = key.create_decryptor(&nonce); //! dec.do_update_aad(aad).unwrap(); //! let mut pt = vec![0u8; ct.len()]; //! let written = dec.do_decrypt_out(&ct, &mut pt).unwrap(); @@ -112,33 +110,31 @@ //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! # use toy_key::ToyKey; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{ //! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, //! }; -//! use bouncycastle_cipher::modes::{Gcm, GcmKeyState}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_cipher::modes::GCM; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGcm<'k, Dir> = Gcm<'k, ToyBlockCipher, Dir, 16>; -//! -//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) +//! let material = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let state = GcmKeyState::new(ToyBlockCipher::new(&key).unwrap()); +//! let key = ToyKey::::from_key_material(&material).expect("a valid key"); //! let aad = b"some associated data"; //! let message = b"a message that streams in over more than one call"; //! -//! let (mut enc, nonce) = -//! ToyGcm::::from_key_state(&state, &mut HashDRBG_SHA512::new_from_os()).unwrap(); +//! let (mut enc, nonce) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); //! enc.do_update_aad(aad).unwrap(); //! let mut ct = vec![0u8; message.len()]; //! enc.do_encrypt_out(message, &mut ct).unwrap(); //! let (tag_block, tag_len) = enc.do_encrypt_final().unwrap(); //! ct.extend_from_slice(&tag_block[..tag_len]); //! -//! let mut dec = ToyGcm::::from_key_state(&state, &nonce); +//! let mut dec = key.create_decryptor(&nonce); //! dec.do_update_aad(aad).unwrap(); //! let mut pt = vec![0u8; ct.len()]; //! let written = dec.do_decrypt_out(&ct, &mut pt).unwrap(); @@ -163,7 +159,7 @@ //! As with all symmetric cipher modes, repeated key and nonce for multiple messages is catastrophic //! for security, which is why the key's `create_encryptor` draws the nonce from the caller's RNG. //! The forms that take a supplied nonce, `create_encryptor_nonce` and the `_nonce` one-shots on -//! the key and [`Gcm::from_key_state_nonce`] here, leave uniqueness to the caller. +//! the key, leave uniqueness to the caller. //! //! ## Invocation limit //! @@ -293,8 +289,8 @@ where /// What a GCM key keeps per key: the keyed permutation and the hash subkey `H = CIPH_K(0^128)` /// (SP 800-38D Algorithm 4 step 1), derived once here rather than once per message. It is the -/// `KeyState` of [`GCM`] over any permutation: built by [`new`](Self::new) or through the mode -/// impls, and consumed through them. +/// `KeyState` of [`GCM`] over any permutation, built and consumed through the mode impls; a +/// key object reaches it only to resume a suspended worker. #[derive(Clone)] pub struct GcmKeyState

{ perm: P, @@ -314,7 +310,7 @@ impl Eq for GcmKeyState

{} impl

GcmKeyState

{ /// Derives `H` from `perm`. - pub fn new(perm: P) -> Self + pub(crate) fn new(perm: P) -> Self where P: BlockPermutation<16>, { @@ -540,13 +536,12 @@ where P: BlockPermutation<16> + ModeNames, { /// Begins an encryption over a key's stored state under a nonce drawn from `rng`: the - /// encryption constructor a permutation crate's `AeadEncryptMode` impl for [`GCM`] calls. - /// Nothing is copied out of `state`; the worker reads the schedule and `H` through the - /// borrow and cannot outlive it. + /// encryption constructor [`GCM`]'s `AeadEncryptMode` impl calls. Nothing is copied out of + /// `state`; the worker reads the schedule and `H` through the borrow and cannot outlive it. /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. - pub fn from_key_state( + pub(crate) fn from_key_state( state: &'k GcmKeyState

, rng: &mut dyn RNG, ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { @@ -555,9 +550,12 @@ where Ok((Self::setup_from_state(state, nonce), nonce)) } - /// As [`Self::from_key_state`] under a nonce the caller supplies: the constructor a - /// permutation crate's `AeadEncryptMode::encrypt_init_nonce` impl for [`GCM`] calls. - pub fn from_key_state_nonce(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { + /// As [`Self::from_key_state`] under a nonce the caller supplies: the constructor [`GCM`]'s + /// `AeadEncryptMode::encrypt_init_nonce` impl calls. + pub(crate) fn from_key_state_nonce( + state: &'k GcmKeyState

, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self { Self::setup_from_state(state, *nonce) } @@ -649,9 +647,9 @@ where P: BlockPermutation<16> + ModeNames, { /// Begins the decryption of a message encrypted under `nonce` from a key's stored state: the - /// decryption constructor a permutation crate's `AeadDecryptMode` impl for [`GCM`] calls. - /// Infallible: the state is already keyed and the nonce length is in the type. - pub fn from_key_state(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { + /// decryption constructor [`GCM`]'s `AeadDecryptMode` impl calls. Infallible: the state is + /// already keyed and the nonce length is in the type. + pub(crate) fn from_key_state(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { Self::setup_from_state(state, *nonce) } diff --git a/crypto/cipher/src/modes/gmac.rs b/crypto/cipher/src/modes/gmac.rs index 4c91574e9..1a5c816d8 100644 --- a/crypto/cipher/src/modes/gmac.rs +++ b/crypto/cipher/src/modes/gmac.rs @@ -20,29 +20,29 @@ //! //! # Usage Examples //! -//! From a key state, which is what a mode-bound key holds and what the workers borrow: +//! From a mode-bound key, which holds the key state the workers borrow; over the toy permutation +//! the key is the test suite's `ToyKey`, the shape of the AES one: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_cipher::modes::{GcmKeyState, Gmac}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! # use toy_key::ToyKey; +//! use bouncycastle_cipher::modes::GMAC; //! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::symmetric_key::FromKeyMaterial; //! use bouncycastle_rng::HashDRBG_SHA512; //! -//! type ToyGmac<'k, Dir> = Gmac<'k, ToyBlockCipher, Dir, 16>; -//! //! let mut rng = HashDRBG_SHA512::new_from_os(); -//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); -//! let state = GcmKeyState::new(ToyBlockCipher::new(&key).unwrap()); +//! let material = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::MACKey).unwrap(); +//! let key = ToyKey::::from_key_material(&material).unwrap(); //! -//! let (mut generator, nonce) = ToyGmac::::from_key_state(&state, &mut rng).unwrap(); +//! let (mut generator, nonce) = key.create_generator(&mut rng).unwrap(); //! generator.do_update(b"a message "); //! generator.do_update(b"in two parts"); //! let tag = generator.do_final(); //! -//! let mut verifier = ToyGmac::::from_key_state(&state, &nonce); +//! let mut verifier = key.create_verifier(&nonce); //! verifier.do_update(b"a message in two parts"); //! assert!(verifier.do_verify_final(&tag)); //! ``` @@ -151,7 +151,7 @@ where /// /// # Errors /// [`SymmetricCipherError::RNGError`] if the nonce draw fails; nothing else can. - pub fn from_key_state( + pub(crate) fn from_key_state( state: &'k GcmKeyState

, rng: &mut dyn RNG, ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { @@ -161,7 +161,10 @@ where /// As [`from_key_state`](Self::from_key_state) under a nonce the caller supplies, who takes /// on the obligation that it never repeat under this key. - pub fn from_key_state_nonce(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { + pub(crate) fn from_key_state_nonce( + state: &'k GcmKeyState

, + nonce: &[u8; GCM_NONCE_LEN], + ) -> Self { Self { gcm: Gcm::from_key_state_nonce(state, nonce) } } @@ -189,7 +192,7 @@ where /// Begins verifying a tag made under `nonce` from a key's stored state: what a mode-bound /// key's `create_verifier` calls. Infallible: the state is keyed and the nonce length is in /// the type. - pub fn from_key_state(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { + pub(crate) fn from_key_state(state: &'k GcmKeyState

, nonce: &[u8; GCM_NONCE_LEN]) -> Self { Self { gcm: Gcm::::from_key_state(state, nonce) } } diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index 7e1cb0afb..29367ed00 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -27,6 +27,7 @@ use bouncycastle_utils::suspendable_state::LIB_VERSION_LEN; #[path = "common/toy_block_cipher.rs"] mod toy_block_cipher; #[path = "common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use toy_block_cipher::ToyBlockCipher; use toy_key::ToyKey; diff --git a/crypto/cipher/tests/common/toy_key.rs b/crypto/cipher/tests/common/toy_key.rs index 563fa8b8f..314aec73a 100644 --- a/crypto/cipher/tests/common/toy_key.rs +++ b/crypto/cipher/tests/common/toy_key.rs @@ -2,8 +2,8 @@ // written here so the modes can be driven through the key path without a real cipher. The role // impls forward to whichever mode traits `M` implements over `P`, exactly as the AES key's do. // -// Pulled in with `#[path]` from the tests, so every item is `pub` and unused ones are allowed. -#![allow(dead_code)] +// Pulled in with `#[path]` from the tests and `include!`d by the doc examples, so every item is +// `pub`, and the site that pulls it in allows the unused ones. use bouncycastle_cipher::modes::{GCM_NONCE_LEN, GMAC, Gmac, ModeNames}; use bouncycastle_cipher::{Decrypting, Encrypting}; diff --git a/crypto/cipher/tests/modes/cbc_tests.rs b/crypto/cipher/tests/modes/cbc_tests.rs index 3a9a7cb24..dc0175cad 100644 --- a/crypto/cipher/tests/modes/cbc_tests.rs +++ b/crypto/cipher/tests/modes/cbc_tests.rs @@ -7,6 +7,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::{Blocks, CBC, ModeNames}; diff --git a/crypto/cipher/tests/modes/ccm_tests.rs b/crypto/cipher/tests/modes/ccm_tests.rs index 98945dc57..35c275cb7 100644 --- a/crypto/cipher/tests/modes/ccm_tests.rs +++ b/crypto/cipher/tests/modes/ccm_tests.rs @@ -16,6 +16,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::{CCM, CCM_Packet, ModeNames}; diff --git a/crypto/cipher/tests/modes/cfb8_tests.rs b/crypto/cipher/tests/modes/cfb8_tests.rs index ad2f1922d..026b52622 100644 --- a/crypto/cipher/tests/modes/cfb8_tests.rs +++ b/crypto/cipher/tests/modes/cfb8_tests.rs @@ -13,6 +13,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::{CFB, CFB8, Cfb8, ModeNames}; diff --git a/crypto/cipher/tests/modes/cfb_tests.rs b/crypto/cipher/tests/modes/cfb_tests.rs index 70020468a..8b697dcc6 100644 --- a/crypto/cipher/tests/modes/cfb_tests.rs +++ b/crypto/cipher/tests/modes/cfb_tests.rs @@ -14,6 +14,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::{Blocks, CBC, CFB, Cbc, Cfb, ModeNames}; diff --git a/crypto/cipher/tests/modes/cmac_tests.rs b/crypto/cipher/tests/modes/cmac_tests.rs index b744847a4..b00a685d2 100644 --- a/crypto/cipher/tests/modes/cmac_tests.rs +++ b/crypto/cipher/tests/modes/cmac_tests.rs @@ -5,11 +5,12 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; -use bouncycastle_cipher::modes::{CMAC, CMAC_MIN_TAG_LEN, Cmac, CmacKeyState, ModeNames}; +use bouncycastle_cipher::modes::{CMAC, CMAC_MIN_TAG_LEN, Cmac, ModeNames}; use bouncycastle_core::errors::{MACError, SuspendableError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::{ElectronicCodeBook, MacMode, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromKeyMaterial, MacGenerationKey, MacVerificationKey}; @@ -32,15 +33,20 @@ fn mac_key() -> KeyMaterial { KeyMaterial::::from_bytes_as_type(toy_key().ref_to_bytes(), KeyType::MACKey).unwrap() } -fn key_state() -> CmacKeyState { - CmacKeyState::new(Toy::new(&toy_key()).unwrap()) +/// The mode's own route onto the worker, through the hazmat mode traits the key forwards to. +fn key_state() -> >::KeyState { + >::key_state(Toy::new(&toy_key()).unwrap()) +} + +fn mode_worker(state: &>::KeyState) -> ToyCmac<'_> { + >::mac_init(state) } fn message(len: usize) -> Vec { (0..len).map(|i| (i as u8).wrapping_mul(29).wrapping_add(3)).collect() } -/// The key object and the bare key state give the same tag, and the key passes the shared MAC +/// The key object and the mode's own route give the same tag, and the key passes the shared MAC /// key suite against it. #[test] fn the_key_and_the_key_state_agree() { @@ -49,9 +55,9 @@ fn the_key_and_the_key_state_agree() { for len in [0, 1, 15, 16, 17, 32, 33, 100] { let msg = message(len); let from_key = key.mac(&msg); - let from_state = ToyCmac::from_key_state(&state).mac(&msg); + let from_state = mode_worker(&state).mac(&msg); assert_eq!(from_state, from_key, "len {len}"); - assert!(ToyCmac::from_key_state(&state).verify(&msg, &from_key)); + assert!(mode_worker(&state).verify(&msg, &from_key)); assert!(key.verify(&msg, &from_state)); TestFrameworkMAC::new().test_mac_key(&key, &msg, &from_state); } @@ -61,17 +67,17 @@ fn the_key_and_the_key_state_agree() { /// out the same wherever the call boundaries fall, including a boundary exactly on a block edge. #[test] fn streaming_split_points() { - let state = key_state(); + let key = cmac_key(); for mlen in [16, 20, 32, 33, 64] { let msg = message(mlen); - let expected = ToyCmac::from_key_state(&state).mac(&msg); + let expected = key.create_generator().mac(&msg); for split in 0..=mlen { - let mut cmac = ToyCmac::from_key_state(&state); + let mut cmac = key.create_generator(); cmac.do_update(&msg[..split]); cmac.do_update(&msg[split..]); assert_eq!(cmac.do_final(), expected, "mlen {mlen}, split at {split}"); } - let mut cmac = ToyCmac::from_key_state(&state); + let mut cmac = key.create_generator(); for b in &msg { cmac.do_update(core::slice::from_ref(b)); } @@ -83,16 +89,13 @@ fn streaming_split_points() { /// same message with the `10^j` padding spelled out must differ. #[test] fn a_complete_final_block_and_its_padded_twin_differ() { - let state = key_state(); + let key = cmac_key(); let short = message(20); let mut padded = short.clone(); padded.push(0x80); padded.resize(32, 0); - assert_ne!( - ToyCmac::from_key_state(&state).mac(&short), - ToyCmac::from_key_state(&state).mac(&padded) - ); - assert_ne!(ToyCmac::from_key_state(&state).mac(&[]), ToyCmac::from_key_state(&state).mac(&[0])); + assert_ne!(key.create_generator().mac(&short), key.create_generator().mac(&padded)); + assert_ne!(key.create_generator().mac(&[]), key.create_generator().mac(&[0])); } /// The key's construction suite: a `MACKey` of the permutation's length and strength, bound to @@ -106,37 +109,37 @@ fn the_key_enforces_the_material_policy() { #[test] fn truncation() { - let state = key_state(); + let key = cmac_key(); let msg = message(20); - let full = ToyCmac::from_key_state(&state).mac(&msg); + let full = key.create_generator().mac(&msg); assert_eq!(full.len(), TOY_LEN); - assert_eq!(ToyCmac::from_key_state(&state).output_len(), TOY_LEN); + assert_eq!(key.create_generator().output_len(), TOY_LEN); let mut out = [0xAAu8; CMAC_MIN_TAG_LEN]; - assert_eq!(ToyCmac::from_key_state(&state).mac_out(&msg, &mut out).unwrap(), 8); + assert_eq!(key.create_generator().mac_out(&msg, &mut out).unwrap(), 8); assert_eq!(out[..], full[..8]); let mut long = [0xAAu8; 24]; - assert_eq!(ToyCmac::from_key_state(&state).mac_out(&msg, &mut long).unwrap(), 16); + assert_eq!(key.create_generator().mac_out(&msg, &mut long).unwrap(), 16); assert_eq!(long[..16], full[..]); assert_eq!(long[16..], [0u8; 8]); // Below 64 bits the call fails, and the buffer is left zeroized. let mut out = [0xAAu8; CMAC_MIN_TAG_LEN - 1]; assert!(matches!( - ToyCmac::from_key_state(&state).mac_out(&msg, &mut out), + key.create_generator().mac_out(&msg, &mut out), Err(MACError::InvalidLength(_)) )); assert_eq!(out, [0u8; CMAC_MIN_TAG_LEN - 1]); // `verify` wants the full tag. - assert!(ToyCmac::from_key_state(&state).verify(&msg, &full)); - assert!(!ToyCmac::from_key_state(&state).verify(&msg, &full[..15])); + assert!(key.create_verifier().verify(&msg, &full)); + assert!(!key.create_verifier().verify(&msg, &full[..15])); let mut long = full.clone(); long.push(0); - assert!(!ToyCmac::from_key_state(&state).verify(&msg, &long)); + assert!(!key.create_verifier().verify(&msg, &long)); let mut wrong = full.clone(); wrong[15] ^= 1; - assert!(!ToyCmac::from_key_state(&state).verify(&msg, &wrong)); + assert!(!key.create_verifier().verify(&msg, &wrong)); } #[test] diff --git a/crypto/cipher/tests/modes/ctr_tests.rs b/crypto/cipher/tests/modes/ctr_tests.rs index 7d8b0c63d..5ca4f219f 100644 --- a/crypto/cipher/tests/modes/ctr_tests.rs +++ b/crypto/cipher/tests/modes/ctr_tests.rs @@ -23,6 +23,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::{CTR, Ctr, ModeNames}; diff --git a/crypto/cipher/tests/modes/ecb_tests.rs b/crypto/cipher/tests/modes/ecb_tests.rs index eff627534..85d062936 100644 --- a/crypto/cipher/tests/modes/ecb_tests.rs +++ b/crypto/cipher/tests/modes/ecb_tests.rs @@ -13,6 +13,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; diff --git a/crypto/cipher/tests/modes/gcm_tests.rs b/crypto/cipher/tests/modes/gcm_tests.rs index 869711b00..2c5fe4c9d 100644 --- a/crypto/cipher/tests/modes/gcm_tests.rs +++ b/crypto/cipher/tests/modes/gcm_tests.rs @@ -8,6 +8,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN, Gcm, ModeNames}; diff --git a/crypto/cipher/tests/modes/gmac_tests.rs b/crypto/cipher/tests/modes/gmac_tests.rs index 9cbf33b5c..2a1c042bc 100644 --- a/crypto/cipher/tests/modes/gmac_tests.rs +++ b/crypto/cipher/tests/modes/gmac_tests.rs @@ -1,15 +1,15 @@ -//! Structural tests for GMAC, driven by a toy permutation: the key and the key state give the tag -//! `Gcm` gives over AAD alone, streaming splits, tag, nonce and message sensitivity, and key +//! Structural tests for GMAC, driven by a toy permutation: the key and the mode's own route give +//! the tag `Gcm` gives over AAD alone, streaming splits, tag, nonce and message sensitivity, and key //! validation. //! The known-answer tests (ACVP, Wycheproof) are in the `aes` crate, `crypto/aes/tests/gmac_*.rs`. mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; -use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN, GMAC, GcmKeyState, Gmac, ModeNames}; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_cipher::modes::{GCM, GCM_NONCE_LEN, GMAC, ModeNames}; +use bouncycastle_core::hazmat::{ElectronicCodeBook, Mode, NonceMacMode}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{AeadEncryptionKey, FromKeyMaterial}; @@ -18,7 +18,6 @@ use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMat use common::{TOY_LEN, Toy, toy_key}; use toy_key::ToyKey; -type ToyGmac<'k, Dir, const TAG_LEN: usize = 16> = Gmac<'k, Toy, Dir, TAG_LEN>; type ToyGmacKey = ToyKey>; fn gmac_key() -> ToyGmacKey { @@ -31,8 +30,9 @@ fn mac_key() -> KeyMaterial { KeyMaterial::::from_bytes_as_type(toy_key().ref_to_bytes(), KeyType::MACKey).unwrap() } -fn key_state() -> GcmKeyState { - GcmKeyState::new(Toy::new(&toy_key()).unwrap()) +/// The mode's own route onto the workers, through the hazmat mode trait the key forwards to. +fn key_state() -> >::KeyState { + >::key_state(Toy::new(&toy_key()).unwrap()) } fn message(len: usize) -> Vec { @@ -67,35 +67,48 @@ fn every_route_gives_gcm_s_aad_only_tag() { ); assert!(key.create_verifier(&NONCE).verify(&msg, &expected)); - let (gen_, nonce) = ToyGmac::::from_key_state(&state, &mut rng()).unwrap(); + type Full = GMAC<16>; + type Short = GMAC<12>; + let (gen_, nonce) = + >::generate_init(&state, &mut rng()) + .unwrap(); assert_eq!(nonce, NONCE, "len {len}"); - assert_eq!(gen_.mac(&msg), expected, "from_key_state, len {len}"); + assert_eq!(gen_.mac(&msg), expected, "generate_init, len {len}"); assert_eq!( - ToyGmac::::from_key_state_nonce(&state, &NONCE).mac(&msg), + >::generate_init_nonce(&state, &NONCE) + .mac(&msg), expected, - "from_key_state_nonce, len {len}" + "generate_init_nonce, len {len}" + ); + assert!( + >::verify_init(&state, &NONCE) + .verify(&msg, &expected) ); - assert!(ToyGmac::::from_key_state(&state, &NONCE).verify(&msg, &expected)); - let short = ToyGmac::::from_key_state_nonce(&state, &NONCE).mac(&msg); + let short = + >::generate_init_nonce(&state, &NONCE) + .mac(&msg); assert_eq!(short[..], expected[..12], "len {len}"); - assert!(ToyGmac::::from_key_state(&state, &NONCE).verify(&msg, &short)); + assert!( + >::verify_init(&state, &NONCE) + .verify(&msg, &short) + ); } } #[test] fn streaming_split_points() { - let state = key_state(); + let key = gmac_key::<16>(); for mlen in [16, 20, 32, 33, 64] { let msg = message(mlen); - let expected = ToyGmac::::from_key_state_nonce(&state, &NONCE).mac(&msg); + let expected = key.create_generator_nonce(&NONCE).mac(&msg); for split in 0..=mlen { - let mut gen_ = ToyGmac::::from_key_state_nonce(&state, &NONCE); + let mut gen_ = key.create_generator_nonce(&NONCE); gen_.do_update(&msg[..split]); gen_.do_update(&msg[split..]); assert_eq!(gen_.do_final(), expected, "generate, mlen {mlen}, split at {split}"); - let mut ver = ToyGmac::::from_key_state(&state, &NONCE); + let mut ver = key.create_verifier(&NONCE); ver.do_update(&msg[..split]); ver.do_update(&msg[split..]); assert!(ver.do_verify_final(&expected), "verify, mlen {mlen}, split at {split}"); @@ -105,11 +118,11 @@ fn streaming_split_points() { #[test] fn rejects_changed_tag_nonce_or_message() { - let state = key_state(); + let key = gmac_key::<16>(); let msg = message(100); - let tag = ToyGmac::::from_key_state_nonce(&state, &NONCE).mac(&msg); + let tag = key.create_generator_nonce(&NONCE).mac(&msg); let verify = |nonce: &[u8; GCM_NONCE_LEN], msg: &[u8], tag: &[u8; 16]| { - ToyGmac::::from_key_state(&state, nonce).verify(msg, tag) + key.create_verifier(nonce).verify(msg, tag) }; assert!(verify(&NONCE, &msg, &tag)); @@ -124,7 +137,7 @@ fn rejects_changed_tag_nonce_or_message() { assert!(!verify(&NONCE, &msg[..99], &tag)); // The empty message has a tag of its own, which is not the tag of anything else. - let empty_tag = ToyGmac::::from_key_state_nonce(&state, &NONCE).mac(&[]); + let empty_tag = key.create_generator_nonce(&NONCE).mac(&[]); assert!(verify(&NONCE, &[], &empty_tag)); assert!(!verify(&NONCE, &[0], &empty_tag)); } @@ -132,10 +145,10 @@ fn rejects_changed_tag_nonce_or_message() { /// A nonce drawn per generator: two draws from one RNG are two nonces, and so two tags. #[test] fn a_fresh_nonce_per_generator() { - let state = key_state(); + let key = gmac_key::<16>(); let mut rng = FixedSeedRNG::<64>::new(core::array::from_fn(|i| i as u8)); - let (g1, n1) = ToyGmac::::from_key_state(&state, &mut rng).unwrap(); - let (g2, n2) = ToyGmac::::from_key_state(&state, &mut rng).unwrap(); + let (g1, n1) = key.create_generator(&mut rng).unwrap(); + let (g2, n2) = key.create_generator(&mut rng).unwrap(); assert_ne!(n1, n2); assert_ne!(g1.mac(b"m"), g2.mac(b"m")); } diff --git a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs index 935edafd2..c35c8b680 100644 --- a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs +++ b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs @@ -18,6 +18,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::{CFB, CFB8, CTR, Ctr}; diff --git a/crypto/cipher/tests/padding/padded_tests.rs b/crypto/cipher/tests/padding/padded_tests.rs index dbf949bbf..e97673deb 100644 --- a/crypto/cipher/tests/padding/padded_tests.rs +++ b/crypto/cipher/tests/padding/padded_tests.rs @@ -6,6 +6,7 @@ #[path = "../common/toy_block_cipher.rs"] mod toy_block_cipher; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::{Blocks, CBC, ModeNames}; diff --git a/crypto/cipher/tests/suspend_tests.rs b/crypto/cipher/tests/suspend_tests.rs index f20b5136b..71256137b 100644 --- a/crypto/cipher/tests/suspend_tests.rs +++ b/crypto/cipher/tests/suspend_tests.rs @@ -31,6 +31,7 @@ use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendabl #[path = "common/toy_block_cipher.rs"] mod toy_block_cipher; #[path = "common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use toy_block_cipher::ToyBlockCipher; use toy_key::{ToyKey, os_rng}; From 4e8576b5688cd46235f6e0740b458bd1fd0d484d Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 17:37:10 +1100 Subject: [PATCH 53/54] QUALITY_AND_STYLE: cipher and MAC workers do not suspend; hashes, XOFs, HMAC, HKDF and KMAC do The architecture section asked every multi-call algorithm to implement a suspend trait, under a name the tree never had. The #173 decision (2026-10-30) is that cipher and MAC workers neither suspend nor clone, since a second copy of a nonce, counter or keystream position is a keystream reuse waiting to happen, and that the worker traits stay dyn-compatible so a message is paused by holding its worker. The sentence now says which types suspend and why the workers do not, ahead of the sweep that removes the impls. Assisted-by: Claude:claude-mythos-5-1 --- QUALITY_AND_STYLE.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index 2e2bf6993..c942b30ed 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -12,9 +12,12 @@ lib.rs for all crates needs to contain: `#![forbid(missing_docs)]`, `#![no_std]` All primitives must be accompanied by a CLI in `/cli`. -All algorithms with a state that is exercised across multiple API calls -- typically through a `do_update()`, -`do_final()` pattern -- should implement SerializableState so that the user can pause the execution of this algorithm to -a cache and resume later. +Hashes, XOFs and the MACs and KDFs built on them (KMAC, HMAC, HKDF) implement `Suspendable` or `SuspendableKeyed`, so +that a computation in progress can be written to a cache and resumed later. Cipher and MAC workers -- the encryptors, +decryptors, generators and verifiers a key object hands out -- do not: a suspended or cloned worker is a second copy of +a nonce, counter or keystream position, and two copies that both go on to encrypt reuse keystream (#173). Those workers +implement neither suspend trait nor `Clone`; a message is paused by holding its worker, and the worker traits are kept +`dyn`-compatible so it can be held as `&mut dyn` or, where allocation is acceptable, `Box`. # Quality From 12786cbf2d06fa220368a792ac82add8f6fcbaa6 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 19:06:40 +1100 Subject: [PATCH 54/54] QUALITY_AND_STYLE: the suspend sentence says the remaining worker impls come out in the #173 sweep From mwcw's verification note of 4e8576b5: the sentence stated a rule the tree does not yet meet, since Gcm, Ccm and Cmac still suspend and clone until the sweep removes the impls, and the document did not say so. One clause does. Assisted-by: Claude:claude-mythos-5-1 --- QUALITY_AND_STYLE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index c942b30ed..ad10025ef 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -17,7 +17,8 @@ that a computation in progress can be written to a cache and resumed later. Ciph decryptors, generators and verifiers a key object hands out -- do not: a suspended or cloned worker is a second copy of a nonce, counter or keystream position, and two copies that both go on to encrypt reuse keystream (#173). Those workers implement neither suspend trait nor `Clone`; a message is paused by holding its worker, and the worker traits are kept -`dyn`-compatible so it can be held as `&mut dyn` or, where allocation is acceptable, `Box`. +`dyn`-compatible so it can be held as `&mut dyn` or, where allocation is acceptable, `Box`. The suspend and `Clone` +impls the cipher workers still carry come out in the #173 sweep. # Quality