1use async_trait::async_trait;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use crate::crypto::Aead;
7use crate::storage::{StorageBackend, StorageEntry, StorageResult};
8
9pub struct Barrier<B: StorageBackend> {
12 inner: B,
13 aead: Arc<dyn Aead>,
14}
15
16impl<B: StorageBackend> Barrier<B> {
17 pub fn new(inner: B, aead: Arc<dyn Aead>) -> Self {
18 Self { inner, aead }
19 }
20}
21
22#[derive(Debug, Default, Clone, Serialize, Deserialize)]
26pub struct RewrapReport {
27 pub scanned: usize,
28 pub rewrapped: usize,
29 pub unchanged: usize,
30 pub contended: usize,
31 pub failed: usize,
32}
33
34#[async_trait]
39pub trait KeyRotation: Send + Sync {
40 async fn rewrap_all(&self) -> StorageResult<RewrapReport>;
44
45 fn active_key_id(&self) -> String;
48}
49
50#[async_trait]
51impl<B: StorageBackend> KeyRotation for Barrier<B> {
52 async fn rewrap_all(&self) -> StorageResult<RewrapReport> {
53 let mut report = RewrapReport::default();
54
55 for path in self.inner.list("").await? {
58 report.scanned += 1;
59 let Some(entry) = self.inner.get(&path).await? else {
60 continue; };
62 if self.aead.is_current(&entry.value) {
63 report.unchanged += 1;
64 continue;
65 }
66
67 let Ok(plaintext) = self.aead.open(&entry.value) else {
68 tracing::error!(path, "cannot rewrap: no key in the ring opens this value");
72 report.failed += 1;
73 continue;
74 };
75 let resealed = self
76 .aead
77 .seal(&plaintext)
78 .map_err(|e| crate::storage::StorageError::Backend(e.to_string()))?;
79
80 let replaced = self
84 .inner
85 .replace_if_unchanged(
86 &path,
87 &entry.value,
88 StorageEntry {
89 value: resealed,
90 expires_at: entry.expires_at,
91 },
92 )
93 .await?;
94 if replaced {
95 report.rewrapped += 1;
96 } else {
97 report.contended += 1;
98 }
99 }
100
101 tracing::info!(
102 scanned = report.scanned,
103 rewrapped = report.rewrapped,
104 failed = report.failed,
105 "rewrap pass complete"
106 );
107 Ok(report)
108 }
109
110 fn active_key_id(&self) -> String {
111 self.aead.active_key_id()
112 }
113}
114
115#[async_trait]
116impl<B: StorageBackend> StorageBackend for Barrier<B> {
117 async fn get(&self, path: &str) -> StorageResult<Option<StorageEntry>> {
118 let Some(entry) = self.inner.get(path).await? else {
119 return Ok(None);
120 };
121 let plaintext = self
122 .aead
123 .open(&entry.value)
124 .map_err(|e| crate::storage::StorageError::Backend(e.to_string()))?;
125 Ok(Some(StorageEntry {
126 value: plaintext,
127 expires_at: entry.expires_at,
128 }))
129 }
130
131 async fn put(&self, path: &str, entry: StorageEntry) -> StorageResult<()> {
132 let ciphertext = self
133 .aead
134 .seal(&entry.value)
135 .map_err(|e| crate::storage::StorageError::Backend(e.to_string()))?;
136 self.inner
137 .put(
138 path,
139 StorageEntry {
140 value: ciphertext,
141 expires_at: entry.expires_at,
142 },
143 )
144 .await
145 }
146
147 async fn delete(&self, path: &str) -> StorageResult<()> {
148 self.inner.delete(path).await
149 }
150
151 async fn list(&self, prefix: &str) -> StorageResult<Vec<String>> {
152 self.inner.list(prefix).await
153 }
154
155 async fn ping(&self) -> StorageResult<()> {
159 self.inner.ping().await
160 }
161
162 async fn list_expired(
163 &self,
164 prefix: &str,
165 now: chrono::DateTime<chrono::Utc>,
166 ) -> StorageResult<Vec<String>> {
167 self.inner.list_expired(prefix, now).await
170 }
171
172 async fn replace_if_unchanged(
173 &self,
174 path: &str,
175 expected: &[u8],
176 entry: StorageEntry,
177 ) -> StorageResult<bool> {
178 let ciphertext = self
179 .aead
180 .seal(&entry.value)
181 .map_err(|e| crate::storage::StorageError::Backend(e.to_string()))?;
182 self.inner
183 .replace_if_unchanged(
184 path,
185 expected,
186 StorageEntry {
187 value: ciphertext,
188 expires_at: entry.expires_at,
189 },
190 )
191 .await
192 }
193
194 async fn try_acquire_lock(&self, key: &str) -> StorageResult<bool> {
195 self.inner.try_acquire_lock(key).await
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::crypto::{Aes256GcmAead, KeyRing};
203 use crate::storage::StorageResult;
204 use std::collections::HashMap;
205 use std::sync::Mutex;
206
207 const OLD: [u8; 32] = [7u8; 32];
208 const NEW: [u8; 32] = [11u8; 32];
209
210 #[derive(Default)]
211 struct MemStorage(Mutex<HashMap<String, StorageEntry>>);
212
213 #[async_trait]
214 impl StorageBackend for MemStorage {
215 async fn get(&self, path: &str) -> StorageResult<Option<StorageEntry>> {
216 Ok(self.0.lock().unwrap().get(path).cloned())
217 }
218 async fn put(&self, path: &str, entry: StorageEntry) -> StorageResult<()> {
219 self.0.lock().unwrap().insert(path.to_string(), entry);
220 Ok(())
221 }
222 async fn delete(&self, path: &str) -> StorageResult<()> {
223 self.0.lock().unwrap().remove(path);
224 Ok(())
225 }
226 async fn list(&self, prefix: &str) -> StorageResult<Vec<String>> {
227 Ok(self
228 .0
229 .lock()
230 .unwrap()
231 .keys()
232 .filter(|k| k.starts_with(prefix))
233 .cloned()
234 .collect())
235 }
236 }
237
238 fn entry(value: &[u8]) -> StorageEntry {
239 StorageEntry {
240 value: value.to_vec(),
241 expires_at: None,
242 }
243 }
244
245 #[tokio::test]
246 async fn rewrap_moves_pre_rotation_values_onto_the_active_key() {
247 let raw = MemStorage::default();
249 let legacy = Aes256GcmAead::new(&OLD);
250 raw.put("secret/a", entry(&legacy.seal(b"alpha").unwrap()))
251 .await
252 .unwrap();
253
254 let barrier = Barrier::new(raw, Arc::new(KeyRing::new(&NEW, &[OLD])));
255 assert_eq!(barrier.get("secret/a").await.unwrap().unwrap().value, b"alpha");
256
257 let report = barrier.rewrap_all().await.unwrap();
258 assert_eq!((report.scanned, report.rewrapped, report.failed), (1, 1, 0));
259 assert_eq!(barrier.get("secret/a").await.unwrap().unwrap().value, b"alpha");
260
261 let rotated = Barrier::new(barrier.inner, Arc::new(KeyRing::new(&NEW, &[])));
263 assert_eq!(rotated.get("secret/a").await.unwrap().unwrap().value, b"alpha");
264 }
265
266 #[tokio::test]
267 async fn rewrap_is_idempotent() {
268 let barrier = Barrier::new(MemStorage::default(), Arc::new(KeyRing::new(&NEW, &[OLD])));
269 barrier.put("secret/a", entry(b"alpha")).await.unwrap();
270
271 let first = barrier.rewrap_all().await.unwrap();
272 assert_eq!(first.unchanged, 1, "a fresh write is already on the active key");
273
274 let second = barrier.rewrap_all().await.unwrap();
275 assert_eq!((second.rewrapped, second.unchanged), (0, 1));
276 }
277
278 #[tokio::test]
281 async fn rewrap_reports_values_it_cannot_open() {
282 let raw = MemStorage::default();
283 let orphan = KeyRing::new(&OLD, &[]);
284 raw.put("secret/orphan", entry(&orphan.seal(b"lost").unwrap()))
285 .await
286 .unwrap();
287
288 let barrier = Barrier::new(raw, Arc::new(KeyRing::new(&NEW, &[])));
289 barrier.put("secret/fine", entry(b"kept")).await.unwrap();
290
291 let report = barrier.rewrap_all().await.unwrap();
292 assert_eq!(report.failed, 1, "the orphan should be reported");
293 assert_eq!(report.unchanged, 1, "the healthy value should still be seen");
294 assert_eq!(barrier.get("secret/fine").await.unwrap().unwrap().value, b"kept");
295 }
296
297 #[tokio::test]
301 async fn conditional_replace_declines_when_the_value_moved() {
302 let raw = MemStorage::default();
303 raw.put("secret/a", entry(b"first")).await.unwrap();
304
305 let replaced = raw
306 .replace_if_unchanged("secret/a", b"stale-expectation", entry(b"second"))
307 .await
308 .unwrap();
309 assert!(!replaced);
310 assert_eq!(raw.get("secret/a").await.unwrap().unwrap().value, b"first");
311
312 let replaced = raw
313 .replace_if_unchanged("secret/a", b"first", entry(b"second"))
314 .await
315 .unwrap();
316 assert!(replaced);
317 assert_eq!(raw.get("secret/a").await.unwrap().unwrap().value, b"second");
318 }
319}