From e537e2313cda983d07b822dafe998a4a75d6fde5 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 30 Aug 2026 17:24:33 +1000 Subject: [PATCH 1/4] core: split BlockCipher into block-aligned BlockCipherEncryptor/Decryptor Rework the block cipher streaming traits ahead of the first mode implementations: - Split the single BlockCipher trait into BlockCipherEncryptor and BlockCipherDecryptor (mirroring KEMEncapsulator/KEMDecapsulator) so the direction can be encoded in the implementing type. A minimal BlockCipher supertrait carries the shared MAX_SECURITY_STRENGTH. The SymmetricCipher one-shot API is no longer a supertrait. - Replace the single-block do_{en,de}crypt_block[_out] with do_{en,de}crypt_blocks[_out], taking &[[u8; BLOCK_LEN]; N] so the block count is compile-time and in/out lengths cannot disagree. - Add do_encrypt_init_rng(key, &mut dyn RNG) alongside do_encrypt_init, matching the encaps/encaps_rng pattern. - Remove the do_{en,de}crypt_final[_out] methods. The traits are now strictly block-aligned; padding of arbitrary-length data belongs to a separate PaddedEncryptor/PaddedDecryptor layer to be built on top. Update the core-test-framework block cipher test to take separate encryptor/decryptor type parameters and to exercise N = 1 and N = 2, including mixed single/multi-block encrypt vs decrypt sequences. Co-Authored-By: Claude Fable 5 --- .../src/symmetric_ciphers.rs | 73 +++++++++---- crypto/core/src/traits.rs | 103 +++++++++--------- 2 files changed, 107 insertions(+), 69 deletions(-) diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 57fc0ee1..67f10793 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -6,7 +6,7 @@ use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle_core::traits::{ - AEADCipher, BlockCipher, SecurityStrength, StreamCipher, SymmetricCipher, + AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength, StreamCipher, SymmetricCipher, }; /// Instance of the test framework. @@ -124,7 +124,8 @@ impl TestFrameworkBlockCipher { const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, - C: BlockCipher, + E: BlockCipherEncryptor, + D: BlockCipherDecryptor, >( &self, ) { @@ -135,42 +136,76 @@ impl TestFrameworkBlockCipher { .unwrap(); // to test blocks, we'll chunk our dummy seed - let (mut encryptor, iv) = C::do_encrypt_init(&key).unwrap(); - let mut decryptor = C::do_decrypt_init(&key, &iv).unwrap(); + let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); + let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); + // one block at a time (N = 1) for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct = encryptor.do_encrypt_block(msg_chunk).unwrap(); - let pt = decryptor.do_decrypt_block(&ct).unwrap(); + let ct = encryptor.do_encrypt_blocks(&[*msg_chunk]).unwrap(); + let [pt] = decryptor.do_decrypt_blocks(&ct).unwrap(); assert_eq!(msg_chunk, &pt); } // do it again using the _out versions - let (mut encryptor, iv) = C::do_encrypt_init(&key).unwrap(); - let mut decryptor = C::do_decrypt_init(&key, &iv).unwrap(); + let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); + let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); - let mut ct = [0u8; BLOCK_LEN]; - let mut pt = [0u8; BLOCK_LEN]; + let mut ct = [[0u8; BLOCK_LEN]; 1]; + let mut pt = [[0u8; BLOCK_LEN]; 1]; for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct_bytes_written = encryptor.do_encrypt_block_out(msg_chunk, &mut ct).unwrap(); + let ct_bytes_written = encryptor + .do_encrypt_blocks_out(&[*msg_chunk], &mut ct) + .unwrap(); assert_eq!(ct_bytes_written, BLOCK_LEN); - let pt_bytes_written = decryptor.do_decrypt_block_out(&ct, &mut pt).unwrap(); + let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap(); assert_eq!(pt_bytes_written, BLOCK_LEN); - assert_eq!(msg_chunk, &pt); + assert_eq!(msg_chunk, &pt[0]); + } + + // multi-block (N = 2): blocks encrypted together must decrypt both together and one at a time, + // and blocks encrypted one at a time must decrypt together. + let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); + let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); + + let mut ct = [[0u8; BLOCK_LEN]; 2]; + let mut pt = [[0u8; BLOCK_LEN]; 2]; + for msg_pair in DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0.iter() { + // encrypt together, decrypt together (by value) + let ct_by_value = encryptor.do_encrypt_blocks(msg_pair).unwrap(); + let pt_by_value = decryptor.do_decrypt_blocks(&ct_by_value).unwrap(); + assert_eq!(msg_pair, &pt_by_value); + + // encrypt together (_out), decrypt one at a time + let ct_bytes_written = encryptor.do_encrypt_blocks_out(msg_pair, &mut ct).unwrap(); + assert_eq!(ct_bytes_written, 2 * BLOCK_LEN); + for (msg_chunk, ct_chunk) in msg_pair.iter().zip(ct.iter()) { + let [pt] = decryptor.do_decrypt_blocks(&[*ct_chunk]).unwrap(); + assert_eq!(msg_chunk, &pt); + } + + // encrypt one at a time, decrypt together (_out) + for (msg_chunk, ct_chunk) in msg_pair.iter().zip(ct.iter_mut()) { + let [c] = encryptor.do_encrypt_blocks(&[*msg_chunk]).unwrap(); + *ct_chunk = c; + } + let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap(); + assert_eq!(pt_bytes_written, 2 * BLOCK_LEN); + assert_eq!(msg_pair, &pt); } // test that the iv is random (ie not the same on two runs) - let (_encryptor, iv1) = C::do_encrypt_init(&key).unwrap(); - let (_encryptor, iv2) = C::do_encrypt_init(&key).unwrap(); + 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 C::do_encrypt_init(&mac_key) { + match E::do_encrypt_init(&mac_key) { Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } _ => panic!("Unexpected error"), }; @@ -194,15 +229,15 @@ impl TestFrameworkBlockCipher { // (and bypasses the key-length guard) without complaining. do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); - match C::do_encrypt_init(&key) { + match E::do_encrypt_init(&key) { Ok(_) => { - if ss >= &C::MAX_SECURITY_STRENGTH { /* good */ + if ss >= &E::MAX_SECURITY_STRENGTH { /* good */ } else { panic!("Should have been a strong enough key"); } } Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &C::MAX_SECURITY_STRENGTH { /* good */ + if ss < &E::MAX_SECURITY_STRENGTH { /* good */ } else { panic!("Should not have accepted a key weaker than algorithm"); } diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 7e23d516..78e3698b 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -80,72 +80,75 @@ pub trait SymmetricCipher: Alg ) -> Result; } -/// The basic functions of a block cipher. +/// Metadata shared by [`BlockCipherEncryptor`] and [`BlockCipherDecryptor`]. +pub trait BlockCipher { + /// Maximum security strength supported by the algorithm; keys tagged with a lower strength are + /// rejected by the `_init` constructors. + const MAX_SECURITY_STRENGTH: SecurityStrength; +} + +/// The encryption half of a block cipher's streaming API. Strictly block-aligned: whole blocks in, whole +/// blocks out, no finalization step. Padding of non-block-aligned data is handled by a separate layer +/// (`PaddedEncryptor` / `PaddedDecryptor`) built on top of this trait. +/// +/// Encryption and decryption are separate traits (as with [`KEMEncapsulator`] / [`KEMDecapsulator`]) so +/// that the direction can be encoded in the type, and so that a policy can permit decryption of an +/// algorithm while forbidding new encryptions. +/// /// This trait allows for a block cipher to generate initialization data, such as an Initialization Vector (IV) or Counter (CTR) /// which is not technically part of the ciphertext, but must be transmitted along with the ciphertext in order for the /// recipient to perform successful decryption. The length of the initialization data is specified by the implementing struct /// via the `INIT_DATA_LEN` constant. -/// In order for these one-shot APIs to be usable securely in all contexts, the init data will be generated +/// In order for these APIs to be usable securely in all contexts, the init data will be generated /// securely by the block cipher implementation and returned along with the ciphertext, and there is no API for the /// user to provide the init data. If you require this functionality, see the documentation for the underlying implementation. -pub trait BlockCipher: - SymmetricCipher + Sized +pub trait BlockCipherEncryptor: + BlockCipher + Sized { - /// Constructor that begins a flow of the streaming API for encrypting one block at a time. - /// Allows for the implementation to return init data such as an IV which is generated prior to encrypting the first block. + /// 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>; - /// Encrypts a single block of plaintext. - fn do_encrypt_block( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts a single block of plaintext and writes the ciphertext to the provided buffer. - fn do_encrypt_block_out( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Encrypts the final block of plaintext. - fn do_encrypt_final( + /// As [`BlockCipherEncryptor::do_encrypt_init`], but sources randomness from the provided RNG. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + /// Encrypts `N` consecutive blocks of plaintext. A sequence of calls is equivalent to one call over + /// the concatenation. + fn do_encrypt_blocks( &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts the final block of plaintext and writes the ciphertext to the provided buffer. - fn do_encrypt_final_out( + plaintext: &[[u8; BLOCK_LEN]; N], + ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError>; + /// Encrypts `N` consecutive blocks of plaintext into the provided buffer. Returns `N * BLOCK_LEN`. + fn do_encrypt_blocks_out( &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], + plaintext: &[[u8; BLOCK_LEN]; N], + ciphertext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; - /// Constructor that begins a flow of the streaming API for decryption one block at a time. +} + +/// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`]. +pub trait BlockCipherDecryptor: + BlockCipher + 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; - /// Decrypts a single block of ciphertext. - fn do_decrypt_block( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Decrypts a single block of ciphertext and writes the plaintext to the provided buffer. - fn do_decrypt_block_out( + /// Decrypts `N` consecutive blocks of ciphertext. A sequence of calls is equivalent to one call over + /// the concatenation. + fn do_decrypt_blocks( &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Decrypts the final block of ciphertext. - /// This is the decryption counterpart to [`BlockCipher::do_encrypt_final`] and is where an - /// implementation validates and strips any padding (or otherwise finalizes the flow). - fn do_decrypt_final( + ciphertext: &[[u8; BLOCK_LEN]; N], + ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError>; + /// Decrypts `N` consecutive blocks of ciphertext into the provided buffer. Returns `N * BLOCK_LEN`. + fn do_decrypt_blocks_out( &mut self, - ciphertext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Decrypts the final block of ciphertext and writes the plaintext to the provided buffer. - fn do_decrypt_final_out( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], + ciphertext: &[[u8; BLOCK_LEN]; N], + plaintext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; } @@ -170,7 +173,7 @@ pub trait AEADCipher, @@ -178,7 +181,7 @@ pub trait AEADCipher Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError>; - /// All AEAD ciphers will also be either a [`BlockCipher`] or a [`StreamCipher`], and so will already + /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a [`StreamCipher`], and so will already /// have a streaming API. /// This allows you to finish either style of streaming API flow with AEAD specific do_final() /// that computes and returns the authentication tag. @@ -207,7 +210,7 @@ pub trait AEADCipher Result; - /// All AEAD ciphers will also be either a [`BlockCipher`] or a [`StreamCipher`], and so will already + /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a [`StreamCipher`], and so will already /// have a streaming API. /// This allows you to finish either style of streaming API flow with AEAD specific do_final() /// that computes and returns the authentication tag. From e6ad944cc89aaef03a113edd733644325caf3401 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 30 Aug 2026 18:03:56 +1000 Subject: [PATCH 2/4] rustfmt: wrap long trait signatures and imports Formatting for the previous commit; no semantic change. Co-Authored-By: Claude Fable 5 --- .../core-test-framework/src/symmetric_ciphers.rs | 7 +++---- crypto/core/src/traits.rs | 14 ++++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 67f10793..14f0eed7 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -6,7 +6,8 @@ use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle_core::traits::{ - AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength, StreamCipher, SymmetricCipher, + AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength, StreamCipher, + SymmetricCipher, }; /// Instance of the test framework. @@ -154,9 +155,7 @@ impl TestFrameworkBlockCipher { let mut ct = [[0u8; BLOCK_LEN]; 1]; let mut pt = [[0u8; BLOCK_LEN]; 1]; for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct_bytes_written = encryptor - .do_encrypt_blocks_out(&[*msg_chunk], &mut ct) - .unwrap(); + let ct_bytes_written = encryptor.do_encrypt_blocks_out(&[*msg_chunk], &mut ct).unwrap(); assert_eq!(ct_bytes_written, BLOCK_LEN); let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap(); diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 78e3698b..4f24d842 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -102,8 +102,11 @@ pub trait BlockCipher { /// In order for these APIs to be usable securely in all contexts, the init data will be generated /// securely by the block cipher implementation and returned along with the ciphertext, and there is no API for the /// user to provide the init data. If you require this functionality, see the documentation for the underlying implementation. -pub trait BlockCipherEncryptor: - BlockCipher + Sized +pub trait BlockCipherEncryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: BlockCipher + Sized { /// Begins a streaming encryption flow, returning the generated init data (e.g. IV). /// Sources randomness from the library's default OS-backed RNG. @@ -130,8 +133,11 @@ pub trait BlockCipherEncryptor: - BlockCipher + Sized +pub trait BlockCipherDecryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: BlockCipher + Sized { /// Begins a streaming decryption flow from the init data returned by [`BlockCipherEncryptor::do_encrypt_init`]. fn do_decrypt_init( From 1a44d5bcf68547886be67d92b7315ce6836640f9 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 30 Aug 2026 20:47:27 +1000 Subject: [PATCH 3/4] core: add one-shot encrypt_blocks/decrypt_blocks to the block cipher traits Provided (default) methods on BlockCipherEncryptor -- encrypt_blocks, encrypt_blocks_rng, encrypt_blocks_out, encrypt_blocks_out_rng -- and on BlockCipherDecryptor -- decrypt_blocks, decrypt_blocks_out -- implemented once in the trait as init + blocks, so every block-aligned mode gets the house-standard take-data-return-result static API at no cost to implementors. Arbitrary-length one-shots remain the padding layer's job. The core-test-framework block cipher test now checks the one-shots agree with the streaming API and round-trip. Co-Authored-By: Claude Fable 5 --- .../src/symmetric_ciphers.rs | 15 +++++ crypto/core/src/traits.rs | 56 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 14f0eed7..6e1c8534 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -195,6 +195,21 @@ impl TestFrameworkBlockCipher { assert_eq!(msg_pair, &pt); } + // one-shot API: must agree with the streaming API for the same key, and round-trip + let two_blocks: &[[u8; BLOCK_LEN]; 2] = + &DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0[0]; + let (iv, ct) = E::encrypt_blocks(&key, two_blocks).unwrap(); + assert_eq!(D::decrypt_blocks(&key, &iv, &ct).unwrap(), *two_blocks); + let mut streamed = D::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(streamed.do_decrypt_blocks(&ct).unwrap(), *two_blocks); + + let mut ct = [[0u8; BLOCK_LEN]; 2]; + let mut pt = [[0u8; BLOCK_LEN]; 2]; + let (iv, n) = E::encrypt_blocks_out(&key, two_blocks, &mut ct).unwrap(); + assert_eq!(n, 2 * BLOCK_LEN); + assert_eq!(D::decrypt_blocks_out(&key, &iv, &ct, &mut pt).unwrap(), 2 * BLOCK_LEN); + assert_eq!(pt, *two_blocks); + // test that the iv is random (ie not the same on two runs) let (_encryptor, iv1) = E::do_encrypt_init(&key).unwrap(); let (_encryptor, iv2) = E::do_encrypt_init(&key).unwrap(); diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 4f24d842..e13cbc8b 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -130,6 +130,44 @@ pub trait BlockCipherEncryptor< plaintext: &[[u8; BLOCK_LEN]; N], ciphertext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; + + /// One-shot: encrypts `N` blocks under a fresh init. Returns the generated init data and the ciphertext. + fn encrypt_blocks( + key: &KeyMaterial, + plaintext: &[[u8; BLOCK_LEN]; N], + ) -> Result<([u8; INIT_DATA_LEN], [[u8; BLOCK_LEN]; N]), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + Ok((init_data, enc.do_encrypt_blocks(plaintext)?)) + } + /// As [`BlockCipherEncryptor::encrypt_blocks`], but sources randomness from the provided RNG. + fn encrypt_blocks_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + plaintext: &[[u8; BLOCK_LEN]; N], + ) -> Result<([u8; INIT_DATA_LEN], [[u8; BLOCK_LEN]; N]), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + Ok((init_data, enc.do_encrypt_blocks(plaintext)?)) + } + /// One-shot: encrypts `N` blocks under a fresh init into the provided buffer. + /// Returns the generated init data and `N * BLOCK_LEN`. + fn encrypt_blocks_out( + key: &KeyMaterial, + plaintext: &[[u8; BLOCK_LEN]; N], + ciphertext: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + Ok((init_data, enc.do_encrypt_blocks_out(plaintext, ciphertext)?)) + } + /// As [`BlockCipherEncryptor::encrypt_blocks_out`], but sources randomness from the provided RNG. + fn encrypt_blocks_out_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + plaintext: &[[u8; BLOCK_LEN]; N], + ciphertext: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + Ok((init_data, enc.do_encrypt_blocks_out(plaintext, ciphertext)?)) + } } /// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`]. @@ -156,6 +194,24 @@ pub trait BlockCipherDecryptor< ciphertext: &[[u8; BLOCK_LEN]; N], plaintext: &mut [[u8; BLOCK_LEN]; N], ) -> Result; + + /// One-shot: decrypts `N` blocks from the given init data. + fn decrypt_blocks( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[[u8; BLOCK_LEN]; N], + ) -> Result<[[u8; BLOCK_LEN]; N], SymmetricCipherError> { + Self::do_decrypt_init(key, init_data)?.do_decrypt_blocks(ciphertext) + } + /// One-shot: decrypts `N` blocks from the given init data into the provided buffer. Returns `N * BLOCK_LEN`. + fn decrypt_blocks_out( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[[u8; BLOCK_LEN]; N], + plaintext: &mut [[u8; BLOCK_LEN]; N], + ) -> Result { + Self::do_decrypt_init(key, init_data)?.do_decrypt_blocks_out(ciphertext, plaintext) + } } /// The basic functions of an Authenticated Encryption with Addititional Data cipher. From b770f56f355956483adf508604bea714623f12b6 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 31 Aug 2026 11:55:51 +1000 Subject: [PATCH 4/4] Add 0.1.3 release notes for the block cipher trait changes (PR #96) Co-Authored-By: Claude Fable 5 --- alpha_0.1.3_release_notes.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index 210a5aeb..da7af240 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -3,3 +3,26 @@ ## Major features ## Minor features / bug fixes + +Block cipher traits (PR #96): + +* The single `BlockCipher` streaming trait is split into `BlockCipherEncryptor` and `BlockCipherDecryptor` (mirroring + `KEMEncapsulator` / `KEMDecapsulator`) so the direction is encoded in the implementing type. A minimal `BlockCipher` + supertrait carries the shared `MAX_SECURITY_STRENGTH`; the `SymmetricCipher` one-shot API is no longer a supertrait. +* The single-block `do_{en,de}crypt_block[_out]` methods are replaced by multi-block + `do_{en,de}crypt_blocks[_out]`, taking `&[[u8; BLOCK_LEN]; N]` so the block count is compile-time and + input/output lengths cannot disagree. +* `do_encrypt_init_rng(key, &mut dyn RNG)` is added alongside `do_encrypt_init`, matching the `encaps` / `encaps_rng` + pattern. +* The `do_{en,de}crypt_final[_out]` methods are removed: the traits are now strictly block-aligned, and padding of + arbitrary-length data belongs to a separate `PaddedEncryptor` / `PaddedDecryptor` layer built on top. +* One-shot static APIs are provided (default) methods implemented once in the traits -- `encrypt_blocks`, + `encrypt_blocks_rng`, `encrypt_blocks_out`, `encrypt_blocks_out_rng` on `BlockCipherEncryptor` and `decrypt_blocks`, + `decrypt_blocks_out` on `BlockCipherDecryptor` -- so every block-aligned mode gets the house-standard + take-data-return-result API at no cost to implementors. + +Testing: + +* The core-test-framework block cipher test now takes separate encryptor/decryptor type parameters, exercises N = 1 and + N = 2 (including mixed single/multi-block encrypt vs decrypt sequences), and checks the one-shots agree with the + streaming API and round-trip.