huskarl_core/crypto/cipher/
retrying.rs1use crate::{
2 crypto::{
3 KeyMatchStrength,
4 cipher::{AeadDecryptor, CipherMatch, DecryptError},
5 },
6 platform::MaybeSendBoxFuture,
7};
8
9#[derive(Debug, Clone)]
36pub struct RetryingDecryptor<D> {
37 inner: D,
38}
39
40impl<D: AeadDecryptor> RetryingDecryptor<D> {
41 pub fn new(inner: D) -> Self {
43 Self { inner }
44 }
45}
46
47impl<D: AeadDecryptor> AeadDecryptor for RetryingDecryptor<D> {
48 fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
49 self.inner.cipher_match(m)
50 }
51
52 fn decrypt<'a>(
53 &'a self,
54 cipher_match: Option<&'a CipherMatch<'a>>,
55 nonce: &'a [u8],
56 ciphertext: &'a [u8],
57 tag: &'a [u8],
58 aad: &'a [u8],
59 ) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
60 Box::pin(async move {
61 match self
62 .inner
63 .decrypt(cipher_match, nonce, ciphertext, tag, aad)
64 .await
65 {
66 Err(DecryptError::NoMatchingKey) => {
67 if self.inner.try_refresh().await {
68 self.inner
69 .decrypt(cipher_match, nonce, ciphertext, tag, aad)
70 .await
71 } else {
72 Err(DecryptError::NoMatchingKey)
73 }
74 }
75 other => other,
76 }
77 })
78 }
79
80 fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
81 self.inner.try_refresh()
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use std::{
88 pin::Pin,
89 sync::{
90 Arc,
91 atomic::{AtomicU32, Ordering},
92 },
93 };
94
95 use super::*;
96 use crate::{
97 crypto::cipher::{MultiKeyDecryptor, RefreshableCipher},
98 error::Error,
99 platform::MaybeSendFuture,
100 };
101
102 #[derive(Debug)]
105 struct KeyedDecryptor {
106 kid: String,
107 }
108
109 impl AeadDecryptor for KeyedDecryptor {
110 fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
111 m.strength_for("mock", Some(&self.kid))
112 }
113
114 fn decrypt<'a>(
115 &'a self,
116 _cipher_match: Option<&'a CipherMatch<'a>>,
117 _nonce: &'a [u8],
118 ciphertext: &'a [u8],
119 tag: &'a [u8],
120 _aad: &'a [u8],
121 ) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
122 Box::pin(async move {
123 if tag == self.kid.as_bytes() {
124 Ok(ciphertext.to_vec())
125 } else {
126 Err(Error::from(crate::error::ErrorKind::Crypto).into())
127 }
128 })
129 }
130 }
131
132 async fn rotating_decryptor() -> RefreshableCipher<MultiKeyDecryptor> {
135 let generation = Arc::new(AtomicU32::new(0));
136 let factory =
137 move || -> Pin<Box<dyn MaybeSendFuture<Output = Result<MultiKeyDecryptor, Error>>>> {
138 let generation = Arc::clone(&generation);
139 Box::pin(async move {
140 let n = generation.fetch_add(1, Ordering::SeqCst);
141 let keys = (0..=n)
142 .map(|v| {
143 Arc::new(KeyedDecryptor {
144 kid: format!("v{v}"),
145 }) as Arc<dyn AeadDecryptor>
146 })
147 .collect();
148 Ok(MultiKeyDecryptor::new(keys))
149 })
150 };
151 RefreshableCipher::builder()
152 .factory(factory)
153 .build()
154 .await
155 .unwrap()
156 }
157
158 #[tokio::test]
159 async fn retries_after_refresh_on_no_matching_key() {
160 let decryptor = RetryingDecryptor::new(rotating_decryptor().await);
161
162 let m = CipherMatch::builder().enc("mock").kid("v1").build();
165 let plaintext = decryptor
166 .decrypt(Some(&m), b"", b"hello", b"v1", b"")
167 .await
168 .unwrap();
169 assert_eq!(plaintext, b"hello");
170 }
171
172 #[tokio::test]
173 async fn retries_only_once() {
174 let decryptor = RetryingDecryptor::new(rotating_decryptor().await);
175
176 let m = CipherMatch::builder().enc("mock").kid("v9").build();
179 let err = decryptor
180 .decrypt(Some(&m), b"", b"hello", b"v9", b"")
181 .await
182 .unwrap_err();
183 assert!(matches!(err, DecryptError::NoMatchingKey));
184 let m = CipherMatch::builder().kid("v1").build();
185 assert_eq!(decryptor.cipher_match(&m), Some(KeyMatchStrength::ByKeyId));
186 }
187
188 #[tokio::test]
189 async fn real_failures_are_not_retried() {
190 let decryptor = RetryingDecryptor::new(rotating_decryptor().await);
191
192 let m = CipherMatch::builder().enc("mock").kid("v0").build();
194 let err = decryptor
195 .decrypt(Some(&m), b"", b"hello", b"tampered", b"")
196 .await
197 .unwrap_err();
198 assert!(matches!(err, DecryptError::Other { .. }));
199 let m = CipherMatch::builder().kid("v1").build();
201 assert_eq!(decryptor.cipher_match(&m), None);
202 }
203}