use std::fmt;
pub mod local;
pub use local::{LocalKeyProvider, LocalKeyProviderError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyPurpose {
TenantDataEncryption,
BackupBlobEncryption,
ClusterTls,
AuditSigning,
}
impl KeyPurpose {
pub const fn as_str(self) -> &'static str {
match self {
KeyPurpose::TenantDataEncryption => "tenant_data_encryption",
KeyPurpose::BackupBlobEncryption => "backup_blob_encryption",
KeyPurpose::ClusterTls => "cluster_tls",
KeyPurpose::AuditSigning => "audit_signing",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeyHandle {
pub tenant_id: String,
pub purpose: KeyPurpose,
pub version: u32,
}
pub struct KeyMaterial {
bytes: Vec<u8>,
handle: KeyHandle,
}
impl KeyMaterial {
pub fn new(handle: KeyHandle, bytes: Vec<u8>) -> Self {
Self { bytes, handle }
}
pub fn handle(&self) -> &KeyHandle {
&self.handle
}
pub fn version(&self) -> u32 {
self.handle.version
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
impl Drop for KeyMaterial {
fn drop(&mut self) {
for b in self.bytes.iter_mut() {
unsafe { std::ptr::write_volatile(b, 0u8) };
}
}
}
impl fmt::Debug for KeyMaterial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeyMaterial")
.field("handle", &self.handle)
.field("len", &self.bytes.len())
.finish_non_exhaustive()
}
}
#[derive(Debug, thiserror::Error)]
pub enum KeyError {
#[error("no key for tenant `{tenant_id}` purpose `{purpose}`")]
NotFound {
tenant_id: String,
purpose: &'static str,
},
#[error("no version {version} for tenant `{tenant_id}` purpose `{purpose}`")]
UnknownVersion {
tenant_id: String,
purpose: &'static str,
version: u32,
},
#[error("key backend unavailable: {0}")]
Backend(String),
#[error("operation requires HSM-aware path: {0}")]
HsmRequired(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
}
#[async_trait::async_trait]
pub trait KeyProvider: Send + Sync {
async fn get_key(&self, tenant_id: &str, purpose: KeyPurpose) -> Result<KeyMaterial, KeyError>;
async fn get_key_version(
&self,
tenant_id: &str,
purpose: KeyPurpose,
version: u32,
) -> Result<KeyMaterial, KeyError>;
async fn rotate_key(&self, tenant_id: &str, purpose: KeyPurpose)
-> Result<KeyHandle, KeyError>;
async fn destroy(&self, tenant_id: &str, purpose: KeyPurpose) -> Result<bool, KeyError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_purpose_strings_pinned() {
assert_eq!(
KeyPurpose::TenantDataEncryption.as_str(),
"tenant_data_encryption"
);
assert_eq!(
KeyPurpose::BackupBlobEncryption.as_str(),
"backup_blob_encryption"
);
assert_eq!(KeyPurpose::ClusterTls.as_str(), "cluster_tls");
assert_eq!(KeyPurpose::AuditSigning.as_str(), "audit_signing");
}
#[test]
fn key_handle_equality_includes_version() {
let h1 = KeyHandle {
tenant_id: "t".into(),
purpose: KeyPurpose::TenantDataEncryption,
version: 1,
};
let h2 = KeyHandle {
version: 2,
..h1.clone()
};
assert_ne!(h1, h2);
}
#[test]
fn key_material_debug_does_not_leak_bytes() {
let h = KeyHandle {
tenant_id: "t".into(),
purpose: KeyPurpose::TenantDataEncryption,
version: 1,
};
let m = KeyMaterial::new(h, vec![0xde, 0xad, 0xbe, 0xef]);
let dbg = format!("{:?}", m);
assert!(!dbg.contains("0xde"));
assert!(!dbg.contains("222")); assert!(!dbg.contains("deadbeef"));
assert!(dbg.contains("len"));
}
#[test]
fn key_material_drop_zeroizes() {
let h = KeyHandle {
tenant_id: "t".into(),
purpose: KeyPurpose::TenantDataEncryption,
version: 1,
};
let m = KeyMaterial::new(h, vec![0xaa, 0xbb, 0xcc]);
let raw = m.as_bytes().as_ptr();
drop(m);
let _ = raw;
}
#[test]
fn key_material_len_matches_input() {
let h = KeyHandle {
tenant_id: "t".into(),
purpose: KeyPurpose::TenantDataEncryption,
version: 1,
};
let m = KeyMaterial::new(h, vec![1, 2, 3, 4, 5]);
assert_eq!(m.len(), 5);
assert!(!m.is_empty());
}
}