use crate::{
crypto::{
KeyMatchStrength,
cipher::{AeadDecryptor, CipherMatch, DecryptError},
},
platform::MaybeSendBoxFuture,
};
#[derive(Debug, Clone)]
pub struct RetryingDecryptor<D> {
inner: D,
}
impl<D: AeadDecryptor> RetryingDecryptor<D> {
pub fn new(inner: D) -> Self {
Self { inner }
}
}
impl<D: AeadDecryptor> AeadDecryptor for RetryingDecryptor<D> {
fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
self.inner.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 {
match self
.inner
.decrypt(cipher_match, nonce, ciphertext, tag, aad)
.await
{
Err(DecryptError::NoMatchingKey) => {
if self.inner.try_refresh().await {
self.inner
.decrypt(cipher_match, nonce, ciphertext, tag, aad)
.await
} else {
Err(DecryptError::NoMatchingKey)
}
}
other => other,
}
})
}
fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
self.inner.try_refresh()
}
}
#[cfg(test)]
mod tests {
use std::{
pin::Pin,
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
};
use super::*;
use crate::{
crypto::cipher::{MultiKeyDecryptor, RefreshableCipher},
error::Error,
platform::MaybeSendFuture,
};
#[derive(Debug)]
struct KeyedDecryptor {
kid: String,
}
impl AeadDecryptor for KeyedDecryptor {
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 rotating_decryptor() -> RefreshableCipher<MultiKeyDecryptor> {
let generation = Arc::new(AtomicU32::new(0));
let factory =
move || -> Pin<Box<dyn MaybeSendFuture<Output = Result<MultiKeyDecryptor, Error>>>> {
let generation = Arc::clone(&generation);
Box::pin(async move {
let n = generation.fetch_add(1, Ordering::SeqCst);
let keys = (0..=n)
.map(|v| {
Arc::new(KeyedDecryptor {
kid: format!("v{v}"),
}) as Arc<dyn AeadDecryptor>
})
.collect();
Ok(MultiKeyDecryptor::new(keys))
})
};
RefreshableCipher::builder()
.factory(factory)
.build()
.await
.unwrap()
}
#[tokio::test]
async fn retries_after_refresh_on_no_matching_key() {
let decryptor = RetryingDecryptor::new(rotating_decryptor().await);
let m = CipherMatch::builder().enc("mock").kid("v1").build();
let plaintext = decryptor
.decrypt(Some(&m), b"", b"hello", b"v1", b"")
.await
.unwrap();
assert_eq!(plaintext, b"hello");
}
#[tokio::test]
async fn retries_only_once() {
let decryptor = RetryingDecryptor::new(rotating_decryptor().await);
let m = CipherMatch::builder().enc("mock").kid("v9").build();
let err = decryptor
.decrypt(Some(&m), b"", b"hello", b"v9", b"")
.await
.unwrap_err();
assert!(matches!(err, DecryptError::NoMatchingKey));
let m = CipherMatch::builder().kid("v1").build();
assert_eq!(decryptor.cipher_match(&m), Some(KeyMatchStrength::ByKeyId));
}
#[tokio::test]
async fn real_failures_are_not_retried() {
let decryptor = RetryingDecryptor::new(rotating_decryptor().await);
let m = CipherMatch::builder().enc("mock").kid("v0").build();
let err = decryptor
.decrypt(Some(&m), b"", b"hello", b"tampered", b"")
.await
.unwrap_err();
assert!(matches!(err, DecryptError::Other { .. }));
let m = CipherMatch::builder().kid("v1").build();
assert_eq!(decryptor.cipher_match(&m), None);
}
}