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> {
for index in 0..self.entry_count() {
match self.decrypt_at(index, encryptor, ciphertext, tenant_id, aad) {
Err(EncryptionError::AuthenticationFailed) => continue,
other => return other,
}
}
Err(EncryptionError::AuthenticationFailed)
}
}
fn derive_encryption_key(master: &[u8], tenant_id: &str) -> Result<[u8; 32], EncryptionError> {
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::*;
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_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>();
}
}