use async_trait::async_trait;
use crate::blob::{BlobError, BlobStore};
use crate::core::{Digest, Timestamp};
use super::{DataKey, KeyError, KeyRing, WrappedKey};
#[derive(Debug)]
pub struct EncryptedBlobs {
inner: std::sync::Arc<dyn BlobStore>,
keys: std::sync::Arc<dyn KeyRing>,
scope: String,
}
impl EncryptedBlobs {
#[must_use]
pub fn new(
inner: std::sync::Arc<dyn BlobStore>,
keys: std::sync::Arc<dyn KeyRing>,
scope: impl Into<String>,
) -> Self {
Self {
inner,
keys,
scope: scope.into(),
}
}
fn cipher(key: &DataKey) -> chacha20poly1305::XChaCha20Poly1305 {
use chacha20poly1305::KeyInit as _;
chacha20poly1305::XChaCha20Poly1305::new(key.expose().into())
}
}
fn corrupt(digest: Digest, why: &str) -> BlobError {
BlobError::Corrupt {
expected: digest.to_hex(),
actual: why.to_owned(),
}
}
fn split_envelope(digest: Digest, envelope: &[u8]) -> Result<(WrappedKey, &[u8]), BlobError> {
let Some(head) = envelope.get(..4) else {
return Err(corrupt(digest, "the envelope has no length prefix"));
};
let len = u32::from_be_bytes(head.try_into().expect("a four-byte slice")) as usize;
let Some(bytes) = envelope.get(4..4 + len) else {
return Err(corrupt(
digest,
"the envelope claims a wrapped key longer than itself",
));
};
let wrapped = serde_json::from_slice(bytes)
.map_err(|_| corrupt(digest, "the envelope's wrapped key does not parse"))?;
Ok((wrapped, &envelope[4 + len..]))
}
fn erased(e: KeyError) -> BlobError {
match e {
KeyError::Destroyed { scope, at, reason } => BlobError::Expired {
digest: String::new(),
at: at.unix_timestamp(),
reason: format!("the data key for scope '{scope}' was destroyed: {reason}"),
},
other => BlobError::Backend(other.to_string()),
}
}
#[async_trait]
impl BlobStore for EncryptedBlobs {
async fn put(&self, bytes: &[u8]) -> Result<Digest, BlobError> {
use chacha20poly1305::aead::{Aead, AeadCore, OsRng};
let digest = Digest::of(bytes);
let (key, wrapped) = self.keys.data_key(&self.scope).await.map_err(erased)?;
let cipher = Self::cipher(&key);
let nonce = chacha20poly1305::XChaCha20Poly1305::generate_nonce(&mut OsRng);
let sealed = cipher
.encrypt(
&nonce,
chacha20poly1305::aead::Payload {
msg: bytes,
aad: digest.to_hex().as_bytes(),
},
)
.map_err(|e| BlobError::Backend(format!("sealing a payload failed: {e}")))?;
let wrapped_bytes = crate::core::canon::to_bytes(&wrapped)
.map_err(|e| BlobError::Backend(format!("a wrapped key would not serialise: {e}")))?;
let len = u32::try_from(wrapped_bytes.len())
.map_err(|_| BlobError::Backend("the wrapped key is implausibly large".to_owned()))?;
let mut envelope = Vec::with_capacity(4 + wrapped_bytes.len() + 24 + sealed.len());
envelope.extend_from_slice(&len.to_be_bytes());
envelope.extend_from_slice(&wrapped_bytes);
envelope.extend_from_slice(&nonce);
envelope.extend_from_slice(&sealed);
self.inner.put_at(digest, &envelope).await?;
Ok(digest)
}
async fn get(&self, digest: Digest) -> Result<Vec<u8>, BlobError> {
use chacha20poly1305::aead::Aead;
let envelope = self.inner.get_raw(digest).await?;
let (wrapped, rest) = split_envelope(digest, &envelope)?;
if rest.len() < 24 {
return Err(corrupt(digest, "the envelope is shorter than its nonce"));
}
let (nonce, sealed) = rest.split_at(24);
let key = self.keys.open(&wrapped).await.map_err(erased)?;
let plain = Self::cipher(&key)
.decrypt(
nonce.into(),
chacha20poly1305::aead::Payload {
msg: sealed,
aad: digest.to_hex().as_bytes(),
},
)
.map_err(|_| corrupt(digest, "the sealed payload did not authenticate"))?;
let actual = Digest::of(&plain);
if actual != digest {
return Err(BlobError::Corrupt {
expected: digest.to_hex(),
actual: actual.to_hex(),
});
}
Ok(plain)
}
async fn put_at(&self, digest: Digest, bytes: &[u8]) -> Result<(), BlobError> {
self.inner.put_at(digest, bytes).await
}
async fn get_raw(&self, digest: Digest) -> Result<Vec<u8>, BlobError> {
self.inner.get_raw(digest).await
}
async fn expire(&self, digest: Digest, at: Timestamp, reason: &str) -> Result<(), BlobError> {
self.inner.expire(digest, at, reason).await
}
async fn has(&self, digest: Digest) -> Result<bool, BlobError> {
self.inner.has(digest).await
}
}