use crate::backend::{CipherKey, NONCE_LEN, TAG_LEN};
use crate::Key;
use std::any::Any;
use std::borrow::Cow;
use std::collections::HashSet;
use vitaminc_aead::{
Aad, Cipher, CipherText, CipherTextBuilder, Decipher, DecipherVisitor, Decrypt, Encrypt,
IntoAad, LocalCipherText, MapAccess, MapCipher, NonceGenerator, RandomNonceGenerator,
SeqAccess, SeqCipher, Unspecified, WIRE_VERSION,
};
use vitaminc_protected::{Controlled, Protected};
pub type BoxedPassthrough = Box<dyn Any + Send + 'static>;
pub type AesCipherText = CipherText<LocalCipherText, BoxedPassthrough>;
pub struct Aes256Cipher {
pub(crate) nonce_generator: RandomNonceGenerator<NONCE_LEN>,
pub(crate) key: CipherKey,
}
impl Aes256Cipher {
pub fn new(key: &Key) -> Result<Self, Unspecified> {
Ok(Self {
nonce_generator: RandomNonceGenerator::init()?,
key: key.cipher_key()?,
})
}
fn seal_empty_marker<'a, A>(&self, aad: A) -> Result<LocalCipherText, Unspecified>
where
A: IntoAad<'a>,
{
seal_leaf(self, Protected::new(Vec::new()), aad)
}
}
pub(crate) fn seal_leaf<'a, A>(
cipher: &Aes256Cipher,
data: Protected<Vec<u8>>,
aad: A,
) -> Result<LocalCipherText, Unspecified>
where
A: IntoAad<'a>,
{
let nonce = cipher.nonce_generator.generate()?;
let nonce_bytes: [u8; NONCE_LEN] = nonce.as_ref().try_into().map_err(|_| Unspecified)?;
let aad = aad.into_aad().for_leaf(WIRE_VERSION);
CipherTextBuilder::new()
.append_nonce(nonce)
.append_target_plaintext(data)
.accepts_ciphertext_and_tag_ok(|mut buf| {
cipher
.key
.seal(&nonce_bytes, aad.as_bytes(), &mut buf)
.map(|()| buf)
})
.build()
}
pub(crate) fn open_leaf<'a, A>(
cipher: &Aes256Cipher,
ct: LocalCipherText,
aad: A,
) -> Result<Protected<Vec<u8>>, Unspecified>
where
A: IntoAad<'a>,
{
let (nonce, reader) = ct.into_reader().read_version()?.read_nonce::<NONCE_LEN>()?;
let nonce_bytes = nonce.into_inner();
let aad = aad.into_aad().for_leaf(WIRE_VERSION);
reader
.accepts_plaintext_ok(|data| cipher.key.open(&nonce_bytes, aad.as_bytes(), data))
.read()
}
impl<'c> Cipher for &'c Aes256Cipher {
type Ok = AesCipherText;
type Error = Unspecified;
type Passthrough = BoxedPassthrough;
type SeqCipher = AesSeqCipher<'c>;
type MapCipher = AesMapCipher<'c>;
fn encrypt_bytes_vec<'a, A>(
self,
data: Protected<Vec<u8>>,
aad: A,
) -> Result<Self::Ok, Self::Error>
where
A: IntoAad<'a>,
{
seal_leaf(self, data, aad).map(AesCipherText::Single)
}
fn encrypt_bytes_array<'a, const N: usize, A>(
self,
data: Protected<[u8; N]>,
aad: A,
) -> Result<Self::Ok, Self::Error>
where
A: IntoAad<'a>,
{
let nonce = self.nonce_generator.generate()?;
let nonce_bytes: [u8; NONCE_LEN] = nonce.as_ref().try_into().map_err(|_| Unspecified)?;
let aad = aad.into_aad().for_leaf(WIRE_VERSION);
CipherTextBuilder::new()
.append_nonce(nonce)
.append_target_plaintext_array(data, TAG_LEN)
.accepts_ciphertext_and_tag_ok(|mut buf| {
self.key
.seal(&nonce_bytes, aad.as_bytes(), &mut buf)
.map(|()| buf)
})
.build()
.map(AesCipherText::Single)
}
fn encrypt_seq<'a, A>(self, size_hint: Option<usize>, aad: A) -> Self::SeqCipher
where
A: IntoAad<'a>,
{
let aad = aad.into_aad().into_owned();
AesSeqCipher {
cipher: self,
items: Vec::with_capacity(size_hint.unwrap_or(0)),
element_aad: aad.for_sequence_element(),
aad,
encrypted: false,
}
}
fn encrypt_map<'a, A>(self, aad: A) -> Self::MapCipher
where
A: IntoAad<'a>,
{
AesMapCipher {
cipher: self,
entries: Vec::new(),
seen_keys: HashSet::new(),
current_key: None,
aad: aad.into_aad().into_owned(),
encrypted: false,
}
}
fn encrypt_none<'a, A>(self, aad: A) -> Result<Self::Ok, Self::Error>
where
A: IntoAad<'a>,
{
self.seal_empty_marker(aad.into_aad().for_none())
.map(AesCipherText::None)
}
fn passthrough(self, value: Self::Passthrough) -> Result<Self::Ok, Self::Error> {
Ok(AesCipherText::Passthrough(value))
}
fn passthrough_boxed(
self,
value: Box<dyn Any + Send + 'static>,
) -> Result<Self::Ok, Self::Error> {
self.passthrough(value)
}
}
pub struct AesSeqCipher<'c> {
cipher: &'c Aes256Cipher,
items: Vec<AesCipherText>,
aad: Aad<'static>,
element_aad: Aad<'static>,
encrypted: bool,
}
impl<'c> SeqCipher for AesSeqCipher<'c> {
type Ok = AesCipherText;
type Error = Unspecified;
type Passthrough = BoxedPassthrough;
fn encrypt_next<T>(mut self, data: T) -> Result<Self, Self::Error>
where
T: Encrypt,
{
let encrypted =
data.encrypt_with_aad(self.cipher, Aad::from_slice(self.element_aad.as_bytes()))?;
self.encrypted |= !matches!(encrypted, AesCipherText::Passthrough(_));
self.items.push(encrypted);
Ok(self)
}
fn passthrough_next(mut self, value: Self::Passthrough) -> Result<Self, Self::Error> {
self.items.push(AesCipherText::Passthrough(value));
Ok(self)
}
fn end(self) -> Result<Self::Ok, Self::Error> {
if self.items.is_empty() {
self.cipher
.seal_empty_marker(self.aad.for_empty_sequence())
.map(AesCipherText::EmptySequence)
} else if !self.encrypted {
Err(Unspecified)
} else {
Ok(AesCipherText::Sequence(self.items))
}
}
}
pub struct AesMapCipher<'c> {
cipher: &'c Aes256Cipher,
entries: Vec<(String, AesCipherText)>,
seen_keys: HashSet<String>,
current_key: Option<Cow<'static, str>>,
aad: Aad<'static>,
encrypted: bool,
}
impl<'c> MapCipher for AesMapCipher<'c> {
type Ok = AesCipherText;
type Error = Unspecified;
type Passthrough = BoxedPassthrough;
fn encrypt_key<K>(mut self, key: K) -> Result<Self, Self::Error>
where
K: Into<Cow<'static, str>>,
{
if self.current_key.is_some() {
return Err(Unspecified);
}
let key = key.into();
if !self.seen_keys.insert(key.as_ref().to_owned()) {
return Err(Unspecified);
}
self.current_key = Some(key);
Ok(self)
}
fn encrypt_value<U>(mut self, value: U) -> Result<Self, Self::Error>
where
U: Encrypt,
{
let key = self.current_key.take().ok_or(Unspecified)?;
let entry_aad = self.aad.for_map_entry(&key);
let encrypted = value.encrypt_with_aad(self.cipher, entry_aad)?;
self.encrypted |= !matches!(encrypted, AesCipherText::Passthrough(_));
self.entries.push((key.into_owned(), encrypted));
Ok(self)
}
fn passthrough_entry<K>(mut self, key: K, value: Self::Passthrough) -> Result<Self, Self::Error>
where
K: Into<Cow<'static, str>>,
{
if self.current_key.is_some() {
return Err(Unspecified);
}
let key = key.into();
if !self.seen_keys.insert(key.as_ref().to_owned()) {
return Err(Unspecified);
}
self.entries
.push((key.into_owned(), AesCipherText::Passthrough(value)));
Ok(self)
}
fn passthrough_entry_boxed<K>(
self,
key: K,
value: Box<dyn Any + Send + 'static>,
) -> Result<Self, Self::Error>
where
K: Into<Cow<'static, str>>,
{
self.passthrough_entry(key, value)
}
fn end(self) -> Result<Self::Ok, Self::Error> {
if self.current_key.is_some() {
return Err(Unspecified);
}
if self.entries.is_empty() {
self.cipher
.seal_empty_marker(self.aad.for_empty_map())
.map(AesCipherText::EmptyMap)
} else if !self.encrypted {
Err(Unspecified)
} else {
Ok(AesCipherText::Map(self.entries))
}
}
}
impl Aes256Cipher {
pub fn decrypt<'c, T: Decrypt<'c> + 'c>(
&'c self,
ciphertext: AesCipherText,
) -> Result<T, Unspecified> {
self.decrypt_with_aad(ciphertext, ())
}
pub fn decrypt_with_aad<'c, 'a, T, A>(
&'c self,
ciphertext: AesCipherText,
aad: A,
) -> Result<T, Unspecified>
where
T: Decrypt<'c> + 'c,
A: IntoAad<'a>,
{
T::decrypt_with_aad(self.decipher(ciphertext), aad)
}
pub fn decipher(&self, ciphertext: AesCipherText) -> AesDecipher<'_> {
AesDecipher {
cipher: self,
ciphertext,
}
}
}
pub struct AesDecipher<'c> {
cipher: &'c Aes256Cipher,
ciphertext: AesCipherText,
}
impl AesDecipher<'_> {
pub fn decrypt_passthrough_as<T>(self) -> Result<T, Unspecified>
where
T: Any + Send + 'static,
{
self.decrypt_passthrough()?
.downcast::<T>()
.map(|b| *b)
.map_err(|_| Unspecified)
}
fn decrypt_local_ciphertext(
cipher: &Aes256Cipher,
ct: LocalCipherText,
aad: &[u8],
) -> Result<Protected<Vec<u8>>, Unspecified> {
open_leaf(cipher, ct, Aad::from_slice(aad))
}
fn verify_empty_marker(
cipher: &Aes256Cipher,
ct: LocalCipherText,
aad: &[u8],
) -> Result<(), Unspecified> {
let plaintext = Self::decrypt_local_ciphertext(cipher, ct, aad)?;
if plaintext.risky_ref().is_empty() {
Ok(())
} else {
Err(Unspecified)
}
}
}
impl<'c> Decipher<'c> for AesDecipher<'c> {
type Ok<T>
= Result<T, Unspecified>
where
T: Send + 'c;
type Passthrough = BoxedPassthrough;
fn map_ok<T, U, F>(ok: Self::Ok<T>, f: F) -> Self::Ok<U>
where
T: Send + 'c,
U: Send + 'c,
F: FnOnce(T) -> U,
{
ok.map(f)
}
fn decrypt_bytes<'a, V, A>(self, visitor: V, aad: A) -> Self::Ok<V::Value>
where
V: DecipherVisitor<'c> + Send + 'c,
A: IntoAad<'a>,
{
match self.ciphertext {
AesCipherText::Single(ct) => {
let aad = aad.into_aad();
let bytes = Self::decrypt_local_ciphertext(self.cipher, ct, aad.as_bytes())?;
visitor.visit_bytes_vec(bytes)
}
_ => Err(Unspecified),
}
}
fn decrypt_seq<'a, V, A>(self, visitor: V, aad: A) -> Self::Ok<V::Value>
where
V: DecipherVisitor<'c> + Send + 'c,
A: IntoAad<'a>,
{
match self.ciphertext {
AesCipherText::Sequence(items)
if items
.iter()
.any(|i| !matches!(i, AesCipherText::Passthrough(_))) =>
{
let seq_access = AesSeqAccess {
cipher: self.cipher,
items: items.into_iter(),
element_aad: aad.into_aad().for_sequence_element(),
};
visitor.visit_seq(seq_access)
}
AesCipherText::EmptySequence(ct) => {
let aad = aad.into_aad();
Self::verify_empty_marker(self.cipher, ct, aad.for_empty_sequence().as_bytes())?;
let seq_access = AesSeqAccess {
cipher: self.cipher,
items: Vec::new().into_iter(),
element_aad: aad.for_sequence_element(),
};
visitor.visit_seq(seq_access)
}
_ => Err(Unspecified),
}
}
fn decrypt_map<'a, V, A>(self, visitor: V, aad: A) -> Self::Ok<V::Value>
where
V: DecipherVisitor<'c> + Send + 'c,
A: IntoAad<'a>,
{
match self.ciphertext {
AesCipherText::Map(entries)
if entries
.iter()
.any(|(_, v)| !matches!(v, AesCipherText::Passthrough(_))) =>
{
let mut seen = std::collections::HashSet::with_capacity(entries.len());
if !entries.iter().all(|(key, _)| seen.insert(key.as_str())) {
return Err(Unspecified);
}
let map_access = AesMapAccess {
cipher: self.cipher,
entries: entries.into_iter(),
aad: aad.into_aad(),
pending: None,
};
visitor.visit_map(map_access)
}
AesCipherText::EmptyMap(ct) => {
let aad = aad.into_aad();
Self::verify_empty_marker(self.cipher, ct, aad.for_empty_map().as_bytes())?;
let map_access = AesMapAccess {
cipher: self.cipher,
entries: Vec::new().into_iter(),
aad,
pending: None,
};
visitor.visit_map(map_access)
}
_ => Err(Unspecified),
}
}
fn decrypt_any<'a, V, A>(self, visitor: V, aad: A) -> Self::Ok<V::Value>
where
V: DecipherVisitor<'c> + Send + 'c,
A: IntoAad<'a>,
{
let AesDecipher { cipher, ciphertext } = self;
match ciphertext {
ct @ AesCipherText::Single(_) => cipher.decipher(ct).decrypt_bytes(visitor, aad),
ct @ (AesCipherText::Sequence(_) | AesCipherText::EmptySequence(_)) => {
cipher.decipher(ct).decrypt_seq(visitor, aad)
}
ct @ (AesCipherText::Map(_) | AesCipherText::EmptyMap(_)) => {
cipher.decipher(ct).decrypt_map(visitor, aad)
}
AesCipherText::None(ct) => {
let aad = aad.into_aad();
Self::verify_empty_marker(cipher, ct, aad.for_none().as_bytes())?;
visitor.visit_none()
}
AesCipherText::Passthrough(boxed) => visitor.visit_passthrough(boxed),
}
}
fn decrypt_passthrough(self) -> Self::Ok<Self::Passthrough> {
match self.ciphertext {
AesCipherText::Passthrough(boxed) => Ok(boxed),
_ => Err(Unspecified),
}
}
fn decrypt_option<'a, T, A>(self, aad: A) -> Self::Ok<Option<T>>
where
T: Decrypt<'c> + 'c,
A: IntoAad<'a>,
{
match self.ciphertext {
AesCipherText::None(ct) => {
let aad = aad.into_aad();
Self::verify_empty_marker(self.cipher, ct, aad.for_none().as_bytes())?;
Ok(None)
}
AesCipherText::Passthrough(_) => Err(Unspecified),
other => {
let inner = self.cipher.decipher(other);
T::decrypt_with_aad(inner, aad).map(Some)
}
}
}
}
struct AesSeqAccess<'c> {
cipher: &'c Aes256Cipher,
items: std::vec::IntoIter<AesCipherText>,
element_aad: Aad<'static>,
}
impl<'c> SeqAccess<'c> for AesSeqAccess<'c> {
type Error = Unspecified;
fn next_element<T: Decrypt<'c> + 'c>(&mut self) -> Result<Option<T>, Self::Error> {
let ct = match self.items.next() {
Some(ct) => ct,
None => return Ok(None),
};
let decipher = self.cipher.decipher(ct);
T::decrypt_with_aad(decipher, self.element_aad.as_bytes()).map(Some)
}
}
struct AesMapAccess<'c, 'a> {
cipher: &'c Aes256Cipher,
entries: std::vec::IntoIter<(String, AesCipherText)>,
aad: Aad<'a>,
pending: Option<(String, AesCipherText)>,
}
impl<'c> MapAccess<'c> for AesMapAccess<'c, '_> {
type Error = Unspecified;
fn next_key(&mut self) -> Result<Option<String>, Self::Error> {
if self.pending.is_some() {
return Err(Unspecified);
}
match self.entries.next() {
Some((key, ct)) => {
self.pending = Some((key.clone(), ct));
Ok(Some(key))
}
None => Ok(None),
}
}
fn next_value<T: Decrypt<'c> + 'c>(&mut self) -> Result<T, Self::Error> {
let (key, ct) = self.pending.take().ok_or(Unspecified)?;
let decipher = self.cipher.decipher(ct);
let entry_aad = self.aad.for_map_entry(&key);
T::decrypt_with_aad(decipher, entry_aad)
}
fn next_passthrough(&mut self) -> Result<Box<dyn Any + Send + 'static>, Self::Error> {
let (_key, ct) = self.pending.take().ok_or(Unspecified)?;
match ct {
AesCipherText::Passthrough(value) => Ok(value),
_ => Err(Unspecified),
}
}
}
#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use crate::key::tests::DifferingKeyPair;
use quickcheck_macros::quickcheck;
use std::collections::HashMap;
use vitaminc_aead::{Element, Encrypt};
#[quickcheck]
fn roundtrip_byte_array(key: Key, plaintext: [u8; 16]) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext.encrypt(&cipher).expect("Encryption failed");
let decrypted: [u8; 16] = cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted == plaintext
}
#[quickcheck]
fn roundtrip_string(key: Key, plaintext: String) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext
.clone()
.encrypt(&cipher)
.expect("Encryption failed");
let decrypted: String = cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted == plaintext
}
#[quickcheck]
fn roundtrip_str(key: Key, plaintext: String) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext
.as_str()
.encrypt(&cipher)
.expect("Encryption failed");
let decrypted: String = cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted == plaintext
}
#[quickcheck]
fn roundtrip_u32(key: Key, plaintext: u32) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext.encrypt(&cipher).expect("Encryption failed");
let decrypted: u32 = cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted == plaintext
}
#[quickcheck]
fn roundtrip_vec_of_strings(key: Key, plaintext: Vec<String>) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext
.clone()
.encrypt(&cipher)
.expect("Encryption failed");
let decrypted: Vec<String> = cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted == plaintext
}
#[quickcheck]
fn roundtrip_string_with_aad(key: Key, plaintext: String) -> bool {
let aad = "test-aad";
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext
.clone()
.encrypt_with_aad(&cipher, aad)
.expect("Encryption failed");
let decrypted: String = cipher
.decrypt_with_aad(ciphertext, aad)
.expect("Decryption failed");
decrypted == plaintext
}
#[quickcheck]
fn decrypt_fails_with_wrong_aad(key: Key, plaintext: String) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext
.encrypt_with_aad(&cipher, "correct-aad")
.expect("Encryption failed");
cipher
.decrypt_with_aad::<String, _>(ciphertext, "wrong-aad")
.is_err()
}
#[quickcheck]
fn decrypt_seq_fails_with_wrong_aad(key: Key, plaintext: Vec<String>) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext
.encrypt_with_aad(&cipher, "correct-aad")
.expect("Encryption failed");
cipher
.decrypt_with_aad::<Vec<String>, _>(ciphertext, "wrong-aad")
.is_err()
}
#[quickcheck]
fn roundtrip_vec_with_aad(key: Key, plaintext: Vec<String>) -> bool {
let aad = "seq-aad";
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = plaintext
.clone()
.encrypt_with_aad(&cipher, aad)
.expect("Encryption failed");
let decrypted: Vec<String> = cipher
.decrypt_with_aad(ciphertext, aad)
.expect("Decryption failed");
decrypted == plaintext
}
#[quickcheck]
fn decrypt_fails_with_wrong_key(keys: DifferingKeyPair, plaintext: String) -> bool {
let DifferingKeyPair(key_a, key_b) = keys;
let cipher_a = Aes256Cipher::new(&key_a).expect("Failed to create cipher A");
let cipher_b = Aes256Cipher::new(&key_b).expect("Failed to create cipher B");
let ciphertext = plaintext.encrypt(&cipher_a).expect("Encryption failed");
cipher_b.decrypt::<String>(ciphertext).is_err()
}
#[test]
fn roundtrip_hashmap() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let mut plaintext = HashMap::new();
plaintext.insert("name", "Alice");
plaintext.insert("city", "Sydney");
let ciphertext = plaintext.encrypt(&cipher).expect("Encryption failed");
let decrypted: HashMap<String, String> =
cipher.decrypt(ciphertext).expect("Decryption failed");
assert_eq!(decrypted.len(), 2);
assert_eq!(decrypted.get("name").unwrap(), "Alice");
assert_eq!(decrypted.get("city").unwrap(), "Sydney");
}
#[test]
fn roundtrip_hashmap_with_aad() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let aad = "map-context";
let mut plaintext = HashMap::new();
plaintext.insert("name", "Alice");
plaintext.insert("city", "Sydney");
let ciphertext = plaintext
.encrypt_with_aad(&cipher, aad)
.expect("Encryption failed");
let decrypted: HashMap<String, String> = cipher
.decrypt_with_aad(ciphertext, aad)
.expect("Decryption failed");
assert_eq!(decrypted.len(), 2);
assert_eq!(decrypted.get("name").unwrap(), "Alice");
assert_eq!(decrypted.get("city").unwrap(), "Sydney");
}
#[test]
fn roundtrip_empty_hashmap_with_aad() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let plaintext = HashMap::<String, String>::new();
let ciphertext = plaintext
.clone()
.encrypt_with_aad(&cipher, "map-context")
.expect("Encryption failed");
let decrypted: HashMap<String, String> = cipher
.decrypt_with_aad(ciphertext, "map-context")
.expect("Decryption failed");
assert_eq!(decrypted, plaintext);
}
#[test]
fn decrypt_hashmap_fails_with_wrong_aad() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let mut plaintext = HashMap::new();
plaintext.insert("name", "Alice");
let ciphertext = plaintext
.encrypt_with_aad(&cipher, "correct-aad")
.expect("Encryption failed");
assert!(cipher
.decrypt_with_aad::<HashMap<String, String>, _>(ciphertext, "wrong-aad")
.is_err());
}
#[test]
fn empty_composites_fail_with_wrong_aad() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let empty_sequence = Vec::<String>::new()
.encrypt_with_aad(&cipher, "correct-aad")
.expect("Encryption failed");
assert!(cipher
.decrypt_with_aad::<Vec<String>, _>(empty_sequence, "wrong-aad")
.is_err());
let empty_map = HashMap::<String, String>::new()
.encrypt_with_aad(&cipher, "correct-aad")
.expect("Encryption failed");
assert!(cipher
.decrypt_with_aad::<HashMap<String, String>, _>(empty_map, "wrong-aad")
.is_err());
}
#[quickcheck]
fn roundtrip_protected_string(key: Key, plaintext: String) -> bool {
use vitaminc_protected::{Controlled, Protected};
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let protected = Protected::new(plaintext.clone());
let ciphertext = protected.encrypt(&cipher).expect("Encryption failed");
let decrypted: Protected<String> = cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted.risky_unwrap() == plaintext
}
#[quickcheck]
fn roundtrip_equatable_string(key: Key, plaintext: String) -> bool {
use vitaminc_protected::{Controlled, Equatable, Protected};
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let value: Equatable<Protected<String>> = Equatable::new(plaintext.clone());
let ciphertext = value.encrypt(&cipher).expect("Encryption failed");
let decrypted: Equatable<Protected<String>> =
cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted.risky_unwrap() == plaintext
}
#[quickcheck]
fn roundtrip_equatable_u32(key: Key, plaintext: u32) -> bool {
use vitaminc_protected::{Controlled, Equatable, Protected};
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let value: Equatable<Protected<u32>> = Equatable::new(plaintext);
let ciphertext = value.encrypt(&cipher).expect("Encryption failed");
let decrypted: Equatable<Protected<u32>> =
cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted.risky_unwrap() == plaintext
}
#[quickcheck]
fn roundtrip_equatable_with_aad(key: Key, plaintext: String) -> bool {
use vitaminc_protected::{Controlled, Equatable, Protected};
let aad = "equatable-aad";
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let value: Equatable<Protected<String>> = Equatable::new(plaintext.clone());
let ciphertext = value
.encrypt_with_aad(&cipher, aad)
.expect("Encryption failed");
let decrypted: Equatable<Protected<String>> = cipher
.decrypt_with_aad(ciphertext, aad)
.expect("Decryption failed");
let expected: Equatable<Protected<String>> = Equatable::new(plaintext.clone());
decrypted == expected && decrypted.risky_unwrap() == plaintext
}
#[quickcheck]
fn decrypt_equatable_fails_with_wrong_aad(key: Key, plaintext: String) -> bool {
use vitaminc_protected::{Equatable, Protected};
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let value: Equatable<Protected<String>> = Equatable::new(plaintext);
let ciphertext = value
.encrypt_with_aad(&cipher, "correct-aad")
.expect("Encryption failed");
cipher
.decrypt_with_aad::<Equatable<Protected<String>>, _>(ciphertext, "wrong-aad")
.is_err()
}
#[quickcheck]
fn decrypt_equatable_fails_with_wrong_key(keys: DifferingKeyPair, plaintext: String) -> bool {
use vitaminc_protected::{Equatable, Protected};
let DifferingKeyPair(key_a, key_b) = keys;
let cipher_a = Aes256Cipher::new(&key_a).expect("Failed to create cipher A");
let cipher_b = Aes256Cipher::new(&key_b).expect("Failed to create cipher B");
let value: Equatable<Protected<String>> = Equatable::new(plaintext);
let ciphertext = value.encrypt(&cipher_a).expect("Encryption failed");
cipher_b
.decrypt::<Equatable<Protected<String>>>(ciphertext)
.is_err()
}
#[quickcheck]
fn equatable_ciphertext_is_wrapper_agnostic(key: Key, plaintext: String) -> bool {
use vitaminc_protected::{Controlled, Equatable, Protected};
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let eq_ciphertext = Equatable::<Protected<String>>::new(plaintext.clone())
.encrypt(&cipher)
.expect("Encryption failed");
let as_protected: Protected<String> = cipher
.decrypt(eq_ciphertext)
.expect("decrypt as Protected failed");
let bare_ciphertext = plaintext
.clone()
.encrypt(&cipher)
.expect("Encryption failed");
let as_equatable: Equatable<Protected<String>> = cipher
.decrypt(bare_ciphertext)
.expect("decrypt as Equatable failed");
as_protected.risky_unwrap() == plaintext && as_equatable.risky_unwrap() == plaintext
}
#[quickcheck]
fn roundtrip_option_some_string(key: Key, plaintext: String) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let value = Some(plaintext.clone());
let ciphertext = value.encrypt(&cipher).expect("Encryption failed");
let decrypted: Option<String> = cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted == Some(plaintext)
}
#[test]
fn roundtrip_option_none_string() {
let key = Key::from([7u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let value: Option<String> = None;
let ciphertext = value.encrypt(&cipher).expect("Encryption failed");
let decrypted: Option<String> = cipher.decrypt(ciphertext).expect("Decryption failed");
assert_eq!(decrypted, None);
}
#[quickcheck]
fn roundtrip_option_some_with_aad(key: Key, plaintext: String) -> bool {
let aad = "opt-aad";
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = Some(plaintext.clone())
.encrypt_with_aad(&cipher, aad)
.expect("Encryption failed");
let decrypted: Option<String> = cipher
.decrypt_with_aad(ciphertext, aad)
.expect("Decryption failed");
decrypted == Some(plaintext)
}
#[quickcheck]
fn roundtrip_option_none_with_aad(key: Key) -> bool {
let aad = "opt-none-aad";
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = None::<String>
.encrypt_with_aad(&cipher, aad)
.expect("Encryption failed");
let decrypted: Option<String> = cipher
.decrypt_with_aad(ciphertext, aad)
.expect("Decryption failed");
decrypted.is_none()
}
#[quickcheck]
fn decrypt_option_none_fails_with_wrong_aad(key: Key) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = None::<String>
.encrypt_with_aad(&cipher, "correct")
.expect("Encryption failed");
cipher
.decrypt_with_aad::<Option<String>, _>(ciphertext, "wrong")
.is_err()
}
#[test]
fn decrypt_string_rejects_none_ciphertext() {
let key = Key::from([9u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = None::<String>.encrypt(&cipher).expect("Encryption failed");
assert!(cipher.decrypt::<String>(ciphertext).is_err());
}
#[test]
fn decrypt_option_rejects_passthrough_ciphertext() {
let key = Key::from([11u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = (&cipher)
.passthrough(Box::new(42u32))
.expect("passthrough failed");
assert!(cipher.decrypt::<Option<u32>>(ciphertext).is_err());
}
#[test]
fn roundtrip_passthrough_u32() {
let key = Key::from([1u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = (&cipher)
.passthrough(Box::new(12345u32))
.expect("passthrough failed");
let decrypted: u32 = decrypt_passthrough_via(&cipher, ciphertext).expect("decode failed");
assert_eq!(decrypted, 12345u32);
}
#[test]
fn roundtrip_passthrough_string() {
let key = Key::from([2u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = (&cipher)
.passthrough(Box::new(String::from("version-tag")))
.expect("passthrough failed");
let decrypted: String =
decrypt_passthrough_via(&cipher, ciphertext).expect("decode failed");
assert_eq!(decrypted, "version-tag");
}
#[test]
fn passthrough_type_mismatch_returns_err() {
let key = Key::from([3u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = (&cipher)
.passthrough(Box::new(42u32))
.expect("passthrough failed");
let result: Result<String, _> = decrypt_passthrough_via(&cipher, ciphertext);
assert!(result.is_err());
}
fn decrypt_passthrough_via<T>(
cipher: &Aes256Cipher,
ciphertext: AesCipherText,
) -> Result<T, Unspecified>
where
T: Any + Send + 'static,
{
cipher.decipher(ciphertext).decrypt_passthrough_as::<T>()
}
#[quickcheck]
fn roundtrip_vec_of_option_string(key: Key, items: Vec<Option<String>>) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = items.clone().encrypt(&cipher).expect("Encryption failed");
let decrypted: Vec<Option<String>> = cipher.decrypt(ciphertext).expect("Decryption failed");
decrypted == items
}
#[test]
fn encrypt_key_twice_without_value_fails() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let map = (&cipher).encrypt_map(()).encrypt_key("first").unwrap();
assert!(map.encrypt_key("second").is_err());
}
#[test]
fn encrypt_value_without_pending_key_fails() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let map = (&cipher).encrypt_map(());
assert!(map.encrypt_value("orphan-value").is_err());
}
#[test]
fn passthrough_entry_with_pending_key_fails() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let map = (&cipher).encrypt_map(()).encrypt_key("pending").unwrap();
assert!(map.passthrough_entry("other", Box::new(42u32)).is_err());
}
#[test]
fn end_with_pending_key_fails() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let map = (&cipher).encrypt_map(()).encrypt_key("pending").unwrap();
assert!(map.end().is_err());
}
#[test]
fn passthrough_entry_succeeds_with_no_pending_key() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ciphertext = (&cipher)
.encrypt_map(())
.passthrough_entry("version", Box::new(1u32))
.and_then(|m| m.encrypt_key("email"))
.and_then(|m| m.encrypt_value("ada@example.com"))
.and_then(|m| m.end())
.expect("passthrough_entry should succeed without a pending key");
match ciphertext {
AesCipherText::Map(entries) => {
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].0, "version");
assert!(matches!(entries[0].1, AesCipherText::Passthrough(_)));
}
_ => panic!("expected Map ciphertext"),
}
}
fn encrypted_two_entry_map(cipher: &Aes256Cipher) -> Vec<(String, AesCipherText)> {
let mut map = HashMap::new();
map.insert("a", 1u32);
map.insert("b", 2u32);
match map.encrypt(cipher).expect("Encryption failed") {
AesCipherText::Map(entries) => entries,
_ => panic!("expected Map ciphertext"),
}
}
#[test]
fn swapped_map_keys_fail_decryption() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let mut entries = encrypted_two_entry_map(&cipher);
let (k0, k1) = (entries[0].0.clone(), entries[1].0.clone());
entries[0].0 = k1;
entries[1].0 = k0;
let result: Result<HashMap<String, u32>, _> = cipher.decrypt(AesCipherText::Map(entries));
assert!(result.is_err());
}
#[test]
fn renamed_map_key_fails_decryption() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let mut entries = encrypted_two_entry_map(&cipher);
entries[0].0 = "evil".to_string();
let result: Result<HashMap<String, u32>, _> = cipher.decrypt(AesCipherText::Map(entries));
assert!(result.is_err());
}
#[test]
fn renamed_map_key_with_empty_sequence_value_fails_decryption() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let mut map = HashMap::new();
map.insert("roles".to_string(), Vec::<String>::new());
let mut entries = match map.encrypt(&cipher).expect("Encryption failed") {
AesCipherText::Map(entries) => entries,
_ => panic!("expected Map ciphertext"),
};
entries[0].0 = "is_admin".to_string();
let result: Result<HashMap<String, Vec<String>>, _> =
cipher.decrypt(AesCipherText::Map(entries));
assert!(result.is_err());
}
#[test]
fn renamed_map_key_with_empty_map_value_fails_decryption() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let mut map = HashMap::new();
map.insert("roles".to_string(), HashMap::<String, String>::new());
let mut entries = match map.encrypt(&cipher).expect("Encryption failed") {
AesCipherText::Map(entries) => entries,
_ => panic!("expected Map ciphertext"),
};
entries[0].0 = "is_admin".to_string();
let result: Result<HashMap<String, HashMap<String, String>>, _> =
cipher.decrypt(AesCipherText::Map(entries));
assert!(result.is_err());
}
#[test]
fn unauthenticated_empty_composite_shapes_are_rejected() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
assert!(cipher
.decrypt::<Vec<String>>(AesCipherText::Sequence(Vec::new()))
.is_err());
assert!(cipher
.decrypt::<HashMap<String, String>>(AesCipherText::Map(Vec::new()))
.is_err());
}
struct Tags(Vec<String>);
impl Encrypt for Tags {
fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result<C::Ok, C::Error>
where
C: Cipher,
A: IntoAad<'a>,
{
let len = self.0.len();
self.0
.into_iter()
.try_fold(cipher.encrypt_seq(Some(len), aad), |c, item| {
c.encrypt_next(item)
})?
.end()
}
}
#[test]
fn hand_written_impl_roundtrips_empty_and_nonempty() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ct = Tags(vec!["a".into(), "b".into()])
.encrypt_with_aad(&cipher, "user:42")
.expect("Encryption failed");
let out: Vec<String> = cipher
.decrypt_with_aad(ct, "user:42")
.expect("non-empty should decrypt");
assert_eq!(out, vec!["a".to_string(), "b".to_string()]);
let ct = Tags(Vec::new())
.encrypt_with_aad(&cipher, "user:42")
.expect("Encryption failed");
let out: Vec<String> = cipher
.decrypt_with_aad(ct, "user:42")
.expect("empty should decrypt under the same AAD");
assert!(out.is_empty());
let ct = Tags(Vec::new())
.encrypt_with_aad(&cipher, "user:42")
.expect("Encryption failed");
assert!(cipher
.decrypt_with_aad::<Vec<String>, _>(ct, "user:99")
.is_err());
}
#[test]
fn all_passthrough_sequence_fails_to_encrypt() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let result = (&cipher)
.encrypt_seq(Some(1), "context")
.passthrough_next(Box::new(1u32))
.and_then(|s| s.end());
assert!(result.is_err());
}
#[test]
fn all_passthrough_map_fails_to_encrypt() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let result = (&cipher)
.encrypt_map("context")
.passthrough_entry("version", Box::new(1u32))
.and_then(|m| m.end());
assert!(result.is_err());
}
#[test]
fn all_passthrough_composites_fail_to_decrypt() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let hostile_seq = AesCipherText::Sequence(vec![AesCipherText::Passthrough(Box::new(1u32))]);
assert!(cipher.decrypt::<Vec<String>>(hostile_seq).is_err());
let hostile_map = AesCipherText::Map(vec![(
"version".to_string(),
AesCipherText::Passthrough(Box::new(1u32)),
)]);
assert!(cipher
.decrypt::<HashMap<String, String>>(hostile_map)
.is_err());
}
#[test]
fn single_retagged_as_none_fails_to_decrypt() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let leaf = match "secret".encrypt_with_aad(&cipher, "context").unwrap() {
AesCipherText::Single(leaf) => leaf,
_ => panic!("expected Single ciphertext"),
};
let result: Result<Option<String>, _> =
cipher.decrypt_with_aad(AesCipherText::None(leaf), "context");
assert!(result.is_err());
}
#[test]
fn empty_single_retagged_as_none_fails_to_decrypt() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let leaf = match "".encrypt_with_aad(&cipher, "context").unwrap() {
AesCipherText::Single(leaf) => leaf,
_ => panic!("expected Single ciphertext"),
};
let result: Result<Option<String>, _> =
cipher.decrypt_with_aad(AesCipherText::None(leaf), "context");
assert!(result.is_err());
}
#[test]
fn none_marker_still_roundtrips() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ct = Option::<String>::None
.encrypt_with_aad(&cipher, "context")
.unwrap();
let decrypted: Option<String> = cipher.decrypt_with_aad(ct, "context").unwrap();
assert_eq!(decrypted, None);
}
#[test]
fn empty_composite_markers_are_domain_separated() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let sequence_marker = match Vec::<String>::new()
.encrypt_with_aad(&cipher, "context")
.expect("Encryption failed")
{
AesCipherText::EmptySequence(marker) => marker,
_ => panic!("expected EmptySequence ciphertext"),
};
assert!(cipher
.decrypt_with_aad::<HashMap<String, String>, _>(
AesCipherText::EmptyMap(sequence_marker),
"context",
)
.is_err());
let map_marker = match HashMap::<String, String>::new()
.encrypt_with_aad(&cipher, "context")
.expect("Encryption failed")
{
AesCipherText::EmptyMap(marker) => marker,
_ => panic!("expected EmptyMap ciphertext"),
};
assert!(
cipher
.decrypt_with_aad::<Vec<String>, _>(
AesCipherText::EmptySequence(map_marker),
"context",
)
.is_err()
);
}
#[test]
fn empty_composite_markers_reject_nonempty_plaintext() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let aad = Aad::from_slice(b"context").for_empty_sequence();
let marker = match "not-empty"
.encrypt_with_aad(&cipher, aad)
.expect("Encryption failed")
{
AesCipherText::Single(marker) => marker,
_ => panic!("expected Single ciphertext"),
};
assert!(cipher
.decrypt_with_aad::<Vec<String>, _>(AesCipherText::EmptySequence(marker), "context",)
.is_err());
}
#[test]
fn reordered_map_entries_still_decrypt() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let mut entries = encrypted_two_entry_map(&cipher);
entries.swap(0, 1);
let decrypted: HashMap<String, u32> = cipher
.decrypt(AesCipherText::Map(entries))
.expect("reordered entries should decrypt");
assert_eq!(decrypted.len(), 2);
}
#[test]
fn runtime_string_keys_roundtrip() {
let key = Key::from([42u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let mut map: HashMap<String, String> = HashMap::new();
for id in 0..3 {
map.insert(format!("user:{id}"), format!("value-{id}"));
}
let ciphertext = map
.clone()
.encrypt_with_aad(&cipher, "context")
.expect("Encryption failed");
let decrypted: HashMap<String, String> = cipher
.decrypt_with_aad(ciphertext, "context")
.expect("Decryption failed");
assert_eq!(decrypted, map);
}
#[quickcheck]
fn nonce_is_unique_across_encryptions(key: Key, plaintext: String) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ct1 = plaintext
.clone()
.encrypt(&cipher)
.expect("Encryption failed");
let ct2 = plaintext.encrypt(&cipher).expect("Encryption failed");
match (ct1, ct2) {
(AesCipherText::Single(a), AesCipherText::Single(b)) => a.as_ref() != b.as_ref(),
_ => panic!("expected Single ciphertexts"),
}
}
fn single_ct(cipher: &Aes256Cipher) -> AesCipherText {
"single".to_string().encrypt(cipher).expect("encrypt")
}
fn sequence_ct(cipher: &Aes256Cipher) -> AesCipherText {
vec!["a".to_string(), "b".to_string()]
.encrypt(cipher)
.expect("encrypt")
}
fn map_ct(cipher: &Aes256Cipher) -> AesCipherText {
let mut m = HashMap::new();
m.insert("k", "v");
m.encrypt(cipher).expect("encrypt")
}
fn none_ct(cipher: &Aes256Cipher) -> AesCipherText {
None::<String>.encrypt(cipher).expect("encrypt")
}
fn passthrough_ct(cipher: &Aes256Cipher) -> AesCipherText {
cipher.passthrough(Box::new(7u32)).expect("passthrough")
}
#[test]
fn decrypt_bytes_rejects_non_single_variants() {
let cipher = Aes256Cipher::new(&Key::from([20u8; 32])).expect("Failed to create cipher");
assert!(
cipher.decrypt::<String>(sequence_ct(&cipher)).is_err(),
"Sequence"
);
assert!(cipher.decrypt::<String>(map_ct(&cipher)).is_err(), "Map");
assert!(cipher.decrypt::<String>(none_ct(&cipher)).is_err(), "None");
assert!(
cipher.decrypt::<String>(passthrough_ct(&cipher)).is_err(),
"Passthrough"
);
}
#[test]
fn decrypt_seq_rejects_non_sequence_variants() {
let cipher = Aes256Cipher::new(&Key::from([21u8; 32])).expect("Failed to create cipher");
assert!(
cipher.decrypt::<Vec<String>>(single_ct(&cipher)).is_err(),
"Single"
);
assert!(
cipher.decrypt::<Vec<String>>(map_ct(&cipher)).is_err(),
"Map"
);
assert!(
cipher.decrypt::<Vec<String>>(none_ct(&cipher)).is_err(),
"None"
);
assert!(
cipher
.decrypt::<Vec<String>>(passthrough_ct(&cipher))
.is_err(),
"Passthrough"
);
}
#[test]
fn decrypt_map_rejects_non_map_variants() {
let cipher = Aes256Cipher::new(&Key::from([22u8; 32])).expect("Failed to create cipher");
assert!(
cipher
.decrypt::<HashMap<String, String>>(single_ct(&cipher))
.is_err(),
"Single"
);
assert!(
cipher
.decrypt::<HashMap<String, String>>(sequence_ct(&cipher))
.is_err(),
"Sequence"
);
assert!(
cipher
.decrypt::<HashMap<String, String>>(none_ct(&cipher))
.is_err(),
"None"
);
assert!(
cipher
.decrypt::<HashMap<String, String>>(passthrough_ct(&cipher))
.is_err(),
"Passthrough"
);
}
#[test]
fn decrypt_passthrough_rejects_non_passthrough_variants() {
let cipher = Aes256Cipher::new(&Key::from([23u8; 32])).expect("Failed to create cipher");
assert!(
decrypt_passthrough_via::<u32>(&cipher, single_ct(&cipher)).is_err(),
"Single"
);
assert!(
decrypt_passthrough_via::<u32>(&cipher, sequence_ct(&cipher)).is_err(),
"Sequence"
);
assert!(
decrypt_passthrough_via::<u32>(&cipher, map_ct(&cipher)).is_err(),
"Map"
);
assert!(
decrypt_passthrough_via::<u32>(&cipher, none_ct(&cipher)).is_err(),
"None"
);
}
#[quickcheck]
fn roundtrip_option_some_u32(key: Key, value: u32) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ct = Some(value).encrypt(&cipher).expect("Encryption failed");
cipher
.decrypt::<Option<u32>>(ct)
.expect("Decryption failed")
== Some(value)
}
#[quickcheck]
fn roundtrip_option_some_vec_of_strings(items: Vec<String>) -> bool {
let cipher = Aes256Cipher::new(&Key::from([31u8; 32])).expect("Failed to create cipher");
let ct = Some(items.clone())
.encrypt(&cipher)
.expect("Encryption failed");
cipher
.decrypt::<Option<Vec<String>>>(ct)
.expect("Decryption failed")
== Some(items)
}
#[test]
fn roundtrip_option_some_hashmap() {
let cipher = Aes256Cipher::new(&Key::from([32u8; 32])).expect("Failed to create cipher");
let mut m = HashMap::new();
m.insert("name", "Alice");
let ct = Some(m).encrypt(&cipher).expect("Encryption failed");
let decoded: Option<HashMap<String, String>> =
cipher.decrypt(ct).expect("Decryption failed");
assert_eq!(
decoded.unwrap().get("name").map(String::as_str),
Some("Alice")
);
}
#[quickcheck]
fn roundtrip_option_some_protected_string(plaintext: String) -> bool {
use vitaminc_protected::{Controlled, Protected};
let cipher = Aes256Cipher::new(&Key::from([33u8; 32])).expect("Failed to create cipher");
let ct = Some(Protected::new(plaintext.clone()))
.encrypt(&cipher)
.expect("Encryption failed");
let decoded: Option<Protected<String>> = cipher.decrypt(ct).expect("Decryption failed");
decoded.map(Controlled::risky_unwrap) == Some(plaintext)
}
#[quickcheck]
fn decrypt_option_some_fails_with_wrong_aad(plaintext: String) -> bool {
let cipher = Aes256Cipher::new(&Key::from([34u8; 32])).expect("Failed to create cipher");
let ct = Some(plaintext)
.encrypt_with_aad(&cipher, "correct")
.expect("Encryption failed");
cipher
.decrypt_with_aad::<Option<String>, _>(ct, "wrong")
.is_err()
}
#[test]
fn nested_option_shape_is_caller_decided() {
let cipher = Aes256Cipher::new(&Key::from([35u8; 32])).expect("Failed to create cipher");
let outer: Option<Option<String>> = Some(Some("x".into()));
let ct = outer.encrypt(&cipher).expect("Encryption failed");
let decoded: Option<String> = cipher.decrypt(ct).expect("Decryption failed");
assert_eq!(decoded, Some("x".into()));
}
#[test]
fn seq_cipher_accepts_mixed_encrypt_next_and_passthrough_next() {
let cipher = Aes256Cipher::new(&Key::from([40u8; 32])).expect("Failed to create cipher");
let ct = (&cipher)
.encrypt_seq(Some(3), ())
.encrypt_next("first")
.unwrap()
.passthrough_next(Box::new(99u32))
.unwrap()
.encrypt_next("third")
.unwrap()
.end()
.unwrap();
match ct {
AesCipherText::Sequence(items) => {
assert_eq!(items.len(), 3);
assert!(matches!(items[0], AesCipherText::Single(_)));
assert!(matches!(items[1], AesCipherText::Passthrough(_)));
assert!(matches!(items[2], AesCipherText::Single(_)));
}
_ => panic!("expected Sequence"),
}
}
#[test]
fn map_with_mixed_entries_cannot_decode_as_uniform_hashmap() {
let cipher = Aes256Cipher::new(&Key::from([41u8; 32])).expect("Failed to create cipher");
let ct = (&cipher)
.encrypt_map(())
.encrypt_key("name")
.unwrap()
.encrypt_value("Alice".to_string())
.unwrap()
.passthrough_entry("schema_version", Box::new(1u32))
.unwrap()
.end()
.unwrap();
assert!(cipher.decrypt::<HashMap<String, String>>(ct).is_err());
}
#[test]
fn roundtrip_hashmap_of_option_values() {
let cipher = Aes256Cipher::new(&Key::from([42u8; 32])).expect("Failed to create cipher");
let mut m: HashMap<&'static str, Option<String>> = HashMap::new();
m.insert("present", Some("Alice".to_string()));
m.insert("absent", None);
let ct = m.encrypt(&cipher).expect("Encryption failed");
let decoded: HashMap<String, Option<String>> =
cipher.decrypt(ct).expect("Decryption failed");
assert_eq!(decoded.get("present"), Some(&Some("Alice".to_string())));
assert_eq!(decoded.get("absent"), Some(&None));
}
#[test]
fn passthrough_option_is_distinct_type_from_inner() {
let cipher = Aes256Cipher::new(&Key::from([43u8; 32])).expect("Failed to create cipher");
let ct = cipher
.passthrough(Box::new(Some(42u32)))
.expect("passthrough failed");
let decoded: Option<u32> =
decrypt_passthrough_via(&cipher, ct).expect("decode as Option<u32> should succeed");
assert_eq!(decoded, Some(42u32));
let ct2 = cipher
.passthrough(Box::new(Some(42u32)))
.expect("passthrough failed");
assert!(
decrypt_passthrough_via::<u32>(&cipher, ct2).is_err(),
"Option<u32> passthrough must not downcast to u32"
);
}
#[test]
fn duplicate_map_keys_fail_decryption() {
let cipher = Aes256Cipher::new(&Key::from([50u8; 32])).expect("Failed to create cipher");
let mut v1 = HashMap::new();
v1.insert("balance", "100");
v1.insert("owner", "alice");
let mut v2 = HashMap::new();
v2.insert("balance", "5");
v2.insert("owner", "alice");
let stale_balance = match v1.encrypt(&cipher).expect("encrypt v1") {
AesCipherText::Map(entries) => entries
.into_iter()
.find(|(k, _)| k == "balance")
.expect("balance entry"),
_ => panic!("expected Map"),
};
let tampered = match v2.encrypt(&cipher).expect("encrypt v2") {
AesCipherText::Map(mut entries) => {
entries.push(stale_balance);
AesCipherText::Map(entries)
}
_ => panic!("expected Map"),
};
assert!(cipher.decrypt::<HashMap<String, String>>(tampered).is_err());
}
#[test]
fn single_rewrapped_as_sequence_fails_decryption() {
let cipher = Aes256Cipher::new(&Key::from([51u8; 32])).expect("Failed to create cipher");
let single = "x".to_string().encrypt(&cipher).expect("encrypt");
let forged = AesCipherText::Sequence(vec![single]);
assert!(cipher.decrypt::<Vec<String>>(forged).is_err());
}
#[test]
fn every_leaf_carries_the_wire_version_prefix() {
use vitaminc_aead::WIRE_VERSION;
let cipher = Aes256Cipher::new(&Key::from([54u8; 32])).expect("Failed to create cipher");
fn leaf_versions(ct: &AesCipherText) -> Vec<Option<u8>> {
match ct {
AesCipherText::Single(l)
| AesCipherText::None(l)
| AesCipherText::EmptySequence(l)
| AesCipherText::EmptyMap(l) => vec![l.wire_version()],
AesCipherText::Sequence(items) => items.iter().flat_map(leaf_versions).collect(),
AesCipherText::Map(entries) => {
entries.iter().flat_map(|(_, v)| leaf_versions(v)).collect()
}
AesCipherText::Passthrough(_) => vec![],
}
}
let mut map = HashMap::new();
map.insert("k", "v");
let cts = [
"single".to_string().encrypt(&cipher).expect("encrypt"),
None::<String>.encrypt(&cipher).expect("encrypt"),
Vec::<String>::new().encrypt(&cipher).expect("encrypt"),
HashMap::<&str, &str>::new()
.encrypt(&cipher)
.expect("encrypt"),
vec!["a".to_string(), "b".to_string()]
.encrypt(&cipher)
.expect("encrypt"),
map.encrypt(&cipher).expect("encrypt"),
];
for ct in &cts {
let versions = leaf_versions(ct);
assert!(!versions.is_empty());
assert!(versions.iter().all(|v| *v == Some(WIRE_VERSION)));
}
}
#[test]
fn relabeled_wire_version_fails_decryption() {
let cipher = Aes256Cipher::new(&Key::from([55u8; 32])).expect("Failed to create cipher");
let leaf = match "x".to_string().encrypt(&cipher).expect("encrypt") {
AesCipherText::Single(leaf) => leaf,
_ => panic!("expected Single"),
};
let mut bytes = leaf.as_ref().to_vec();
bytes[0] ^= 0x02;
let relabeled = AesCipherText::Single(LocalCipherText::from(bytes));
assert!(cipher.decrypt::<String>(relabeled).is_err());
}
#[test]
fn sequence_element_rehomed_to_top_level_fails_decryption() {
let cipher = Aes256Cipher::new(&Key::from([52u8; 32])).expect("Failed to create cipher");
let element = match vec!["x".to_string()].encrypt(&cipher).expect("encrypt") {
AesCipherText::Sequence(mut items) => items.pop().expect("one element"),
_ => panic!("expected Sequence"),
};
assert!(cipher.decrypt::<String>(element).is_err());
}
#[test]
fn element_decrypts_a_stored_sequence_element() {
let cipher = Aes256Cipher::new(&Key::from([56u8; 32])).expect("Failed to create cipher");
let ct = vec!["a".to_string(), "b".to_string()]
.encrypt_with_aad(&cipher, "users")
.expect("encrypt");
let element = match ct {
AesCipherText::Sequence(mut items) => items.pop().expect("two elements"),
_ => panic!("expected Sequence"),
};
let row: Element<String> = cipher.decrypt_with_aad(element, "users").expect("decrypt");
assert_eq!(row.into_inner(), "b");
}
#[test]
fn element_encrypted_alone_decrypts_within_a_sequence() {
let cipher = Aes256Cipher::new(&Key::from([57u8; 32])).expect("Failed to create cipher");
let ct1 = Element("a".to_string())
.encrypt_with_aad(&cipher, "users")
.expect("encrypt");
let ct2 = Element("b".to_string())
.encrypt_with_aad(&cipher, "users")
.expect("encrypt");
let collected = AesCipherText::Sequence(vec![ct1, ct2]);
let rows: Vec<String> = cipher
.decrypt_with_aad(collected, "users")
.expect("decrypt");
assert_eq!(rows, ["a", "b"]);
}
#[test]
fn element_is_not_interchangeable_with_bare_values() {
let cipher = Aes256Cipher::new(&Key::from([58u8; 32])).expect("Failed to create cipher");
let bare = "x"
.to_string()
.encrypt_with_aad(&cipher, "users")
.expect("encrypt");
assert!(cipher
.decrypt_with_aad::<Element<String>, _>(bare, "users")
.is_err());
let element = Element("x".to_string())
.encrypt_with_aad(&cipher, "users")
.expect("encrypt");
assert!(cipher
.decrypt_with_aad::<String, _>(element, "users")
.is_err());
}
#[test]
fn vec_of_elements_double_derives_and_does_not_interchange_with_vec() {
let cipher = Aes256Cipher::new(&Key::from([59u8; 32])).expect("Failed to create cipher");
let ct = vec![Element("a".to_string())]
.encrypt_with_aad(&cipher, "users")
.expect("encrypt");
assert!(cipher
.decrypt_with_aad::<Vec<String>, _>(ct, "users")
.is_err());
let ct = vec![Element("a".to_string())]
.encrypt_with_aad(&cipher, "users")
.expect("encrypt");
let rows: Vec<Element<String>> = cipher.decrypt_with_aad(ct, "users").expect("decrypt");
assert_eq!(rows, [Element("a".to_string())]);
}
#[test]
fn empty_sequence_marker_nested_one_level_fails_decryption() {
let cipher = Aes256Cipher::new(&Key::from([53u8; 32])).expect("Failed to create cipher");
let marker = Vec::<String>::new().encrypt(&cipher).expect("encrypt");
assert!(matches!(marker, AesCipherText::EmptySequence(_)));
let forged = AesCipherText::Sequence(vec![marker]);
assert!(cipher.decrypt::<Vec<Vec<String>>>(forged).is_err());
}
#[quickcheck]
fn decrypt_byte_array_ciphertext_as_vec_u8(key: Key, bytes: [u8; 16]) -> bool {
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let ct = bytes.encrypt(&cipher).expect("Encryption failed");
let decoded: Vec<u8> = cipher.decrypt(ct).expect("Decryption failed");
decoded == bytes.to_vec()
}
}