Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aead/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ heapless = { version = "0.8", optional = true, default-features = false }
[features]
default = ["rand_core"]
alloc = []
dev = ["blobby"]
dev = ["blobby", "alloc"]
os_rng = ["crypto-common/os_rng", "rand_core"]
rand_core = ["crypto-common/rand_core"]

Expand Down
150 changes: 96 additions & 54 deletions aead/src/dev.rs
Original file line number Diff line number Diff line change
@@ -1,74 +1,116 @@
//! Development-related functionality
use crate::{
Aead, AeadInOut, Nonce, Payload, Tag, TagPosition, array::typenum::Unsigned, inout::InOutBuf,
};
pub use blobby;

/// Run AEAD test for the provided passing test vector
pub fn run_pass_test<C: AeadInOut>(
cipher: &C,
nonce: &Nonce<C>,
aad: &[u8],
pt: &[u8],
ct: &[u8],
) -> Result<(), &'static str> {
let res = cipher
.encrypt(nonce, Payload { aad, msg: pt })
.map_err(|_| "encryption failure")?;
if res != ct {
return Err("encrypted data is different from target ciphertext");
}

let res = cipher
.decrypt(nonce, Payload { aad, msg: ct })
.map_err(|_| "decryption failure")?;
if res != pt {
return Err("decrypted data is different from target plaintext");
}

let (ct, tag) = match C::TAG_POSITION {
TagPosition::Prefix => {
let (tag, ct) = ct.split_at(C::TagSize::USIZE);
(ct, tag)
}
TagPosition::Postfix => ct.split_at(pt.len()),
};
let tag: &Tag<C> = tag.try_into().expect("tag has correct length");

// Fill output buffer with "garbage" to test that its data does not get read during encryption
let mut buf: alloc::vec::Vec<u8> = (0..pt.len()).map(|i| i as u8).collect();
Copy link
Member

@baloo baloo Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not every consumer use the aead::new_test (at least deoxys and ascon-aead does not) this is why the MockBuffer was made available in #1797

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can leave the ascon test as-is for now. We should migrate them to new_test either way. The current approach with the huge number of testing functions is slow and unwieldy.

let inout_buf = InOutBuf::new(pt, &mut buf).expect("pt and buf have the same length");

let calc_tag = cipher
.encrypt_inout_detached(nonce, aad, inout_buf)
.map_err(|_| "encrypt_inout_detached: encryption failure")?;
if tag != &calc_tag {
return Err("encrypt_inout_detached: tag mismatch");
}
if ct != buf {
return Err("encrypt_inout_detached: ciphertext mismatch");
}

// Fill output buffer with "garbage"
buf.iter_mut().enumerate().for_each(|(i, v)| *v = i as u8);

let inout_buf = InOutBuf::new(ct, &mut buf).expect("ct and buf have the same length");
cipher
.decrypt_inout_detached(nonce, aad, inout_buf, tag)
.map_err(|_| "decrypt_inout_detached: decryption failure")?;
if pt != buf {
return Err("decrypt_inout_detached: plaintext mismatch");
}

Ok(())
}

/// Run AEAD test for the provided failing test vector
pub fn run_fail_test<C: AeadInOut>(
cipher: &C,
nonce: &Nonce<C>,
aad: &[u8],
ct: &[u8],
) -> Result<(), &'static str> {
let res = cipher.decrypt(nonce, Payload { aad, msg: ct });
if res.is_ok() {
Err("decryption must return error")
} else {
Ok(())
}
}

/// Define AEAD test
#[macro_export]
macro_rules! new_test {
($name:ident, $test_name:expr, $cipher:ty $(,)?) => {
#[test]
fn $name() {
use aead::{
Aead, KeyInit, Payload,
array::{Array, typenum::Unsigned},
dev::blobby::Blob6Iterator,
};

fn run_test(
key: &[u8],
nonce: &[u8],
aad: &[u8],
pt: &[u8],
ct: &[u8],
pass: bool,
) -> Result<(), &'static str> {
let key = key.try_into().map_err(|_| "wrong key size")?;
let cipher = <$cipher>::new(key);
let nonce = nonce.try_into().map_err(|_| "wrong nonce size")?;

if !pass {
let res = cipher.decrypt(nonce, Payload { aad: aad, msg: ct });
if res.is_ok() {
return Err("decryption must return error");
}
return Ok(());
}

let res = cipher
.encrypt(nonce, Payload { aad: aad, msg: pt })
.map_err(|_| "encryption failure")?;
if res != ct {
return Err("encrypted data is different from target ciphertext");
}
let res = cipher
.decrypt(nonce, Payload { aad: aad, msg: ct })
.map_err(|_| "decryption failure")?;
if res != pt {
return Err("decrypted data is different from target plaintext");
}
Ok(())
}
use $crate::KeyInit;
use $crate::dev::blobby::Blob6Iterator;

let data = include_bytes!(concat!("data/", $test_name, ".blb"));
for (i, row) in Blob6Iterator::new(data).unwrap().enumerate() {
let [key, nonce, aad, pt, ct, status] = row.unwrap();
let pass = match status[0] {
0 => false,
1 => true,
let key = key.try_into().expect("wrong key size");
let nonce = nonce.try_into().expect("wrong nonce size");
let cipher = <$cipher as KeyInit>::new(key);

let res = match status {
[0] => $crate::dev::run_fail_test(&cipher, nonce, aad, ct),
[1] => $crate::dev::run_pass_test(&cipher, nonce, aad, pt, ct),
Copy link
Member Author

@newpavlov newpavlov Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the failing test ignores plaintext. Maybe it's worth to introduce two separate macros for successful and failing tests? In other words, we would store passing and failing vectors in separate files:

aead::new_pass_test!(my_aead_pass, "my_aead_pass", MyAead);
aead::new_fail_test!(my_aead_fail, "my_aead_fail", MyAead);

_ => panic!("invalid value for pass flag"),
};
if let Err(reason) = run_test(key, nonce, aad, pt, ct, pass) {
let mut pass = status[0] == 1;
if let Err(reason) = res {
panic!(
"\n\
Failed test №{}\n\
reason: \t{:?}\n\
key:\t{:?}\n\
nonce:\t{:?}\n\
aad:\t{:?}\n\
plaintext:\t{:?}\n\
ciphertext:\t{:?}\n\
pass:\t{}\n\
",
i, reason, key, nonce, aad, pt, ct, pass,
Failed test #{i}\n\
reason:\t{reason:?}\n\
key:\t{key:?}\n\
nonce:\t{nonce:?}\n\
aad:\t{aad:?}\n\
plaintext:\t{pt:?}\n\
ciphertext:\t{ct:?}\n\
pass:\t{pass}\n"
);
}
}
Expand Down
Binary file added aead/tests/data/postfix.blb
Binary file not shown.
Binary file added aead/tests/data/prefix.blb
Binary file not shown.
175 changes: 175 additions & 0 deletions aead/tests/dummy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! This module defines dummy (horribly insecure!) AEAD implementations
//! to test implementation of the AEAD traits and helper macros in the `dev` module.
use aead::{
AeadCore, AeadInOut, Error, Key, KeyInit, KeySizeUser, Nonce, Result, Tag, TagPosition,
array::Array, consts::U8,
};
use inout::InOutBuf;

struct DummyAead {
key: [u8; 8],
}

impl DummyAead {
fn process_aad(&self, nonce: &[u8; 8], aad: &[u8]) -> u64 {
let mut tag = u64::from_le_bytes(*nonce);
let key = u64::from_le_bytes(self.key);

let mut aad_iter = aad.chunks_exact(8);
for chunk in &mut aad_iter {
tag ^= u64::from_le_bytes(chunk.try_into().unwrap());
tag = tag.wrapping_add(key);
}
let aad_rem = aad_iter.remainder();
if !aad_rem.is_empty() {
let mut chunk = [0u8; 8];
chunk[..aad_rem.len()].copy_from_slice(aad_rem);
tag ^= u64::from_le_bytes(chunk);
tag = tag.wrapping_add(key);
}

tag
}

fn encrypt_inner(
&self,
nonce: &[u8; 8],
aad: &[u8],
buffer: InOutBuf<'_, '_, u8>,
) -> Result<[u8; 8]> {
let mut tag = self.process_aad(nonce, aad);

let (blocks, mut rem) = buffer.into_chunks::<U8>();
for mut block in blocks {
block.xor_in2out(&self.key.into());
tag ^= u64::from_be_bytes(block.get_out().0);
}

if !rem.is_empty() {
rem.xor_in2out(&self.key[..rem.len()]);

let out_rem = rem.get_out();
let mut block = [0u8; 8];
block[..out_rem.len()].copy_from_slice(out_rem);
tag ^= u64::from_le_bytes(block);
}

Ok(tag.to_le_bytes())
}

fn decrypt_inner(
&self,
nonce: &[u8; 8],
aad: &[u8],
mut buffer: InOutBuf<'_, '_, u8>,
tag: &[u8; 8],
) -> Result<()> {
let exp_tag = u64::from_le_bytes(*tag);
let mut tag = self.process_aad(nonce, aad);

let (blocks, mut rem) = buffer.reborrow().into_chunks::<U8>();
for mut block in blocks {
tag ^= u64::from_be_bytes(block.get_in().0);
block.xor_in2out(&self.key.into());
}

if !rem.is_empty() {
let in_rem = rem.get_in();
let mut block = [0u8; 8];
block[..in_rem.len()].copy_from_slice(in_rem);
tag ^= u64::from_le_bytes(block);

rem.xor_in2out(&self.key[..rem.len()]);
}

if tag == exp_tag {
Ok(())
} else {
buffer.get_out().fill(0);
Err(Error)
}
}
}

struct PrefixDummyAead(DummyAead);

impl KeySizeUser for PrefixDummyAead {
type KeySize = U8;
}

impl KeyInit for PrefixDummyAead {
fn new(key: &Key<Self>) -> Self {
Self(DummyAead { key: key.0 })
}
}

impl AeadCore for PrefixDummyAead {
type NonceSize = U8;
type TagSize = U8;
const TAG_POSITION: TagPosition = TagPosition::Prefix;
}

impl AeadInOut for PrefixDummyAead {
fn encrypt_inout_detached(
&self,
nonce: &Nonce<Self>,
aad: &[u8],
buffer: InOutBuf<'_, '_, u8>,
) -> Result<Tag<Self>> {
self.0.encrypt_inner(nonce.into(), aad, buffer).map(Array)
}

fn decrypt_inout_detached(
&self,
nonce: &Nonce<Self>,
aad: &[u8],
buffer: InOutBuf<'_, '_, u8>,
tag: &Tag<Self>,
) -> Result<()> {
self.0.decrypt_inner(nonce.into(), aad, buffer, tag.into())
}
}

struct PostfixDummyAead(DummyAead);

impl KeySizeUser for PostfixDummyAead {
type KeySize = U8;
}

impl KeyInit for PostfixDummyAead {
fn new(key: &Key<Self>) -> Self {
Self(DummyAead { key: key.0 })
}
}

impl AeadCore for PostfixDummyAead {
type NonceSize = U8;
type TagSize = U8;
const TAG_POSITION: TagPosition = TagPosition::Postfix;
}

impl AeadInOut for PostfixDummyAead {
fn encrypt_inout_detached(
&self,
nonce: &Nonce<Self>,
aad: &[u8],
buffer: InOutBuf<'_, '_, u8>,
) -> Result<Tag<Self>> {
self.0.encrypt_inner(nonce.into(), aad, buffer).map(Array)
}

fn decrypt_inout_detached(
&self,
nonce: &Nonce<Self>,
aad: &[u8],
buffer: InOutBuf<'_, '_, u8>,
tag: &Tag<Self>,
) -> Result<()> {
self.0.decrypt_inner(nonce.into(), aad, buffer, tag.into())
}
}

#[cfg(feature = "dev")]
aead::new_test!(dummy_prefix, "prefix", PrefixDummyAead);
#[cfg(feature = "dev")]
aead::new_test!(dummy_postfix, "postfix", PostfixDummyAead);