use serde::{Deserialize, Serialize};
use std::fmt;
use zeroize::Zeroize;
const MAILBOX_AEAD_AAD: &[u8] = b"atp-mailbox-v1";
#[derive(Clone)]
pub struct MailboxKey {
key_material: [u8; 32],
}
impl MailboxKey {
#[must_use]
pub fn generate() -> Self {
let mut key_material = [0u8; 32];
getrandom::fill(&mut key_material).expect("OS entropy unavailable for mailbox key");
Self { key_material }
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self {
key_material: bytes,
}
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.key_material
}
}
impl fmt::Debug for MailboxKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MailboxKey")
.field("key_material", &"[redacted]")
.finish()
}
}
impl Drop for MailboxKey {
fn drop(&mut self) {
self.key_material.zeroize();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedChunk {
pub data: Vec<u8>,
pub nonce: ChunkNonce,
pub tag: [u8; 16],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkNonce {
pub bytes: [u8; 12],
}
impl ChunkNonce {
pub fn generate() -> Result<Self, String> {
let mut bytes = [0u8; 12];
getrandom::fill(&mut bytes)
.map_err(|err| format!("OS entropy unavailable for mailbox nonce: {err}"))?;
Ok(Self { bytes })
}
}
impl EncryptedChunk {
pub fn encrypt(data: &[u8], key: &MailboxKey) -> Result<Self, String> {
use aes_gcm::aead::{AeadInPlace, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce};
let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
.map_err(|err| format!("invalid mailbox key: {err}"))?;
let nonce = ChunkNonce::generate()?;
let mut encrypted = data.to_vec();
let tag = cipher
.encrypt_in_place_detached(
Nonce::from_slice(&nonce.bytes),
MAILBOX_AEAD_AAD,
&mut encrypted,
)
.map_err(|err| format!("mailbox encryption failed: {err}"))?;
let mut tag_bytes = [0u8; 16];
tag_bytes.copy_from_slice(tag.as_slice());
Ok(Self {
data: encrypted,
nonce,
tag: tag_bytes,
})
}
pub fn decrypt(&self, key: &MailboxKey) -> Result<Vec<u8>, String> {
use aes_gcm::aead::{AeadInPlace, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce, Tag};
let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
.map_err(|err| format!("invalid mailbox key: {err}"))?;
let mut decrypted = self.data.clone();
cipher
.decrypt_in_place_detached(
Nonce::from_slice(&self.nonce.bytes),
MAILBOX_AEAD_AAD,
&mut decrypted,
Tag::from_slice(&self.tag),
)
.map_err(|_| "mailbox authentication failed".to_string())?;
Ok(decrypted)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mailbox_key_generation() {
let key = MailboxKey::generate();
assert_eq!(key.as_bytes().len(), 32);
}
#[test]
fn mailbox_key_debug_redacts_key_material() {
let key = MailboxKey::from_bytes([0xab; 32]);
let debug = format!("{key:?}");
assert!(debug.contains("[redacted]"));
assert!(!debug.contains("abababab"));
}
#[test]
fn test_encryption_roundtrip() {
let key = MailboxKey::generate();
let data = b"test data";
let encrypted = EncryptedChunk::encrypt(data, &key).unwrap();
let decrypted = encrypted.decrypt(&key).unwrap();
assert_eq!(data.to_vec(), decrypted);
}
#[test]
fn test_chunk_nonce_generation() {
let nonce = ChunkNonce::generate().unwrap();
assert_eq!(nonce.bytes.len(), 12);
}
}