mod error;
#[cfg(feature = "metrics")]
mod metrics_decryptor;
mod multi;
mod refreshable;
mod retrying;
mod scheduled;
use std::{borrow::Cow, sync::Arc};
use bon::Builder;
pub use error::{DecryptError, UnsealError};
#[cfg(feature = "metrics")]
pub use metrics_decryptor::MetricsAeadDecryptor;
pub use multi::{MultiKeyCipher, MultiKeyDecryptor};
pub use refreshable::RefreshableCipher;
pub use retrying::RetryingDecryptor;
pub use scheduled::ScheduledRefreshCipher;
use crate::{
crypto::KeyMatchStrength,
error::{Error, ErrorKind},
platform::{MaybeSendBoxFuture, MaybeSendSync},
};
pub struct AeadOutput {
pub nonce: Vec<u8>,
pub ciphertext: Vec<u8>,
pub tag: Vec<u8>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Builder)]
pub struct CipherMatch<'a> {
pub enc: Option<&'a str>,
pub kid: Option<&'a str>,
}
impl CipherMatch<'_> {
#[must_use]
pub fn strength_for(
&self,
enc_algorithm: &str,
registered_kid: Option<&str>,
) -> Option<KeyMatchStrength> {
if let Some(enc) = self.enc
&& enc != enc_algorithm
{
return None;
}
crate::crypto::kid_match_strength(self.kid, registered_kid)
}
}
pub trait AeadEncryptor: std::fmt::Debug + MaybeSendSync {
fn enc_algorithm(&self) -> Cow<'_, str>;
fn key_id(&self) -> Option<Cow<'_, str>>;
fn encrypt<'a>(
&'a self,
plaintext: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>>;
}
pub trait AeadDecryptor: std::fmt::Debug + MaybeSendSync {
fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength>;
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>>;
fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
Box::pin(async { false })
}
}
pub trait AeadCipher: AeadEncryptor + AeadDecryptor {}
impl<T: AeadEncryptor + AeadDecryptor + ?Sized> AeadCipher for T {}
pub trait AeadCipherSelector: std::fmt::Debug + MaybeSendSync {
fn select_cipher(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadEncryptor>>;
}
pub trait AeadSealer: AeadEncryptor {
fn seal<'a>(
&'a self,
plaintext: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, Error>>;
}
pub trait AeadUnsealer: AeadDecryptor {
fn unseal<'a>(
&'a self,
cipher_match: Option<&'a CipherMatch<'a>>,
bundle: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>>;
}
pub trait SealedAeadCipher: AeadSealer + AeadUnsealer {}
impl<T: AeadSealer + AeadUnsealer + ?Sized> SealedAeadCipher for T {}
pub trait AeadSealerSelector: std::fmt::Debug + MaybeSendSync {
fn select_sealer(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadSealer>>;
}
pub trait SealedAeadCipherSelector: AeadSealerSelector + AeadUnsealer {}
impl<T: AeadSealerSelector + AeadUnsealer + ?Sized> SealedAeadCipherSelector for T {}
macro_rules! forward_aead_encryptor {
($wrapper:ty) => {
impl<T: AeadEncryptor + ?Sized> AeadEncryptor for $wrapper {
fn enc_algorithm(&self) -> Cow<'_, str> {
(**self).enc_algorithm()
}
fn key_id(&self) -> Option<Cow<'_, str>> {
(**self).key_id()
}
fn encrypt<'a>(
&'a self,
plaintext: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>> {
(**self).encrypt(plaintext, aad)
}
}
};
}
forward_aead_encryptor!(&T);
forward_aead_encryptor!(Box<T>);
forward_aead_encryptor!(Arc<T>);
macro_rules! forward_aead_decryptor {
($wrapper:ty) => {
impl<T: AeadDecryptor + ?Sized> AeadDecryptor for $wrapper {
fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
(**self).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>> {
(**self).decrypt(cipher_match, nonce, ciphertext, tag, aad)
}
fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
(**self).try_refresh()
}
}
};
}
forward_aead_decryptor!(&T);
forward_aead_decryptor!(Box<T>);
forward_aead_decryptor!(Arc<T>);
macro_rules! forward_aead_cipher_selector {
($wrapper:ty) => {
impl<T: AeadCipherSelector + ?Sized> AeadCipherSelector for $wrapper {
fn select_cipher(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadEncryptor>> {
(**self).select_cipher()
}
}
};
}
forward_aead_cipher_selector!(&T);
forward_aead_cipher_selector!(Box<T>);
forward_aead_cipher_selector!(Arc<T>);
macro_rules! forward_aead_sealer {
($wrapper:ty) => {
impl<T: AeadSealer + ?Sized> AeadSealer for $wrapper {
fn seal<'a>(
&'a self,
plaintext: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, Error>> {
(**self).seal(plaintext, aad)
}
}
};
}
forward_aead_sealer!(&T);
forward_aead_sealer!(Box<T>);
forward_aead_sealer!(Arc<T>);
macro_rules! forward_aead_unsealer {
($wrapper:ty) => {
impl<T: AeadUnsealer + ?Sized> AeadUnsealer for $wrapper {
fn unseal<'a>(
&'a self,
cipher_match: Option<&'a CipherMatch<'a>>,
bundle: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
(**self).unseal(cipher_match, bundle, aad)
}
}
};
}
forward_aead_unsealer!(&T);
forward_aead_unsealer!(Box<T>);
forward_aead_unsealer!(Arc<T>);
macro_rules! forward_aead_sealer_selector {
($wrapper:ty) => {
impl<T: AeadSealerSelector + ?Sized> AeadSealerSelector for $wrapper {
fn select_sealer(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadSealer>> {
(**self).select_sealer()
}
}
};
}
forward_aead_sealer_selector!(&T);
forward_aead_sealer_selector!(Box<T>);
forward_aead_sealer_selector!(Arc<T>);
#[derive(Debug)]
pub struct AeadV1Cipher<C>(C);
impl<C> AeadV1Cipher<C> {
pub fn new(cipher: C) -> Self {
Self(cipher)
}
}
impl<C: AeadEncryptor> AeadEncryptor for AeadV1Cipher<C> {
fn enc_algorithm(&self) -> Cow<'_, str> {
self.0.enc_algorithm()
}
fn key_id(&self) -> Option<Cow<'_, str>> {
self.0.key_id()
}
fn encrypt<'a>(
&'a self,
plaintext: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>> {
self.0.encrypt(plaintext, aad)
}
}
impl<C: AeadEncryptor> AeadSealer for AeadV1Cipher<C> {
fn seal<'a>(
&'a self,
plaintext: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, Error>> {
Box::pin(async move {
let output = self.encrypt(plaintext, aad).await?;
let nonce_len: u8 = output.nonce.len().try_into().map_err(|_| {
Error::new(crate::ErrorKind::Crypto, "nonce length exceeds u8::MAX")
})?;
let tag_len: u8 =
output.tag.len().try_into().map_err(|_| {
Error::new(crate::ErrorKind::Crypto, "tag length exceeds u8::MAX")
})?;
let mut bundle = Vec::with_capacity(
3 + output.nonce.len() + output.ciphertext.len() + output.tag.len(),
);
bundle.push(0x01);
bundle.push(nonce_len);
bundle.push(tag_len);
bundle.extend_from_slice(&output.nonce);
bundle.extend_from_slice(&output.ciphertext);
bundle.extend_from_slice(&output.tag);
Ok(bundle)
})
}
}
fn invalid_bundle() -> Error {
Error::new(ErrorKind::Crypto, UnsealError::InvalidBundle)
}
impl<C: AeadDecryptor> AeadDecryptor for AeadV1Cipher<C> {
fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
self.0.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>> {
self.0.decrypt(cipher_match, nonce, ciphertext, tag, aad)
}
fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
self.0.try_refresh()
}
}
impl<C: AeadDecryptor> AeadUnsealer for AeadV1Cipher<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 {
if bundle.len() < 3 || bundle[0] != 0x01 {
return Err(invalid_bundle().into());
}
let nonce_len = bundle[1] as usize;
let tag_len = bundle[2] as usize;
if bundle.len() < 3 + nonce_len + tag_len {
return Err(invalid_bundle().into());
}
let nonce = &bundle[3..3 + nonce_len];
let tag = &bundle[bundle.len() - tag_len..];
let ciphertext = &bundle[3 + nonce_len..bundle.len() - tag_len];
self.decrypt(cipher_match, nonce, ciphertext, tag, aad)
.await
})
}
}
#[derive(Debug)]
pub struct StaticAeadCipher<C> {
cipher: Arc<C>,
}
impl<C> StaticAeadCipher<C> {
pub fn new(cipher: C) -> Self {
Self {
cipher: Arc::new(cipher),
}
}
}
impl<C: AeadEncryptor + 'static> AeadCipherSelector for StaticAeadCipher<C> {
fn select_cipher(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadEncryptor>> {
let encryptor: Arc<dyn AeadEncryptor> = self.cipher.clone();
Box::pin(async move { encryptor })
}
}
impl<C: AeadEncryptor + 'static> AeadSealerSelector for StaticAeadCipher<C> {
fn select_sealer(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadSealer>> {
let sealer: Arc<dyn AeadSealer> = Arc::new(AeadV1Cipher::new(Arc::clone(&self.cipher)));
Box::pin(async move { sealer })
}
}
impl<C: AeadDecryptor + 'static> AeadDecryptor for StaticAeadCipher<C> {
fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
self.cipher.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>> {
self.cipher
.decrypt(cipher_match, nonce, ciphertext, tag, aad)
}
fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
self.cipher.try_refresh()
}
}
impl<C: AeadDecryptor + 'static> AeadUnsealer for StaticAeadCipher<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(Arc::clone(&self.cipher))
.unseal(cipher_match, bundle, aad)
.await
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct MockEncryptor;
impl AeadEncryptor for MockEncryptor {
fn enc_algorithm(&self) -> Cow<'_, str> {
"mock".into()
}
fn key_id(&self) -> Option<Cow<'_, str>> {
None
}
fn encrypt<'a>(
&'a self,
plaintext: &'a [u8],
_aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>> {
Box::pin(async move {
Ok(AeadOutput {
nonce: vec![1, 2, 3],
ciphertext: plaintext.iter().map(|b| b ^ 0xFF).collect(),
tag: vec![4, 5, 6, 7],
})
})
}
}
#[derive(Debug)]
struct MockDecryptor;
impl AeadDecryptor for MockDecryptor {
fn cipher_match(&self, _m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
Some(KeyMatchStrength::ByAlgorithm)
}
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 { Ok(ciphertext.iter().map(|b| b ^ 0xFF).collect()) })
}
}
#[derive(Debug)]
struct MockCipher;
impl AeadEncryptor for MockCipher {
fn enc_algorithm(&self) -> Cow<'_, str> {
MockEncryptor.enc_algorithm()
}
fn key_id(&self) -> Option<Cow<'_, str>> {
MockEncryptor.key_id()
}
fn encrypt<'a>(
&'a self,
plaintext: &'a [u8],
aad: &'a [u8],
) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>> {
Box::pin(async move { MockEncryptor.encrypt(plaintext, aad).await })
}
}
impl AeadDecryptor for MockCipher {
fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
MockDecryptor.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 {
MockDecryptor
.decrypt(cipher_match, nonce, ciphertext, tag, aad)
.await
})
}
}
fn assert_invalid_bundle(err: &DecryptError) {
let DecryptError::Other { source } = err else {
unreachable!("expected DecryptError::Other, got {err:?}");
};
assert_eq!(source.kind(), ErrorKind::Crypto);
assert_eq!(source.to_string(), "cryptographic operation failed");
assert_eq!(
std::error::Error::source(source)
.expect("source")
.to_string(),
"invalid bundle"
);
}
#[tokio::test]
async fn seal_roundtrip() {
let plaintext = b"hello world";
let aad = b"associated";
let sealer = AeadV1Cipher::new(MockEncryptor);
let bundle = sealer.seal(plaintext, aad).await.unwrap();
let unsealer = AeadV1Cipher::new(MockDecryptor);
let recovered = unsealer.unseal(None, &bundle, aad).await.unwrap();
assert_eq!(recovered, plaintext);
}
#[tokio::test]
async fn erased_cipher_roundtrip() {
let sealer: Arc<dyn AeadSealer> = Arc::new(AeadV1Cipher::new(MockEncryptor));
let bundle = sealer.seal(b"hello", b"").await.unwrap();
let unsealer: Arc<dyn AeadUnsealer> = Arc::new(AeadV1Cipher::new(MockDecryptor));
let recovered = unsealer.unseal(None, &bundle, b"").await.unwrap();
assert_eq!(recovered, b"hello");
}
#[tokio::test]
async fn one_object_sealed_cipher_roundtrip() {
async fn roundtrip(cipher: impl AeadSealer + AeadUnsealer) {
let bundle = cipher.seal(b"hello", b"aad").await.unwrap();
let recovered = cipher.unseal(None, &bundle, b"aad").await.unwrap();
assert_eq!(recovered, b"hello");
}
roundtrip(AeadV1Cipher::new(MockCipher)).await;
let erased: Arc<dyn SealedAeadCipher> = Arc::new(AeadV1Cipher::new(MockCipher));
roundtrip(erased).await;
}
#[tokio::test]
async fn bundle_format() {
let plaintext = b"AB";
let sealer = AeadV1Cipher::new(MockEncryptor);
let bundle = sealer.seal(plaintext, b"").await.unwrap();
assert_eq!(bundle[0], 0x01);
assert_eq!(bundle[1], 3); assert_eq!(bundle[2], 4); assert_eq!(&bundle[3..6], &[1, 2, 3]); assert_eq!(&bundle[6..8], &[0x41 ^ 0xFF, 0x42 ^ 0xFF]); assert_eq!(&bundle[8..12], &[4, 5, 6, 7]); }
#[tokio::test]
async fn unseal_wrong_version() {
let unsealer = AeadV1Cipher::new(MockDecryptor);
let err = unsealer.unseal(None, &[0x02, 0, 0], b"").await.unwrap_err();
assert_invalid_bundle(&err);
}
#[tokio::test]
async fn unseal_too_short() {
let unsealer = AeadV1Cipher::new(MockDecryptor);
let err = unsealer.unseal(None, &[0x01], b"").await.unwrap_err();
assert_invalid_bundle(&err);
}
#[tokio::test]
async fn unseal_truncated() {
let unsealer = AeadV1Cipher::new(MockDecryptor);
let err = unsealer
.unseal(None, &[0x01, 10, 10, 0x00], b"")
.await
.unwrap_err();
assert_invalid_bundle(&err);
}
#[tokio::test]
async fn unseal_empty() {
let unsealer = AeadV1Cipher::new(MockDecryptor);
let err = unsealer.unseal(None, &[], b"").await.unwrap_err();
assert_invalid_bundle(&err);
}
fn cipher_match<'a>(enc: Option<&'a str>, kid: Option<&'a str>) -> CipherMatch<'a> {
CipherMatch { enc, kid }
}
#[test]
fn strength_for_enc_mismatch() {
let m = cipher_match(Some("A128GCM"), Some("kid-1"));
assert_eq!(m.strength_for("A256GCM", Some("kid-1")), None);
}
#[test]
fn strength_for_no_enc_is_compatible() {
let m = cipher_match(None, None);
assert_eq!(
m.strength_for("A256GCM", None),
Some(KeyMatchStrength::ByAlgorithm)
);
}
#[test]
fn strength_for_matching_kid() {
let m = cipher_match(Some("A256GCM"), Some("kid-1"));
assert_eq!(
m.strength_for("A256GCM", Some("kid-1")),
Some(KeyMatchStrength::ByKeyId)
);
}
#[test]
fn strength_for_kid_mismatch_is_none_not_by_algorithm() {
let m = cipher_match(None, Some("kid-1"));
assert_eq!(m.strength_for("A256GCM", Some("kid-2")), None);
}
#[test]
fn strength_for_missing_kid_falls_back_to_algorithm() {
let m = cipher_match(Some("A256GCM"), None);
assert_eq!(
m.strength_for("A256GCM", Some("kid-1")),
Some(KeyMatchStrength::ByAlgorithm)
);
let m = cipher_match(Some("A256GCM"), Some("kid-1"));
assert_eq!(
m.strength_for("A256GCM", None),
Some(KeyMatchStrength::ByAlgorithm)
);
}
}