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)
This commit is contained in:
Kristofers Solo 2025-12-31 00:58:13 +02:00
parent dd691cfa18
commit 454d1d6011
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
3 changed files with 159 additions and 3 deletions

View File

@ -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"));

155
aes/src/cbc.rs Normal file
View File

@ -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<Key>, iv: impl Into<Iv>) -> 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<Vec<u8>> {
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<Vec<u8>> {
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));
}
}

View File

@ -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};