mirror of
https://github.com/kristoferssolo/cipher-workshop.git
synced 2026-01-14 04:36:04 +00:00
feat(aes-cbc): embed IV in encrypted output
This commit is contained in:
parent
8490e594ea
commit
6eb3668147
@ -44,13 +44,19 @@ impl AesCbc {
|
|||||||
|
|
||||||
/// Encrypts plaintext using CBC mode with PKCS#7 padding.
|
/// Encrypts plaintext using CBC mode with PKCS#7 padding.
|
||||||
///
|
///
|
||||||
|
/// The output format is: `[16-byte IV][ciphertext...]`
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `CipherError` if encryption fails.
|
/// Returns `CipherError` if encryption fails.
|
||||||
#[allow(clippy::missing_panics_doc)]
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn encrypt(&self, plaintext: &[u8]) -> CipherResult<Vec<u8>> {
|
pub fn encrypt(&self, plaintext: &[u8]) -> CipherResult<Vec<u8>> {
|
||||||
let padded = pkcs7_pad(plaintext, BLOCK_SIZE);
|
let padded = pkcs7_pad(plaintext, BLOCK_SIZE);
|
||||||
let mut ciphertext = Vec::with_capacity(padded.len());
|
let mut output = Vec::with_capacity(BLOCK_SIZE + padded.len());
|
||||||
|
|
||||||
|
// Prepend IV to output
|
||||||
|
output.extend_from_slice(&self.iv.to_be_bytes());
|
||||||
|
|
||||||
let mut prev_block = self.iv.to_block();
|
let mut prev_block = self.iv.to_block();
|
||||||
|
|
||||||
for chunk in padded.chunks_exact(BLOCK_SIZE) {
|
for chunk in padded.chunks_exact(BLOCK_SIZE) {
|
||||||
@ -58,30 +64,36 @@ impl AesCbc {
|
|||||||
let plain_block = Block128::from_be_bytes(chunk.try_into().expect("exact chunk size"));
|
let plain_block = Block128::from_be_bytes(chunk.try_into().expect("exact chunk size"));
|
||||||
let xored = plain_block ^ prev_block.as_u128();
|
let xored = plain_block ^ prev_block.as_u128();
|
||||||
let encrypted = self.aes.encrypt_block(xored);
|
let encrypted = self.aes.encrypt_block(xored);
|
||||||
ciphertext.extend_from_slice(&encrypted.to_be_bytes());
|
output.extend_from_slice(&encrypted.to_be_bytes());
|
||||||
prev_block = encrypted;
|
prev_block = encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ciphertext)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypts ciphertext using CBC mode and removes PKCS#7 padding.
|
/// Decrypts ciphertext using CBC mode and removes PKCS#7 padding.
|
||||||
///
|
///
|
||||||
|
/// Expects input format: `[16-byte IV][ciphertext...]`
|
||||||
|
/// The IV is extracted from the input; the IV stored in `self` is ignored.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `CipherError::InvalidBlockSize` if ciphertext length is not a multiple of 16.
|
/// Returns `CipherError::InvalidBlockSize` if input length is not a multiple of 16
|
||||||
|
/// or is less than 32 bytes (IV + at least one block).
|
||||||
/// Returns `CipherError::InvalidPadding` if padding is invalid.
|
/// Returns `CipherError::InvalidPadding` if padding is invalid.
|
||||||
#[allow(clippy::missing_panics_doc)]
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn decrypt(&self, ciphertext: &[u8]) -> CipherResult<Vec<u8>> {
|
pub fn decrypt(&self, data: &[u8]) -> CipherResult<Vec<u8>> {
|
||||||
if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(BLOCK_SIZE) {
|
// Need at least IV (16 bytes) + one ciphertext block (16 bytes)
|
||||||
return Err(CipherError::invalid_block_size(
|
if data.len() < BLOCK_SIZE * 2 || !data.len().is_multiple_of(BLOCK_SIZE) {
|
||||||
BLOCK_SIZE,
|
return Err(CipherError::invalid_block_size(BLOCK_SIZE, data.len()));
|
||||||
ciphertext.len(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract IV from first block
|
||||||
|
let iv = Iv::from_be_bytes(data[..BLOCK_SIZE].try_into().expect("exact IV size"));
|
||||||
|
let ciphertext = &data[BLOCK_SIZE..];
|
||||||
|
|
||||||
let mut plaintext = Vec::with_capacity(ciphertext.len());
|
let mut plaintext = Vec::with_capacity(ciphertext.len());
|
||||||
let mut prev_block = self.iv.to_block();
|
let mut prev_block = iv.to_block();
|
||||||
|
|
||||||
for chunk in ciphertext.chunks_exact(BLOCK_SIZE) {
|
for chunk in ciphertext.chunks_exact(BLOCK_SIZE) {
|
||||||
// chunks_exact guarantees exactly BLOCK_SIZE bytes
|
// chunks_exact guarantees exactly BLOCK_SIZE bytes
|
||||||
@ -123,8 +135,8 @@ mod tests {
|
|||||||
|
|
||||||
let plaintext = [0u8; 16];
|
let plaintext = [0u8; 16];
|
||||||
let ciphertext = assert_ok!(cipher.encrypt(&plaintext));
|
let ciphertext = assert_ok!(cipher.encrypt(&plaintext));
|
||||||
// Padded to 32 bytes (16 data + 16 padding)
|
// 16 IV + 16 data + 16 padding = 48 bytes
|
||||||
assert_eq!(ciphertext.len(), 32);
|
assert_eq!(ciphertext.len(), 48);
|
||||||
|
|
||||||
let decrypted = assert_ok!(cipher.decrypt(&ciphertext));
|
let decrypted = assert_ok!(cipher.decrypt(&ciphertext));
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
|
|||||||
@ -33,9 +33,10 @@ fn nist_single_block_encrypt() {
|
|||||||
|
|
||||||
let ciphertext = assert_ok!(cipher.encrypt(&plaintext));
|
let ciphertext = assert_ok!(cipher.encrypt(&plaintext));
|
||||||
|
|
||||||
// Result includes PKCS#7 padding (16 bytes padding for aligned input)
|
// 16 IV + 16 block + 16 padding = 48 bytes
|
||||||
assert_eq!(ciphertext.len(), 32);
|
assert_eq!(ciphertext.len(), 48);
|
||||||
assert_eq!(&ciphertext[..16], &expected);
|
// First 16 bytes are IV, next 16 are the ciphertext
|
||||||
|
assert_eq!(&ciphertext[16..32], &expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -68,10 +69,10 @@ fn nist_multi_block_encrypt() {
|
|||||||
|
|
||||||
let ciphertext = assert_ok!(cipher.encrypt(&plaintext));
|
let ciphertext = assert_ok!(cipher.encrypt(&plaintext));
|
||||||
|
|
||||||
// Result includes padding (64 + 16 = 80 bytes)
|
// 16 IV + 64 blocks + 16 padding = 96 bytes
|
||||||
assert_eq!(ciphertext.len(), 80);
|
assert_eq!(ciphertext.len(), 96);
|
||||||
// First 3 blocks should match NIST vectors exactly
|
// First 16 bytes are IV, then ciphertext blocks
|
||||||
assert_eq!(&ciphertext[..48], &expected[..48]);
|
assert_eq!(&ciphertext[16..64], &expected[..48]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -94,8 +95,8 @@ fn empty_plaintext() {
|
|||||||
let cipher = AesCbc::new(NIST_KEY, Iv::new(NIST_IV));
|
let cipher = AesCbc::new(NIST_KEY, Iv::new(NIST_IV));
|
||||||
|
|
||||||
let ciphertext = assert_ok!(cipher.encrypt(&[]));
|
let ciphertext = assert_ok!(cipher.encrypt(&[]));
|
||||||
// Empty input gets full block of padding
|
// 16 IV + 16 padding = 32 bytes
|
||||||
assert_eq!(ciphertext.len(), 16);
|
assert_eq!(ciphertext.len(), 32);
|
||||||
|
|
||||||
let decrypted = assert_ok!(cipher.decrypt(&ciphertext));
|
let decrypted = assert_ok!(cipher.decrypt(&ciphertext));
|
||||||
assert!(decrypted.is_empty());
|
assert!(decrypted.is_empty());
|
||||||
|
|||||||
@ -79,11 +79,15 @@ impl Algorithm {
|
|||||||
|
|
||||||
/// Decrypts data using CBC mode and removes PKCS#7 padding.
|
/// Decrypts data using CBC mode and removes PKCS#7 padding.
|
||||||
///
|
///
|
||||||
|
/// The IV is extracted from the first 16 bytes of the ciphertext.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `CipherError` if decryption fails or padding is invalid.
|
/// Returns `CipherError` if decryption fails or padding is invalid.
|
||||||
pub fn decrypt_cbc(&self, key: &str, iv: &str, ciphertext: &[u8]) -> CipherResult<Vec<u8>> {
|
pub fn decrypt_cbc(&self, key: &str, ciphertext: &[u8]) -> CipherResult<Vec<u8>> {
|
||||||
let cipher = self.new_cbc_cipher(key, iv)?;
|
// IV is embedded in ciphertext, use dummy IV for cipher construction
|
||||||
|
let dummy_iv = "0x00000000000000000000000000000000";
|
||||||
|
let cipher = self.new_cbc_cipher(key, dummy_iv)?;
|
||||||
cipher.decrypt(ciphertext)
|
cipher.decrypt(ciphertext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,13 +47,16 @@ pub fn CipherFormCbc() -> AnyView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IV is only required for encryption (it's embedded in ciphertext for decryption)
|
||||||
|
let formatted_iv = if mode.get() == OperationMode::Encrypt {
|
||||||
if iv.is_empty() {
|
if iv.is_empty() {
|
||||||
set_error_msg("Please enter an initialization vector (IV).".to_string());
|
set_error_msg("Please enter an initialization vector (IV).".to_string());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
format!("0x{iv}")
|
||||||
// Format IV with 0x prefix (key keeps user format, IV is always hex)
|
} else {
|
||||||
let formatted_iv = format!("0x{iv}");
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
// Get input data
|
// Get input data
|
||||||
let input_data = match input_mode.get() {
|
let input_data = match input_mode.get() {
|
||||||
@ -97,20 +100,16 @@ pub fn CipherFormCbc() -> AnyView {
|
|||||||
Err(e) => set_error_msg(e.to_string()),
|
Err(e) => set_error_msg(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OperationMode::Decrypt => {
|
OperationMode::Decrypt => match Algorithm::AesCbc.decrypt_cbc(&key, &input_data) {
|
||||||
match Algorithm::AesCbc.decrypt_cbc(&key, &formatted_iv, &input_data) {
|
|
||||||
Ok(plaintext) => {
|
Ok(plaintext) => {
|
||||||
set_output_bytes(Some(plaintext.clone()));
|
set_output_bytes(Some(plaintext.clone()));
|
||||||
let formatted = match output_fmt.get() {
|
let formatted = match output_fmt.get() {
|
||||||
OutputFormat::Text => {
|
OutputFormat::Text => String::from_utf8(plaintext).unwrap_or_else(|_| {
|
||||||
String::from_utf8(plaintext).unwrap_or_else(|_| {
|
|
||||||
set_error_msg(
|
set_error_msg(
|
||||||
"Output contains invalid UTF-8. Try Hex format."
|
"Output contains invalid UTF-8. Try Hex format.".to_string(),
|
||||||
.to_string(),
|
|
||||||
);
|
);
|
||||||
String::new()
|
String::new()
|
||||||
})
|
}),
|
||||||
}
|
|
||||||
OutputFormat::Hex => bytes_to_hex(&plaintext),
|
OutputFormat::Hex => bytes_to_hex(&plaintext),
|
||||||
OutputFormat::Binary => bytes_to_binary(&plaintext),
|
OutputFormat::Binary => bytes_to_binary(&plaintext),
|
||||||
OutputFormat::Octal => bytes_to_octal(&plaintext),
|
OutputFormat::Octal => bytes_to_octal(&plaintext),
|
||||||
@ -118,8 +117,7 @@ pub fn CipherFormCbc() -> AnyView {
|
|||||||
set_output(formatted);
|
set_output(formatted);
|
||||||
}
|
}
|
||||||
Err(e) => set_error_msg(e.to_string()),
|
Err(e) => set_error_msg(e.to_string()),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -164,7 +162,13 @@ pub fn CipherFormCbc() -> AnyView {
|
|||||||
update_output=update_output
|
update_output=update_output
|
||||||
/>
|
/>
|
||||||
<KeyInput key_input=key_input set_key_input=set_key_input key_size=KeySize::Aes128 />
|
<KeyInput key_input=key_input set_key_input=set_key_input key_size=KeySize::Aes128 />
|
||||||
<IvInput iv_input=iv_input set_iv_input=set_iv_input />
|
{move || {
|
||||||
|
if mode.get() == OperationMode::Encrypt {
|
||||||
|
view! { <IvInput iv_input=iv_input set_iv_input=set_iv_input /> }.into_any()
|
||||||
|
} else {
|
||||||
|
view! { <span></span> }.into_any()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
<FileTextInput
|
<FileTextInput
|
||||||
input_mode=input_mode
|
input_mode=input_mode
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user