use std::fmt;
use cheetah_string::CheetahString;
use zeroize::Zeroize;
use crate::SecurityContractViolation;
use crate::SecurityIdentifierRule;
use crate::SecurityProviderError;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecretProviderId(CheetahString);
impl SecretProviderId {
pub fn new(value: impl Into<CheetahString>) -> Result<Self, SecurityContractViolation> {
let value = value.into();
validate_identifier(&value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for SecretProviderId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("SecretProviderId").field(&self.0).finish()
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecretName(CheetahString);
impl SecretName {
pub fn new(value: impl Into<CheetahString>) -> Result<Self, SecurityContractViolation> {
let value = value.into();
validate_identifier(&value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for SecretName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("SecretName([REDACTED])")
}
}
fn validate_identifier(value: &str) -> Result<(), SecurityContractViolation> {
if value.is_empty() {
return Err(SecurityContractViolation::InvalidIdentifier {
rule: SecurityIdentifierRule::NotEmpty,
});
}
if value.len() > 128 {
return Err(SecurityContractViolation::InvalidIdentifier {
rule: SecurityIdentifierRule::MaximumLength,
});
}
if !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
|| value == "."
|| value == ".."
|| value.starts_with('.')
{
return Err(SecurityContractViolation::InvalidIdentifier {
rule: SecurityIdentifierRule::CanonicalCharacters,
});
}
Ok(())
}
pub struct SecretMaterial(Vec<u8>);
impl SecretMaterial {
pub fn new(value: Vec<u8>) -> Result<Self, SecurityContractViolation> {
if value.is_empty() {
return Err(SecurityContractViolation::EmptySecretMaterial);
}
Ok(Self(value))
}
pub fn expose_secret(&self) -> &[u8] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for SecretMaterial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[REDACTED]")
}
}
impl Zeroize for SecretMaterial {
fn zeroize(&mut self) {
self.0.zeroize();
}
}
impl Drop for SecretMaterial {
fn drop(&mut self) {
self.zeroize();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecretVersion(u64);
impl SecretVersion {
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn get(self) -> u64 {
self.0
}
}
pub struct VersionedSecret {
material: SecretMaterial,
version: Option<SecretVersion>,
}
impl VersionedSecret {
pub fn new(material: SecretMaterial, version: Option<SecretVersion>) -> Self {
Self { material, version }
}
pub fn material(&self) -> &SecretMaterial {
&self.material
}
pub fn into_material(self) -> SecretMaterial {
self.material
}
pub const fn version(&self) -> Option<SecretVersion> {
self.version
}
}
impl fmt::Debug for VersionedSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VersionedSecret")
.field("material", &"[REDACTED]")
.field("version", &self.version)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecretAccess {
ReadOnly,
ReadWrite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecretPersistence {
ProcessEnvironment,
EncryptedLocalStorage,
ExternalService,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecretVersioning {
Unversioned,
Optimistic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecretProviderCapabilities {
access: SecretAccess,
persistence: SecretPersistence,
versioning: SecretVersioning,
}
impl SecretProviderCapabilities {
pub const fn new(access: SecretAccess, persistence: SecretPersistence, versioning: SecretVersioning) -> Self {
Self {
access,
persistence,
versioning,
}
}
pub const fn access(self) -> SecretAccess {
self.access
}
pub const fn persistence(self) -> SecretPersistence {
self.persistence
}
pub const fn versioning(self) -> SecretVersioning {
self.versioning
}
}
pub trait SecretProvider: Send + Sync {
fn id(&self) -> &SecretProviderId;
fn capabilities(&self) -> SecretProviderCapabilities;
fn read(&self, name: &SecretName) -> Result<VersionedSecret, SecurityProviderError>;
fn write(
&self,
name: &SecretName,
material: SecretMaterial,
expected_version: Option<SecretVersion>,
) -> Result<SecretVersion, SecurityProviderError>;
}
#[cfg(test)]
mod tests {
use zeroize::Zeroize;
use super::*;
#[test]
fn identifiers_reject_traversal_and_separators() {
for value in ["", ".", "..", ".hidden", "a/b", "a\\b", "a:b", "a b"] {
assert!(SecretName::new(value).is_err(), "accepted {value:?}");
}
assert_eq!(
SecretName::new("broker-admin_key.1").unwrap().as_str(),
"broker-admin_key.1"
);
}
#[test]
fn material_and_name_debug_are_redacted() {
let material = SecretMaterial::new(b"visible-only-through-explicit-access".to_vec()).unwrap();
let name = SecretName::new("cluster-private-key").unwrap();
assert_eq!(format!("{material:?}"), "[REDACTED]");
assert_eq!(format!("{name:?}"), "SecretName([REDACTED])");
}
#[test]
fn explicit_zeroize_clears_material() {
let mut material = SecretMaterial::new(vec![7; 32]).unwrap();
material.zeroize();
assert!(material.is_empty());
}
}