use zeroize::{Zeroize, ZeroizeOnDrop};
use super::core::{EncryptionError, ZeroKnowledgeEncryptor};
use super::key_derivation::{derive_domain_key, key_fingerprint};
use super::KeyDomain;
pub const MAX_DECRYPT_ONLY_KEYS: usize = 3;
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Keyring {
current: Vec<u8>,
decrypt_only: Vec<Vec<u8>>,
}
impl Keyring {
pub fn new(current: &[u8], decrypt_only: &[&[u8]]) -> Result<Self, EncryptionError> {
if decrypt_only.len() > MAX_DECRYPT_ONLY_KEYS {
return Err(EncryptionError::KeyringCapExceeded(decrypt_only.len()));
}
for key in std::iter::once(current).chain(decrypt_only.iter().copied()) {
if key.len() < 16 {
return Err(EncryptionError::InvalidMasterKeyLength(key.len()));
}
}
if decrypt_only.contains(¤t) {
return Err(EncryptionError::CurrentKeyInDecryptOnlyList);
}
Ok(Self {
current: current.to_vec(),
decrypt_only: decrypt_only.iter().map(|key| key.to_vec()).collect(),
})
}
fn entry_count(&self) -> usize {
1 + self.decrypt_only.len()
}
fn entries(&self) -> impl Iterator<Item = &[u8]> {
std::iter::once(self.current.as_slice())
.chain(self.decrypt_only.iter().map(|key| key.as_slice()))
}
pub fn encryption_fingerprints(
&self,
tenant_id: &str,
) -> Result<Vec<[u8; 16]>, EncryptionError> {
self.entries()
.map(|master| {
let mut key = derive_encryption_key(master, tenant_id)?;
let fingerprint = key_fingerprint(&key);
key.zeroize();
Ok(fingerprint)
})
.collect()
}
pub fn decrypt_at(
&self,
index: usize,
encryptor: &ZeroKnowledgeEncryptor,
ciphertext: &[u8],
tenant_id: &str,
aad: &[u8],
) -> Result<Vec<u8>, EncryptionError> {
let master = self
.entries()
.nth(index)
.ok_or(EncryptionError::KeyringIndexOutOfRange {
index,
count: self.entry_count(),
})?;
let mut key = derive_encryption_key(master, tenant_id)?;
let result = encryptor.decrypt_aes_gcm(ciphertext, &key, aad);
key.zeroize();
result
}
pub fn decrypt(
&self,
encryptor: &ZeroKnowledgeEncryptor,
ciphertext: &[u8],
tenant_id: &str,
aad: &[u8],
) -> Result<Vec<u8>, EncryptionError> {
self.decrypt_indexed(encryptor, ciphertext, tenant_id, aad)
.map(|(plaintext, _)| plaintext)
}
pub fn decrypt_indexed(
&self,
encryptor: &ZeroKnowledgeEncryptor,
ciphertext: &[u8],
tenant_id: &str,
aad: &[u8],
) -> Result<(Vec<u8>, usize), EncryptionError> {
for index in 0..self.entry_count() {
match self.decrypt_at(index, encryptor, ciphertext, tenant_id, aad) {
Ok(plaintext) => return Ok((plaintext, index)),
Err(EncryptionError::AuthenticationFailed) => continue,
Err(err) => return Err(err),
}
}
Err(EncryptionError::AuthenticationFailed)
}
pub fn for_tenant(self, tenant_id: &str) -> Result<TenantKeyring, EncryptionError> {
let mut keys: Vec<[u8; 32]> = Vec::with_capacity(self.entry_count());
for master in self.entries() {
match derive_encryption_key(master, tenant_id) {
Ok(key) => keys.push(key),
Err(err) => {
keys.zeroize();
return Err(err);
}
}
}
Ok(TenantKeyring { keys })
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct TenantKeyring {
keys: Vec<[u8; 32]>,
}
impl TenantKeyring {
pub fn encryption_fingerprints(&self) -> Vec<[u8; 16]> {
self.keys.iter().map(|key| key_fingerprint(key)).collect()
}
pub fn decrypt_at(
&self,
index: usize,
encryptor: &ZeroKnowledgeEncryptor,
ciphertext: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, EncryptionError> {
let key = self
.keys
.get(index)
.ok_or(EncryptionError::KeyringIndexOutOfRange {
index,
count: self.keys.len(),
})?;
encryptor.decrypt_aes_gcm(ciphertext, key, aad)
}
pub fn decrypt(
&self,
encryptor: &ZeroKnowledgeEncryptor,
ciphertext: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, EncryptionError> {
self.decrypt_indexed(encryptor, ciphertext, aad)
.map(|(plaintext, _)| plaintext)
}
pub fn decrypt_indexed(
&self,
encryptor: &ZeroKnowledgeEncryptor,
ciphertext: &[u8],
aad: &[u8],
) -> Result<(Vec<u8>, usize), EncryptionError> {
for index in 0..self.keys.len() {
match self.decrypt_at(index, encryptor, ciphertext, aad) {
Ok(plaintext) => return Ok((plaintext, index)),
Err(EncryptionError::AuthenticationFailed) => continue,
Err(err) => return Err(err),
}
}
Err(EncryptionError::AuthenticationFailed)
}
}
fn derive_encryption_key(master: &[u8], tenant_id: &str) -> Result<[u8; 32], EncryptionError> {
#[cfg(all(test, not(target_arch = "wasm32")))]
tests::HKDF_DERIVATIONS.with(|count| count.set(count.get() + 1));
Ok(derive_domain_key(
master,
KeyDomain::Encryption.as_str(),
tenant_id.as_bytes(),
)?)
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::super::key_derivation::derive_tenant_keys;
use super::*;
thread_local! {
pub(super) static HKDF_DERIVATIONS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
}
fn hkdf_derivations() -> usize {
HKDF_DERIVATIONS.with(|count| count.get())
}
const K1: [u8; 32] = [0x11; 32];
const K2: [u8; 32] = [0x22; 32];
const TENANT: &str = "tenant-123";
const AAD: &[u8] = b"test_aad";
fn encrypt_under(master: &[u8], plaintext: &[u8]) -> Vec<u8> {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let key = derive_encryption_key(master, TENANT).unwrap();
encryptor.encrypt_aes_gcm(plaintext, &key, AAD).unwrap()
}
#[test]
fn test_previous_key_entry_decrypts_after_rotation() {
let ciphertext = encrypt_under(&K1, b"secret");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
let plaintext = keyring
.decrypt(&encryptor, &ciphertext, TENANT, AAD)
.unwrap();
assert_eq!(plaintext, b"secret");
let cut_over = Keyring::new(&K2, &[]).unwrap();
let result = cut_over.decrypt(&encryptor, &ciphertext, TENANT, AAD);
assert!(matches!(result, Err(EncryptionError::AuthenticationFailed)));
}
#[test]
fn test_current_key_decrypts_first() {
let ciphertext = encrypt_under(&K2, b"fresh write");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
let plaintext = keyring
.decrypt_at(0, &encryptor, &ciphertext, TENANT, AAD)
.unwrap();
assert_eq!(plaintext, b"fresh write");
}
#[test]
fn test_cap_rejected_never_truncated() {
let a = [0x01u8; 32];
let b = [0x02u8; 32];
let c = [0x03u8; 32];
let d = [0x04u8; 32];
assert!(Keyring::new(&K2, &[&a, &b, &c]).is_ok());
let result = Keyring::new(&K2, &[&a, &b, &c, &d]);
assert!(matches!(
result,
Err(EncryptionError::KeyringCapExceeded(4))
));
}
#[test]
fn test_current_key_in_decrypt_only_list_rejected() {
let result = Keyring::new(&K2, &[&K1, &K2]);
assert!(matches!(
result,
Err(EncryptionError::CurrentKeyInDecryptOnlyList)
));
}
#[test]
fn test_short_master_key_rejected() {
let short = [0x01u8; 15];
assert!(matches!(
Keyring::new(&short, &[]),
Err(EncryptionError::InvalidMasterKeyLength(15))
));
assert!(matches!(
Keyring::new(&K2, &[&short[..]]),
Err(EncryptionError::InvalidMasterKeyLength(15))
));
}
#[test]
fn test_fingerprints_are_derived_key_fingerprints() {
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
let fingerprints = keyring.encryption_fingerprints(TENANT).unwrap();
let k2_tenant = derive_tenant_keys(&K2, TENANT).unwrap();
let k1_tenant = derive_tenant_keys(&K1, TENANT).unwrap();
assert_eq!(fingerprints.len(), 2);
assert_eq!(fingerprints[0], k2_tenant.encryption_fingerprint());
assert_eq!(fingerprints[1], k1_tenant.encryption_fingerprint());
assert_ne!(fingerprints[0], key_fingerprint(&K2));
assert_ne!(fingerprints[1], key_fingerprint(&K1));
}
#[test]
fn test_identical_aad_required_across_all_attempts() {
let ciphertext = encrypt_under(&K1, b"secret");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
let result = keyring.decrypt(&encryptor, &ciphertext, TENANT, b"different_aad");
assert!(matches!(result, Err(EncryptionError::AuthenticationFailed)));
}
#[test]
fn test_structural_error_is_terminal() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
let result = keyring.decrypt(&encryptor, b"too short", TENANT, AAD);
assert!(matches!(result, Err(EncryptionError::InvalidCiphertext(_))));
}
#[test]
fn test_decrypt_at_out_of_range() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let keyring = Keyring::new(&K2, &[]).unwrap();
let ciphertext = encrypt_under(&K2, b"x");
let result = keyring.decrypt_at(1, &encryptor, &ciphertext, TENANT, AAD);
assert!(matches!(
result,
Err(EncryptionError::KeyringIndexOutOfRange { index: 1, count: 1 })
));
}
#[test]
fn test_bad_tenant_id_is_config_error_not_miss() {
let ciphertext = encrypt_under(&K2, b"x");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
let result = keyring.decrypt(&encryptor, &ciphertext, "", AAD);
assert!(matches!(result, Err(EncryptionError::KeyDerivation(_))));
}
#[test]
fn test_decrypt_indexed_reports_winning_entry() {
let ciphertext_current = encrypt_under(&K2, b"fresh");
let ciphertext_previous = encrypt_under(&K1, b"old");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
let (plaintext, index) = keyring
.decrypt_indexed(&encryptor, &ciphertext_current, TENANT, AAD)
.unwrap();
assert_eq!((plaintext.as_slice(), index), (b"fresh".as_slice(), 0));
let (plaintext, index) = keyring
.decrypt_indexed(&encryptor, &ciphertext_previous, TENANT, AAD)
.unwrap();
assert_eq!((plaintext.as_slice(), index), (b"old".as_slice(), 1));
}
#[test]
fn test_decrypt_indexed_exhaustion_and_terminal_errors_match_decrypt() {
let ciphertext = encrypt_under(&K1, b"secret");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let cut_over = Keyring::new(&K2, &[]).unwrap();
assert!(matches!(
cut_over.decrypt_indexed(&encryptor, &ciphertext, TENANT, AAD),
Err(EncryptionError::AuthenticationFailed)
));
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
assert!(matches!(
keyring.decrypt_indexed(&encryptor, b"too short", TENANT, AAD),
Err(EncryptionError::InvalidCiphertext(_))
));
assert!(matches!(
keyring.decrypt_indexed(&encryptor, &ciphertext, "", AAD),
Err(EncryptionError::KeyDerivation(_))
));
}
#[test]
fn test_tenant_keyring_decrypt_indexed_matches_unbound() {
let ciphertext_current = encrypt_under(&K2, b"fresh");
let ciphertext_previous = encrypt_under(&K1, b"old");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let ring = Keyring::new(&K2, &[&K1])
.unwrap()
.for_tenant(TENANT)
.unwrap();
let (plaintext, index) = ring
.decrypt_indexed(&encryptor, &ciphertext_current, AAD)
.unwrap();
assert_eq!((plaintext.as_slice(), index), (b"fresh".as_slice(), 0));
let (plaintext, index) = ring
.decrypt_indexed(&encryptor, &ciphertext_previous, AAD)
.unwrap();
assert_eq!((plaintext.as_slice(), index), (b"old".as_slice(), 1));
let cut_over = Keyring::new(&K2, &[]).unwrap().for_tenant(TENANT).unwrap();
assert!(matches!(
cut_over.decrypt_indexed(&encryptor, &ciphertext_previous, AAD),
Err(EncryptionError::AuthenticationFailed)
));
assert!(matches!(
ring.decrypt_indexed(&encryptor, b"too short", AAD),
Err(EncryptionError::InvalidCiphertext(_))
));
}
#[test]
fn test_tenant_keyring_derives_exactly_once() {
let ciphertext_current = encrypt_under(&K2, b"fresh");
let ciphertext_previous = encrypt_under(&K1, b"old");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let before = hkdf_derivations();
let ring = Keyring::new(&K2, &[&K1])
.unwrap()
.for_tenant(TENANT)
.unwrap();
assert_eq!(
hkdf_derivations() - before,
2,
"construction derives exactly one key per keyring entry"
);
let at_steady_state = hkdf_derivations();
for _ in 0..10 {
ring.decrypt(&encryptor, &ciphertext_current, AAD).unwrap();
ring.decrypt(&encryptor, &ciphertext_previous, AAD).unwrap();
ring.decrypt_at(0, &encryptor, &ciphertext_current, AAD)
.unwrap();
ring.encryption_fingerprints();
}
assert_eq!(
hkdf_derivations(),
at_steady_state,
"steady-state decrypts and fingerprints perform zero HKDF derivations"
);
}
#[test]
fn test_tenant_keyring_sequential_semantics_match_keyring() {
let ciphertext = encrypt_under(&K1, b"secret");
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let ring = Keyring::new(&K2, &[&K1])
.unwrap()
.for_tenant(TENANT)
.unwrap();
assert_eq!(
ring.decrypt(&encryptor, &ciphertext, AAD).unwrap(),
b"secret"
);
let cut_over = Keyring::new(&K2, &[]).unwrap().for_tenant(TENANT).unwrap();
assert!(matches!(
cut_over.decrypt(&encryptor, &ciphertext, AAD),
Err(EncryptionError::AuthenticationFailed)
));
assert!(matches!(
ring.decrypt(&encryptor, &ciphertext, b"different_aad"),
Err(EncryptionError::AuthenticationFailed)
));
}
#[test]
fn test_tenant_keyring_structural_error_is_terminal() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let ring = Keyring::new(&K2, &[&K1])
.unwrap()
.for_tenant(TENANT)
.unwrap();
let result = ring.decrypt(&encryptor, b"too short", AAD);
assert!(matches!(result, Err(EncryptionError::InvalidCiphertext(_))));
}
#[test]
fn test_tenant_keyring_decrypt_at_out_of_range() {
let encryptor = ZeroKnowledgeEncryptor::new().unwrap();
let ring = Keyring::new(&K2, &[]).unwrap().for_tenant(TENANT).unwrap();
let ciphertext = encrypt_under(&K2, b"x");
let result = ring.decrypt_at(1, &encryptor, &ciphertext, AAD);
assert!(matches!(
result,
Err(EncryptionError::KeyringIndexOutOfRange { index: 1, count: 1 })
));
}
#[test]
fn test_tenant_keyring_bad_tenant_id_is_config_error_at_construction() {
let result = Keyring::new(&K2, &[&K1]).unwrap().for_tenant("");
assert!(matches!(result, Err(EncryptionError::KeyDerivation(_))));
}
#[test]
fn test_tenant_keyring_fingerprints_match_unbound_keyring() {
let keyring = Keyring::new(&K2, &[&K1]).unwrap();
let unbound = keyring.encryption_fingerprints(TENANT).unwrap();
let bound = keyring
.for_tenant(TENANT)
.unwrap()
.encryption_fingerprints();
assert_eq!(bound, unbound);
assert_eq!(bound.len(), 2);
assert_eq!(
bound[0],
derive_tenant_keys(&K2, TENANT)
.unwrap()
.encryption_fingerprint()
);
assert_eq!(
bound[1],
derive_tenant_keys(&K1, TENANT)
.unwrap()
.encryption_fingerprint()
);
}
#[test]
fn test_tenant_keyring_derived_keys_zeroize() {
let mut ring = Keyring::new(&K2, &[&K1])
.unwrap()
.for_tenant(TENANT)
.unwrap();
ring.zeroize();
assert!(ring.keys.is_empty());
fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
assert_zeroize_on_drop::<TenantKeyring>();
}
#[test]
fn test_all_key_material_zeroizes() {
let mut keyring = Keyring::new(&K2, &[&K1]).unwrap();
keyring.zeroize();
assert!(keyring.current.iter().all(|&b| b == 0) || keyring.current.is_empty());
assert!(keyring
.decrypt_only
.iter()
.all(|key| key.iter().all(|&b| b == 0) || key.is_empty()));
fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
assert_zeroize_on_drop::<Keyring>();
}
}