diff --git a/deoxys/src/modes.rs b/deoxys/src/modes.rs index b9be8bda..49c3a9c6 100644 --- a/deoxys/src/modes.rs +++ b/deoxys/src/modes.rs @@ -180,7 +180,7 @@ where fn decrypt_inout( nonce: &Array, associated_data: &[u8], - buffer: InOutBuf<'_, '_, u8>, + mut buffer: InOutBuf<'_, '_, u8>, tag: &Tag, subkeys: &Array, ) -> Result<(), aead::Error> { @@ -208,7 +208,7 @@ where tweak[0] = (tweak[0] & 0xf) | TWEAK_M; - let (data_blocks, mut tail) = buffer.into_chunks(); + let (data_blocks, mut tail) = buffer.reborrow().into_chunks(); let mut data_blocks_len = data_blocks.len(); for (index, mut data) in data_blocks.into_iter().enumerate() { @@ -271,6 +271,8 @@ where if tag.ct_eq(&computed_tag).into() { Ok(()) } else { + // Do not leave unauthenticated plaintext in the caller's buffer. + buffer.get_out().fill(0); Err(aead::Error) } } @@ -457,6 +459,8 @@ where if tag.ct_eq(&computed_tag).into() { Ok(()) } else { + // Do not leave unauthenticated plaintext in the caller's buffer. + buffer.get_out().fill(0); Err(aead::Error) } } diff --git a/deoxys/tests/decrypt_failure.rs b/deoxys/tests/decrypt_failure.rs new file mode 100644 index 00000000..72b78eed --- /dev/null +++ b/deoxys/tests/decrypt_failure.rs @@ -0,0 +1,83 @@ +//! Regression tests for clearing unauthenticated plaintext. + +use deoxys::aead::{AeadInOut, Key, KeyInit, Nonce, inout::InOutBuf}; +use deoxys::{DeoxysI128, DeoxysI256, DeoxysII128, DeoxysII256}; + +fn assert_failed_decrypt_clears() +where + C: AeadInOut + KeyInit, +{ + let key = Key::::default(); + let cipher = C::new(&key); + for len in [0, 1, 15, 16, 17, 31, 32, 33, 64, 65] { + let mut nonce = Nonce::::default(); + nonce[0] = len; + let mut ciphertext = [0x42; 65]; + let ciphertext = &mut ciphertext[..usize::from(len)]; + let tag = cipher + .encrypt_inout_detached(&nonce, b"associated data", ciphertext.into()) + .expect("encryption failed"); + + for tamper in 0..4 { + if tamper == 3 && ciphertext.is_empty() { + continue; + } + let mut bad_tag = tag.clone(); + let mut bad_nonce = nonce.clone(); + let mut input = ciphertext.to_vec(); + let aad: &[u8] = match tamper { + 0 => { + bad_tag[0] ^= 1; + b"associated data" + } + 1 => b"different data", + 2 => { + bad_nonce[0] ^= 1; + b"associated data" + } + _ => { + input[0] ^= 1; + b"associated data" + } + }; + + let mut output = vec![0xa5; input.len()]; + let buffer = InOutBuf::new(&input, &mut output).expect("equal buffer lengths"); + cipher + .decrypt_inout_detached(&bad_nonce, aad, buffer, &bad_tag) + .expect_err("tampered message was accepted"); + assert!( + output.iter().all(|&byte| byte == 0), + "len={len}, tamper={tamper}" + ); + + cipher + .decrypt_inout_detached(&bad_nonce, aad, input.as_mut_slice().into(), &bad_tag) + .expect_err("tampered message was accepted"); + assert!( + input.iter().all(|&byte| byte == 0), + "len={len}, tamper={tamper}" + ); + } + } +} + +#[test] +fn deoxys_i_128_failed_decrypt_clears_output() { + assert_failed_decrypt_clears::(); +} + +#[test] +fn deoxys_i_256_failed_decrypt_clears_output() { + assert_failed_decrypt_clears::(); +} + +#[test] +fn deoxys_ii_128_failed_decrypt_clears_output() { + assert_failed_decrypt_clears::(); +} + +#[test] +fn deoxys_ii_256_failed_decrypt_clears_output() { + assert_failed_decrypt_clears::(); +} diff --git a/ocb3/src/lib.rs b/ocb3/src/lib.rs index f729546c..8abdb029 100644 --- a/ocb3/src/lib.rs +++ b/ocb3/src/lib.rs @@ -258,13 +258,16 @@ where &self, nonce: &Nonce, associated_data: &[u8], - buffer: InOutBuf<'_, '_, u8>, + mut buffer: InOutBuf<'_, '_, u8>, tag: &aead::Tag, ) -> aead::Result<()> { - let expected_tag = self.decrypt_inout_return_tag(nonce, associated_data, buffer)?; + let expected_tag = + self.decrypt_inout_return_tag(nonce, associated_data, buffer.reborrow())?; if expected_tag.ct_eq(tag).into() { Ok(()) } else { + // Do not leave unauthenticated plaintext in the caller's buffer. + buffer.get_out().fill(0); Err(Error) } } diff --git a/ocb3/tests/kats.rs b/ocb3/tests/kats.rs index 8dee9c23..ae137050 100644 --- a/ocb3/tests/kats.rs +++ b/ocb3/tests/kats.rs @@ -109,6 +109,85 @@ type Aes256Ocb3Tag64 = Ocb3; type Aes128Ocb3 = Ocb3; type Aes256Ocb3 = Ocb3; +fn assert_failed_decrypt_clears() +where + C: AeadInOut + KeyInit, +{ + let key = aead::Key::::default(); + let cipher = C::new(&key); + for len in [0, 1, 15, 16, 17, 31, 32, 33, 64, 65] { + let mut nonce = aead::Nonce::::default(); + nonce[0] = len; + let mut ciphertext = [0x42; 65]; + let ciphertext = &mut ciphertext[..usize::from(len)]; + let tag = cipher + .encrypt_inout_detached(&nonce, b"associated data", ciphertext.into()) + .unwrap(); + + for tamper in 0..4 { + if tamper == 3 && ciphertext.is_empty() { + continue; + } + let mut bad_tag = tag.clone(); + let mut bad_nonce = nonce.clone(); + let mut input = ciphertext.to_vec(); + let aad: &[u8] = match tamper { + 0 => { + bad_tag[0] ^= 1; + b"associated data" + } + 1 => b"different data", + 2 => { + bad_nonce[0] ^= 1; + b"associated data" + } + _ => { + input[0] ^= 1; + b"associated data" + } + }; + + let mut output = vec![0xa5; input.len()]; + let buffer = aead::inout::InOutBuf::new(&input, &mut output).unwrap(); + cipher + .decrypt_inout_detached(&bad_nonce, aad, buffer, &bad_tag) + .unwrap_err(); + assert!( + output.iter().all(|&byte| byte == 0), + "len={len}, tamper={tamper}" + ); + + cipher + .decrypt_inout_detached(&bad_nonce, aad, input.as_mut_slice().into(), &bad_tag) + .unwrap_err(); + assert!( + input.iter().all(|&byte| byte == 0), + "len={len}, tamper={tamper}" + ); + } + } +} + +#[test] +fn aes128_ocb3_failed_decrypt_clears_output() { + assert_failed_decrypt_clears::(); +} + +#[test] +fn aes256_ocb3_failed_decrypt_clears_output() { + assert_failed_decrypt_clears::(); +} + +#[test] +fn aes128_ocb3_tag96_failed_decrypt_clears_output() { + assert_failed_decrypt_clears::(); +} + +#[test] +fn aes128_ocb3_tag64_failed_decrypt_clears_output() { + assert_failed_decrypt_clears::(); +} + /// Test vectors from Page 18 of https://www.rfc-editor.org/rfc/rfc7253.html#appendix-A #[test] fn rfc7253_more_sample_results() {