use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use rand::rngs::OsRng;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::Zeroizing;
use crate::error::StoreError;
pub const KEY_LEN: usize = 32;
pub const NONCE_LEN: usize = 12;
pub const TAG_LEN: usize = 16;
#[derive(Debug, Error)]
pub enum CryptoError {
#[error("vault is cold; unlock required")]
Cold,
#[error("AEAD failure: {0}")]
Aead(String),
#[error("unwrapped DEK has wrong length: expected {KEY_LEN}, got {0}")]
BadDekLength(usize),
#[error("sealed row too short: {0} bytes")]
ShortRow(usize),
}
impl From<CryptoError> for StoreError {
fn from(e: CryptoError) -> Self {
match e {
CryptoError::Cold => StoreError::Cold,
other => StoreError::Crypto(other.to_string()),
}
}
}
pub trait KeyProvider: Send + Sync {
fn vault_key(&self) -> Result<Zeroizing<[u8; KEY_LEN]>, CryptoError>;
}
#[derive(Clone)]
pub struct StaticKeyProvider {
key: Option<[u8; KEY_LEN]>,
}
impl StaticKeyProvider {
pub fn new(key: [u8; KEY_LEN]) -> Self {
Self { key: Some(key) }
}
pub fn cold() -> Self {
Self { key: None }
}
}
impl KeyProvider for StaticKeyProvider {
fn vault_key(&self) -> Result<Zeroizing<[u8; KEY_LEN]>, CryptoError> {
self.key.map(Zeroizing::new).ok_or(CryptoError::Cold)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WrappedDek {
pub wrap_nonce: [u8; NONCE_LEN],
pub wrap_ciphertext: Vec<u8>,
}
fn cipher_for(key: &[u8; KEY_LEN]) -> Aes256Gcm {
Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_slice()))
}
pub fn wrap_fresh_dek(
vault_key: &[u8; KEY_LEN],
collection_id: &str,
) -> Result<(WrappedDek, Zeroizing<[u8; KEY_LEN]>), CryptoError> {
let mut dek = Zeroizing::new([0u8; KEY_LEN]);
OsRng.fill_bytes(dek.as_mut());
let mut nonce = [0u8; NONCE_LEN];
OsRng.fill_bytes(&mut nonce);
let ciphertext = cipher_for(vault_key)
.encrypt(
Nonce::from_slice(&nonce),
Payload {
msg: dek.as_slice(),
aad: collection_id.as_bytes(),
},
)
.map_err(|e| CryptoError::Aead(format!("wrap: {e}")))?;
Ok((
WrappedDek {
wrap_nonce: nonce,
wrap_ciphertext: ciphertext,
},
dek,
))
}
pub fn unwrap_dek(
vault_key: &[u8; KEY_LEN],
collection_id: &str,
wrapped: &WrappedDek,
) -> Result<Zeroizing<[u8; KEY_LEN]>, CryptoError> {
let plaintext = Zeroizing::new(
cipher_for(vault_key)
.decrypt(
Nonce::from_slice(&wrapped.wrap_nonce),
Payload {
msg: &wrapped.wrap_ciphertext,
aad: collection_id.as_bytes(),
},
)
.map_err(|e| CryptoError::Aead(format!("unwrap: {e}")))?,
);
if plaintext.len() != KEY_LEN {
return Err(CryptoError::BadDekLength(plaintext.len()));
}
let mut dek = Zeroizing::new([0u8; KEY_LEN]);
dek.copy_from_slice(&plaintext);
Ok(dek)
}
pub fn rewrap_dek(
old_vault_key: &[u8; KEY_LEN],
new_vault_key: &[u8; KEY_LEN],
collection_id: &str,
wrapped: &WrappedDek,
) -> Result<WrappedDek, CryptoError> {
let dek = unwrap_dek(old_vault_key, collection_id, wrapped)?;
let mut nonce = [0u8; NONCE_LEN];
OsRng.fill_bytes(&mut nonce);
let ciphertext = cipher_for(new_vault_key)
.encrypt(
Nonce::from_slice(&nonce),
Payload {
msg: dek.as_slice(),
aad: collection_id.as_bytes(),
},
)
.map_err(|e| CryptoError::Aead(format!("rewrap: {e}")))?;
Ok(WrappedDek {
wrap_nonce: nonce,
wrap_ciphertext: ciphertext,
})
}
fn row_aad(table: &str, key: &[u8], schema_version: u32) -> Vec<u8> {
let mut aad = Vec::with_capacity(4 + table.len() + 4 + key.len() + 4);
aad.extend_from_slice(&(table.len() as u32).to_be_bytes());
aad.extend_from_slice(table.as_bytes());
aad.extend_from_slice(&(key.len() as u32).to_be_bytes());
aad.extend_from_slice(key);
aad.extend_from_slice(&schema_version.to_be_bytes());
aad
}
pub fn seal_row(
dek: &[u8; KEY_LEN],
table: &str,
key: &[u8],
schema_version: u32,
plaintext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let mut nonce = [0u8; NONCE_LEN];
OsRng.fill_bytes(&mut nonce);
let aad = row_aad(table, key, schema_version);
let ciphertext = cipher_for(dek)
.encrypt(
Nonce::from_slice(&nonce),
Payload {
msg: plaintext,
aad: &aad,
},
)
.map_err(|e| CryptoError::Aead(format!("seal: {e}")))?;
let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len());
out.extend_from_slice(&nonce);
out.extend_from_slice(&ciphertext);
Ok(out)
}
pub fn open_row(
dek: &[u8; KEY_LEN],
table: &str,
key: &[u8],
schema_version: u32,
sealed: &[u8],
) -> Result<Vec<u8>, CryptoError> {
if sealed.len() < NONCE_LEN {
return Err(CryptoError::ShortRow(sealed.len()));
}
let (nonce, ciphertext) = sealed.split_at(NONCE_LEN);
let aad = row_aad(table, key, schema_version);
cipher_for(dek)
.decrypt(
Nonce::from_slice(nonce),
Payload {
msg: ciphertext,
aad: &aad,
},
)
.map_err(|e| CryptoError::Aead(format!("open: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
const VK_A: [u8; KEY_LEN] = [0xAA; KEY_LEN];
const VK_B: [u8; KEY_LEN] = [0xBB; KEY_LEN];
#[test]
fn wrap_then_unwrap_yields_same_dek() {
let (w, dek) = wrap_fresh_dek(&VK_A, "people").unwrap();
let dek2 = unwrap_dek(&VK_A, "people", &w).unwrap();
assert_eq!(dek.as_slice(), dek2.as_slice());
}
#[test]
fn fresh_dek_is_not_all_zero() {
let (_w, dek) = wrap_fresh_dek(&VK_A, "c").unwrap();
assert!(dek.iter().any(|b| *b != 0));
}
#[test]
fn different_collections_get_different_deks() {
let (_w1, d1) = wrap_fresh_dek(&VK_A, "c1").unwrap();
let (_w2, d2) = wrap_fresh_dek(&VK_A, "c2").unwrap();
assert_ne!(d1.as_slice(), d2.as_slice());
}
#[test]
fn unwrap_with_wrong_vault_key_fails() {
let (w, _dek) = wrap_fresh_dek(&VK_A, "c").unwrap();
let err = unwrap_dek(&VK_B, "c", &w).unwrap_err();
assert!(matches!(err, CryptoError::Aead(_)));
}
#[test]
fn unwrap_with_wrong_collection_id_fails_on_aad() {
let (w, _dek) = wrap_fresh_dek(&VK_A, "people").unwrap();
let err = unwrap_dek(&VK_A, "passwords", &w).unwrap_err();
assert!(matches!(err, CryptoError::Aead(_)));
}
#[test]
fn tampered_wrap_ciphertext_fails() {
let (mut w, _dek) = wrap_fresh_dek(&VK_A, "c").unwrap();
w.wrap_ciphertext[0] ^= 0x01;
let err = unwrap_dek(&VK_A, "c", &w).unwrap_err();
assert!(matches!(err, CryptoError::Aead(_)));
}
#[test]
fn rewrap_rotates_vault_key_without_changing_the_dek() {
let (w_a, dek_a) = wrap_fresh_dek(&VK_A, "c").unwrap();
let w_b = rewrap_dek(&VK_A, &VK_B, "c", &w_a).unwrap();
assert!(unwrap_dek(&VK_A, "c", &w_b).is_err());
let dek_b = unwrap_dek(&VK_B, "c", &w_b).unwrap();
assert_eq!(dek_a.as_slice(), dek_b.as_slice());
}
#[test]
fn seal_then_open_round_trips() {
let dek = [0x11; KEY_LEN];
let sealed = seal_row(&dek, "people", b"alice", 1, b"payload").unwrap();
assert!(!sealed
.windows(b"payload".len())
.any(|w| w == b"payload"), "engine bytes must be ciphertext");
let opened = open_row(&dek, "people", b"alice", 1, &sealed).unwrap();
assert_eq!(opened, b"payload");
}
#[test]
fn row_with_wrong_dek_fails() {
let sealed = seal_row(&[0x11; KEY_LEN], "t", b"k", 1, b"v").unwrap();
let err = open_row(&[0x22; KEY_LEN], "t", b"k", 1, &sealed).unwrap_err();
assert!(matches!(err, CryptoError::Aead(_)));
}
#[test]
fn row_relocated_to_different_key_fails_on_aad() {
let dek = [0x11; KEY_LEN];
let sealed = seal_row(&dek, "t", b"key1", 1, b"v").unwrap();
assert!(open_row(&dek, "t", b"key2", 1, &sealed).is_err());
}
#[test]
fn row_relocated_to_different_table_fails_on_aad() {
let dek = [0x11; KEY_LEN];
let sealed = seal_row(&dek, "table_a", b"k", 1, b"v").unwrap();
assert!(open_row(&dek, "table_b", b"k", 1, &sealed).is_err());
}
#[test]
fn row_with_wrong_schema_version_fails_on_aad() {
let dek = [0x11; KEY_LEN];
let sealed = seal_row(&dek, "t", b"k", 1, b"v").unwrap();
assert!(open_row(&dek, "t", b"k", 2, &sealed).is_err());
}
#[test]
fn tampered_row_fails() {
let dek = [0x11; KEY_LEN];
let mut sealed = seal_row(&dek, "t", b"k", 1, b"v").unwrap();
let last = sealed.len() - 1;
sealed[last] ^= 0x01; assert!(open_row(&dek, "t", b"k", 1, &sealed).is_err());
}
#[test]
fn open_short_row_is_typed_error() {
let err = open_row(&[0; KEY_LEN], "t", b"k", 1, &[0u8; 4]).unwrap_err();
assert!(matches!(err, CryptoError::ShortRow(4)));
}
#[test]
fn static_provider_warm_and_cold() {
assert_eq!(StaticKeyProvider::new(VK_A).vault_key().unwrap().as_slice(), &VK_A);
assert!(matches!(
StaticKeyProvider::cold().vault_key().unwrap_err(),
CryptoError::Cold
));
}
}