use aes_gcm::aead::{Aead, Payload};
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use rand::rngs::OsRng;
use rand::RngCore;
use zeroize::Zeroizing;
use crate::persist::RecordKey;
use crate::store::{RecordCodec, StoreError};
const FORMAT_VERSION: u8 = 1;
const NONCE_LEN: usize = 12;
struct SealedBlob {
nonce: [u8; NONCE_LEN],
ciphertext: Vec<u8>,
}
fn seal_bytes(key: &[u8; 32], plaintext: &[u8], aad: &[u8]) -> SealedBlob {
let cipher = Aes256Gcm::new_from_slice(key)
.expect("Aes256Gcm::new_from_slice cannot fail for a 32-byte key");
let mut nonce_bytes = [0u8; NONCE_LEN];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, Payload { msg: plaintext, aad })
.expect("AES-256-GCM encryption cannot fail for a valid key and a fresh nonce");
SealedBlob { nonce: nonce_bytes, ciphertext }
}
fn open_bytes(key: &[u8; 32], nonce: &[u8; NONCE_LEN], ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>, ()> {
let cipher = Aes256Gcm::new_from_slice(key)
.expect("Aes256Gcm::new_from_slice cannot fail for a 32-byte key");
cipher
.decrypt(Nonce::from_slice(nonce), Payload { msg: ciphertext, aad })
.map_err(|_| ())
}
pub struct SealedRecordCodec {
key: Zeroizing<[u8; 32]>,
}
impl SealedRecordCodec {
pub fn new(key: Zeroizing<[u8; 32]>) -> Self {
Self { key }
}
}
impl RecordCodec for SealedRecordCodec {
fn seal(&self, key: &RecordKey, plaintext: &[u8]) -> Vec<u8> {
let sealed = seal_bytes(&self.key, plaintext, key.as_str().as_bytes());
let mut out = Vec::with_capacity(1 + NONCE_LEN + sealed.ciphertext.len());
out.push(FORMAT_VERSION);
out.extend_from_slice(&sealed.nonce);
out.extend_from_slice(&sealed.ciphertext);
out
}
fn open(&self, key: &RecordKey, sealed: &[u8]) -> Result<Vec<u8>, StoreError> {
let reject = || StoreError::CodecOpen { key: key.clone() };
let (&version, rest) = sealed.split_first().ok_or_else(reject)?;
if version != FORMAT_VERSION {
return Err(reject());
}
if rest.len() < NONCE_LEN {
return Err(reject());
}
let (nonce_bytes, ciphertext) = rest.split_at(NONCE_LEN);
let mut nonce = [0u8; NONCE_LEN];
nonce.copy_from_slice(nonce_bytes);
open_bytes(&self.key, &nonce, ciphertext, key.as_str().as_bytes()).map_err(|_| reject())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn codec(key_byte: u8) -> SealedRecordCodec {
SealedRecordCodec::new(Zeroizing::new([key_byte; 32]))
}
#[test]
fn sealed_record_round_trips() {
let codec = codec(1);
let key = RecordKey::new("messenger/DEV1/account");
let sealed = codec.seal(&key, b"pickled-account-bytes");
let opened = codec.open(&key, &sealed).expect("opens under the same key and record key");
assert_eq!(opened, b"pickled-account-bytes");
}
#[test]
fn wrong_key_fails_to_open() {
let key = RecordKey::new("messenger/DEV1/account");
let sealed = codec(1).seal(&key, b"secret");
assert!(matches!(codec(2).open(&key, &sealed), Err(StoreError::CodecOpen { .. })));
}
#[test]
fn record_moved_under_another_key_fails_to_open() {
let codec = codec(1);
let original_key = RecordKey::new("messenger/DEV1/account");
let other_key = RecordKey::new("messenger/DEV1/backup_key");
let sealed = codec.seal(&original_key, b"secret");
match codec.open(&other_key, &sealed) {
Err(StoreError::CodecOpen { key }) => assert_eq!(key, other_key),
other => panic!("expected CodecOpen naming {other_key}, got {other:?}"),
}
}
#[test]
fn tampered_ciphertext_fails() {
let codec = codec(1);
let key = RecordKey::new("messenger/DEV1/account");
let mut sealed = codec.seal(&key, b"secret");
let last = sealed.len() - 1;
sealed[last] ^= 0xFF;
assert!(matches!(codec.open(&key, &sealed), Err(StoreError::CodecOpen { .. })));
}
#[test]
fn nonces_are_unique_across_many_seals() {
let codec = codec(1);
let key = RecordKey::new("messenger/DEV1/account");
let mut nonces = HashSet::new();
for _ in 0..256 {
let sealed = codec.seal(&key, b"same plaintext every time");
let nonce = sealed[1..1 + NONCE_LEN].to_vec();
assert!(nonces.insert(nonce), "a repeated nonce under the same key is an AES-GCM confidentiality break");
}
}
#[test]
fn unknown_version_is_refused_naming_the_key() {
let codec = codec(1);
let key = RecordKey::new("messenger/DEV1/account");
let mut sealed = codec.seal(&key, b"secret");
sealed[0] = 0xFF;
match codec.open(&key, &sealed) {
Err(StoreError::CodecOpen { key: got }) => assert_eq!(got, key),
other => panic!("expected CodecOpen naming {key}, got {other:?}"),
}
}
}