huskarl_core/crypto/cipher/
refreshable.rs1use std::{borrow::Cow, pin::Pin, sync::Arc};
2
3use crate::{
4 crypto::{
5 KeyMatchStrength,
6 cipher::{
7 AeadCipherSelector, AeadDecryptor, AeadEncryptor, AeadOutput, AeadSealer,
8 AeadSealerSelector, AeadUnsealer, AeadV1Cipher, CipherMatch, DecryptError,
9 },
10 refreshable::Refreshable,
11 },
12 error::Error,
13 platform::{MaybeSendBoxFuture, MaybeSendFuture, MaybeSendSync},
14};
15
16#[derive(Debug)]
44pub struct RefreshableCipher<C> {
45 inner: Arc<Refreshable<C>>,
46}
47
48impl<C> Clone for RefreshableCipher<C> {
49 fn clone(&self) -> Self {
50 Self {
51 inner: Arc::clone(&self.inner),
52 }
53 }
54}
55
56#[bon::bon]
57impl<C: std::fmt::Debug + MaybeSendSync + 'static> RefreshableCipher<C> {
58 #[builder]
68 pub async fn new(
69 factory: impl Fn() -> Pin<Box<dyn MaybeSendFuture<Output = Result<C, Error>>>>
70 + MaybeSendSync
71 + 'static,
72 ) -> Result<Self, Error> {
73 let refreshable = Refreshable::builder().factory(factory).build().await?;
74 Ok(Self {
75 inner: Arc::new(refreshable),
76 })
77 }
78
79 pub async fn refresh(&self) -> Result<bool, Error> {
93 self.inner.refresh().await
94 }
95}
96
97impl<C: AeadEncryptor + 'static> AeadEncryptor for RefreshableCipher<C> {
98 fn enc_algorithm(&self) -> Cow<'_, str> {
99 Cow::Owned(self.inner.load().enc_algorithm().into_owned())
100 }
101
102 fn key_id(&self) -> Option<Cow<'_, str>> {
103 self.inner
104 .load()
105 .key_id()
106 .map(|kid| Cow::Owned(kid.into_owned()))
107 }
108
109 fn encrypt<'a>(
110 &'a self,
111 plaintext: &'a [u8],
112 aad: &'a [u8],
113 ) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>> {
114 Box::pin(async move { self.inner.load_full().encrypt(plaintext, aad).await })
115 }
116}
117
118impl<C: AeadDecryptor + 'static> AeadDecryptor for RefreshableCipher<C> {
119 fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
120 self.inner.load().cipher_match(m)
121 }
122
123 fn decrypt<'a>(
124 &'a self,
125 cipher_match: Option<&'a CipherMatch<'a>>,
126 nonce: &'a [u8],
127 ciphertext: &'a [u8],
128 tag: &'a [u8],
129 aad: &'a [u8],
130 ) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
131 Box::pin(async move {
132 self.inner
133 .load_full()
134 .decrypt(cipher_match, nonce, ciphertext, tag, aad)
135 .await
136 })
137 }
138
139 fn try_refresh(&self) -> MaybeSendBoxFuture<'_, bool> {
140 Box::pin(async move { self.refresh().await.is_ok() })
144 }
145}
146
147impl<C: AeadEncryptor + 'static> AeadCipherSelector for RefreshableCipher<C> {
148 fn select_cipher(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadEncryptor>> {
149 let encryptor: Arc<dyn AeadEncryptor> = self.inner.load_full();
153 Box::pin(async move { encryptor })
154 }
155}
156
157impl<C: AeadEncryptor + 'static> AeadSealerSelector for RefreshableCipher<C> {
158 fn select_sealer(&self) -> MaybeSendBoxFuture<'_, Arc<dyn AeadSealer>> {
159 let sealer: Arc<dyn AeadSealer> = Arc::new(AeadV1Cipher::new(self.inner.load_full()));
163 Box::pin(async move { sealer })
164 }
165}
166
167impl<C: AeadDecryptor + 'static> AeadUnsealer for RefreshableCipher<C> {
168 fn unseal<'a>(
169 &'a self,
170 cipher_match: Option<&'a CipherMatch<'a>>,
171 bundle: &'a [u8],
172 aad: &'a [u8],
173 ) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
174 Box::pin(async move {
175 AeadV1Cipher::new(self.inner.load_full())
176 .unseal(cipher_match, bundle, aad)
177 .await
178 })
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use std::sync::atomic::{AtomicU32, Ordering};
185
186 use super::*;
187
188 #[derive(Debug)]
189 struct KeyedCipher {
190 kid: String,
191 }
192
193 impl AeadEncryptor for KeyedCipher {
194 fn enc_algorithm(&self) -> Cow<'_, str> {
195 "mock".into()
196 }
197
198 fn key_id(&self) -> Option<Cow<'_, str>> {
199 Some(Cow::Borrowed(&self.kid))
200 }
201
202 fn encrypt<'a>(
203 &'a self,
204 plaintext: &'a [u8],
205 _aad: &'a [u8],
206 ) -> MaybeSendBoxFuture<'a, Result<AeadOutput, Error>> {
207 Box::pin(async move {
208 Ok(AeadOutput {
209 nonce: vec![],
210 ciphertext: plaintext.to_vec(),
211 tag: self.kid.clone().into_bytes(),
212 })
213 })
214 }
215 }
216
217 impl AeadDecryptor for KeyedCipher {
218 fn cipher_match(&self, m: &CipherMatch<'_>) -> Option<KeyMatchStrength> {
219 m.strength_for("mock", Some(&self.kid))
220 }
221
222 fn decrypt<'a>(
223 &'a self,
224 _cipher_match: Option<&'a CipherMatch<'a>>,
225 _nonce: &'a [u8],
226 ciphertext: &'a [u8],
227 tag: &'a [u8],
228 _aad: &'a [u8],
229 ) -> MaybeSendBoxFuture<'a, Result<Vec<u8>, DecryptError>> {
230 Box::pin(async move {
231 if tag == self.kid.as_bytes() {
232 Ok(ciphertext.to_vec())
233 } else {
234 Err(Error::from(crate::error::ErrorKind::Crypto).into())
235 }
236 })
237 }
238 }
239
240 async fn versioned_cipher() -> RefreshableCipher<KeyedCipher> {
241 let counter = Arc::new(AtomicU32::new(0));
242 let factory =
243 move || -> Pin<Box<dyn MaybeSendFuture<Output = Result<KeyedCipher, Error>>>> {
244 let counter = Arc::clone(&counter);
245 Box::pin(async move {
246 let n = counter.fetch_add(1, Ordering::SeqCst);
247 Ok(KeyedCipher {
248 kid: format!("v{n}"),
249 })
250 })
251 };
252 RefreshableCipher::builder()
253 .factory(factory)
254 .build()
255 .await
256 .unwrap()
257 }
258
259 #[tokio::test]
260 async fn refresh_swaps_cipher_for_all_clones() {
261 let cipher = versioned_cipher().await;
262 let clone = cipher.clone();
263
264 assert_eq!(cipher.key_id().as_deref(), Some("v0"));
265 assert!(cipher.refresh().await.unwrap());
266 assert_eq!(clone.key_id().as_deref(), Some("v1"));
267 }
268
269 #[tokio::test]
270 async fn try_refresh_swaps_through_dyn_decryptor() {
271 let cipher = versioned_cipher().await;
272 let output = cipher.encrypt(b"hello", b"").await.unwrap();
273
274 let decryptor: Arc<dyn AeadDecryptor> = Arc::new(cipher);
275 decryptor
276 .decrypt(None, &output.nonce, &output.ciphertext, &output.tag, b"")
277 .await
278 .unwrap();
279
280 assert!(decryptor.try_refresh().await);
281
282 let err = decryptor
284 .decrypt(None, &output.nonce, &output.ciphertext, &output.tag, b"")
285 .await
286 .unwrap_err();
287 let DecryptError::Other { source } = err else {
288 unreachable!("expected DecryptError::Other, got {err:?}");
289 };
290 assert_eq!(source.kind(), crate::error::ErrorKind::Crypto);
291
292 let m = CipherMatch::builder().kid("v1").build();
293 assert_eq!(decryptor.cipher_match(&m), Some(KeyMatchStrength::ByKeyId));
294 }
295}