use std::collections::BTreeMap;
use std::time::SystemTime;
use async_trait::async_trait;
use corium_crypt::{SecretKey, decrypt_blob, encrypt_blob, parse_blob_header};
use crate::{BlobId, BlobIdStream, BlobStore, RootStore, StoreError, digest};
#[derive(Clone)]
pub struct EncryptedBlobStore<S> {
inner: S,
current_epoch: u32,
keys: BTreeMap<u32, SecretKey>,
}
impl<S> EncryptedBlobStore<S> {
pub fn new(
inner: S,
current_epoch: u32,
keys: impl IntoIterator<Item = (u32, SecretKey)>,
) -> Result<Self, StoreError> {
let keys = keys.into_iter().collect::<BTreeMap<_, _>>();
if !keys.contains_key(¤t_epoch) {
return Err(StoreError::MissingEncryptionKey(current_epoch));
}
Ok(Self {
inner,
current_epoch,
keys,
})
}
#[must_use]
pub fn with_key(inner: S, epoch: u32, key: SecretKey) -> Self {
Self {
inner,
current_epoch: epoch,
keys: BTreeMap::from([(epoch, key)]),
}
}
#[must_use]
pub fn inner(&self) -> &S {
&self.inner
}
#[must_use]
pub fn into_inner(self) -> S {
self.inner
}
#[must_use]
pub fn current_epoch(&self) -> u32 {
self.current_epoch
}
fn current_key(&self) -> &SecretKey {
self.keys
.get(&self.current_epoch)
.expect("current encryption epoch must have key material")
}
fn key(&self, epoch: u32) -> Result<&SecretKey, StoreError> {
self.keys
.get(&epoch)
.ok_or(StoreError::MissingEncryptionKey(epoch))
}
}
#[async_trait]
impl<S: BlobStore> BlobStore for EncryptedBlobStore<S> {
async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
let encrypted = encrypt_blob(self.current_key(), self.current_epoch, bytes)?;
let expected = digest(&encrypted);
let actual = self.inner.put(&encrypted).await?;
if actual != expected {
return Err(StoreError::BlobIdMismatch { expected, actual });
}
Ok(actual)
}
async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
let Some(encrypted) = self.inner.get(id).await? else {
return Ok(None);
};
if digest(&encrypted) != *id {
return Err(StoreError::CorruptBlob(id.clone()));
}
let header = parse_blob_header(&encrypted)?;
let plaintext = decrypt_blob(self.key(header.epoch)?, &encrypted)?;
Ok(Some(plaintext))
}
async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
self.inner.contains(id).await
}
async fn put_if_absent(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
let encrypted = encrypt_blob(self.current_key(), self.current_epoch, bytes)?;
let expected = digest(&encrypted);
if self.inner.contains(&expected).await? {
return Ok(expected);
}
let actual = self.inner.put(&encrypted).await?;
if actual != expected {
return Err(StoreError::BlobIdMismatch { expected, actual });
}
Ok(actual)
}
async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
self.inner.delete(id).await
}
async fn list(&self) -> Result<BlobIdStream, StoreError> {
self.inner.list().await
}
async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
self.inner.modified_at(id).await
}
}
#[async_trait]
impl<S: RootStore> RootStore for EncryptedBlobStore<S> {
async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
self.inner.get_root(name).await
}
async fn cas_root(
&self,
name: &str,
expected: Option<&[u8]>,
new: &[u8],
) -> Result<(), StoreError> {
self.inner.cas_root(name, expected, new).await
}
async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
self.inner.delete_root(name).await
}
async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
self.inner.list_roots(prefix).await
}
}