DEV Community

Cover image for CryptoPals Crypto Challenges Using Rust: Implement CBC Mode
Naveen ⚑
Naveen ⚑

Posted on

CryptoPals Crypto Challenges Using Rust: Implement CBC Mode

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:
cbc mode
(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.
cbc mode
(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()
}
Enter fullscreen mode Exit fullscreen mode

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 iterators 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>>())
}
Enter fullscreen mode Exit fullscreen mode

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>()
}
Enter fullscreen mode Exit fullscreen mode

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

naveeen.com

Top comments (0)