#![forbid(unsafe_code)]
mod local;
mod worker_secret;
use async_trait::async_trait;
use thiserror::Error;
use zeroize::{Zeroize, Zeroizing};
pub use local::LocalFileKms;
pub use worker_secret::WorkerSecretKms;
pub struct Dek(Zeroizing<Vec<u8>>);
pub const DEK_LEN: usize = 32;
impl Dek {
pub fn generate() -> Result<Self, KmsError> {
let mut bytes = vec![0_u8; DEK_LEN];
getrandom::fill(&mut bytes).map_err(|err| {
bytes.zeroize();
KmsError::Unavailable(format!("the OS random source failed: {err}"))
})?;
Ok(Self(Zeroizing::new(bytes)))
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, KmsError> {
if bytes.len() != DEK_LEN {
let mut bytes = bytes;
bytes.zeroize();
return Err(KmsError::Invalid(format!("a data key is {DEK_LEN} bytes")));
}
Ok(Self(Zeroizing::new(bytes)))
}
#[must_use]
pub fn expose(&self) -> &[u8] {
&self.0
}
}
impl std::fmt::Debug for Dek {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Dek([redacted])")
}
}
#[derive(Debug, Error)]
pub enum KmsError {
#[error("the KMS is unavailable: {0}")]
Unavailable(String),
#[error("the KMS denied the request: {0}")]
Denied(String),
#[error("the wrapped key failed authentication: {0}")]
Tampered(String),
#[error("invalid input: {0}")]
Invalid(String),
#[error("{0}")]
Refused(String),
}
impl KmsError {
#[must_use]
pub fn is_retryable(&self) -> bool {
matches!(self, KmsError::Unavailable(_))
}
}
#[async_trait]
pub trait Kms: Send + Sync {
fn provider(&self) -> &'static str;
fn key_ref(&self) -> &str;
async fn wrap(&self, dek: &Dek) -> Result<Vec<u8>, KmsError>;
async fn unwrap(&self, wrapped: &[u8]) -> Result<Dek, KmsError>;
}
pub async fn conformance(kms: &dyn Kms) {
assert!(
!kms.provider().is_empty(),
"a provider name is stored with every wrapped key; it cannot be empty"
);
assert!(!kms.key_ref().is_empty(), "a key reference cannot be empty");
let dek = Dek::generate().expect("the OS RNG answers");
let wrapped = kms.wrap(&dek).await.expect("wrap succeeds");
let recovered = kms.unwrap(&wrapped).await.expect("unwrap succeeds");
assert_eq!(
dek.expose(),
recovered.expose(),
"unwrap must return the key that was wrapped"
);
let again = kms.wrap(&dek).await.expect("wrap succeeds twice");
assert_ne!(
wrapped, again,
"each wrap uses a fresh nonce, so a wrapped key is not a stable identifier"
);
let recovered = kms.unwrap(&again).await.expect("the second wrap unwraps");
assert_eq!(dek.expose(), recovered.expose());
for index in 0..wrapped.len() {
let mut altered = wrapped.clone();
altered[index] ^= 0b0000_0001;
match kms.unwrap(&altered).await {
Err(KmsError::Tampered(_)) => {}
Err(other) => panic!("a flipped bit at {index} must be Tampered, got {other}"),
Ok(_) => panic!("a flipped bit at {index} was accepted"),
}
}
for (what, blob) in [
("empty", Vec::new()),
("truncated", wrapped[..wrapped.len() / 2].to_vec()),
("one byte", vec![0]),
] {
match kms.unwrap(&blob).await {
Err(KmsError::Tampered(_) | KmsError::Invalid(_)) => {}
Err(other) => panic!("a {what} blob must be refused cleanly, got {other}"),
Ok(_) => panic!("a {what} blob was accepted"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dek_does_not_print_its_material() {
let dek = Dek::from_bytes(vec![7; DEK_LEN]).expect("32 bytes");
let shown = format!("{dek:?}");
assert_eq!(shown, "Dek([redacted])");
assert!(!shown.contains('7'), "{shown}");
}
#[test]
fn a_dek_is_exactly_32_bytes() {
assert!(Dek::from_bytes(vec![0; 31]).is_err());
assert!(Dek::from_bytes(vec![0; 33]).is_err());
assert!(Dek::from_bytes(vec![0; DEK_LEN]).is_ok());
assert_eq!(Dek::generate().expect("rng").expose().len(), DEK_LEN);
}
#[test]
fn only_unavailable_is_retryable() {
assert!(KmsError::Unavailable("x".into()).is_retryable());
for error in [
KmsError::Denied("x".into()),
KmsError::Tampered("x".into()),
KmsError::Invalid("x".into()),
KmsError::Refused("x".into()),
] {
assert!(!error.is_retryable(), "{error} must not be retried");
}
}
}