use std::collections::HashMap;
use std::fmt;
use ring::aead::{Aad, CHACHA20_POLY1305, LessSafeKey, NONCE_LEN, Nonce, UnboundKey};
use ring::rand::{SecureRandom, SystemRandom};
use zeroize::Zeroizing;
pub const KEK_LEN: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CryptoError {
InvalidKek,
UnknownKekId,
SealFailed,
OpenFailed,
}
impl fmt::Display for CryptoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidKek => {
write!(f, "invalid key-encryption key: must be {KEK_LEN} raw bytes")
}
Self::UnknownKekId => {
write!(f, "no key-encryption key configured for this ciphertext")
}
Self::SealFailed => write!(f, "could not seal secret"),
Self::OpenFailed => write!(f, "could not open secret"),
}
}
}
impl std::error::Error for CryptoError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecretBinding<'a> {
pub namespace: &'a str,
pub provider: &'a str,
}
impl SecretBinding<'_> {
fn to_aad_bytes(self) -> Vec<u8> {
let ns = self.namespace.as_bytes();
let pr = self.provider.as_bytes();
let mut out = Vec::with_capacity(16 + ns.len() + pr.len());
out.extend_from_slice(&(ns.len() as u64).to_be_bytes());
out.extend_from_slice(ns);
out.extend_from_slice(&(pr.len() as u64).to_be_bytes());
out.extend_from_slice(pr);
out
}
}
#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SealedSecret {
pub kek_id: String,
#[serde(with = "hex_array")]
pub nonce: [u8; NONCE_LEN],
#[serde(with = "hex_vec")]
pub ciphertext: Vec<u8>,
}
mod hex_array {
use super::NONCE_LEN;
use serde::{Deserialize, Deserializer, Serializer};
pub(super) fn serialize<S: Serializer>(v: &[u8; NONCE_LEN], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&super::to_hex(v))
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(
d: D,
) -> Result<[u8; NONCE_LEN], D::Error> {
let raw = String::deserialize(d)?;
let bytes = super::decode_hex(&raw).map_err(serde::de::Error::custom)?;
<[u8; NONCE_LEN]>::try_from(bytes.as_slice())
.map_err(|_| serde::de::Error::custom(format!("nonce must be {NONCE_LEN} bytes")))
}
}
mod hex_vec {
use serde::{Deserialize, Deserializer, Serializer};
pub(super) fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&super::to_hex(v))
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let raw = String::deserialize(d)?;
super::decode_hex(&raw)
.map(|z| z.to_vec())
.map_err(serde::de::Error::custom)
}
}
fn to_hex(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut acc, b| {
let _ = write!(acc, "{b:02x}");
acc
})
}
impl fmt::Debug for SealedSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SealedSecret")
.field("kek_id", &self.kek_id)
.field("ciphertext_len", &self.ciphertext.len())
.finish_non_exhaustive()
}
}
pub struct KeyRing {
keys: HashMap<String, LessSafeKey>,
current: String,
rng: SystemRandom,
}
impl fmt::Debug for KeyRing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ids: Vec<&str> = self.keys.keys().map(String::as_str).collect();
ids.sort_unstable();
f.debug_struct("KeyRing")
.field("kek_ids", &ids)
.field("current", &self.current)
.finish_non_exhaustive()
}
}
impl KeyRing {
pub fn new<I>(keys: I, current: impl Into<String>) -> Result<Self, CryptoError>
where
I: IntoIterator<Item = (String, Zeroizing<Vec<u8>>)>,
{
let mut map = HashMap::new();
for (id, raw) in keys {
if raw.len() != KEK_LEN {
return Err(CryptoError::InvalidKek);
}
let unbound =
UnboundKey::new(&CHACHA20_POLY1305, &raw).map_err(|_| CryptoError::InvalidKek)?;
map.insert(id, LessSafeKey::new(unbound));
}
let current = current.into();
if !map.contains_key(¤t) {
return Err(CryptoError::InvalidKek);
}
Ok(Self {
keys: map,
current,
rng: SystemRandom::new(),
})
}
#[must_use]
pub fn current_kek_id(&self) -> &str {
&self.current
}
pub fn seal(
&self,
plaintext: &[u8],
binding: SecretBinding<'_>,
) -> Result<SealedSecret, CryptoError> {
let key = self
.keys
.get(&self.current)
.ok_or(CryptoError::UnknownKekId)?;
let mut nonce_bytes = [0u8; NONCE_LEN];
self.rng
.fill(&mut nonce_bytes)
.map_err(|_| CryptoError::SealFailed)?;
let aad = binding.to_aad_bytes();
let mut buf = plaintext.to_vec();
key.seal_in_place_append_tag(
Nonce::assume_unique_for_key(nonce_bytes),
Aad::from(&aad),
&mut buf,
)
.map_err(|_| CryptoError::SealFailed)?;
Ok(SealedSecret {
kek_id: self.current.clone(),
nonce: nonce_bytes,
ciphertext: buf,
})
}
pub fn open(
&self,
sealed: &SealedSecret,
binding: SecretBinding<'_>,
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
let key = self
.keys
.get(&sealed.kek_id)
.ok_or(CryptoError::UnknownKekId)?;
let aad = binding.to_aad_bytes();
let mut buf = sealed.ciphertext.clone();
let plaintext = key
.open_in_place(
Nonce::assume_unique_for_key(sealed.nonce),
Aad::from(&aad),
&mut buf,
)
.map_err(|_| CryptoError::OpenFailed)?;
Ok(Zeroizing::new(plaintext.to_vec()))
}
pub fn reseal(
&self,
sealed: &SealedSecret,
binding: SecretBinding<'_>,
) -> Result<SealedSecret, CryptoError> {
let plaintext = self.open(sealed, binding)?;
self.seal(&plaintext, binding)
}
}
impl KeyRing {
pub fn from_spec(spec: &str, current: &str) -> Result<Self, CryptoError> {
let mut keys: Vec<(String, Zeroizing<Vec<u8>>)> = Vec::new();
for entry in spec.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let (id, hex) = entry.split_once(':').ok_or(CryptoError::InvalidKek)?;
let id = id.trim();
if id.is_empty() || hex.contains(':') {
return Err(CryptoError::InvalidKek);
}
keys.push((id.to_string(), decode_hex(hex.trim())?));
}
if keys.is_empty() {
return Err(CryptoError::InvalidKek);
}
Self::new(keys, current)
}
}
pub const KEK_SPEC_ENV: &str = "BRAIN_EDGE_LLM_KEKS";
pub const KEK_CURRENT_ENV: &str = "BRAIN_EDGE_LLM_KEK_CURRENT";
pub fn keyring_from_env() -> Result<Option<KeyRing>, String> {
let Ok(spec) = std::env::var(KEK_SPEC_ENV) else {
return Ok(None);
};
if spec.trim().is_empty() {
return Ok(None);
}
let ids: Vec<String> = spec
.split(',')
.filter_map(|e| e.split_once(':').map(|(id, _)| id.trim().to_string()))
.filter(|id| !id.is_empty())
.collect();
let current = match std::env::var(KEK_CURRENT_ENV) {
Ok(c) if !c.trim().is_empty() => c.trim().to_string(),
_ => {
if ids.len() == 1 {
ids[0].clone()
} else {
return Err(format!(
"{KEK_CURRENT_ENV} must name which key id to seal under when \
{KEK_SPEC_ENV} carries more than one key"
));
}
}
};
KeyRing::from_spec(&spec, ¤t)
.map(Some)
.map_err(|e| format!("{KEK_SPEC_ENV}: {e}"))
}
fn decode_hex(hex: &str) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
if !hex.len().is_multiple_of(2) {
return Err(CryptoError::InvalidKek);
}
let (pairs, rest) = hex.as_bytes().as_chunks::<2>();
debug_assert!(rest.is_empty(), "length checked above");
let mut out = Zeroizing::new(Vec::with_capacity(hex.len() / 2));
for pair in pairs {
let hi = hex_nibble(pair[0]).ok_or(CryptoError::InvalidKek)?;
let lo = hex_nibble(pair[1]).ok_or(CryptoError::InvalidKek)?;
out.push((hi << 4) | lo);
}
Ok(out)
}
const fn hex_nibble(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kek(byte: u8) -> Zeroizing<Vec<u8>> {
Zeroizing::new(vec![byte; KEK_LEN])
}
fn ring_with(id: &str, byte: u8) -> KeyRing {
KeyRing::new([(id.to_string(), kek(byte))], id).expect("valid ring")
}
fn binding<'a>(ns: &'a str, provider: &'a str) -> SecretBinding<'a> {
SecretBinding {
namespace: ns,
provider,
}
}
#[test]
fn seals_and_opens_round_trip() {
let r = ring_with("k1", 7);
let b = binding("acme", "openai");
let sealed = r.seal(b"sk-secret-value", b).unwrap();
assert_eq!(&*r.open(&sealed, b).unwrap(), b"sk-secret-value");
}
#[test]
fn ciphertext_does_not_contain_the_plaintext() {
let r = ring_with("k1", 7);
let secret = b"sk-live-0123456789";
let sealed = r.seal(secret, binding("acme", "openai")).unwrap();
assert!(
!sealed.ciphertext.windows(secret.len()).any(|w| w == secret),
"plaintext leaked into ciphertext"
);
}
#[test]
fn a_ciphertext_cannot_be_replayed_under_another_namespace() {
let r = ring_with("k1", 7);
let sealed = r.seal(b"acme-key", binding("acme", "openai")).unwrap();
assert_eq!(
r.open(&sealed, binding("globex", "openai")).unwrap_err(),
CryptoError::OpenFailed
);
}
#[test]
fn a_ciphertext_cannot_be_replayed_under_another_provider() {
let r = ring_with("k1", 7);
let sealed = r.seal(b"oa-key", binding("acme", "openai")).unwrap();
assert_eq!(
r.open(&sealed, binding("acme", "anthropic")).unwrap_err(),
CryptoError::OpenFailed
);
}
#[test]
fn binding_is_unambiguous_across_separator_lookalikes() {
let r = ring_with("k1", 7);
let sealed = r.seal(b"v", binding("a:b", "c")).unwrap();
assert_eq!(
r.open(&sealed, binding("a", "b:c")).unwrap_err(),
CryptoError::OpenFailed
);
}
#[test]
fn a_tampered_ciphertext_does_not_open() {
let r = ring_with("k1", 7);
let b = binding("acme", "openai");
let mut sealed = r.seal(b"sk-secret", b).unwrap();
sealed.ciphertext[0] ^= 0xFF;
assert_eq!(r.open(&sealed, b).unwrap_err(), CryptoError::OpenFailed);
}
#[test]
fn a_wrong_key_does_not_open() {
let a = ring_with("k1", 1);
let bogus = ring_with("k1", 2); let b = binding("acme", "openai");
let sealed = a.seal(b"sk-secret", b).unwrap();
assert_eq!(bogus.open(&sealed, b).unwrap_err(), CryptoError::OpenFailed);
}
#[test]
fn nonces_differ_across_seals_of_identical_input() {
let r = ring_with("k1", 7);
let b = binding("acme", "openai");
let one = r.seal(b"same", b).unwrap();
let two = r.seal(b"same", b).unwrap();
assert_ne!(one.nonce, two.nonce);
assert_ne!(one.ciphertext, two.ciphertext);
}
#[test]
fn rotation_keeps_old_ciphertext_readable() {
let r = KeyRing::new(
[("old".to_string(), kek(1)), ("new".to_string(), kek(2))],
"new",
)
.unwrap();
let b = binding("acme", "openai");
let old_ring = ring_with("old", 1);
let old_sealed = old_ring.seal(b"legacy", b).unwrap();
assert_eq!(old_sealed.kek_id, "old");
assert_eq!(&*r.open(&old_sealed, b).unwrap(), b"legacy");
let resealed = r.reseal(&old_sealed, b).unwrap();
assert_eq!(resealed.kek_id, "new");
assert_eq!(&*r.open(&resealed, b).unwrap(), b"legacy");
}
#[test]
fn an_unknown_kek_id_is_distinguishable_from_a_failed_open() {
let r = ring_with("k1", 7);
let b = binding("acme", "openai");
let mut sealed = r.seal(b"v", b).unwrap();
sealed.kek_id = "retired".to_string();
assert_eq!(r.open(&sealed, b).unwrap_err(), CryptoError::UnknownKekId);
}
#[test]
fn a_short_key_is_rejected_at_construction() {
let short = Zeroizing::new(vec![0u8; KEK_LEN - 1]);
assert_eq!(
KeyRing::new([("k1".to_string(), short)], "k1").unwrap_err(),
CryptoError::InvalidKek
);
}
#[test]
fn a_current_id_not_in_the_ring_is_rejected() {
assert_eq!(
KeyRing::new([("k1".to_string(), kek(1))], "k2").unwrap_err(),
CryptoError::InvalidKek
);
}
#[test]
fn debug_impls_do_not_leak_secrets() {
let r = ring_with("k1", 7);
let sealed = r
.seal(b"sk-super-secret", binding("acme", "openai"))
.unwrap();
let d = format!("{sealed:?}");
assert!(!d.contains("sk-super-secret"));
assert!(!d.contains("nonce"), "nonce must not be printed: {d}");
assert!(d.contains("k1"), "kek_id is safe and useful: {d}");
let rd = format!("{r:?}");
assert!(rd.contains("k1"));
assert!(!rd.contains('\u{7}'));
}
}
#[cfg(test)]
mod spec_tests {
use super::*;
const K1: &str = "1111111111111111111111111111111111111111111111111111111111111111";
const K2: &str = "2222222222222222222222222222222222222222222222222222222222222222";
#[test]
fn parses_a_single_key_spec() {
let r = KeyRing::from_spec(&format!("k1:{K1}"), "k1").unwrap();
assert_eq!(r.current_kek_id(), "k1");
}
#[test]
fn parses_a_rotation_spec_and_seals_under_current() {
let r = KeyRing::from_spec(&format!("old:{K1},new:{K2}"), "new").unwrap();
assert_eq!(r.current_kek_id(), "new");
let sealed = r
.seal(
b"v",
SecretBinding {
namespace: "acme",
provider: "openai",
},
)
.unwrap();
assert_eq!(sealed.kek_id, "new");
}
#[test]
fn rejects_malformed_specs_rather_than_guessing() {
for bad in [
"", "k1", &format!("k1:{K1}:extra"), &format!(":{K1}"), "k1:zz", "k1:111", &format!("k1:{}", &K1[..62]), ] {
assert_eq!(
KeyRing::from_spec(bad, "k1").unwrap_err(),
CryptoError::InvalidKek,
"should reject {bad:?}"
);
}
}
#[test]
fn rejects_a_current_id_absent_from_the_spec() {
assert_eq!(
KeyRing::from_spec(&format!("k1:{K1}"), "k2").unwrap_err(),
CryptoError::InvalidKek
);
}
#[test]
fn accepts_uppercase_hex_and_surrounding_whitespace() {
let spec = format!(" k1 : {} ", K1.to_uppercase());
assert!(KeyRing::from_spec(&spec, "k1").is_ok());
}
}