use chacha20poly1305::{
AeadCore, ChaCha20Poly1305, KeyInit, Nonce,
aead::{Aead as _, OsRng as AeadOsRng, rand_core::RngCore as _},
};
use zeroize::Zeroizing;
pub const DEK_LEN: usize = 32;
pub const NONCE_LEN: usize = 12;
pub type Dek = Zeroizing<[u8; DEK_LEN]>;
pub fn generate_dek() -> Dek {
let mut bytes = [0u8; DEK_LEN];
AeadOsRng.fill_bytes(&mut bytes);
Zeroizing::new(bytes)
}
pub fn seal(dek: &Dek, plaintext: &[u8]) -> crate::Result<Vec<u8>> {
let cipher = ChaCha20Poly1305::new(dek.as_slice().into());
let nonce = ChaCha20Poly1305::generate_nonce(&mut AeadOsRng);
let ciphertext = cipher
.encrypt(&nonce, plaintext)
.map_err(|e| anyhow_serde::Error::msg(format!("AEAD encryption failed: {e}")))?;
let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len());
out.extend_from_slice(nonce.as_slice());
out.extend_from_slice(&ciphertext);
Ok(out)
}
pub fn unseal(dek: &Dek, framed: &[u8]) -> crate::Result<Zeroizing<Vec<u8>>> {
if framed.len() < NONCE_LEN {
crate::bail!(
"sealed payload too short ({} bytes); expected at least {NONCE_LEN}-byte nonce",
framed.len()
);
}
let (nonce_bytes, ct_and_tag) = framed.split_at(NONCE_LEN);
let nonce = Nonce::from_slice(nonce_bytes);
let cipher = ChaCha20Poly1305::new(dek.as_slice().into());
let plaintext = cipher
.decrypt(nonce, ct_and_tag)
.map_err(|e| anyhow_serde::Error::msg(format!("AEAD decryption failed: {e}")))?;
Ok(Zeroizing::new(plaintext))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip() {
let dek = generate_dek();
let pt = b"hunter2 is not a good password";
let ct = seal(&dek, pt).unwrap();
let recovered = unseal(&dek, &ct).unwrap();
assert_eq!(&*recovered, pt);
}
#[test]
fn distinct_nonces_per_encryption() {
let dek = generate_dek();
let pt = b"the quick brown fox";
let a = seal(&dek, pt).unwrap();
let b = seal(&dek, pt).unwrap();
assert_ne!(a, b);
}
#[test]
fn tamper_detection() {
let dek = generate_dek();
let pt = b"important";
let mut ct = seal(&dek, pt).unwrap();
ct[NONCE_LEN] ^= 0x01;
assert!(unseal(&dek, &ct).is_err());
}
#[test]
fn wrong_key_fails() {
let dek_a = generate_dek();
let dek_b = generate_dek();
let ct = seal(&dek_a, b"x").unwrap();
assert!(unseal(&dek_b, &ct).is_err());
}
#[test]
fn too_short_payload_rejected() {
let dek = generate_dek();
assert!(unseal(&dek, b"short").is_err());
}
}