This is Challenge 10 of Cryptopals challenges implemented using Rust language.
Context 💡
This asks us to implement CBC (cipher block chaining) mode of AES-128 encryption. Straightforward. Highly recommend checking out Challenge 7 and Challenge 9, if you haven't yet.
Like ECB mode, CBC is also a block cipher mode. It also encrypts blocks of fixed size of the plaintext with necessary paddings. The difference from ECB comes from the fact that before each block is encrypted with AES-128, it is XORed with the encrypted block (or Initialization Vector, IV for first block) that came before it. So, in a sense, each block is mixed with previous cipher block and then encrypted. Like shown in the following figure:
(Image source: Wikipedia)
Given key and IV, decryption is simply reverse of it. Each block is XORed with previous encrypted block (or IV) after being decrypted using AES.
(Image source: Wikipedia)
Code 🕶
This is going to use aes
crate for AES-128 encryptions and our pkcs#7 padding implementation from Challenge 9.
I'm going to create a quick utility function to xor two slices of bytes:
pub fn xor_bytes(bytes1: &[u8], bytes2: &[u8]) -> Vec<u8> {
bytes1
.iter()
.zip(bytes2.iter())
.map(|(&b1, &b2)| b1 ^ b2)
.collect()
}
The encryption function is straightforward too. It will take message, key and IV as params. Since, we're implementing AES-128, block size is 16. So, encryption will be processed 16 bytes block at a time. step_by()
method of iterator
s is available for this purpose. After each block is encrypted and collected in a Vec
it is hex encoded using hex
crate.
use crate::set_2_block_crypto::c9_implement_pkcs_padding::pad_pkcs7;
use crate::utils::bitwise::xor_bytes;
use aes::cipher::{generic_array::GenericArray, BlockDecrypt, BlockEncrypt, NewBlockCipher};
use aes::Aes128;
pub fn aes_128_cbc_encrypt(message: &str, key_str: &str, iv_str: &str) -> String {
// Normalize message by pkcs7 padding
let padded_message = pad_pkcs7(message, 16);
let msg_bytes = padded_message.as_bytes();
let iv = iv_str.as_bytes().to_vec();
let key = GenericArray::clone_from_slice(key_str.as_bytes());
let cipher = Aes128::new(&key);
let mut encrypted_blocks: Vec<Vec<u8>> = Vec::new();
(0..message.len()).step_by(16).for_each(|x| {
// Take last encrypted block or IV for first block iteration
let last = encrypted_blocks.last().unwrap_or(&iv);
// XOR last encrypted block with current msg block & encrypt result
let xor_block = xor_bytes(last, &msg_bytes[x..x + 16]);
let mut block = GenericArray::clone_from_slice(&xor_block);
cipher.encrypt_block(&mut block);
encrypted_blocks.push(block.into_iter().collect::<Vec<u8>>());
});
hex::encode(encrypted_blocks.into_iter().flatten().collect::<Vec<u8>>())
}
The decryption process is easy to follow too. Except, after being decrypted last byte is read to determine padding that was applied. And remove it to yield original message.
pub fn aes_128_cbc_decrypt(cipher_hex: &str, key_str: &str, iv_str: &str) -> String {
let encrypted_bytes = hex::decode(cipher_hex).unwrap();
let key = GenericArray::clone_from_slice(key_str.as_bytes());
let iv = iv_str.as_bytes();
let cipher = Aes128::new(&key);
let mut decrypted_blocks: Vec<Vec<u8>> = Vec::new();
(0..encrypted_bytes.len()).step_by(16).for_each(|x| {
// Take last of encrypted block or IV in case of first block iteration
let last = if x == 0 {
&iv
} else {
&encrypted_bytes[x - 16..x]
};
// Decrypt AES
let mut block = GenericArray::clone_from_slice(&encrypted_bytes[x..x + 16]);
cipher.decrypt_block(&mut block);
let decrypted_block = block.into_iter().collect::<Vec<u8>>();
// XOR decrypted block with last encrypted block to undo xor during encryption
let xor_block = xor_bytes(last, &decrypted_block);
decrypted_blocks.push(xor_block);
});
// Get number of padding bytes applied during encryption & remove padding
let padding_byte = *decrypted_blocks.last().unwrap().last().unwrap() as usize;
decrypted_blocks
.into_iter()
.flatten()
.take(encrypted_bytes.len() - padding_byte)
.map(|x| x as char)
.collect::<String>()
}
This was a quick & simple implementation for learning purpose. Serious implementations would enforce additional checks like validation of padding applied. And way more efficient operation like parallel decryption of blocks.
See code on GitHub
Find me on:
Twitter - @heyNvN
Top comments (0)