use ring::aead::{self, AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
use crate::error::{DbError, DbResult};
const HKDF_INFO: &[u8] = b"armdb-txlog-v1";
pub(crate) struct RecordCipher {
key: LessSafeKey,
}
impl RecordCipher {
pub(crate) fn derive(master: &[u8; 32], salt: &[u8; 16]) -> DbResult<Self> {
use ring::hkdf::{HKDF_SHA256, Salt};
let prk = Salt::new(HKDF_SHA256, salt).extract(master);
let info: [&[u8]; 1] = [HKDF_INFO];
let okm = prk
.expand(&info, HKDF_SHA256)
.map_err(|_| DbError::EncryptionError("txlog hkdf expand failed".into()))?;
let mut key = [0u8; 32];
okm.fill(&mut key)
.map_err(|_| DbError::EncryptionError("txlog hkdf fill failed".into()))?;
let unbound = UnboundKey::new(&AES_256_GCM, &key)
.map_err(|_| DbError::EncryptionError("invalid txlog key".into()))?;
Ok(Self {
key: LessSafeKey::new(unbound),
})
}
pub(crate) fn seal(&self, tx_id: u64, aad: &[u8], plaintext: &[u8]) -> DbResult<Vec<u8>> {
let mut buf = plaintext.to_vec();
let tag = self
.key
.seal_in_place_separate_tag(nonce_for(tx_id), Aad::from(aad), &mut buf)
.map_err(|_| DbError::EncryptionError("txlog record encryption failed".into()))?;
buf.extend_from_slice(tag.as_ref());
Ok(buf)
}
pub(crate) fn open(&self, tx_id: u64, aad: &[u8], sealed: &[u8]) -> DbResult<Vec<u8>> {
let mut buf = sealed.to_vec();
let plaintext = self
.key
.open_in_place(nonce_for(tx_id), Aad::from(aad), &mut buf)
.map_err(|_| {
DbError::EncryptionError(format!(
"txlog record {tx_id}: decryption/authentication failed"
))
})?;
Ok(plaintext.to_vec())
}
}
fn nonce_for(tx_id: u64) -> Nonce {
let mut bytes = [0u8; aead::NONCE_LEN];
bytes[..8].copy_from_slice(&tx_id.to_le_bytes());
Nonce::assume_unique_for_key(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn txlog_cipher_round_trips_under_the_same_tx_id() {
let c = RecordCipher::derive(&[0x11; 32], &[0x22; 16]).unwrap();
let sealed = c.seal(7, b"header", b"payload bytes").unwrap();
assert_ne!(sealed, b"payload bytes");
assert_eq!(c.open(7, b"header", &sealed).unwrap(), b"payload bytes");
}
#[test]
fn txlog_cipher_rejects_a_different_tx_id_key_or_aad() {
let c = RecordCipher::derive(&[0x11; 32], &[0x22; 16]).unwrap();
let sealed = c.seal(7, b"header", b"payload").unwrap();
assert!(c.open(8, b"header", &sealed).is_err(), "nonce is the tx_id");
assert!(
c.open(7, b"other!", &sealed).is_err(),
"header is authenticated"
);
let other_key = RecordCipher::derive(&[0x33; 32], &[0x22; 16]).unwrap();
assert!(other_key.open(7, b"header", &sealed).is_err());
let other_salt = RecordCipher::derive(&[0x11; 32], &[0x44; 16]).unwrap();
assert!(other_salt.open(7, b"header", &sealed).is_err());
}
#[test]
fn txlog_cipher_is_deterministic_for_one_tx_id() {
let c = RecordCipher::derive(&[0x11; 32], &[0x22; 16]).unwrap();
assert_eq!(
c.seal(9, b"h", b"p").unwrap(),
c.seal(9, b"h", b"p").unwrap()
);
}
#[test]
fn txlog_cipher_detects_tampering() {
let c = RecordCipher::derive(&[0x11; 32], &[0x22; 16]).unwrap();
let mut sealed = c.seal(1, b"h", b"payload").unwrap();
sealed[0] ^= 0xFF;
assert!(c.open(1, b"h", &sealed).is_err());
}
}