Created
April 3, 2024 03:27
-
-
Save leiless/b0e9aba1e629745610ed577b50bf0927 to your computer and use it in GitHub Desktop.
Rust: AES-256, CBC mode, PKCS#7 padding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
Author
leiless
commented
Apr 3, 2024
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment