diff --git a/aes/src/cbc.rs b/aes/src/cbc.rs index dbca2e2..4a331eb 100644 --- a/aes/src/cbc.rs +++ b/aes/src/cbc.rs @@ -44,13 +44,19 @@ impl AesCbc { /// Encrypts plaintext using CBC mode with PKCS#7 padding. /// + /// The output format is: `[16-byte IV][ciphertext...]` + /// /// # Errors /// /// Returns `CipherError` if encryption fails. #[allow(clippy::missing_panics_doc)] pub fn encrypt(&self, plaintext: &[u8]) -> CipherResult> { 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(); 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 xored = plain_block ^ prev_block.as_u128(); 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; } - Ok(ciphertext) + Ok(output) } /// 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 /// - /// 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. #[allow(clippy::missing_panics_doc)] - pub fn decrypt(&self, ciphertext: &[u8]) -> CipherResult> { - if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(BLOCK_SIZE) { - return Err(CipherError::invalid_block_size( - BLOCK_SIZE, - ciphertext.len(), - )); + pub fn decrypt(&self, data: &[u8]) -> CipherResult> { + // Need at least IV (16 bytes) + one ciphertext block (16 bytes) + if data.len() < BLOCK_SIZE * 2 || !data.len().is_multiple_of(BLOCK_SIZE) { + return Err(CipherError::invalid_block_size(BLOCK_SIZE, data.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 prev_block = self.iv.to_block(); + let mut prev_block = iv.to_block(); for chunk in ciphertext.chunks_exact(BLOCK_SIZE) { // chunks_exact guarantees exactly BLOCK_SIZE bytes @@ -123,8 +135,8 @@ mod tests { let plaintext = [0u8; 16]; let ciphertext = assert_ok!(cipher.encrypt(&plaintext)); - // Padded to 32 bytes (16 data + 16 padding) - assert_eq!(ciphertext.len(), 32); + // 16 IV + 16 data + 16 padding = 48 bytes + assert_eq!(ciphertext.len(), 48); let decrypted = assert_ok!(cipher.decrypt(&ciphertext)); assert_eq!(decrypted, plaintext); diff --git a/aes/tests/aes_cbc.rs b/aes/tests/aes_cbc.rs index ec5a8eb..c704682 100644 --- a/aes/tests/aes_cbc.rs +++ b/aes/tests/aes_cbc.rs @@ -33,9 +33,10 @@ fn nist_single_block_encrypt() { let ciphertext = assert_ok!(cipher.encrypt(&plaintext)); - // Result includes PKCS#7 padding (16 bytes padding for aligned input) - assert_eq!(ciphertext.len(), 32); - assert_eq!(&ciphertext[..16], &expected); + // 16 IV + 16 block + 16 padding = 48 bytes + assert_eq!(ciphertext.len(), 48); + // First 16 bytes are IV, next 16 are the ciphertext + assert_eq!(&ciphertext[16..32], &expected); } #[test] @@ -68,10 +69,10 @@ fn nist_multi_block_encrypt() { let ciphertext = assert_ok!(cipher.encrypt(&plaintext)); - // Result includes padding (64 + 16 = 80 bytes) - assert_eq!(ciphertext.len(), 80); - // First 3 blocks should match NIST vectors exactly - assert_eq!(&ciphertext[..48], &expected[..48]); + // 16 IV + 64 blocks + 16 padding = 96 bytes + assert_eq!(ciphertext.len(), 96); + // First 16 bytes are IV, then ciphertext blocks + assert_eq!(&ciphertext[16..64], &expected[..48]); } #[test] @@ -94,8 +95,8 @@ fn empty_plaintext() { let cipher = AesCbc::new(NIST_KEY, Iv::new(NIST_IV)); let ciphertext = assert_ok!(cipher.encrypt(&[])); - // Empty input gets full block of padding - assert_eq!(ciphertext.len(), 16); + // 16 IV + 16 padding = 32 bytes + assert_eq!(ciphertext.len(), 32); let decrypted = assert_ok!(cipher.decrypt(&ciphertext)); assert!(decrypted.is_empty()); diff --git a/cipher-factory/src/algorithm.rs b/cipher-factory/src/algorithm.rs index c2c2156..a73d1f6 100644 --- a/cipher-factory/src/algorithm.rs +++ b/cipher-factory/src/algorithm.rs @@ -79,11 +79,15 @@ impl Algorithm { /// Decrypts data using CBC mode and removes PKCS#7 padding. /// + /// The IV is extracted from the first 16 bytes of the ciphertext. + /// /// # Errors /// /// Returns `CipherError` if decryption fails or padding is invalid. - pub fn decrypt_cbc(&self, key: &str, iv: &str, ciphertext: &[u8]) -> CipherResult> { - let cipher = self.new_cbc_cipher(key, iv)?; + pub fn decrypt_cbc(&self, key: &str, ciphertext: &[u8]) -> CipherResult> { + // 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) } diff --git a/web/src/components/cipher_form_cbc.rs b/web/src/components/cipher_form_cbc.rs index 17babdf..188e8d3 100644 --- a/web/src/components/cipher_form_cbc.rs +++ b/web/src/components/cipher_form_cbc.rs @@ -47,13 +47,16 @@ pub fn CipherFormCbc() -> AnyView { return; } - if iv.is_empty() { - set_error_msg("Please enter an initialization vector (IV).".to_string()); - return; - } - - // Format IV with 0x prefix (key keeps user format, IV is always hex) - let formatted_iv = format!("0x{iv}"); + // 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() { + set_error_msg("Please enter an initialization vector (IV).".to_string()); + return; + } + format!("0x{iv}") + } else { + String::new() + }; // Get input data let input_data = match input_mode.get() { @@ -97,29 +100,24 @@ pub fn CipherFormCbc() -> AnyView { Err(e) => set_error_msg(e.to_string()), } } - OperationMode::Decrypt => { - match Algorithm::AesCbc.decrypt_cbc(&key, &formatted_iv, &input_data) { - Ok(plaintext) => { - set_output_bytes(Some(plaintext.clone())); - let formatted = match output_fmt.get() { - OutputFormat::Text => { - String::from_utf8(plaintext).unwrap_or_else(|_| { - set_error_msg( - "Output contains invalid UTF-8. Try Hex format." - .to_string(), - ); - String::new() - }) - } - OutputFormat::Hex => bytes_to_hex(&plaintext), - OutputFormat::Binary => bytes_to_binary(&plaintext), - OutputFormat::Octal => bytes_to_octal(&plaintext), - }; - set_output(formatted); - } - Err(e) => set_error_msg(e.to_string()), + OperationMode::Decrypt => match Algorithm::AesCbc.decrypt_cbc(&key, &input_data) { + Ok(plaintext) => { + set_output_bytes(Some(plaintext.clone())); + let formatted = match output_fmt.get() { + OutputFormat::Text => String::from_utf8(plaintext).unwrap_or_else(|_| { + set_error_msg( + "Output contains invalid UTF-8. Try Hex format.".to_string(), + ); + String::new() + }), + OutputFormat::Hex => bytes_to_hex(&plaintext), + OutputFormat::Binary => bytes_to_binary(&plaintext), + OutputFormat::Octal => bytes_to_octal(&plaintext), + }; + set_output(formatted); } - } + Err(e) => set_error_msg(e.to_string()), + }, } }; @@ -164,7 +162,13 @@ pub fn CipherFormCbc() -> AnyView { update_output=update_output /> - + {move || { + if mode.get() == OperationMode::Encrypt { + view! { }.into_any() + } else { + view! { }.into_any() + } + }}