use std::sync::atomic::{AtomicU64, Ordering};
use zeroize::Zeroizing;
use cachekit_core::{derive_domain_key, KeyDomain, Keyring, TenantKeyring, ZeroKnowledgeEncryptor};
use crate::error::CachekitError;
const AAD_VERSION: u8 = 0x03;
const RAW_KEY_LEN: usize = 32;
pub struct EncryptionLayer {
encryptor: ZeroKnowledgeEncryptor,
derived_key: Zeroizing<[u8; 32]>,
keyring: TenantKeyring,
tenant_id: String,
previous_key_hits: Vec<AtomicU64>,
}
impl EncryptionLayer {
pub fn new(master_key_bytes: &[u8], tenant_id: &str) -> Result<Self, CachekitError> {
Self::with_previous_keys(master_key_bytes, &[], tenant_id)
}
pub fn with_previous_keys(
master_key_bytes: &[u8],
previous_keys: &[&[u8]],
tenant_id: &str,
) -> Result<Self, CachekitError> {
for (i, key) in previous_keys.iter().enumerate() {
if key.len() != RAW_KEY_LEN {
return Err(CachekitError::Config(format!(
"previous master key {i} must be exactly {RAW_KEY_LEN} bytes; got {}",
key.len()
)));
}
}
if master_key_bytes.len() != RAW_KEY_LEN {
return Err(CachekitError::Config(format!(
"master key must be exactly {RAW_KEY_LEN} bytes; got {} \
(pass hex-encoded keys to `.encryption()`, not their ASCII bytes)",
master_key_bytes.len()
)));
}
Self::from_hex_decoded_keys(master_key_bytes, previous_keys, tenant_id)
}
pub(crate) fn from_hex_decoded_keys(
master_key_bytes: &[u8],
previous_keys: &[&[u8]],
tenant_id: &str,
) -> Result<Self, CachekitError> {
for (i, key) in previous_keys.iter().enumerate() {
if key.len() < 32 {
return Err(CachekitError::Config(format!(
"previous master key {i} must be at least 32 bytes; got {}",
key.len()
)));
}
}
if master_key_bytes.len() < 32 {
return Err(CachekitError::Config(format!(
"master key must be at least 32 bytes; got {}",
master_key_bytes.len()
)));
}
if tenant_id.is_empty() {
return Err(CachekitError::Config(
"tenant_id must not be empty".to_owned(),
));
}
if tenant_id.len() > 255 {
return Err(CachekitError::Config(format!(
"tenant_id must be at most 255 bytes; got {}",
tenant_id.len()
)));
}
let derived_key = Zeroizing::new(
derive_domain_key(
master_key_bytes,
KeyDomain::Encryption.as_str(),
tenant_id.as_bytes(),
)
.map_err(|e| CachekitError::Config(format!("key derivation failed: {e}")))?,
);
let encryptor = ZeroKnowledgeEncryptor::new()
.map_err(|e| CachekitError::Encryption(format!("encryptor init failed: {e}")))?;
Ok(Self {
encryptor,
derived_key,
keyring: tenant_keyring(master_key_bytes, previous_keys, tenant_id)?,
tenant_id: tenant_id.to_owned(),
previous_key_hits: previous_keys.iter().map(|_| AtomicU64::new(0)).collect(),
})
}
pub fn encrypt(&self, plaintext: &[u8], cache_key: &str) -> Result<Vec<u8>, CachekitError> {
let aad = self.build_aad(cache_key, false);
self.encryptor
.encrypt_aes_gcm(plaintext, &*self.derived_key, &aad)
.map_err(|e| CachekitError::Encryption(format!("encrypt failed: {e}")))
}
pub fn decrypt(&self, ciphertext: &[u8], cache_key: &str) -> Result<Vec<u8>, CachekitError> {
let aad = self.build_aad(cache_key, false);
let (plaintext, index) = self
.keyring
.decrypt_indexed(&self.encryptor, ciphertext, &aad)
.map_err(|e| match e {
cachekit_core::EncryptionError::KeyDerivation(_)
| cachekit_core::EncryptionError::KeyringIndexOutOfRange { .. } => {
CachekitError::Config(format!("keyring decrypt misconfiguration: {e}"))
}
_ => CachekitError::Encryption(format!("decrypt failed: {e}")),
})?;
debug_assert!(
index <= self.previous_key_hits.len(),
"keyring index {index} beyond previous-key slots"
);
if let Some(hits) = index
.checked_sub(1)
.and_then(|i| self.previous_key_hits.get(i))
{
hits.fetch_add(1, Ordering::Relaxed);
}
Ok(plaintext)
}
pub fn previous_key_hits(&self) -> Vec<u64> {
self.previous_key_hits
.iter()
.map(|h| h.load(Ordering::Relaxed))
.collect()
}
pub fn hardware_acceleration_enabled(&self) -> bool {
self.encryptor.hardware_acceleration_enabled()
}
pub fn tenant_id(&self) -> &str {
&self.tenant_id
}
pub fn build_aad(&self, cache_key: &str, compressed: bool) -> Vec<u8> {
let format_str = b"msgpack";
let compressed_str = if compressed {
b"True" as &[u8]
} else {
b"False"
};
let tenant_bytes = self.tenant_id.as_bytes();
let key_bytes = cache_key.as_bytes();
let capacity =
1 + 16 + tenant_bytes.len() + key_bytes.len() + format_str.len() + compressed_str.len();
let mut aad = Vec::with_capacity(capacity);
aad.push(AAD_VERSION);
aad.extend_from_slice(&len_u32(tenant_bytes.len()).to_be_bytes());
aad.extend_from_slice(tenant_bytes);
aad.extend_from_slice(&len_u32(key_bytes.len()).to_be_bytes());
aad.extend_from_slice(key_bytes);
aad.extend_from_slice(&len_u32(format_str.len()).to_be_bytes());
aad.extend_from_slice(format_str);
aad.extend_from_slice(&len_u32(compressed_str.len()).to_be_bytes());
aad.extend_from_slice(compressed_str);
aad
}
}
fn tenant_keyring(
master_key_bytes: &[u8],
previous_keys: &[&[u8]],
tenant_id: &str,
) -> Result<TenantKeyring, CachekitError> {
Keyring::new(master_key_bytes, previous_keys)
.and_then(|keyring| keyring.for_tenant(tenant_id))
.map_err(|e| CachekitError::Config(format!("keyring: {e}")))
}
#[allow(clippy::cast_possible_truncation)]
fn len_u32(len: usize) -> u32 {
u32::try_from(len).unwrap_or(u32::MAX)
}
impl std::fmt::Debug for EncryptionLayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EncryptionLayer")
.field("tenant_id", &self.tenant_id)
.field("derived_key", &"[REDACTED]")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_MASTER_KEY: &[u8] = b"test_master_key_32_bytes_long!!!";
const TEST_TENANT: &str = "test-tenant";
#[test]
fn roundtrip_encrypt_decrypt() {
let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
let plaintext = b"hello, zero-knowledge world";
let ciphertext = layer.encrypt(plaintext, "my:key").unwrap();
let decrypted = layer.decrypt(&ciphertext, "my:key").unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn wrong_cache_key_fails_decryption() {
let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
let ciphertext = layer.encrypt(b"secret", "key:a").unwrap();
let result = layer.decrypt(&ciphertext, "key:b");
assert!(result.is_err(), "decryption with wrong cache key must fail");
}
#[test]
fn different_tenants_produce_different_ciphertext() {
let layer_a = EncryptionLayer::new(TEST_MASTER_KEY, "tenant-a").unwrap();
let layer_b = EncryptionLayer::new(TEST_MASTER_KEY, "tenant-b").unwrap();
let ct_a = layer_a.encrypt(b"same data", "same:key").unwrap();
let ct_b = layer_b.encrypt(b"same data", "same:key").unwrap();
assert_ne!(ct_a, ct_b);
assert!(layer_b.decrypt(&ct_a, "same:key").is_err());
}
#[test]
fn master_key_too_short() {
let result = EncryptionLayer::new(b"short", "tenant");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("exactly 32 bytes"), "got: {msg}");
}
fn assert_config_err(result: &Result<EncryptionLayer, CachekitError>, what: &str) {
assert!(
matches!(result, Err(CachekitError::Config(_))),
"{what}: expected Config error, got {result:?}"
);
}
#[test]
fn new_requires_exactly_32_bytes() {
assert_config_err(&EncryptionLayer::new(&[7; 31], TEST_TENANT), "31 bytes");
assert_config_err(&EncryptionLayer::new(&[7; 33], TEST_TENANT), "33 bytes");
assert!(EncryptionLayer::new(&[7; 32], TEST_TENANT).is_ok());
}
#[test]
fn with_previous_keys_requires_exactly_32_byte_current_key() {
for len in [31, 33, 48] {
let current = vec![7u8; len];
assert_config_err(
&EncryptionLayer::with_previous_keys(¤t, &[K1], TEST_TENANT),
&format!("{len}-byte current key"),
);
}
assert!(EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).is_ok());
}
#[test]
fn with_previous_keys_requires_exactly_32_byte_previous_keys() {
for len in [31, 33] {
let previous = vec![7u8; len];
assert_config_err(
&EncryptionLayer::with_previous_keys(K2, &[K1, &previous], TEST_TENANT),
&format!("{len}-byte previous key"),
);
}
}
#[test]
fn hex_decoded_keys_accept_longer_than_32_bytes() {
let long = [7u8; 48];
let layer =
EncryptionLayer::from_hex_decoded_keys(&long, &[&long[..33]], TEST_TENANT).unwrap();
let ct = layer.encrypt(b"v", "k").unwrap();
assert_eq!(layer.decrypt(&ct, "k").unwrap(), b"v");
assert_config_err(
&EncryptionLayer::from_hex_decoded_keys(&long[..31], &[], TEST_TENANT),
"hex path keeps its 32-byte floor",
);
}
#[test]
fn aad_v03_format() {
let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
let aad = layer.build_aad("user:42", false);
assert_eq!(aad[0], 0x03);
let tenant_len = u32::from_be_bytes(aad[1..5].try_into().unwrap()) as usize;
assert_eq!(tenant_len, TEST_TENANT.len());
assert_eq!(&aad[5..5 + tenant_len], TEST_TENANT.as_bytes());
let offset = 5 + tenant_len;
let key_len = u32::from_be_bytes(aad[offset..offset + 4].try_into().unwrap()) as usize;
assert_eq!(key_len, 7); assert_eq!(&aad[offset + 4..offset + 4 + key_len], b"user:42");
let offset = offset + 4 + key_len;
let fmt_len = u32::from_be_bytes(aad[offset..offset + 4].try_into().unwrap()) as usize;
assert_eq!(&aad[offset + 4..offset + 4 + fmt_len], b"msgpack");
let offset = offset + 4 + fmt_len;
let comp_len = u32::from_be_bytes(aad[offset..offset + 4].try_into().unwrap()) as usize;
assert_eq!(&aad[offset + 4..offset + 4 + comp_len], b"False");
}
#[test]
fn aad_compressed_flag() {
let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
let aad_false = layer.build_aad("k", false);
let aad_true = layer.build_aad("k", true);
assert_ne!(aad_false, aad_true);
assert!(aad_true.ends_with(b"True"));
assert!(aad_false.ends_with(b"False"));
}
const K1: &[u8] = &[0x11; 32];
const K2: &[u8] = &[0x22; 32];
#[test]
fn previous_key_decrypts_after_rotation() {
let old_layer = EncryptionLayer::new(K1, TEST_TENANT).unwrap();
let ciphertext = old_layer.encrypt(b"pre-rotation value", "user:1").unwrap();
let rotated = EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).unwrap();
let plaintext = rotated.decrypt(&ciphertext, "user:1").unwrap();
assert_eq!(plaintext, b"pre-rotation value");
let cut_over = EncryptionLayer::with_previous_keys(K2, &[], TEST_TENANT).unwrap();
assert!(cut_over.decrypt(&ciphertext, "user:1").is_err());
}
#[test]
fn rotated_layer_still_writes_under_current_key() {
let rotated = EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).unwrap();
let ciphertext = rotated.encrypt(b"fresh write", "user:2").unwrap();
let current_only = EncryptionLayer::new(K2, TEST_TENANT).unwrap();
assert_eq!(
current_only.decrypt(&ciphertext, "user:2").unwrap(),
b"fresh write"
);
}
#[test]
fn exactly_three_previous_keys_is_accepted() {
let keys: Vec<[u8; 32]> = (1..=3).map(|i| [i; 32]).collect();
let refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
let layer = EncryptionLayer::with_previous_keys(K2, &refs, TEST_TENANT);
assert!(
layer.is_ok(),
"cap is 3: exactly three previous keys must build"
);
}
#[test]
fn more_than_three_previous_keys_is_config_error() {
let keys: Vec<[u8; 32]> = (1..=4).map(|i| [i; 32]).collect();
let refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
let result = EncryptionLayer::with_previous_keys(K2, &refs, TEST_TENANT);
assert!(
matches!(result, Err(CachekitError::Config(_))),
"cap of 3 must reject, never truncate"
);
}
#[test]
fn current_key_in_previous_list_is_config_error() {
let result = EncryptionLayer::with_previous_keys(K2, &[K1, K2], TEST_TENANT);
assert!(
matches!(result, Err(CachekitError::Config(_))),
"forward-only: current key must not be decrypt-only"
);
}
#[test]
fn short_previous_key_is_config_error() {
let short = [0x01u8; 16]; let result = EncryptionLayer::with_previous_keys(K2, &[&short], TEST_TENANT);
assert!(matches!(result, Err(CachekitError::Config(_))));
}
#[test]
fn keyring_derivation_failure_is_config_error() {
let err = tenant_keyring(K2, &[K1], "").err();
assert!(
matches!(&err, Some(CachekitError::Config(msg)) if msg.contains("Key derivation failed")),
"expected a Config error carrying core's KeyDerivation, got {err:?}"
);
}
#[test]
fn derived_keys_match_the_protocol_vectors() {
use cachekit_core::encryption::key_derivation::{derive_tenant_keys, key_fingerprint};
let doc: serde_json::Value =
serde_json::from_str(include_str!("../tests/vectors/encryption.json")).unwrap();
let text = |v: &serde_json::Value| v.as_str().unwrap().to_owned();
let key = |v: &serde_json::Value| hex::decode(v.as_str().unwrap()).unwrap();
let (main, ring) = (key(&doc["master_key_hex"]), &doc["keyring"]);
let (k1, k2) = (&ring["entries"][0], &ring["entries"][1]);
let fp = |entry: &serde_json::Value| text(&entry["derived_key_fingerprint_hex"]);
let cases = [
(
main.clone(),
vec![],
text(&doc["tenant_id"]),
vec![fp(&doc)],
),
(
main,
vec![],
text(&doc["default_tenant"]["tenant_id"]),
vec![fp(&doc["default_tenant"])],
),
(
key(&k1["master_key_hex"]),
vec![key(&k2["master_key_hex"])],
text(&ring["tenant_id"]),
vec![fp(k1), fp(k2)],
),
(
key(&k2["master_key_hex"]),
vec![key(&k1["master_key_hex"])],
text(&ring["tenant_id"]),
vec![fp(k2), fp(k1)],
),
];
for (current, previous, tenant, expected) in cases {
let previous: Vec<&[u8]> = previous.iter().map(Vec::as_slice).collect();
let layer = EncryptionLayer::with_previous_keys(¤t, &previous, &tenant).unwrap();
let entries: Vec<String> = layer
.keyring
.encryption_fingerprints()
.iter()
.map(hex::encode)
.collect();
assert_eq!(entries, expected, "keyring entries, tenant {tenant}");
assert_eq!(
hex::encode(key_fingerprint(&*layer.derived_key)),
expected[0],
"write key, tenant {tenant}"
);
assert_eq!(
*layer.derived_key,
derive_tenant_keys(¤t, &tenant)
.unwrap()
.encryption_key,
"write key against core, tenant {tenant}"
);
}
}
#[test]
fn current_key_hit_is_not_counted() {
let single = EncryptionLayer::new(K2, TEST_TENANT).unwrap();
let ct = single.encrypt(b"v", "user:3").unwrap();
single.decrypt(&ct, "user:3").unwrap();
assert!(single.previous_key_hits().is_empty());
let rotated = EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).unwrap();
let ct = rotated.encrypt(b"fresh write", "user:3").unwrap();
rotated.decrypt(&ct, "user:3").unwrap();
assert_eq!(rotated.previous_key_hits(), vec![0]);
}
#[test]
fn previous_key_hit_is_counted_at_its_position() {
const K3: &[u8] = &[0x33; 32];
let k1_ct = EncryptionLayer::new(K1, TEST_TENANT)
.unwrap()
.encrypt(b"k1 era", "user:4")
.unwrap();
let k2_ct = EncryptionLayer::new(K2, TEST_TENANT)
.unwrap()
.encrypt(b"k2 era", "user:5")
.unwrap();
let rotated = EncryptionLayer::with_previous_keys(K3, &[K2, K1], TEST_TENANT).unwrap();
assert_eq!(rotated.previous_key_hits(), vec![0, 0]);
rotated.decrypt(&k1_ct, "user:4").unwrap();
assert_eq!(rotated.previous_key_hits(), vec![0, 1], "k1 is previous[1]");
rotated.decrypt(&k2_ct, "user:5").unwrap();
rotated.decrypt(&k2_ct, "user:5").unwrap();
assert_eq!(rotated.previous_key_hits(), vec![2, 1], "k2 is previous[0]");
}
#[test]
fn failed_decrypt_is_not_a_previous_key_hit() {
let k1_ct = EncryptionLayer::new(K1, TEST_TENANT)
.unwrap()
.encrypt(b"v", "key:a")
.unwrap();
let rotated = EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).unwrap();
assert!(rotated.decrypt(&k1_ct, "key:b").is_err());
assert_eq!(rotated.previous_key_hits(), vec![0]);
}
#[test]
fn hardware_acceleration_reports_core_detection() {
let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
let core = ZeroKnowledgeEncryptor::new().unwrap();
assert_eq!(
layer.hardware_acceleration_enabled(),
core.hardware_acceleration_enabled()
);
#[cfg(target_arch = "x86_64")]
assert_eq!(
layer.hardware_acceleration_enabled(),
std::arch::is_x86_feature_detected!("aes")
);
}
#[test]
fn debug_redacts_key() {
let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
let debug = format!("{layer:?}");
assert!(debug.contains("[REDACTED]"));
assert!(!debug.contains("test_master_key"));
}
}