use std::{borrow::Cow, pin::Pin, sync::Arc};
use crate::{
crypto::{
KeyMatchStrength,
cipher::{
AeadCipherSelector, AeadDecryptor, AeadEncryptor, AeadOutput, AeadSealer,
AeadSealerSelector, AeadUnsealer, AeadV1Cipher, CipherMatch, DecryptError,
},
refreshable::Refreshable,
},
error::Error,
platform::{MaybeSendBoxFuture, MaybeSendFuture, MaybeSendSync},
};
#[derive(Debug)]
pub struct RefreshableCipher<C> {
inner: Arc<Refreshable<C>>,
}
impl<C> Clone for RefreshableCipher<C> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
#[bon::bon]
impl<C: std::fmt::Debug + MaybeSendSync + 'static> RefreshableCipher<C> {
#[builder]
pub async fn new(
factory: impl Fn() -> Pin<Box<dyn MaybeSendFuture<Output = Result<C, Error>>>>
+ MaybeSendSync
+ 'static,
) -> Result<Self, Error> {
let refreshable = Refreshable::builder().factory(factory).build().await?;
Ok(Self {
inner: Arc::new(refreshable),
})
}
pub async fn refresh(&self) -> Result<bool, Error> {
self.inner.refresh().await
}
}
impl<C: AeadEncryptor + 'static> AeadEncryptor for RefreshableCipher<C> {
fn enc_algorithm(&self) -> Cow<'_, str> {
Cow::Owned(self.inner.load().enc_algorithm().into_owned())
}
fn key_id(&self) -> Option<Cow<'_, str>> {
self.inner
.load()
.key_id()
.map(|kid| Cow::Owned(kid.into_owned()))
}
fn encrypt<'a>(
&'a self,
plaintext: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>> {
Box::pin(async move { self.inner.load_full().encrypt(plaintext, aad).await })
}
}
impl<C: AeadDecryptor + 'static> AeadDecryptor for RefreshableCipher<C> {
fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
self.inner.load().cipher_match(m)
}
fn decrypt<'a>(
&'a self,
cipher_match: Option<&'a CipherMatch<'a>>,
nonce: &'a [u8],
ciphertext: &'a [u8],
tag: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
Box::pin(async move {
self.inner
.load_full()
.decrypt(cipher_match, nonce, ciphertext, tag, aad)
.await
})
}
fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
Box::pin(async move { self.refresh().await.is_ok() })
}
}
impl<C: AeadEncryptor + 'static> AeadCipherSelector for RefreshableCipher<C> {
fn select_cipher(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadEncryptor>> {
let encryptor: Arc<dyn AeadEncryptor> = self.inner.load_full();
Box::pin(async move { encryptor })
}
}
impl<C: AeadEncryptor + 'static> AeadSealerSelector for RefreshableCipher<C> {
fn select_sealer(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadSealer>> {
let sealer: Arc<dyn AeadSealer> = Arc::new(AeadV1Cipher::new(self.inner.load_full()));
Box::pin(async move { sealer })
}
}
impl<C: AeadDecryptor + 'static> AeadUnsealer for RefreshableCipher<C> {
fn unseal<'a>(
&'a self,
cipher_match: Option<&'a CipherMatch<'a>>,
bundle: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
Box::pin(async move {
AeadV1Cipher::new(self.inner.load_full())
.unseal(cipher_match, bundle, aad)
.await
})
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU32, Ordering};
use super::*;
#[derive(Debug)]
struct KeyedCipher {
kid: String,
}
impl AeadEncryptor for KeyedCipher {
fn enc_algorithm(&self) -> Cow<'_, str> {
"mock".into()
}
fn key_id(&self) -> Option<Cow<'_, str>> {
Some(Cow::Borrowed(&self.kid))
}
fn encrypt<'a>(
&'a self,
plaintext: &'a [u8],
_aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>> {
Box::pin(async move {
Ok(AeadOutput {
nonce: vec![],
ciphertext: plaintext.to_vec(),
tag: self.kid.clone().into_bytes(),
})
})
}
}
impl AeadDecryptor for KeyedCipher {
fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
m.strength_for("mock", Some(&self.kid))
}
fn decrypt<'a>(
&'a self,
_cipher_match: Option<&'a CipherMatch<'a>>,
_nonce: &'a [u8],
ciphertext: &'a [u8],
tag: &'a [u8],
_aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
Box::pin(async move {
if tag == self.kid.as_bytes() {
Ok(ciphertext.to_vec())
} else {
Err(Error::from(crate::error::ErrorKind::Crypto).into())
}
})
}
}
async fn versioned_cipher() -> RefreshableCipher<KeyedCipher> {
let counter = Arc::new(AtomicU32::new(0));
let factory =
move || -> Pin<Box<dyn MaybeSendFuture<Output = Result<KeyedCipher, Error>>>> {
let counter = Arc::clone(&counter);
Box::pin(async move {
let n = counter.fetch_add(1, Ordering::SeqCst);
Ok(KeyedCipher {
kid: format!("v{n}"),
})
})
};
RefreshableCipher::builder()
.factory(factory)
.build()
.await
.unwrap()
}
#[tokio::test]
async fn refresh_swaps_cipher_for_all_clones() {
let cipher = versioned_cipher().await;
let clone = cipher.clone();
assert_eq!(cipher.key_id().as_deref(), Some("v0"));
assert!(cipher.refresh().await.unwrap());
assert_eq!(clone.key_id().as_deref(), Some("v1"));
}
#[tokio::test]
async fn try_refresh_swaps_through_dyn_decryptor() {
let cipher = versioned_cipher().await;
let output = cipher.encrypt(b"hello", b"").await.unwrap();
let decryptor: Arc<dyn AeadDecryptor> = Arc::new(cipher);
decryptor
.decrypt(None, &output.nonce, &output.ciphertext, &output.tag, b"")
.await
.unwrap();
assert!(decryptor.try_refresh().await);
let err = decryptor
.decrypt(None, &output.nonce, &output.ciphertext, &output.tag, b"")
.await
.unwrap_err();
let DecryptError::Other { source } = err else {
unreachable!("expected DecryptError::Other, got {err:?}");
};
assert_eq!(source.kind(), crate::error::ErrorKind::Crypto);
let m = CipherMatch::builder().kid("v1").build();
assert_eq!(decryptor.cipher_match(&m), Some(KeyMatchStrength::ByKeyId));
}
}