use std::collections::HashMap;
use std::sync::Arc;
use aes_gcm::aead::consts::U12;
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::aead::{Aead, KeyInit, OsRng, Payload};
use aes_gcm::{AeadCore, Aes256Gcm, Key, Nonce};
use zeroize::Zeroizing;
use crate::{Error, Result};
pub type KekId = String;
const ENVELOPE_VERSION: u8 = 1;
const KEY_LEN: usize = 32;
const NONCE_LEN: usize = 12;
fn crypto_err(msg: &str) -> Error {
Error::generic(format!("encryption error: {msg}"))
}
#[async_trait::async_trait]
pub trait KeyProvider: Send + Sync + 'static {
fn active_kek_id(&self) -> KekId;
async fn wrap_dek(&self, dek: &[u8], kek_id: &str) -> Result<Vec<u8>>;
async fn unwrap_dek(&self, wrapped: &[u8], kek_id: &str) -> Result<Zeroizing<Vec<u8>>>;
}
pub struct LocalKeyProvider {
active: KekId,
keys: HashMap<KekId, Zeroizing<[u8; KEY_LEN]>>,
}
impl LocalKeyProvider {
pub fn new(
active: impl Into<KekId>,
keys: impl IntoIterator<Item = (KekId, Vec<u8>)>,
) -> Result<Self> {
let active = active.into();
let mut map = HashMap::new();
for (id, bytes) in keys {
if bytes.len() != KEY_LEN {
return Err(Error::invalid_argument(format!(
"KEK '{id}' must be {KEY_LEN} bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; KEY_LEN];
arr.copy_from_slice(&bytes);
map.insert(id, Zeroizing::new(arr));
}
if !map.contains_key(&active) {
return Err(Error::invalid_argument(format!(
"active KEK '{active}' was not provided among the configured keys"
)));
}
Ok(Self { active, keys: map })
}
pub fn single(active: impl Into<KekId>, key: Vec<u8>) -> Result<Self> {
let active = active.into();
Self::new(active.clone(), [(active, key)])
}
pub fn dev_insecure() -> Self {
Self::single("dev", vec![0x42; KEY_LEN]).expect("dev key is 32 bytes")
}
fn cipher(&self, kek_id: &str) -> Result<Aes256Gcm> {
let key = self
.keys
.get(kek_id)
.ok_or_else(|| crypto_err("unknown KEK id"))?;
Ok(Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key.as_ref())))
}
}
#[async_trait::async_trait]
impl KeyProvider for LocalKeyProvider {
fn active_kek_id(&self) -> KekId {
self.active.clone()
}
async fn wrap_dek(&self, dek: &[u8], kek_id: &str) -> Result<Vec<u8>> {
let cipher = self.cipher(kek_id)?;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(
&nonce,
Payload {
msg: dek,
aad: kek_id.as_bytes(),
},
)
.map_err(|_| crypto_err("DEK wrap failed"))?;
let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len());
out.extend_from_slice(nonce.as_slice());
out.extend_from_slice(&ciphertext);
Ok(out)
}
async fn unwrap_dek(&self, wrapped: &[u8], kek_id: &str) -> Result<Zeroizing<Vec<u8>>> {
if wrapped.len() < NONCE_LEN {
return Err(crypto_err("wrapped DEK too short"));
}
let cipher = self.cipher(kek_id)?;
let (nonce_bytes, ciphertext) = wrapped.split_at(NONCE_LEN);
let nonce = Nonce::<U12>::from_slice(nonce_bytes);
let dek = cipher
.decrypt(
nonce,
Payload {
msg: ciphertext,
aad: kek_id.as_bytes(),
},
)
.map_err(|_| crypto_err("DEK unwrap failed"))?;
Ok(Zeroizing::new(dek))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Envelope {
kek_id: KekId,
nonce: Vec<u8>,
wrapped_dek: Vec<u8>,
ciphertext: Vec<u8>,
}
impl Envelope {
fn encode(&self) -> Result<Vec<u8>> {
if self.kek_id.len() > u8::MAX as usize {
return Err(crypto_err("KEK id too long"));
}
if self.nonce.len() > u8::MAX as usize {
return Err(crypto_err("nonce too long"));
}
let wrapped_len = u32::try_from(self.wrapped_dek.len())
.map_err(|_| crypto_err("wrapped DEK too long"))?;
let mut out = Vec::with_capacity(
1 + 1
+ self.kek_id.len()
+ 1
+ self.nonce.len()
+ 4
+ self.wrapped_dek.len()
+ self.ciphertext.len(),
);
out.push(ENVELOPE_VERSION);
out.push(self.kek_id.len() as u8);
out.extend_from_slice(self.kek_id.as_bytes());
out.push(self.nonce.len() as u8);
out.extend_from_slice(&self.nonce);
out.extend_from_slice(&wrapped_len.to_be_bytes());
out.extend_from_slice(&self.wrapped_dek);
out.extend_from_slice(&self.ciphertext);
Ok(out)
}
fn decode(blob: &[u8]) -> Result<Self> {
let mut cur = blob;
let version = take_u8(&mut cur)?;
if version != ENVELOPE_VERSION {
return Err(crypto_err("unsupported envelope version"));
}
let kek_len = take_u8(&mut cur)? as usize;
let kek_bytes = take(&mut cur, kek_len)?;
let kek_id =
String::from_utf8(kek_bytes.to_vec()).map_err(|_| crypto_err("invalid KEK id"))?;
let nonce_len = take_u8(&mut cur)? as usize;
let nonce = take(&mut cur, nonce_len)?.to_vec();
let wrapped_len = take_u32(&mut cur)? as usize;
let wrapped_dek = take(&mut cur, wrapped_len)?.to_vec();
let ciphertext = cur.to_vec();
Ok(Self {
kek_id,
nonce,
wrapped_dek,
ciphertext,
})
}
}
fn take<'a>(cur: &mut &'a [u8], n: usize) -> Result<&'a [u8]> {
if cur.len() < n {
return Err(crypto_err("truncated envelope"));
}
let (head, tail) = cur.split_at(n);
*cur = tail;
Ok(head)
}
fn take_u8(cur: &mut &[u8]) -> Result<u8> {
Ok(take(cur, 1)?[0])
}
fn take_u32(cur: &mut &[u8]) -> Result<u32> {
let bytes = take(cur, 4)?;
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
#[derive(Clone)]
pub struct EnvelopeEncryptor {
provider: Arc<dyn KeyProvider>,
}
impl std::fmt::Debug for EnvelopeEncryptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnvelopeEncryptor")
.field("active_kek_id", &self.provider.active_kek_id())
.finish()
}
}
impl EnvelopeEncryptor {
pub fn new(provider: Arc<dyn KeyProvider>) -> Self {
Self { provider }
}
pub fn local(provider: LocalKeyProvider) -> Self {
Self::new(Arc::new(provider))
}
pub async fn seal(&self, name: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
let mut dek = Zeroizing::new([0u8; KEY_LEN]);
OsRng.fill_bytes(dek.as_mut_slice());
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(dek.as_slice()));
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(
&nonce,
Payload {
msg: plaintext,
aad: name.as_bytes(),
},
)
.map_err(|_| crypto_err("value encryption failed"))?;
let kek_id = self.provider.active_kek_id();
let wrapped_dek = self.provider.wrap_dek(dek.as_slice(), &kek_id).await?;
Envelope {
kek_id,
nonce: nonce.to_vec(),
wrapped_dek,
ciphertext,
}
.encode()
}
pub async fn open(&self, name: &str, blob: &[u8]) -> Result<Vec<u8>> {
let envelope = Envelope::decode(blob)?;
let dek = self
.provider
.unwrap_dek(&envelope.wrapped_dek, &envelope.kek_id)
.await?;
if dek.len() != KEY_LEN {
return Err(crypto_err("unwrapped DEK has wrong length"));
}
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&dek));
if envelope.nonce.len() != NONCE_LEN {
return Err(crypto_err("invalid nonce length"));
}
let nonce = Nonce::<U12>::from_slice(&envelope.nonce);
let plaintext = cipher
.decrypt(
nonce,
Payload {
msg: &envelope.ciphertext,
aad: name.as_bytes(),
},
)
.map_err(|_| crypto_err("value decryption failed"))?;
Ok(plaintext)
}
pub async fn rewrap(&self, blob: &[u8]) -> Result<Option<Vec<u8>>> {
let envelope = Envelope::decode(blob)?;
let active = self.provider.active_kek_id();
if envelope.kek_id == active {
return Ok(None);
}
let dek = self
.provider
.unwrap_dek(&envelope.wrapped_dek, &envelope.kek_id)
.await?;
let wrapped_dek = self.provider.wrap_dek(dek.as_slice(), &active).await?;
let rewrapped = Envelope {
kek_id: active,
nonce: envelope.nonce,
wrapped_dek,
ciphertext: envelope.ciphertext,
}
.encode()?;
Ok(Some(rewrapped))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(byte: u8) -> Vec<u8> {
vec![byte; KEY_LEN]
}
fn encryptor_single() -> EnvelopeEncryptor {
EnvelopeEncryptor::local(LocalKeyProvider::single("v1", key(1)).unwrap())
}
#[tokio::test]
async fn round_trip() {
let enc = encryptor_single();
let blob = enc.seal("my-secret", b"super secret value").await.unwrap();
assert!(
!blob
.windows(b"super secret value".len())
.any(|w| w == b"super secret value")
);
let out = enc.open("my-secret", &blob).await.unwrap();
assert_eq!(out, b"super secret value");
}
#[tokio::test]
async fn wrong_name_aad_fails() {
let enc = encryptor_single();
let blob = enc.seal("name-a", b"value").await.unwrap();
assert!(enc.open("name-b", &blob).await.is_err());
}
#[tokio::test]
async fn tampered_ciphertext_fails() {
let enc = encryptor_single();
let mut blob = enc.seal("n", b"value").await.unwrap();
let last = blob.len() - 1;
blob[last] ^= 0xff;
assert!(enc.open("n", &blob).await.is_err());
}
#[tokio::test]
async fn tampered_wrapped_dek_fails() {
let enc = encryptor_single();
let blob = enc.seal("n", b"value").await.unwrap();
let mut tampered = blob.clone();
let idx = 1 + 1 + 2 + 1 + NONCE_LEN + 4 + 2;
tampered[idx] ^= 0x01;
assert!(enc.open("n", &tampered).await.is_err());
}
#[tokio::test]
async fn rewrap_from_retired_to_active() {
let v1 = EnvelopeEncryptor::local(LocalKeyProvider::single("v1", key(1)).unwrap());
let blob = v1.seal("n", b"value").await.unwrap();
let rotated = EnvelopeEncryptor::local(
LocalKeyProvider::new("v2", [("v1".into(), key(1)), ("v2".into(), key(2))]).unwrap(),
);
assert_eq!(rotated.open("n", &blob).await.unwrap(), b"value");
let rewrapped = rotated.rewrap(&blob).await.unwrap().expect("should rewrap");
assert!(rotated.rewrap(&rewrapped).await.unwrap().is_none());
assert_eq!(rotated.open("n", &rewrapped).await.unwrap(), b"value");
let v2_only = EnvelopeEncryptor::local(LocalKeyProvider::single("v2", key(2)).unwrap());
assert_eq!(v2_only.open("n", &rewrapped).await.unwrap(), b"value");
assert!(v2_only.open("n", &blob).await.is_err());
}
#[test]
fn rejects_bad_key_length() {
assert!(LocalKeyProvider::single("v1", vec![0u8; 16]).is_err());
}
#[test]
fn rejects_missing_active() {
assert!(LocalKeyProvider::new("v9", [("v1".into(), key(1))]).is_err());
}
#[tokio::test]
async fn dev_insecure_round_trips() {
let enc = EnvelopeEncryptor::local(LocalKeyProvider::dev_insecure());
let blob = enc.seal("n", b"value").await.unwrap();
assert_eq!(enc.open("n", &blob).await.unwrap(), b"value");
}
}