use crate::{AreevError, Result};
use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes256Gcm, Nonce};
use zeroize::Zeroizing;
pub(crate) const BLOB_MAGIC: &[u8; 9] = b"MGB-BLOB1";
const NONCE_LEN: usize = 12;
const HKDF_INFO: &[u8] = b"areev.blobs.v1";
pub(crate) fn derive_blob_key(page_key: &[u8; 32]) -> Zeroizing<[u8; 32]> {
let hk = hkdf::Hkdf::<sha2::Sha256>::new(None, page_key);
let mut out = Zeroizing::new([0u8; 32]);
hk.expand(HKDF_INFO, out.as_mut())
.expect("HKDF expand of 32 bytes cannot fail");
out
}
pub(crate) fn seal(key: &[u8; 32], hex_addr: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
let cipher = Aes256Gcm::new(key.into());
let mut nonce_bytes = [0u8; NONCE_LEN];
getrandom::getrandom(&mut nonce_bytes)
.map_err(|e| AreevError::CryptoError(format!("blob nonce: {e}")))?;
let nonce = Nonce::from_slice(&nonce_bytes);
let ct = cipher
.encrypt(nonce, Payload { msg: plaintext, aad: hex_addr.as_bytes() })
.map_err(|_| AreevError::CryptoError("blob encryption failed".into()))?;
let mut out = Vec::with_capacity(BLOB_MAGIC.len() + NONCE_LEN + ct.len());
out.extend_from_slice(BLOB_MAGIC);
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ct);
Ok(out)
}
pub(crate) fn is_sealed(bytes: &[u8]) -> bool {
bytes.len() >= BLOB_MAGIC.len() + NONCE_LEN && &bytes[..BLOB_MAGIC.len()] == BLOB_MAGIC
}
pub(crate) fn open(key: &[u8; 32], hex_addr: &str, sealed: &[u8]) -> Result<Vec<u8>> {
if !is_sealed(sealed) {
return Err(AreevError::CryptoError("not an encrypted blob".into()));
}
let cipher = Aes256Gcm::new(key.into());
let nonce = Nonce::from_slice(&sealed[BLOB_MAGIC.len()..BLOB_MAGIC.len() + NONCE_LEN]);
let ct = &sealed[BLOB_MAGIC.len() + NONCE_LEN..];
cipher
.decrypt(nonce, Payload { msg: ct, aad: hex_addr.as_bytes() })
.map_err(|_| {
AreevError::CryptoError(
"blob decryption failed — wrong key, or the blob was tampered with".into(),
)
})
}
pub(crate) fn read_maybe_sealed(
key: Option<&[u8; 32]>,
hex_addr: &str,
raw: Vec<u8>,
) -> Result<Vec<u8>> {
if !is_sealed(&raw) {
return Ok(raw);
}
match key {
Some(k) => open(k, hex_addr, &raw),
None => Err(AreevError::CryptoError(
"this attachment is encrypted; reopen the memory with its key".into(),
)),
}
}
pub(crate) fn write_bytes(
key: Option<&[u8; 32]>,
hex_addr: &str,
plaintext: &[u8],
) -> Result<Vec<u8>> {
match key {
Some(k) => seal(k, hex_addr, plaintext),
None => Ok(plaintext.to_vec()),
}
}
#[cfg(test)]
mod tests {
use super::*;
const ADDR: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
#[test]
fn roundtrip_and_key_separation() {
let page = [7u8; 32];
let k = derive_blob_key(&page);
assert_ne!(k.as_ref(), &page);
assert_eq!(derive_blob_key(&page).as_ref(), k.as_ref());
assert_ne!(derive_blob_key(&[8u8; 32]).as_ref(), k.as_ref());
let sealed = seal(&k, ADDR, b"secret audio bytes").unwrap();
assert!(is_sealed(&sealed));
assert!(
!sealed.windows(6).any(|w| w == b"secret"),
"plaintext must not survive in the sealed bytes"
);
assert_eq!(open(&k, ADDR, &sealed).unwrap(), b"secret audio bytes");
}
#[test]
fn wrong_key_address_or_tamper_is_refused() {
let k = derive_blob_key(&[1u8; 32]);
let other = derive_blob_key(&[2u8; 32]);
let sealed = seal(&k, ADDR, b"payload").unwrap();
assert!(open(&other, ADDR, &sealed).is_err(), "wrong key");
let moved = ADDR.replace("0123", "9999");
assert!(open(&k, &moved, &sealed).is_err(), "address is bound as AAD");
let mut tampered = sealed.clone();
let last = tampered.len() - 1;
tampered[last] ^= 0xff;
assert!(open(&k, ADDR, &tampered).is_err(), "AEAD tag catches tampering");
}
#[test]
fn plaintext_blobs_still_read_and_short_input_is_not_sealed() {
assert!(!is_sealed(b"short"));
assert!(!is_sealed(b"an ordinary plaintext attachment"));
let k = derive_blob_key(&[3u8; 32]);
let plain = b"legacy attachment".to_vec();
assert_eq!(read_maybe_sealed(Some(&k), ADDR, plain.clone()).unwrap(), plain);
let sealed = seal(&k, ADDR, b"x").unwrap();
assert!(read_maybe_sealed(None, ADDR, sealed).is_err());
}
}