Skip to content

Instantly share code, notes, and snippets.

@leiless
Created April 3, 2024 03:27
Show Gist options
  • Save leiless/b0e9aba1e629745610ed577b50bf0927 to your computer and use it in GitHub Desktop.
Save leiless/b0e9aba1e629745610ed577b50bf0927 to your computer and use it in GitHub Desktop.
Rust: AES-256, CBC mode, PKCS#7 padding
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::{KeyIvInit, BlockEncryptMut, BlockDecryptMut};
use rand::RngCore;
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
const AES256_SECRET_SIZE: usize = 32;
const AES256_BLOCK_SIZE: usize = 16;
#[derive(Default)]
pub struct AES256 {
secret: Vec<u8>,
}
impl Drop for AES256 {
fn drop(&mut self) {
self.secret.fill(0);
}
}
impl AES256 {
pub fn new(secret: &[u8]) -> anyhow::Result<Self> {
if secret.len() != AES256_SECRET_SIZE {
return Err(anyhow::anyhow!("AES-256 secret length must be {} bytes, got {} bytes", AES256_SECRET_SIZE, secret.len()));
}
if secret.iter().all(|x| *x == 0) {
return Err(anyhow::anyhow!("All zero-byte secret is not allowed"));
}
Ok(Self {
secret: secret.to_owned(),
})
}
#[inline(always)]
fn _calc_padding_len<T: AsRef<[u8]>>(plaintext: T) -> usize {
let plaintext = plaintext.as_ref();
// Always align to the next block size (even if the plaintext size is already aligned to block size)
((plaintext.len() as f64 / AES256_BLOCK_SIZE as f64 + 1.0).floor() * AES256_BLOCK_SIZE as f64) as usize
}
fn _encrypt<T: AsRef<[u8]>>(&self, plaintext: T, iv: T) -> anyhow::Result<Vec<u8>> {
let plaintext = plaintext.as_ref();
let iv = iv.as_ref();
let padded_len = Self::_calc_padding_len(plaintext);
let mut ciphertext_padded = vec![0u8; padded_len];
Aes256CbcEnc::new(self.secret.as_slice().into(), iv.into())
.encrypt_padded_b2b_mut::<Pkcs7>(plaintext, &mut ciphertext_padded)
.map_err(|err| anyhow::anyhow!("AES-256 encrypt: {}", err))?;
debug_assert!(!ciphertext_padded.iter().all(|v| *v == 0));
Ok(ciphertext_padded)
}
pub fn encrypt<T: AsRef<[u8]>>(&self, plaintext: T) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
let mut iv = vec![0u8; AES256_BLOCK_SIZE];
rand::rngs::OsRng.fill_bytes(&mut iv);
debug_assert!(!iv.iter().all(|v| *v == 0));
let ciphertext_padded = self._encrypt(plaintext.as_ref(), &iv)?;
Ok((ciphertext_padded, iv))
}
pub fn encrypt_with_iv<T: AsRef<[u8]>>(&self, plaintext: T, iv: T) -> anyhow::Result<Vec<u8>> {
let ciphertext_padded = self._encrypt(plaintext.as_ref(), iv.as_ref())?;
Ok(ciphertext_padded)
}
// ciphertext is already padded.
pub fn decrypt<T: AsRef<[u8]>>(&self, ciphertext: T, iv: T) -> anyhow::Result<Vec<u8>> {
let ciphertext = ciphertext.as_ref();
let iv = iv.as_ref();
let mut buf = vec![0u8; ciphertext.len()];
let plaintext = Aes256CbcDec::new(self.secret.as_slice().into(), iv.into())
.decrypt_padded_b2b_mut::<Pkcs7>(ciphertext, &mut buf)
.map_err(|err| anyhow::anyhow!("AES-256 decrypt: {}", err))?;
Ok(plaintext.to_owned())
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_aes256_encrypt() -> () {
let secret = [0x55u8; 32];
let plaintext = b"hello world! this is my plaintext.";
let padded_len = super::AES256::_calc_padding_len(plaintext);
let h = super::AES256::new(&secret).unwrap();
let (ciphertext, iv) = h.encrypt(plaintext).unwrap();
assert_eq!(ciphertext.len(), padded_len);
assert_eq!(iv.len(), super::AES256_BLOCK_SIZE);
let plaintext2 = h.decrypt(&ciphertext, &iv).unwrap();
assert_eq!(plaintext2, plaintext);
let h2 = super::AES256::new(&secret).unwrap();
let plaintext3 = h2.decrypt(&ciphertext, &iv).unwrap();
assert_eq!(plaintext3, plaintext);
}
#[test]
fn test_aes256_encrypt_core() -> () {
let secret = ['a' as u8; 32];
let iv = ['b' as u8; super::AES256_BLOCK_SIZE];
let plaintext = b"hello world! this is my plaintext.";
let padded_len = super::AES256::_calc_padding_len(plaintext);
let expected_ciphertext = b"\x9d\x02\x17$G,\x8d<=\xfcc\x90\xd9\x86/\\\xe0\x95\xb3\x11\x89f\x89j\x1b\x1a\x04\x7f\x01\x88\xb1|\x1b\xfcE-\xa2y\x99\x8c\xb1\x85)\xe3K`\x92\xf7";
let h = super::AES256::new(&secret).unwrap();
let ciphertext = h._encrypt(plaintext.as_slice(), iv.as_slice()).unwrap();
assert_eq!(ciphertext.len(), padded_len);
assert_eq!(ciphertext, expected_ciphertext);
let plaintext2 = h.decrypt(ciphertext.as_slice(), &iv).unwrap();
assert_eq!(plaintext2, plaintext);
}
#[test]
fn test_aes256_encrypt_iter() -> () {
let secret = ['a' as u8; 32];
let iv = ['b' as u8; super::AES256_BLOCK_SIZE];
let long_text = b"Microsoft released the first Windows Server 2025 Insider preview build last week. However, soon after, a newer version was leaked online. As first reported by Windows Latest, the leaked version contains some new in-development features, including new settings for a Windows 'sudo' command.";
for i in 0..long_text.len() {
let plaintext = &long_text[..i];
let padded_len = super::AES256::_calc_padding_len(plaintext);
let h = super::AES256::new(&secret).unwrap();
let ciphertext = h._encrypt(plaintext, iv.as_slice()).unwrap();
assert_eq!(ciphertext.len(), padded_len);
let plaintext2 = h.decrypt(ciphertext.as_slice(), &iv).unwrap();
assert_eq!(plaintext2, plaintext);
}
}
}
@leiless
Copy link
Author

leiless commented Apr 3, 2024

[dependencies]
anyhow = "1.0"
aes = "0.8"
cbc = { version = "0.1", features = ["block-padding"] }
rand = { version = "0.8", features = ["std", "std_rng"] }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment