use crate::Aes256Cipher;
use vitaminc_aead::hlist::{Absent, Encrypted, Entry, StaticCipher};
use vitaminc_aead::{IntoAad, Unspecified};
use vitaminc_protected::{Controlled, Protected};
impl StaticCipher for &Aes256Cipher {
type Error = Unspecified;
fn encrypt_bytes<'a, A>(
self,
data: Protected<Vec<u8>>,
aad: A,
) -> Result<Encrypted, Self::Error>
where
A: IntoAad<'a>,
{
let local = seal_into_local(self, data, aad)?;
Ok(Encrypted::from_local(local))
}
fn encrypt_none<'a, A>(self, aad: A) -> Result<Absent, Self::Error>
where
A: IntoAad<'a>,
{
let local = seal_into_local(self, Protected::new(Vec::new()), aad.into_aad().for_none())?;
Ok(Absent::from_local(local))
}
}
impl Aes256Cipher {
pub fn open<'a, A>(&self, ct: Encrypted, aad: A) -> Result<Protected<Vec<u8>>, Unspecified>
where
A: IntoAad<'a>,
{
open_local(self, ct.into_local(), aad)
}
pub fn verify_absent<'a, A>(&self, ct: Absent, aad: A) -> Result<(), Unspecified>
where
A: IntoAad<'a>,
{
let plaintext = open_local(self, ct.into_local(), aad.into_aad().for_none())?;
if plaintext.risky_ref().is_empty() {
Ok(())
} else {
Err(Unspecified)
}
}
pub fn open_entry<'a, A>(
&self,
entry: Entry<Encrypted>,
aad: A,
) -> Result<Protected<Vec<u8>>, Unspecified>
where
A: IntoAad<'a>,
{
let entry_aad = aad.into_aad().for_map_entry(entry.key);
open_local(self, entry.value.into_local(), entry_aad)
}
pub fn verify_absent_entry<'a, A>(
&self,
entry: Entry<Absent>,
aad: A,
) -> Result<(), Unspecified>
where
A: IntoAad<'a>,
{
let entry_aad = aad.into_aad().for_map_entry(entry.key);
self.verify_absent(entry.value, entry_aad)
}
}
use crate::cipher::{open_leaf as open_local, seal_leaf as seal_into_local};
#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use crate::backend::NONCE_LEN;
use crate::key::tests::DifferingKeyPair;
use crate::Key;
use quickcheck_macros::quickcheck;
use vitaminc_aead::hlist::{Entry, HCons, HNil, Map, Passthrough, StaticCipher};
use vitaminc_aead::{Aad, LocalCipherText};
type UserCiphertext = Map<
HCons<
Entry<Map<HCons<Entry<Encrypted>, HNil>>>, HCons<
Entry<Absent>, HCons<
Entry<Passthrough<u8>>, HCons<
Entry<Encrypted>, HNil,
>,
>,
>,
>,
>;
const AAD: &[u8] = b"user:42";
#[test]
fn static_user_roundtrip() {
let key = Key::from([7u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher init");
let user_ct: UserCiphertext = (&cipher)
.encrypt_map()
.encrypt_entry(
"password_hash",
Protected::new(b"argon2id$hash".to_vec()),
AAD,
)
.expect("encrypt pw")
.passthrough_entry("version", 3u8)
.none_entry("nickname", AAD)
.expect("encrypt none")
.nested_entry("preferences", AAD, |b, aad| {
Ok(
b.encrypt_entry("theme", Protected::new(b"midnight".to_vec()), aad)?
.end(),
)
})
.expect("encrypt prefs")
.end();
let Map(HCons(prefs_e, HCons(nickname_e, HCons(version_e, HCons(pw_e, HNil))))) = user_ct;
assert_eq!(pw_e.key, "password_hash");
assert_eq!(version_e.key, "version");
assert_eq!(nickname_e.key, "nickname");
assert_eq!(prefs_e.key, "preferences");
let version: u8 = version_e.value.0; let pw = String::from_utf8(
cipher
.open_entry(pw_e, AAD)
.expect("open pw")
.risky_unwrap(),
)
.unwrap();
cipher
.verify_absent_entry(nickname_e, AAD)
.expect("verify none");
let inner_aad = prefs_e.nested_aad(AAD);
let Map(HCons(theme_e, HNil)) = prefs_e.value;
let theme = String::from_utf8(
cipher
.open_entry(theme_e, inner_aad)
.expect("open theme")
.risky_unwrap(),
)
.unwrap();
assert_eq!(pw, "argon2id$hash");
assert_eq!(version, 3u8);
assert_eq!(theme, "midnight");
}
#[test]
fn static_entry_with_renamed_key_fails_to_open() {
let key = Key::from([9u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher init");
let map = (&cipher)
.encrypt_map()
.encrypt_entry("email", Protected::new(b"ada@example.com".to_vec()), AAD)
.expect("encrypt")
.end();
let Map(HCons(entry, HNil)) = map;
let renamed = Entry {
key: "backup_email",
value: entry.value,
};
assert!(cipher.open_entry(renamed, AAD).is_err());
}
#[test]
fn static_nested_entry_with_renamed_key_fails_to_open_inner() {
let key = Key::from([10u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher init");
let map = (&cipher)
.encrypt_map()
.nested_entry("preferences", AAD, |b, aad| {
Ok(
b.encrypt_entry("theme", Protected::new(b"midnight".to_vec()), aad)?
.end(),
)
})
.expect("encrypt prefs")
.end();
let Map(HCons(entry, HNil)) = map;
let renamed = Entry {
key: "billing",
value: entry.value,
};
let inner_aad = renamed.nested_aad(AAD);
let Map(HCons(theme_e, HNil)) = renamed.value;
assert!(cipher.open_entry(theme_e, inner_aad).is_err());
}
#[test]
fn static_nested_map_spliced_from_another_record_fails_to_open_inner() {
let key = Key::from([11u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher init");
let build = |aad: &'static [u8]| {
let map = (&cipher)
.encrypt_map()
.nested_entry("preferences", aad, |b, inner| {
Ok(
b.encrypt_entry("theme", Protected::new(b"midnight".to_vec()), inner)?
.end(),
)
})
.expect("encrypt prefs")
.end();
let Map(HCons(entry, HNil)) = map;
entry
};
let victim = build(b"user:42");
let foreign = build(b"user:99");
let spliced = Entry {
key: victim.key,
value: foreign.value,
};
let inner_aad = spliced.nested_aad(b"user:42".as_slice());
let Map(HCons(theme_e, HNil)) = spliced.value;
assert!(cipher.open_entry(theme_e, inner_aad).is_err());
}
#[test]
fn static_absent_entry_with_renamed_key_fails_to_verify() {
let key = Key::from([9u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher init");
let map = (&cipher)
.encrypt_map()
.none_entry("nickname", AAD)
.expect("encrypt none")
.end();
let Map(HCons(entry, HNil)) = map;
let renamed = Entry {
key: "email",
value: entry.value,
};
assert!(cipher.verify_absent_entry(renamed, AAD).is_err());
}
#[test]
fn static_open_fails_with_wrong_aad() {
let key = Key::from([3u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher init");
let leaf = (&cipher)
.encrypt_bytes(Protected::new(b"hello".to_vec()), b"right".as_slice())
.expect("encrypt");
assert!(cipher.open(leaf, b"wrong".as_slice()).is_err());
}
#[test]
fn static_verify_absent_fails_with_wrong_aad() {
let key = Key::from([4u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher init");
let absent = (&cipher)
.encrypt_none(b"right".as_slice())
.expect("encrypt none");
assert!(cipher.verify_absent(absent, b"wrong".as_slice()).is_err());
}
#[test]
fn static_passthrough_keeps_type() {
let key = Key::from([5u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher init");
let p: Passthrough<(u16, &'static str)> = (&cipher).passthrough((42u16, "tag"));
let (n, s) = p.0;
assert_eq!(n, 42u16);
assert_eq!(s, "tag");
}
fn _proof_send() {
fn assert_send<T: Send>() {}
assert_send::<UserCiphertext>();
}
#[quickcheck]
fn open_rejects_arbitrary_bytes(key: Key, bytes: Vec<u8>) -> bool {
let cipher = Aes256Cipher::new(&key).unwrap();
let leaf = Encrypted::from_local(LocalCipherText::from(bytes));
cipher.open(leaf, b"aad".as_slice()).is_err()
}
#[quickcheck]
fn verify_absent_rejects_arbitrary_bytes(key: Key, bytes: Vec<u8>) -> bool {
let cipher = Aes256Cipher::new(&key).unwrap();
let leaf = Absent::from_local(LocalCipherText::from(bytes));
cipher.verify_absent(leaf, b"aad".as_slice()).is_err()
}
#[test]
fn verify_absent_rejects_a_nonempty_payload_under_the_none_derivation() {
let cipher = Aes256Cipher::new(&Key::from([6u8; 32])).unwrap();
let aad = b"aad".as_slice();
let forged = (&cipher)
.encrypt_bytes(
Protected::new(b"payload".to_vec()),
Aad::from_slice(aad).for_none(),
)
.unwrap();
let as_absent = Absent::from_local(forged.into_local());
assert!(cipher.verify_absent(as_absent, aad).is_err());
}
#[test]
fn absent_does_not_validate_as_encrypted_empty() {
let cipher = Aes256Cipher::new(&Key::from([2u8; 32])).unwrap();
let aad = b"shared-aad".as_slice();
let absent = (&cipher).encrypt_none(aad).unwrap();
let as_encrypted = Encrypted::from_local(absent.into_local());
assert!(cipher.open(as_encrypted, aad).is_err());
}
#[test]
fn encrypted_empty_does_not_validate_as_absent() {
let cipher = Aes256Cipher::new(&Key::from([2u8; 32])).unwrap();
let aad = b"shared-aad".as_slice();
let encrypted_empty = (&cipher)
.encrypt_bytes(Protected::new(Vec::new()), aad)
.unwrap();
let as_absent = Absent::from_local(encrypted_empty.into_local());
assert!(cipher.verify_absent(as_absent, aad).is_err());
}
#[quickcheck]
fn open_with_wrong_key_fails(keys: DifferingKeyPair, plaintext: Vec<u8>) -> bool {
let DifferingKeyPair(key_a, key_b) = keys;
let cipher_a = Aes256Cipher::new(&key_a).unwrap();
let cipher_b = Aes256Cipher::new(&key_b).unwrap();
let aad = b"aad".as_slice();
let ct = (&cipher_a)
.encrypt_bytes(Protected::new(plaintext), aad)
.unwrap();
cipher_b.open(ct, aad).is_err()
}
#[quickcheck]
fn verify_absent_with_wrong_key_fails(keys: DifferingKeyPair) -> bool {
let DifferingKeyPair(key_a, key_b) = keys;
let cipher_a = Aes256Cipher::new(&key_a).unwrap();
let cipher_b = Aes256Cipher::new(&key_b).unwrap();
let aad = b"aad".as_slice();
let absent = (&cipher_a).encrypt_none(aad).unwrap();
cipher_b.verify_absent(absent, aad).is_err()
}
#[test]
fn open_fails_on_tampering_any_byte() {
let cipher = Aes256Cipher::new(&Key::from([3u8; 32])).unwrap();
let aad = b"aad".as_slice();
let ct = (&cipher)
.encrypt_bytes(Protected::new(b"hello".to_vec()), aad)
.unwrap();
let bytes = ct.into_local().as_ref().to_vec();
for idx in 0..bytes.len() {
let mut tampered = bytes.clone();
tampered[idx] ^= 1;
let leaf = Encrypted::from_local(LocalCipherText::from(tampered));
assert!(cipher.open(leaf, aad).is_err(), "tamper at byte {idx}");
}
}
#[quickcheck]
fn encrypt_bytes_is_nondeterministic(key: Key, plaintext: Vec<u8>) -> bool {
let cipher = Aes256Cipher::new(&key).unwrap();
let aad = b"aad".as_slice();
let b1 = (&cipher)
.encrypt_bytes(Protected::new(plaintext.clone()), aad)
.unwrap()
.into_local()
.as_ref()
.to_vec();
let b2 = (&cipher)
.encrypt_bytes(Protected::new(plaintext), aad)
.unwrap()
.into_local()
.as_ref()
.to_vec();
b1 != b2 && b1[1..1 + NONCE_LEN] != b2[1..1 + NONCE_LEN]
}
#[quickcheck]
fn encrypt_none_is_nondeterministic(key: Key) -> bool {
let cipher = Aes256Cipher::new(&key).unwrap();
let aad = b"aad".as_slice();
let a1 = (&cipher).encrypt_none(aad).unwrap().into_local();
let a2 = (&cipher).encrypt_none(aad).unwrap().into_local();
a1.as_ref() != a2.as_ref()
}
}