From 454d1d601108acf7da63ffb22768af0c83b48e42 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 31 Dec 2025 00:58:13 +0200 Subject: [PATCH] feat(aes): add AES-CBC mode implementation Add AesCbc struct with: - CBC mode encryption with PKCS#7 padding - CBC mode decryption with padding validation - XOR chaining with IV for first block - Expose encrypt_block/decrypt_block as pub(crate) --- aes/src/aes.rs | 4 +- aes/src/cbc.rs | 155 +++++++++++++++++++++++++++++++++++++++++++++++++ aes/src/lib.rs | 3 +- 3 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 aes/src/cbc.rs diff --git a/aes/src/aes.rs b/aes/src/aes.rs index c8df15a..de4a950 100644 --- a/aes/src/aes.rs +++ b/aes/src/aes.rs @@ -32,7 +32,7 @@ impl Aes { &self.subkeys } - fn encrypt_block(&self, mut state: Block128) -> Block128 { + pub(crate) fn encrypt_block(&self, mut state: Block128) -> Block128 { let mut keys = self.subkeys.chunks(); state = add_round_key(state, keys.next().expect("Round key 0")); @@ -51,7 +51,7 @@ impl Aes { state } - fn decrypt_block(&self, mut state: Block128) -> Block128 { + pub(crate) fn decrypt_block(&self, mut state: Block128) -> Block128 { let mut keys = self.subkeys.chunks_rev(); state = add_round_key(state, keys.next().expect("Final round key")); diff --git a/aes/src/cbc.rs b/aes/src/cbc.rs new file mode 100644 index 0000000..dbca2e2 --- /dev/null +++ b/aes/src/cbc.rs @@ -0,0 +1,155 @@ +//! AES-CBC (Cipher Block Chaining) mode implementation. +//! +//! CBC mode combines each plaintext block with the previous ciphertext block +//! (using XOR) before encryption. The first block uses an Initialization Vector (IV). + +use crate::{Aes, Block128, Iv, key::Key}; +use cipher_core::{CipherError, CipherResult, pkcs7_pad, pkcs7_unpad}; + +const BLOCK_SIZE: usize = 16; + +/// AES cipher in CBC (Cipher Block Chaining) mode. +/// +/// CBC mode provides semantic security by combining each plaintext block +/// with the previous ciphertext block (via XOR) before encryption. +/// +/// # Example +/// +/// ``` +/// use aes::{AesCbc, Iv}; +/// +/// let key = 0x2b7e1516_28aed2a6_abf71588_09cf4f3c_u128; +/// let iv = Iv::new(0x00010203_04050607_08090a0b_0c0d0e0f_u128); +/// let cipher = AesCbc::new(key, iv); +/// +/// let plaintext = b"Hello, World!"; +/// let ciphertext = cipher.encrypt(plaintext).unwrap(); +/// let decrypted = cipher.decrypt(&ciphertext).unwrap(); +/// assert_eq!(decrypted, plaintext); +/// ``` +pub struct AesCbc { + aes: Aes, + iv: Iv, +} + +impl AesCbc { + /// Creates a new AES-CBC cipher with the given key and IV. + #[must_use] + pub fn new(key: impl Into, iv: impl Into) -> Self { + Self { + aes: Aes::from_key(key), + iv: iv.into(), + } + } + + /// Encrypts plaintext using CBC mode with PKCS#7 padding. + /// + /// # Errors + /// + /// Returns `CipherError` if encryption fails. + #[allow(clippy::missing_panics_doc)] + pub fn encrypt(&self, plaintext: &[u8]) -> CipherResult> { + let padded = pkcs7_pad(plaintext, BLOCK_SIZE); + let mut ciphertext = Vec::with_capacity(padded.len()); + let mut prev_block = self.iv.to_block(); + + for chunk in padded.chunks_exact(BLOCK_SIZE) { + // chunks_exact guarantees exactly BLOCK_SIZE bytes + let plain_block = Block128::from_be_bytes(chunk.try_into().expect("exact chunk size")); + let xored = plain_block ^ prev_block.as_u128(); + let encrypted = self.aes.encrypt_block(xored); + ciphertext.extend_from_slice(&encrypted.to_be_bytes()); + prev_block = encrypted; + } + + Ok(ciphertext) + } + + /// Decrypts ciphertext using CBC mode and removes PKCS#7 padding. + /// + /// # Errors + /// + /// Returns `CipherError::InvalidBlockSize` if ciphertext length is not a multiple of 16. + /// Returns `CipherError::InvalidPadding` if padding is invalid. + #[allow(clippy::missing_panics_doc)] + pub fn decrypt(&self, ciphertext: &[u8]) -> CipherResult> { + if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(BLOCK_SIZE) { + return Err(CipherError::invalid_block_size( + BLOCK_SIZE, + ciphertext.len(), + )); + } + + let mut plaintext = Vec::with_capacity(ciphertext.len()); + let mut prev_block = self.iv.to_block(); + + for chunk in ciphertext.chunks_exact(BLOCK_SIZE) { + // chunks_exact guarantees exactly BLOCK_SIZE bytes + let cipher_block = Block128::from_be_bytes(chunk.try_into().expect("exact chunk size")); + let decrypted = self.aes.decrypt_block(cipher_block); + let plain_block = decrypted ^ prev_block.as_u128(); + plaintext.extend_from_slice(&plain_block.to_be_bytes()); + prev_block = cipher_block; + } + + let unpadded = pkcs7_unpad(&plaintext, BLOCK_SIZE)?; + Ok(unpadded.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use claims::{assert_err, assert_ok}; + + #[test] + fn encrypt_decrypt_roundtrip() { + let key = 0x2b7e_1516_28ae_d2a6_abf7_1588_09cf_4f3c_u128; + let iv = Iv::new(0x0001_0203_0405_0607_0809_0a0b_0c0d_0e0f_u128); + let cipher = AesCbc::new(key, iv); + + let plaintext = b"Hello, World!"; + let ciphertext = assert_ok!(cipher.encrypt(plaintext)); + let decrypted = assert_ok!(cipher.decrypt(&ciphertext)); + + assert_eq!(decrypted, plaintext); + } + + #[test] + fn encrypt_decrypt_exact_block() { + let key = 0x2b7e_1516_28ae_d2a6_abf7_1588_09cf_4f3c_u128; + let iv = Iv::new(0x0001_0203_0405_0607_0809_0a0b_0c0d_0e0f_u128); + let cipher = AesCbc::new(key, iv); + + let plaintext = [0u8; 16]; + let ciphertext = assert_ok!(cipher.encrypt(&plaintext)); + // Padded to 32 bytes (16 data + 16 padding) + assert_eq!(ciphertext.len(), 32); + + let decrypted = assert_ok!(cipher.decrypt(&ciphertext)); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn encrypt_decrypt_multiple_blocks() { + let key = 0x2b7e_1516_28ae_d2a6_abf7_1588_09cf_4f3c_u128; + let iv = Iv::new(0x0001_0203_0405_0607_0809_0a0b_0c0d_0e0f_u128); + let cipher = AesCbc::new(key, iv); + + let plaintext = b"The quick brown fox jumps over the lazy dog"; + let ciphertext = assert_ok!(cipher.encrypt(plaintext)); + let decrypted = assert_ok!(cipher.decrypt(&ciphertext)); + + assert_eq!(decrypted, plaintext); + } + + #[test] + fn decrypt_invalid_length_fails() { + let key = 0x2b7e_1516_28ae_d2a6_abf7_1588_09cf_4f3c_u128; + let iv = Iv::new(0x0001_0203_0405_0607_0809_0a0b_0c0d_0e0f_u128); + let cipher = AesCbc::new(key, iv); + + let invalid = [0u8; 15]; + assert_err!(cipher.decrypt(&invalid)); + } +} diff --git a/aes/src/lib.rs b/aes/src/lib.rs index c09ee4e..39c28f5 100644 --- a/aes/src/lib.rs +++ b/aes/src/lib.rs @@ -13,10 +13,11 @@ mod aes; mod block; +mod cbc; mod constants; mod iv; mod key; mod operations; mod sbox; -pub use {aes::Aes, block::Block32, block::Block128, iv::Iv}; +pub use {aes::Aes, block::Block32, block::Block128, cbc::AesCbc, iv::Iv};