From 7891e35f9f28060e1949839bbaa58f5e7415ffcc Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 30 Aug 2026 17:53:35 +1000 Subject: [PATCH] Add Padding trait and bouncycastle-padding crate (PKCS7, Padded{En,De}cryptor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core: - Add `Padding` trait with in-place `pad(block, data_len)` and constant-time `unpad(block) -> data_len`. - Add `PaddingError { DataLengthTooLong, InvalidPadding }` and a `PaddingError` variant (with From) on `SymmetricCipherError`. crypto/padding (new crate, no_std, no unsafe): - `PKCS7`: RFC 5652 §6.3 padding for any block length 1..=255, enforced at compile time. `unpad` examines every byte with `Condition` mask arithmetic and has a single public decision point, so it does not leak a padding oracle through timing or error detail. - `PaddedEncryptor` / `PaddedDecryptor`: adapt a block-aligned BlockCipherEncryptor / BlockCipherDecryptor to arbitrary-length data. Streaming `do_update_out` / `do_final(self)` plus one-shot `encrypt_out` / `decrypt_out`, with exact output-length helpers. The buffered partial plaintext block is held in a `Secret`. The decryptor withholds one complete block until `do_final`, since only the last block carries padding. - Tests derived from the RFC 5652 rule for PKCS7; adapter tests drive the code with a toy XOR-CBC cipher implementing the new block cipher traits, covering every length, ten chunkings in both directions, tampering, malformed lengths, and buffer sizing. Criterion bench. - Registered in the workspace and re-exported as `bouncycastle::padding`. core-test-framework: - Fix the security-strength loops in the symmetric/block/AEAD suites: `set_security_strength` refuses strengths the key length cannot support even inside `do_hazardous_operations`, so the previous unwrap panicked for any key shorter than 32 bytes. Skip those strengths instead and correct the misleading comment. Co-Authored-By: Claude Fable 5 --- Cargo.toml | 2 + .../src/symmetric_ciphers.rs | 39 +- crypto/core/src/errors.rs | 20 + crypto/core/src/traits.rs | 32 +- crypto/padding/Cargo.toml | 17 + crypto/padding/benches/padding_benches.rs | 27 ++ crypto/padding/src/lib.rs | 115 ++++++ crypto/padding/src/padded.rs | 357 ++++++++++++++++++ crypto/padding/tests/padded_tests.rs | 306 +++++++++++++++ crypto/padding/tests/pkcs7_tests.rs | 121 ++++++ src/lib.rs | 1 + 11 files changed, 1019 insertions(+), 18 deletions(-) create mode 100644 crypto/padding/Cargo.toml create mode 100644 crypto/padding/benches/padding_benches.rs create mode 100644 crypto/padding/src/lib.rs create mode 100644 crypto/padding/src/padded.rs create mode 100644 crypto/padding/tests/padded_tests.rs create mode 100644 crypto/padding/tests/pkcs7_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 82b379fe..684c54d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ bouncycastle-mlkem = { path = "./crypto/mlkem" } bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory" } bouncycastle-mldsa = { path = "./crypto/mldsa" } bouncycastle-mldsa-lowmemory = { path = "./crypto/mldsa-lowmemory" } +bouncycastle-padding = { path = "./crypto/padding" } bouncycastle-rng = { path = "./crypto/rng" } bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } @@ -51,6 +52,7 @@ bouncycastle-mldsa.workspace = true bouncycastle-mldsa-lowmemory.workspace = true bouncycastle-mlkem.workspace = true bouncycastle-mlkem-lowmemory.workspace = true +bouncycastle-padding.workspace = true bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 67f10793..8b8a63e8 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -1,12 +1,13 @@ //! Generic behaviour tests for the symmetric cipher traits. use crate::DUMMY_SEED; -use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; 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. @@ -85,9 +86,13 @@ impl TestFrameworkSymmetricCipher { ]; for ss in security_strengths.iter() { // Tag the key at an arbitrary strength for the purpose of this test. Inside a - // do_hazardous_operations() closure, set_security_strength() raises the strength - // (and bypasses the key-length guard) without complaining. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + // do_hazardous_operations() closure, set_security_strength() may raise the strength, + // but it still refuses a strength the key length cannot support; skip those. + match do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())) { + Ok(()) => {} + Err(KeyMaterialError::SecurityStrength(_)) => continue, + Err(e) => panic!("unexpected error tagging key strength: {e:?}"), + } match C::encrypt_out(&key, msg, &mut ct) { Ok(_) => { @@ -154,9 +159,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(); @@ -225,9 +228,13 @@ impl TestFrameworkBlockCipher { ]; for ss in security_strengths.iter() { // Tag the key at an arbitrary strength for the purpose of this test. Inside a - // do_hazardous_operations() closure, set_security_strength() raises the strength - // (and bypasses the key-length guard) without complaining. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + // do_hazardous_operations() closure, set_security_strength() may raise the strength, + // but it still refuses a strength the key length cannot support; skip those. + match do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())) { + Ok(()) => {} + Err(KeyMaterialError::SecurityStrength(_)) => continue, + Err(e) => panic!("unexpected error tagging key strength: {e:?}"), + } match E::do_encrypt_init(&key) { Ok(_) => { @@ -363,9 +370,13 @@ impl TestFrameworkAEADCipher { ]; for ss in security_strengths.iter() { // Tag the key at an arbitrary strength for the purpose of this test. Inside a - // do_hazardous_operations() closure, set_security_strength() raises the strength - // (and bypasses the key-length guard) without complaining. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + // do_hazardous_operations() closure, set_security_strength() may raise the strength, + // but it still refuses a strength the key length cannot support; skip those. + match do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())) { + Ok(()) => {} + Err(KeyMaterialError::SecurityStrength(_)) => continue, + Err(e) => panic!("unexpected error tagging key strength: {e:?}"), + } // The key-strength requirement must be enforced both by the AEAD one-shot and by the // inherited SymmetricCipher one-shot (encrypt_out), so exercise both. diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 7be5197e..146db90f 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -176,12 +176,32 @@ pub enum SymmetricCipherError { /// KeyMaterialError(KeyMaterialError), /// + PaddingError(PaddingError), + /// RNGError(RNGError), /// StateError(&'static str), } +/// Errors from a [`crate::traits::Padding`] scheme. +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PaddingError { + /// `pad()` was asked to pad more data than fits in a block alongside at least one byte of padding. + /// The usize is the maximum permitted data length (`BLOCK_LEN - 1`). + DataLengthTooLong(usize), + /// `unpad()` found the block does not carry well-formed padding. Deliberately carries no detail + /// about *how* the padding was malformed. + InvalidPadding, +} + /*** Promotion functions ***/ +impl From for SymmetricCipherError { + fn from(e: PaddingError) -> SymmetricCipherError { + Self::PaddingError(e) + } +} + impl From for SymmetricCipherError { fn from(e: KeyMaterialError) -> SymmetricCipherError { Self::KeyMaterialError(e) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 78e3698b..f762258f 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( @@ -152,6 +158,24 @@ pub trait BlockCipherDecryptor Result; } +/// A block padding scheme, used to extend arbitrary-length data to a whole number of blocks so that it +/// can be processed by a [`BlockCipherEncryptor`]. Implementations are pure functions of the block +/// contents: no key, no state. +/// +/// Only the final, partial block of a message is ever padded; the padding layer sitting between the +/// caller and the block cipher is responsible for routing whole blocks straight through. +pub trait Padding { + /// Pads `block` in place: bytes `0..data_len` are data and are left untouched, bytes + /// `data_len..BLOCK_LEN` are overwritten with padding. `data_len` must be less than `BLOCK_LEN` + /// (a full block of data requires a whole additional block of padding, which the caller supplies + /// as `data_len = 0`). + fn pad(block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError>; + /// Returns the number of data bytes in a padded `block`, or [`PaddingError::InvalidPadding`]. + /// Implementations must run in constant time with respect to the block contents, so that a + /// decryptor built on them does not leak a padding oracle. + fn unpad(block: &[u8; BLOCK_LEN]) -> Result; +} + /// The basic functions of an Authenticated Encryption with Addititional Data cipher. pub trait AEADCipher: SymmetricCipher + Sized diff --git a/crypto/padding/Cargo.toml b/crypto/padding/Cargo.toml new file mode 100644 index 00000000..315ce973 --- /dev/null +++ b/crypto/padding/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "bouncycastle-padding" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true + +[[bench]] +name = "padding_benches" +harness = false diff --git a/crypto/padding/benches/padding_benches.rs b/crypto/padding/benches/padding_benches.rs new file mode 100644 index 00000000..1e096af1 --- /dev/null +++ b/crypto/padding/benches/padding_benches.rs @@ -0,0 +1,27 @@ +use bouncycastle_core::traits::Padding; +use bouncycastle_padding::PKCS7; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +fn bench_pkcs7(c: &mut Criterion) { + let mut group = c.benchmark_group("padding::PKCS7"); + group.bench_function("pad/16", |b| { + let mut block = [0u8; 16]; + b.iter(|| { + >::pad(black_box(&mut block), black_box(5)).unwrap(); + black_box(&block); + }) + }); + group.bench_function("unpad/16", |b| { + let mut block = [0u8; 16]; + >::pad(&mut block, 5).unwrap(); + b.iter(|| { + let n = >::unpad(black_box(&block)).unwrap(); + black_box(n); + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_pkcs7); +criterion_main!(benches); diff --git a/crypto/padding/src/lib.rs b/crypto/padding/src/lib.rs new file mode 100644 index 00000000..904a0412 --- /dev/null +++ b/crypto/padding/src/lib.rs @@ -0,0 +1,115 @@ +//! Block padding schemes implementing [`bouncycastle_core::traits::Padding`]. +//! +//! * [`PKCS7`] — the padding scheme of RFC 5652 §6.3. +//! * [`PaddedEncryptor`] / [`PaddedDecryptor`] — adapt a block-aligned +//! [`BlockCipherEncryptor`](bouncycastle_core::traits::BlockCipherEncryptor) / +//! [`BlockCipherDecryptor`](bouncycastle_core::traits::BlockCipherDecryptor) to arbitrary-length +//! data, streaming or one-shot. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_core::traits::Padding; +//! use bouncycastle_padding::PKCS7; +//! +//! // 5 data bytes in a 16-byte block: pad with 11 bytes of value 0x0b. +//! let mut block = [0u8; 16]; +//! block[..5].copy_from_slice(b"hello"); +//! >::pad(&mut block, 5).unwrap(); +//! assert_eq!(&block[..5], b"hello"); +//! assert_eq!(&block[5..], &[0x0b; 11]); +//! +//! // Unpadding recovers the data length. +//! let data_len = >::unpad(&block).unwrap(); +//! assert_eq!(data_len, 5); +//! +//! // A block that is not well-formed padding is rejected. +//! block[15] = 0x00; +//! assert!(>::unpad(&block).is_err()); +//! ``` +//! +//! # Memory Usage +//! +//! | Operation | Stack (excluding the caller's buffers and the inner cipher) | +//! |-----------------------|-------------------------------------------------------------| +//! | `PKCS7::pad` | O(1) | +//! | `PKCS7::unpad` | O(1) | +//! | `PaddedEncryptor` | one `BLOCK_LEN` buffer (in a `Secret`) + a length | +//! | `PaddedDecryptor` | two `BLOCK_LEN` buffers + a length | +//! +//! # Security Considerations +//! +//! `unpad` is the classic padding-oracle site: if timing or the error depends on *which* byte was +//! malformed, an attacker who can submit ciphertexts can decrypt them byte by byte. [`PKCS7::unpad`] +//! inspects every byte with constant-time masks and returns a single undifferentiated +//! [`PaddingError::InvalidPadding`]. This does not make unauthenticated encryption safe: still +//! authenticate the ciphertext (MAC or AEAD) so the error is never reachable by an attacker. + +#![forbid(unsafe_code)] +#![forbid(missing_docs)] +#![no_std] + +mod padded; +pub use padded::{PaddedDecryptor, PaddedEncryptor}; + +use bouncycastle_core::errors::PaddingError; +use bouncycastle_core::traits::Padding; +use bouncycastle_utils::ct::Condition; + +/// RFC 5652 §6.3 padding (the CMS successor to PKCS #7): "the input shall be padded at the trailing +/// end with `k-(lth mod k)` octets all having value `k-(lth mod k)`". Defined only for block lengths +/// `0 < k < 256`, enforced at compile time. +pub struct PKCS7; + +impl Padding for PKCS7 { + fn pad(block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError> { + const { + assert!( + BLOCK_LEN > 0 && BLOCK_LEN < 256, + "PKCS7 padding is only defined for block lengths 1..=255 (RFC 5652 §6.3)" + ) + } + if data_len >= BLOCK_LEN { + return Err(PaddingError::DataLengthTooLong(BLOCK_LEN - 1)); + } + // RFC 5652 §6.3: pad with k - (lth mod k) octets of value k - (lth mod k). Here the caller + // has already reduced lth mod k to data_len, so the value is simply BLOCK_LEN - data_len. + // `data_len < BLOCK_LEN < 256` so this fits in a u8. + let pad_byte = (BLOCK_LEN - data_len) as u8; + // Constant-time in data_len: every byte is visited, and a mask selects data vs padding. + for (i, b) in block.iter_mut().enumerate() { + let is_padding = Condition::::is_gte(i as i64, data_len as i64); + *b = is_padding.select(pad_byte as i64, *b as i64) as u8; + } + Ok(()) + } + + fn unpad(block: &[u8; BLOCK_LEN]) -> Result { + const { + assert!( + BLOCK_LEN > 0 && BLOCK_LEN < 256, + "PKCS7 padding is only defined for block lengths 1..=255 (RFC 5652 §6.3)" + ) + } + let k = BLOCK_LEN as i64; + // The last byte declares the padding length p; the block is valid iff 1 <= p <= k and the + // final p bytes all equal p. Every byte is examined regardless, so timing is independent of + // where (or whether) the padding is malformed. + let p = block[BLOCK_LEN - 1] as i64; + let mut valid = Condition::::is_within_range(p, 1, k); + for (i, b) in block.iter().enumerate() { + // Position i is a padding position iff i >= k - p. (If p is out of range this may select + // every position, but `valid` is already FALSE and cannot become TRUE again.) + let in_padding = Condition::::is_gte(i as i64, k - p); + let matches = Condition::::is_equal(*b as i64, p); + valid &= matches | !in_padding; + } + // Single public decision point: the caller learns only valid/invalid. + if valid.to_bool() { + // p is within 1..=k here, so k - p is in 0..k and the cast is lossless. + Ok((k - p) as usize) + } else { + Err(PaddingError::InvalidPadding) + } + } +} diff --git a/crypto/padding/src/padded.rs b/crypto/padding/src/padded.rs new file mode 100644 index 00000000..cf4bc8f1 --- /dev/null +++ b/crypto/padding/src/padded.rs @@ -0,0 +1,357 @@ +//! [`PaddedEncryptor`] / [`PaddedDecryptor`]: adapt a block-aligned [`BlockCipherEncryptor`] / +//! [`BlockCipherDecryptor`] to arbitrary-length data using a [`Padding`] scheme. + +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, Padding, RNG}; +use bouncycastle_utils::secret::Secret; +use core::array::{from_mut, from_ref}; +use core::marker::PhantomData; + +/// Blocks per inner-cipher call on the bulk path; the remainder is processed one at a time. +const GROUP: usize = 8; + +/// Encrypts arbitrary-length data with a block cipher `E`, padding the final block with `P`. +/// +/// Stream with [`do_update_out`](Self::do_update_out) then [`do_final`](Self::do_final), or use the +/// one-shot [`encrypt_out`](Self::encrypt_out). Output is always `plaintext_len / BLOCK_LEN + 1` +/// blocks. The buffered partial plaintext block is held in a [`Secret`]. +pub struct PaddedEncryptor< + E, + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> where + E: BlockCipherEncryptor, + P: Padding, +{ + inner: E, + /// Partial plaintext block; `buf_len < BLOCK_LEN` between calls. + buf: Secret<[u8; BLOCK_LEN]>, + buf_len: usize, + _padding: PhantomData

, +} + +impl + PaddedEncryptor +where + E: BlockCipherEncryptor, + P: Padding, +{ + /// Begins a streaming encryption, returning the generated init data (e.g. IV). + pub fn new( + key: &KeyMaterial, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (inner, init_data) = E::do_encrypt_init(key)?; + Ok((Self::wrap(inner), init_data)) + } + + /// As [`new`](Self::new), but sources randomness from the provided RNG. + pub fn new_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (inner, init_data) = E::do_encrypt_init_rng(key, rng)?; + Ok((Self::wrap(inner), init_data)) + } + + fn wrap(inner: E) -> Self { + Self { inner, buf: Secret::new(), buf_len: 0, _padding: PhantomData } + } + + /// Exact number of bytes [`do_update_out`](Self::do_update_out) will write for `input_len` more bytes. + pub const fn update_out_len(&self, input_len: usize) -> usize { + (self.buf_len + input_len) / BLOCK_LEN * BLOCK_LEN + } + + /// Encrypts all whole blocks available (buffered + `plaintext`) into `ciphertext`, buffering the + /// remainder. `ciphertext` needs [`update_out_len`](Self::update_out_len) bytes; returns bytes written. + pub fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + let out_len = self.update_out_len(plaintext.len()); + if ciphertext.len() < out_len { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("ciphertext", out_len)); + } + // out_len is a multiple of BLOCK_LEN, so the remainder of this split is empty. + let (mut out_blocks, _) = ciphertext[..out_len].as_chunks_mut::(); + let mut plaintext = plaintext; + + // 1. Top up a previously buffered partial block. + if self.buf_len > 0 { + let take = (BLOCK_LEN - self.buf_len).min(plaintext.len()); + self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&plaintext[..take]); + self.buf_len += take; + plaintext = &plaintext[take..]; + if self.buf_len < BLOCK_LEN { + // All input absorbed into the partial block; nothing to emit (out_len == 0). + return Ok(0); + } + // Block completed. out_len >= BLOCK_LEN here, so `split_first_mut` always succeeds. + if let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() { + self.inner.do_encrypt_blocks_out(from_ref(&*self.buf), from_mut(first))?; + out_blocks = rest; + } + self.buf_len = 0; + } + + // 2. Bulk path: whole blocks straight from the input, in groups of GROUP then singly. + let (in_blocks, remainder) = plaintext.as_chunks::(); + debug_assert_eq!(in_blocks.len(), out_blocks.len()); + let (in_groups, in_tail) = in_blocks.as_chunks::(); + let (out_groups, out_tail) = out_blocks.as_chunks_mut::(); + for (i, o) in in_groups.iter().zip(out_groups.iter_mut()) { + self.inner.do_encrypt_blocks_out(i, o)?; + } + for (i, o) in in_tail.iter().zip(out_tail.iter_mut()) { + self.inner.do_encrypt_blocks_out(from_ref(i), from_mut(o))?; + } + + // 3. Buffer the trailing partial block (remainder.len() < BLOCK_LEN). + self.buf[..remainder.len()].copy_from_slice(remainder); + self.buf_len = remainder.len(); + Ok(out_len) + } + + /// Pads and encrypts the buffered partial block, returning the final ciphertext block. + pub fn do_final(self) -> Result<[u8; BLOCK_LEN], SymmetricCipherError> { + let Self { mut inner, mut buf, buf_len, .. } = self; + // buf_len < BLOCK_LEN is an invariant of this type, so pad() cannot fail here. + P::pad(&mut buf, buf_len)?; + let [ct] = inner.do_encrypt_blocks(from_ref(&*buf))?; + Ok(ct) + } + + /// As [`do_final`](Self::do_final), writing the final block into `ciphertext`. Returns `BLOCK_LEN`. + pub fn do_final_out( + self, + ciphertext: &mut [u8; BLOCK_LEN], + ) -> Result { + let Self { mut inner, mut buf, buf_len, .. } = self; + P::pad(&mut buf, buf_len)?; + inner.do_encrypt_blocks_out(from_ref(&*buf), from_mut(ciphertext)) + } + + /// Ciphertext length for a `plaintext_len`-byte plaintext: `(plaintext_len / BLOCK_LEN + 1) * BLOCK_LEN`. + pub const fn encrypt_out_len(plaintext_len: usize) -> usize { + (plaintext_len / BLOCK_LEN + 1) * BLOCK_LEN + } + + /// One-shot encryption. `ciphertext` needs [`encrypt_out_len`](Self::encrypt_out_len) bytes. + /// Returns the generated init data and bytes written. + pub fn encrypt_out( + key: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + let (enc, init_data) = Self::new(key)?; + let written = enc.finish_one_shot(plaintext, ciphertext)?; + Ok((init_data, written)) + } + + /// As [`encrypt_out`](Self::encrypt_out), but sources randomness from the provided RNG. + pub fn encrypt_out_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + let (enc, init_data) = Self::new_rng(key, rng)?; + let written = enc.finish_one_shot(plaintext, ciphertext)?; + Ok((init_data, written)) + } + + fn finish_one_shot( + mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("ciphertext", needed)); + } + let written = self.do_update_out(plaintext, ciphertext)?; + // The final block always exists and is exactly BLOCK_LEN, so the total is `needed`. + let last = self.do_final()?; + ciphertext[written..needed].copy_from_slice(&last); + Ok(needed) + } +} + +/// Decrypts data produced by a [`PaddedEncryptor`] with the matching cipher and padding. +/// +/// Only the last block carries padding, so [`do_update_out`](Self::do_update_out) always withholds +/// the most recent complete block and [`do_final`](Self::do_final) unpads it. One-shot: +/// [`decrypt_out`](Self::decrypt_out). +pub struct PaddedDecryptor< + D, + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> where + D: BlockCipherDecryptor, + P: Padding, +{ + inner: D, + /// Partial ciphertext block; `buf_len < BLOCK_LEN` between calls. + buf: [u8; BLOCK_LEN], + buf_len: usize, + /// Most recent complete ciphertext block, withheld in case it is the last. + held: Option<[u8; BLOCK_LEN]>, + _padding: PhantomData

, +} + +impl + PaddedDecryptor +where + D: BlockCipherDecryptor, + P: Padding, +{ + /// Begins a streaming decryption from the init data returned by the encryptor. + pub fn new( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result { + Ok(Self { + inner: D::do_decrypt_init(key, init_data)?, + buf: [0u8; BLOCK_LEN], + buf_len: 0, + held: None, + _padding: PhantomData, + }) + } + + /// Exact number of bytes [`do_update_out`](Self::do_update_out) will write for `input_len` more bytes. + pub const fn update_out_len(&self, input_len: usize) -> usize { + let complete = self.held.is_some() as usize + (self.buf_len + input_len) / BLOCK_LEN; + // All complete blocks but the most recent one are released. + complete.saturating_sub(1) * BLOCK_LEN + } + + /// Decrypts all complete blocks except the most recent into `plaintext`, buffering the remainder. + /// `plaintext` needs [`update_out_len`](Self::update_out_len) bytes; returns bytes written. + pub fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let out_len = self.update_out_len(ciphertext.len()); + if plaintext.len() < out_len { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("plaintext", out_len)); + } + let (mut out_blocks, _) = plaintext[..out_len].as_chunks_mut::(); + let mut ciphertext = ciphertext; + + // 1. Top up a previously buffered partial block. + if self.buf_len > 0 { + let take = (BLOCK_LEN - self.buf_len).min(ciphertext.len()); + self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&ciphertext[..take]); + self.buf_len += take; + ciphertext = &ciphertext[take..]; + if self.buf_len < BLOCK_LEN { + return Ok(0); + } + self.buf_len = 0; + // The completed block becomes the held block; the previously held block, if any, is + // now known not to be last and can be released. out_blocks has room for it by + // construction of out_len, so `split_first_mut` succeeds. + if let Some(prev) = self.held.replace(self.buf) + && let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() + { + self.inner.do_decrypt_blocks_out(from_ref(&prev), from_mut(first))?; + out_blocks = rest; + } + } + + // 2. Bulk path. + let (in_blocks, remainder) = ciphertext.as_chunks::(); + if let Some((last, release)) = in_blocks.split_last() { + // Release the previously held block first (it precedes everything in `in_blocks`). + if let Some(prev) = self.held.replace(*last) + && let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() + { + self.inner.do_decrypt_blocks_out(from_ref(&prev), from_mut(first))?; + out_blocks = rest; + } + // Then every block of this call except the new held one. + debug_assert_eq!(release.len(), out_blocks.len()); + let (in_groups, in_tail) = release.as_chunks::(); + let (out_groups, out_tail) = out_blocks.as_chunks_mut::(); + for (i, o) in in_groups.iter().zip(out_groups.iter_mut()) { + self.inner.do_decrypt_blocks_out(i, o)?; + } + for (i, o) in in_tail.iter().zip(out_tail.iter_mut()) { + self.inner.do_decrypt_blocks_out(from_ref(i), from_mut(o))?; + } + } + + // 3. Buffer the trailing partial block. + self.buf[..remainder.len()].copy_from_slice(remainder); + self.buf_len = remainder.len(); + Ok(out_len) + } + + /// Decrypts and unpads the held final block. Returns the block and its data length; the rest is + /// padding. `DecryptionFailed` if the ciphertext was empty or not block-aligned; `PaddingError` + /// if the padding is malformed. + pub fn do_final(self) -> Result<([u8; BLOCK_LEN], usize), SymmetricCipherError> { + let Self { mut inner, buf_len, held, .. } = self; + if buf_len != 0 { + return Err(SymmetricCipherError::DecryptionFailed); + } + let Some(last) = held else { + return Err(SymmetricCipherError::DecryptionFailed); + }; + let [pt] = inner.do_decrypt_blocks(from_ref(&last))?; + let data_len = P::unpad(&pt)?; + Ok((pt, data_len)) + } + + /// As [`do_final`](Self::do_final), writing the block into `plaintext`. Returns its data length. + pub fn do_final_out( + self, + plaintext: &mut [u8; BLOCK_LEN], + ) -> Result { + let Self { mut inner, buf_len, held, .. } = self; + if buf_len != 0 { + return Err(SymmetricCipherError::DecryptionFailed); + } + let Some(last) = held else { + return Err(SymmetricCipherError::DecryptionFailed); + }; + inner.do_decrypt_blocks_out(from_ref(&last), from_mut(plaintext))?; + Ok(P::unpad(plaintext)?) + } + + /// Upper bound on the plaintext recovered from `ciphertext_len` bytes: `ciphertext_len - 1`. + pub const fn decrypt_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(1) + } + + /// One-shot decryption. `plaintext` needs [`decrypt_out_max_len`](Self::decrypt_out_max_len) + /// bytes. Returns bytes written. + pub fn decrypt_out( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + if ciphertext.len() < BLOCK_LEN || !ciphertext.len().is_multiple_of(BLOCK_LEN) { + return Err(SymmetricCipherError::DecryptionFailed); + } + let needed = Self::decrypt_out_max_len(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("plaintext", needed)); + } + let mut dec = Self::new(key, init_data)?; + let written = dec.do_update_out(ciphertext, plaintext)?; + let (last, data_len) = dec.do_final()?; + // written == ciphertext.len() - BLOCK_LEN and data_len < BLOCK_LEN, so this fits in `needed`. + plaintext[written..written + data_len].copy_from_slice(&last[..data_len]); + Ok(written + data_len) + } +} diff --git a/crypto/padding/tests/padded_tests.rs b/crypto/padding/tests/padded_tests.rs new file mode 100644 index 00000000..8bd27941 --- /dev/null +++ b/crypto/padding/tests/padded_tests.rs @@ -0,0 +1,306 @@ +//! Tests for PaddedEncryptor / PaddedDecryptor. +//! +//! No real block cipher exists in the workspace yet, 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_core::errors::{KeyMaterialError, PaddingError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{ + BlockCipher, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SecurityStrength, +}; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkBlockCipher; +use bouncycastle_padding::{PKCS7, PaddedDecryptor, PaddedEncryptor}; +use bouncycastle_rng::hash_drbg80090a::{HashDRBG80090A, HashDRBG80090AParams_SHA256}; + +const B: usize = 8; + +/// c_j = p_j ^ c_{j-1} ^ key ; p_j = c_j ^ c_{j-1} ^ key +struct ToyCbc { + key: [u8; B], + chain: [u8; B], +} + +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) + } +} + +impl BlockCipher for ToyCbc { + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} + +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( + &mut self, + plaintext: &[[u8; B]; N], + ) -> Result<[[u8; B]; N], SymmetricCipherError> { + let mut ct = [[0u8; B]; N]; + self.do_encrypt_blocks_out(plaintext, &mut ct)?; + Ok(ct) + } + fn do_encrypt_blocks_out( + &mut self, + plaintext: &[[u8; B]; N], + ciphertext: &mut [[u8; B]; N], + ) -> Result { + for (p, c) in plaintext.iter().zip(ciphertext.iter_mut()) { + for i in 0..B { + c[i] = p[i] ^ self.chain[i] ^ self.key[i]; + } + self.chain = *c; + } + Ok(N * B) + } +} + +impl BlockCipherDecryptor for ToyCbc { + fn do_decrypt_init(key: &KeyMaterial, iv: &[u8; B]) -> Result { + Ok(Self { key: Self::check_key(key)?, chain: *iv }) + } + fn do_decrypt_blocks( + &mut self, + ciphertext: &[[u8; B]; N], + ) -> Result<[[u8; B]; N], SymmetricCipherError> { + let mut pt = [[0u8; B]; N]; + self.do_decrypt_blocks_out(ciphertext, &mut pt)?; + Ok(pt) + } + fn do_decrypt_blocks_out( + &mut self, + ciphertext: &[[u8; B]; N], + plaintext: &mut [[u8; B]; N], + ) -> Result { + for (c, p) in ciphertext.iter().zip(plaintext.iter_mut()) { + for i in 0..B { + p[i] = c[i] ^ self.chain[i] ^ self.key[i]; + } + self.chain = *c; + } + Ok(N * B) + } +} + +type Enc = PaddedEncryptor; +type Dec = PaddedDecryptor; + +fn key() -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&[0x5a; B], KeyType::SymmetricCipherKey).unwrap() +} + +fn msg(len: usize) -> Vec { + (0..len).map(|i| (i * 7 + 3) as u8).collect() +} + +#[test] +fn toy_cipher_passes_core_test_framework() { + TestFrameworkBlockCipher::new().test::(); +} + +#[test] +fn one_shot_roundtrip_all_lengths() { + let key = key(); + 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(); + 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_max_len(n)]; + let m = Dec::decrypt_out(&key, &iv, &ct[..n], &mut out).unwrap(); + assert_eq!(&out[..m], &pt[..]); + } +} + +#[test] +fn streaming_matches_one_shot_for_every_chunking() { + let key = key(); + let len = 5 * B + 3; + let pt = msg(len); + + for chunk in [1usize, 2, 3, 7, 8, 9, 15, 16, 17, len] { + // encrypt in chunks + let (mut enc, iv) = Enc::new(&key).unwrap(); + let mut ct = Vec::new(); + for piece in pt.chunks(chunk) { + let expect = enc.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact"); + ct.extend_from_slice(&buf[..n]); + } + let last = enc.do_final().unwrap(); + ct.extend_from_slice(&last); + assert_eq!(ct.len(), Enc::encrypt_out_len(len)); + + // one-shot decrypt + let mut out = vec![0u8; Dec::decrypt_out_max_len(ct.len())]; + let m = Dec::decrypt_out(&key, &iv, &ct, &mut out).unwrap(); + assert_eq!(&out[..m], &pt[..], "chunk {chunk}"); + + // decrypt in the same chunks + let mut dec = Dec::new(&key, &iv).unwrap(); + let mut rec = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact (decrypt)"); + rec.extend_from_slice(&buf[..n]); + } + let (block, data_len) = dec.do_final().unwrap(); + rec.extend_from_slice(&block[..data_len]); + assert_eq!(rec, pt, "chunk {chunk}"); + } +} + +#[test] +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(); + (iv, ct) + }; + assert_eq!(ct.len(), 3 * B); + let mut dec = Dec::new(&key, &iv).unwrap(); + let mut out = [0u8; 3 * B]; + // first block: nothing can be released yet + assert_eq!(dec.update_out_len(B), 0); + assert_eq!(dec.do_update_out(&ct[..B], &mut out).unwrap(), 0); + // second block: releases the first + assert_eq!(dec.update_out_len(B), B); + assert_eq!(dec.do_update_out(&ct[B..2 * B], &mut out).unwrap(), B); + // third block: releases the second + assert_eq!(dec.do_update_out(&ct[2 * B..], &mut out[B..]).unwrap(), B); + let (last, n) = dec.do_final().unwrap(); + assert_eq!(n, 0, "block-aligned plaintext => final block is all padding"); + assert_eq!(&out[..2 * B], &msg(2 * B)[..]); + let _ = last; +} + +#[test] +fn final_out_variants() { + let key = key(); + let (mut enc, iv) = Enc::new(&key).unwrap(); + let mut ct = [0u8; 2 * B]; + let n = enc.do_update_out(&msg(B + 2), &mut ct).unwrap(); + assert_eq!(n, B); + let mut last = [0u8; B]; + assert_eq!(enc.do_final_out(&mut last).unwrap(), B); + ct[B..].copy_from_slice(&last); + + let mut dec = Dec::new(&key, &iv).unwrap(); + let mut out = [0u8; B]; + assert_eq!(dec.do_update_out(&ct, &mut out).unwrap(), B); + let mut last_pt = [0u8; B]; + let data_len = dec.do_final_out(&mut last_pt).unwrap(); + assert_eq!(data_len, 2); + let mut rec = out.to_vec(); + rec.extend_from_slice(&last_pt[..data_len]); + assert_eq!(rec, msg(B + 2)); +} + +#[test] +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(); + // flipping the low bit of the final byte corrupts the PKCS7 length byte + ct[n - 1] ^= 0x01; + let mut out = vec![0u8; n]; + match Dec::decrypt_out(&key, &iv, &ct, &mut out) { + Err(SymmetricCipherError::PaddingError(PaddingError::InvalidPadding)) => {} + other => panic!("len {len}: expected InvalidPadding, got {other:?}"), + } + } +} + +#[test] +fn malformed_ciphertext_lengths_are_rejected() { + let key = key(); + let iv = [0u8; B]; + let mut out = [0u8; 4 * B]; + + // empty + assert!(matches!( + Dec::decrypt_out(&key, &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), + Err(SymmetricCipherError::DecryptionFailed) + )); + // streaming: partial trailing block at final + let mut dec = Dec::new(&key, &iv).unwrap(); + dec.do_update_out(&[0u8; B + 3], &mut out).unwrap(); + assert!(matches!(dec.do_final(), Err(SymmetricCipherError::DecryptionFailed))); + // streaming: nothing fed at all + let dec = Dec::new(&key, &iv).unwrap(); + assert!(matches!(dec.do_final(), Err(SymmetricCipherError::DecryptionFailed))); +} + +#[test] +fn output_buffer_too_small_reports_required_length() { + let key = key(); + let pt = msg(2 * B + 1); + + let mut small = [0u8; 2 * B]; + match Enc::encrypt_out(&key, &pt, &mut small) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, need)) => assert_eq!(need, 3 * B), + other => panic!("{other:?}"), + } + + let (mut enc, iv) = Enc::new(&key).unwrap(); + let mut tiny = [0u8; B - 1]; + match enc.do_update_out(&pt, &mut tiny) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, need)) => assert_eq!(need, 2 * B), + other => panic!("{other:?}"), + } + drop(enc); + + let ct = [0u8; 3 * B]; + let mut small = [0u8; 3 * B - 2]; + match Dec::decrypt_out(&key, &iv, &ct, &mut small) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, need)) => { + assert_eq!(need, 3 * B - 1) + } + other => panic!("{other:?}"), + } +} + +#[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::new(&mac_key), Err(SymmetricCipherError::KeyMaterialError(_)))); + assert!(matches!( + Dec::new(&mac_key, &[0u8; B]), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); +} diff --git a/crypto/padding/tests/pkcs7_tests.rs b/crypto/padding/tests/pkcs7_tests.rs new file mode 100644 index 00000000..d68de485 --- /dev/null +++ b/crypto/padding/tests/pkcs7_tests.rs @@ -0,0 +1,121 @@ +//! Tests for PKCS7 against the rule of RFC 5652 §6.3: +//! "the input shall be padded at the trailing end with k-(lth mod k) octets all having value +//! k-(lth mod k)". There are no official test vectors for this scheme; expected values below are +//! computed directly from that rule. + +use bouncycastle_core::errors::PaddingError; +use bouncycastle_core::traits::Padding; +use bouncycastle_padding::PKCS7; + +fn roundtrip_all_lengths() { + for data_len in 0..K { + let mut block = [0xA5u8; K]; + for (i, b) in block.iter_mut().enumerate().take(data_len) { + *b = i as u8; + } + let original = block; + + >::pad(&mut block, data_len).unwrap(); + + // data untouched + assert_eq!(&block[..data_len], &original[..data_len]); + // RFC 5652 §6.3: k - (lth mod k) octets, each of value k - (lth mod k) + let expected_pad = K - data_len; + assert_eq!(block[data_len..].len(), expected_pad); + assert!(block[data_len..].iter().all(|&b| b as usize == expected_pad)); + + assert_eq!(>::unpad(&block), Ok(data_len)); + } +} + +#[test] +fn roundtrip_16() { + roundtrip_all_lengths::<16>(); +} + +#[test] +fn roundtrip_8() { + roundtrip_all_lengths::<8>(); +} + +#[test] +fn roundtrip_boundary_block_lengths() { + roundtrip_all_lengths::<1>(); + roundtrip_all_lengths::<255>(); +} + +#[test] +fn rfc5652_worked_examples() { + // RFC 5652 §6.3 lists the padding strings: "01 -- if lth mod k = k-1", "02 02 -- if lth mod k = k-2", + // ..., "k k ... k k -- if lth mod k = 0". + const K: usize = 16; + let mut b = [0xFFu8; K]; + >::pad(&mut b, K - 1).unwrap(); + assert_eq!(b[K - 1], 0x01); + + let mut b = [0xFFu8; K]; + >::pad(&mut b, K - 2).unwrap(); + assert_eq!(&b[K - 2..], &[0x02, 0x02]); + + let mut b = [0xFFu8; K]; + >::pad(&mut b, 0).unwrap(); + assert_eq!(b, [K as u8; K]); +} + +#[test] +fn pad_rejects_full_block() { + let mut b = [0u8; 16]; + assert_eq!(>::pad(&mut b, 16), Err(PaddingError::DataLengthTooLong(15))); + assert_eq!(>::pad(&mut b, 17), Err(PaddingError::DataLengthTooLong(15))); + // block untouched on error + assert_eq!(b, [0u8; 16]); +} + +#[test] +fn unpad_rejects_malformed() { + const K: usize = 16; + + // last byte zero: no such padding string + let mut b = [0x00u8; K]; + assert_eq!(>::unpad(&b), Err(PaddingError::InvalidPadding)); + + // last byte greater than k + b[K - 1] = (K + 1) as u8; + assert_eq!(>::unpad(&b), Err(PaddingError::InvalidPadding)); + b[K - 1] = 0xFF; + assert_eq!(>::unpad(&b), Err(PaddingError::InvalidPadding)); + + // claims 4 bytes of padding but one of them is wrong, at every possible position + for bad in 0..4 { + let mut b = [0x11u8; K]; + b[K - 4..].copy_from_slice(&[0x04; 4]); + b[K - 4 + bad] ^= 0x01; + if bad == 3 { + // corrupting the length byte itself turns it into 0x05; the preceding bytes are 0x04, so + // still invalid + assert_eq!(b[K - 1], 0x05); + } + assert_eq!( + >::unpad(&b), + Err(PaddingError::InvalidPadding), + "bad position {bad}" + ); + } + + // a full padding block with a single wrong byte anywhere is invalid + for pos in 0..K { + let mut b = [K as u8; K]; + b[pos] ^= 0x80; + assert_eq!(>::unpad(&b), Err(PaddingError::InvalidPadding)); + } +} + +#[test] +fn unpad_ignores_data_bytes_that_happen_to_equal_pad_value() { + // data bytes equal to the pad value must not confuse the length recovery + const K: usize = 16; + let mut b = [0x03u8; K]; // 13 data bytes all 0x03, then 3 bytes of 0x03 padding + >::pad(&mut b, 13).unwrap(); + assert_eq!(b, [0x03u8; K]); + assert_eq!(>::unpad(&b), Ok(13)); +} diff --git a/src/lib.rs b/src/lib.rs index b46df8cd..b3a24eac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub use bouncycastle_mldsa as mldsa; pub use bouncycastle_mldsa_lowmemory as mldsa_lowmemory; pub use bouncycastle_mlkem as mlkem; pub use bouncycastle_mlkem_lowmemory as mlkem_lowmemory; +pub use bouncycastle_padding as padding; pub use bouncycastle_rng as rng; pub use bouncycastle_sha2 as sha2; pub use bouncycastle_sha3 as sha3;