1use std::collections::{HashMap, HashSet};
15use std::sync::{Arc, Mutex};
16
17use aes_gcm::aead::{Aead, KeyInit, Payload};
18use aes_gcm::{Aes256Gcm, Nonce};
19use async_trait::async_trait;
20use base64::Engine;
21use hkdf::Hkdf;
22use sha2::Sha256;
23
24use crate::error::{ProtonError, Result};
25
26#[async_trait]
32pub trait CacheRepository: Send + Sync {
33 async fn set(&self, key: &str, value: &str, tags: &[String]) -> Result<()>;
35
36 async fn get(&self, key: &str) -> Result<Option<String>>;
38
39 async fn remove(&self, key: &str) -> Result<()>;
41
42 async fn remove_by_tag(&self, tag: &str) -> Result<()>;
44
45 async fn clear(&self) -> Result<()>;
47
48 async fn get_by_tags(&self, tags: &[String]) -> Result<Vec<(String, String)>>;
52}
53
54pub async fn set_untagged(repo: &dyn CacheRepository, key: &str, value: &str) -> Result<()> {
56 repo.set(key, value, &[]).await
57}
58
59#[derive(Default)]
62pub struct InMemoryCacheRepository {
63 state: Mutex<InMemoryState>,
64}
65
66#[derive(Default)]
67struct InMemoryState {
68 entries: HashMap<String, String>,
69 key_to_tags: HashMap<String, HashSet<String>>,
70 tag_to_keys: HashMap<String, HashSet<String>>,
71}
72
73impl InMemoryCacheRepository {
74 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn shared() -> Arc<dyn CacheRepository> {
81 Arc::new(Self::new())
82 }
83
84 fn clear_tags_for_key(state: &mut InMemoryState, key: &str) {
85 if let Some(tags) = state.key_to_tags.remove(key) {
86 for tag in tags {
87 if let Some(keys) = state.tag_to_keys.get_mut(&tag) {
88 keys.remove(key);
89 if keys.is_empty() {
90 state.tag_to_keys.remove(&tag);
91 }
92 }
93 }
94 }
95 }
96}
97
98#[async_trait]
99impl CacheRepository for InMemoryCacheRepository {
100 async fn set(&self, key: &str, value: &str, tags: &[String]) -> Result<()> {
101 let mut state = self.state.lock().unwrap();
102 Self::clear_tags_for_key(&mut state, key);
103 state.entries.insert(key.to_owned(), value.to_owned());
104 let tag_set: HashSet<String> = tags.iter().cloned().collect();
105 for tag in &tag_set {
106 state
107 .tag_to_keys
108 .entry(tag.clone())
109 .or_default()
110 .insert(key.to_owned());
111 }
112 state.key_to_tags.insert(key.to_owned(), tag_set);
113 Ok(())
114 }
115
116 async fn get(&self, key: &str) -> Result<Option<String>> {
117 let state = self.state.lock().unwrap();
118 Ok(state.entries.get(key).cloned())
119 }
120
121 async fn remove(&self, key: &str) -> Result<()> {
122 let mut state = self.state.lock().unwrap();
123 state.entries.remove(key);
124 Self::clear_tags_for_key(&mut state, key);
125 Ok(())
126 }
127
128 async fn remove_by_tag(&self, tag: &str) -> Result<()> {
129 let mut state = self.state.lock().unwrap();
130 let keys: Vec<String> = state
131 .tag_to_keys
132 .get(tag)
133 .map(|keys| keys.iter().cloned().collect())
134 .unwrap_or_default();
135 for key in keys {
136 state.entries.remove(&key);
137 Self::clear_tags_for_key(&mut state, &key);
138 }
139 Ok(())
140 }
141
142 async fn clear(&self) -> Result<()> {
143 let mut state = self.state.lock().unwrap();
144 state.entries.clear();
145 state.key_to_tags.clear();
146 state.tag_to_keys.clear();
147 Ok(())
148 }
149
150 async fn get_by_tags(&self, tags: &[String]) -> Result<Vec<(String, String)>> {
151 if tags.is_empty() {
152 return Ok(Vec::new());
153 }
154 let state = self.state.lock().unwrap();
155 let mut candidates: Option<HashSet<String>> = None;
156 for tag in tags {
157 match state.tag_to_keys.get(tag) {
158 Some(keys) => {
159 candidates = Some(match candidates {
160 Some(existing) => existing.intersection(keys).cloned().collect(),
161 None => keys.clone(),
162 });
163 }
164 None => return Ok(Vec::new()),
165 }
166 if candidates.as_ref().is_some_and(|c| c.is_empty()) {
167 return Ok(Vec::new());
168 }
169 }
170 let candidates = candidates.unwrap_or_default();
171 Ok(candidates
172 .into_iter()
173 .filter_map(|key| state.entries.get(&key).map(|v| (key.clone(), v.clone())))
174 .collect())
175 }
176}
177
178pub struct EncryptedCacheRepository {
191 inner: Arc<dyn CacheRepository>,
192 encryption_key: Vec<u8>,
193}
194
195const SALT_LEN: usize = 16;
196const KEY_LEN: usize = 32;
197const NONCE_LEN: usize = 12;
198const TAG_LEN: usize = 16;
199const ENCRYPTION_CONTEXT: &[u8] = b"Drive.EncryptedCacheRepository";
200
201impl EncryptedCacheRepository {
202 pub fn new(inner: Arc<dyn CacheRepository>, encryption_key: impl Into<Vec<u8>>) -> Self {
204 Self {
205 inner,
206 encryption_key: encryption_key.into(),
207 }
208 }
209
210 pub fn shared(
212 inner: Arc<dyn CacheRepository>,
213 encryption_key: impl Into<Vec<u8>>,
214 ) -> Arc<dyn CacheRepository> {
215 Arc::new(Self::new(inner, encryption_key))
216 }
217
218 fn derive(&self, salt: &[u8], entry_key: &str) -> Result<([u8; KEY_LEN], [u8; NONCE_LEN])> {
220 let mut info = ENCRYPTION_CONTEXT.to_vec();
221 info.extend_from_slice(entry_key.as_bytes());
222 let hk = Hkdf::<Sha256>::new(Some(salt), &self.encryption_key);
223 let mut okm = [0u8; KEY_LEN + NONCE_LEN];
224 hk.expand(&info, &mut okm)
225 .map_err(|e| ProtonError::invalid_operation(format!("cache HKDF expand: {e}")))?;
226 let mut key = [0u8; KEY_LEN];
227 let mut nonce = [0u8; NONCE_LEN];
228 key.copy_from_slice(&okm[..KEY_LEN]);
229 nonce.copy_from_slice(&okm[KEY_LEN..]);
230 Ok((key, nonce))
231 }
232
233 fn encrypt(&self, entry_key: &str, plaintext: &str) -> Result<String> {
234 let mut salt = [0u8; SALT_LEN];
235 getrandom::fill(&mut salt)
236 .map_err(|e| ProtonError::invalid_operation(format!("cache salt: {e}")))?;
237 let (key, nonce) = self.derive(&salt, entry_key)?;
238 let cipher = Aes256Gcm::new_from_slice(&key)
239 .map_err(|e| ProtonError::invalid_operation(format!("cache cipher: {e}")))?;
240 let sealed = cipher
243 .encrypt(
244 &Nonce::from(nonce),
245 Payload {
246 msg: plaintext.as_bytes(),
247 aad: &[],
248 },
249 )
250 .map_err(|_| ProtonError::invalid_operation("cache encrypt failed"))?;
251 let mut out = Vec::with_capacity(SALT_LEN + sealed.len());
252 out.extend_from_slice(&salt);
253 out.extend_from_slice(&sealed);
254 Ok(base64::engine::general_purpose::STANDARD.encode(out))
255 }
256
257 fn decrypt(&self, entry_key: &str, encoded: &str) -> Result<Option<String>> {
260 let combined = base64::engine::general_purpose::STANDARD
261 .decode(encoded)
262 .map_err(|e| ProtonError::invalid_operation(format!("cache base64: {e}")))?;
263 if combined.len() < SALT_LEN + TAG_LEN {
264 return Err(ProtonError::invalid_operation("cache value too short"));
265 }
266 let (salt, sealed) = combined.split_at(SALT_LEN);
267 let (key, nonce) = self.derive(salt, entry_key)?;
268 let cipher = Aes256Gcm::new_from_slice(&key)
269 .map_err(|e| ProtonError::invalid_operation(format!("cache cipher: {e}")))?;
270 match cipher.decrypt(
271 &Nonce::from(nonce),
272 Payload {
273 msg: sealed,
274 aad: &[],
275 },
276 ) {
277 Ok(plaintext) => {
278 let text = String::from_utf8(plaintext)
279 .map_err(|e| ProtonError::invalid_operation(format!("cache utf8: {e}")))?;
280 Ok(Some(text))
281 }
282 Err(_) => Ok(None),
284 }
285 }
286}
287
288#[async_trait]
289impl CacheRepository for EncryptedCacheRepository {
290 async fn set(&self, key: &str, value: &str, tags: &[String]) -> Result<()> {
291 let encrypted = self.encrypt(key, value)?;
292 self.inner.set(key, &encrypted, tags).await
293 }
294
295 async fn get(&self, key: &str) -> Result<Option<String>> {
296 let Some(encrypted) = self.inner.get(key).await? else {
297 return Ok(None);
298 };
299 match self.decrypt(key, &encrypted)? {
300 Some(value) => Ok(Some(value)),
301 None => {
302 self.inner.clear().await?;
303 Ok(None)
304 }
305 }
306 }
307
308 async fn remove(&self, key: &str) -> Result<()> {
309 self.inner.remove(key).await
310 }
311
312 async fn remove_by_tag(&self, tag: &str) -> Result<()> {
313 self.inner.remove_by_tag(tag).await
314 }
315
316 async fn clear(&self) -> Result<()> {
317 self.inner.clear().await
318 }
319
320 async fn get_by_tags(&self, tags: &[String]) -> Result<Vec<(String, String)>> {
321 let entries = self.inner.get_by_tags(tags).await?;
322 let mut out = Vec::with_capacity(entries.len());
323 for (key, encrypted) in entries {
324 match self.decrypt(&key, &encrypted)? {
325 Some(value) => out.push((key, value)),
326 None => {
327 self.inner.clear().await?;
328 return Ok(Vec::new());
329 }
330 }
331 }
332 Ok(out)
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 fn tags(values: &[&str]) -> Vec<String> {
341 values.iter().map(|s| s.to_string()).collect()
342 }
343
344 #[tokio::test]
345 async fn in_memory_round_trips_and_overwrites() {
346 let cache = InMemoryCacheRepository::new();
347 cache.set("k", "v1", &[]).await.unwrap();
348 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some("v1"));
349 cache.set("k", "v2", &[]).await.unwrap();
350 assert_eq!(cache.get("k").await.unwrap().as_deref(), Some("v2"));
351 cache.remove("k").await.unwrap();
352 assert_eq!(cache.get("k").await.unwrap(), None);
353 }
354
355 #[tokio::test]
356 async fn in_memory_get_by_tags_intersects() {
357 let cache = InMemoryCacheRepository::new();
358 cache.set("a", "1", &tags(&["x", "y"])).await.unwrap();
359 cache.set("b", "2", &tags(&["x"])).await.unwrap();
360 cache.set("c", "3", &tags(&["y"])).await.unwrap();
361
362 let mut both = cache.get_by_tags(&tags(&["x", "y"])).await.unwrap();
363 both.sort();
364 assert_eq!(both, vec![("a".to_string(), "1".to_string())]);
365
366 let mut just_x = cache.get_by_tags(&tags(&["x"])).await.unwrap();
367 just_x.sort();
368 assert_eq!(
369 just_x,
370 vec![
371 ("a".to_string(), "1".to_string()),
372 ("b".to_string(), "2".to_string())
373 ]
374 );
375
376 assert!(cache.get_by_tags(&[]).await.unwrap().is_empty());
377 }
378
379 #[tokio::test]
380 async fn in_memory_remove_by_tag_drops_only_tagged() {
381 let cache = InMemoryCacheRepository::new();
382 cache.set("a", "1", &tags(&["x"])).await.unwrap();
383 cache.set("b", "2", &tags(&["y"])).await.unwrap();
384 cache.remove_by_tag("x").await.unwrap();
385 assert_eq!(cache.get("a").await.unwrap(), None);
386 assert_eq!(cache.get("b").await.unwrap().as_deref(), Some("2"));
387 assert!(cache.get_by_tags(&tags(&["x"])).await.unwrap().is_empty());
389 }
390
391 #[tokio::test]
392 async fn encrypted_round_trips_and_hides_plaintext() {
393 let inner = InMemoryCacheRepository::shared();
394 let cache = EncryptedCacheRepository::new(inner.clone(), b"hunter2-master-key".to_vec());
395 cache
396 .set("share:1", "secret-value", &tags(&["t"]))
397 .await
398 .unwrap();
399
400 let stored = inner.get("share:1").await.unwrap().unwrap();
402 assert_ne!(stored, "secret-value");
403
404 assert_eq!(
406 cache.get("share:1").await.unwrap().as_deref(),
407 Some("secret-value")
408 );
409 let by_tag = cache.get_by_tags(&tags(&["t"])).await.unwrap();
411 assert_eq!(
412 by_tag,
413 vec![("share:1".to_string(), "secret-value".to_string())]
414 );
415 }
416
417 #[tokio::test]
418 async fn encrypted_wrong_key_is_a_miss_and_clears() {
419 let inner = InMemoryCacheRepository::shared();
420 EncryptedCacheRepository::new(inner.clone(), b"key-one".to_vec())
421 .set("k", "v", &[])
422 .await
423 .unwrap();
424
425 let other = EncryptedCacheRepository::new(inner.clone(), b"key-two".to_vec());
427 assert_eq!(other.get("k").await.unwrap(), None);
428 assert_eq!(inner.get("k").await.unwrap(), None);
429 }
430
431 #[tokio::test]
432 async fn encrypted_salt_is_random_per_write() {
433 let inner = InMemoryCacheRepository::shared();
434 let cache = EncryptedCacheRepository::new(inner.clone(), b"k".to_vec());
435 cache.set("k", "same", &[]).await.unwrap();
436 let first = inner.get("k").await.unwrap().unwrap();
437 cache.set("k", "same", &[]).await.unwrap();
438 let second = inner.get("k").await.unwrap().unwrap();
439 assert_ne!(first, second);
441 }
442}