use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes256Gcm, Nonce};
use async_trait::async_trait;
use base64::Engine;
use hkdf::Hkdf;
use sha2::Sha256;
use crate::error::{ProtonError, Result};
#[async_trait]
pub trait CacheRepository: Send + Sync {
async fn set(&self, key: &str, value: &str, tags: &[String]) -> Result<()>;
async fn get(&self, key: &str) -> Result<Option<String>>;
async fn remove(&self, key: &str) -> Result<()>;
async fn remove_by_tag(&self, tag: &str) -> Result<()>;
async fn clear(&self) -> Result<()>;
async fn get_by_tags(&self, tags: &[String]) -> Result<Vec<(String, String)>>;
}
pub async fn set_untagged(repo: &dyn CacheRepository, key: &str, value: &str) -> Result<()> {
repo.set(key, value, &[]).await
}
#[derive(Default)]
pub struct InMemoryCacheRepository {
state: Mutex<InMemoryState>,
}
#[derive(Default)]
struct InMemoryState {
entries: HashMap<String, String>,
key_to_tags: HashMap<String, HashSet<String>>,
tag_to_keys: HashMap<String, HashSet<String>>,
}
impl InMemoryCacheRepository {
pub fn new() -> Self {
Self::default()
}
pub fn shared() -> Arc<dyn CacheRepository> {
Arc::new(Self::new())
}
fn clear_tags_for_key(state: &mut InMemoryState, key: &str) {
if let Some(tags) = state.key_to_tags.remove(key) {
for tag in tags {
if let Some(keys) = state.tag_to_keys.get_mut(&tag) {
keys.remove(key);
if keys.is_empty() {
state.tag_to_keys.remove(&tag);
}
}
}
}
}
}
#[async_trait]
impl CacheRepository for InMemoryCacheRepository {
async fn set(&self, key: &str, value: &str, tags: &[String]) -> Result<()> {
let mut state = self.state.lock().unwrap();
Self::clear_tags_for_key(&mut state, key);
state.entries.insert(key.to_owned(), value.to_owned());
let tag_set: HashSet<String> = tags.iter().cloned().collect();
for tag in &tag_set {
state
.tag_to_keys
.entry(tag.clone())
.or_default()
.insert(key.to_owned());
}
state.key_to_tags.insert(key.to_owned(), tag_set);
Ok(())
}
async fn get(&self, key: &str) -> Result<Option<String>> {
let state = self.state.lock().unwrap();
Ok(state.entries.get(key).cloned())
}
async fn remove(&self, key: &str) -> Result<()> {
let mut state = self.state.lock().unwrap();
state.entries.remove(key);
Self::clear_tags_for_key(&mut state, key);
Ok(())
}
async fn remove_by_tag(&self, tag: &str) -> Result<()> {
let mut state = self.state.lock().unwrap();
let keys: Vec<String> = state
.tag_to_keys
.get(tag)
.map(|keys| keys.iter().cloned().collect())
.unwrap_or_default();
for key in keys {
state.entries.remove(&key);
Self::clear_tags_for_key(&mut state, &key);
}
Ok(())
}
async fn clear(&self) -> Result<()> {
let mut state = self.state.lock().unwrap();
state.entries.clear();
state.key_to_tags.clear();
state.tag_to_keys.clear();
Ok(())
}
async fn get_by_tags(&self, tags: &[String]) -> Result<Vec<(String, String)>> {
if tags.is_empty() {
return Ok(Vec::new());
}
let state = self.state.lock().unwrap();
let mut candidates: Option<HashSet<String>> = None;
for tag in tags {
match state.tag_to_keys.get(tag) {
Some(keys) => {
candidates = Some(match candidates {
Some(existing) => existing.intersection(keys).cloned().collect(),
None => keys.clone(),
});
}
None => return Ok(Vec::new()),
}
if candidates.as_ref().is_some_and(|c| c.is_empty()) {
return Ok(Vec::new());
}
}
let candidates = candidates.unwrap_or_default();
Ok(candidates
.into_iter()
.filter_map(|key| state.entries.get(&key).map(|v| (key.clone(), v.clone())))
.collect())
}
}
pub struct EncryptedCacheRepository {
inner: Arc<dyn CacheRepository>,
encryption_key: Vec<u8>,
}
const SALT_LEN: usize = 16;
const KEY_LEN: usize = 32;
const NONCE_LEN: usize = 12;
const TAG_LEN: usize = 16;
const ENCRYPTION_CONTEXT: &[u8] = b"Drive.EncryptedCacheRepository";
impl EncryptedCacheRepository {
pub fn new(inner: Arc<dyn CacheRepository>, encryption_key: impl Into<Vec<u8>>) -> Self {
Self {
inner,
encryption_key: encryption_key.into(),
}
}
pub fn shared(
inner: Arc<dyn CacheRepository>,
encryption_key: impl Into<Vec<u8>>,
) -> Arc<dyn CacheRepository> {
Arc::new(Self::new(inner, encryption_key))
}
fn derive(&self, salt: &[u8], entry_key: &str) -> Result<([u8; KEY_LEN], [u8; NONCE_LEN])> {
let mut info = ENCRYPTION_CONTEXT.to_vec();
info.extend_from_slice(entry_key.as_bytes());
let hk = Hkdf::<Sha256>::new(Some(salt), &self.encryption_key);
let mut okm = [0u8; KEY_LEN + NONCE_LEN];
hk.expand(&info, &mut okm)
.map_err(|e| ProtonError::invalid_operation(format!("cache HKDF expand: {e}")))?;
let mut key = [0u8; KEY_LEN];
let mut nonce = [0u8; NONCE_LEN];
key.copy_from_slice(&okm[..KEY_LEN]);
nonce.copy_from_slice(&okm[KEY_LEN..]);
Ok((key, nonce))
}
fn encrypt(&self, entry_key: &str, plaintext: &str) -> Result<String> {
let mut salt = [0u8; SALT_LEN];
getrandom::fill(&mut salt)
.map_err(|e| ProtonError::invalid_operation(format!("cache salt: {e}")))?;
let (key, nonce) = self.derive(&salt, entry_key)?;
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| ProtonError::invalid_operation(format!("cache cipher: {e}")))?;
let sealed = cipher
.encrypt(
&Nonce::from(nonce),
Payload {
msg: plaintext.as_bytes(),
aad: &[],
},
)
.map_err(|_| ProtonError::invalid_operation("cache encrypt failed"))?;
let mut out = Vec::with_capacity(SALT_LEN + sealed.len());
out.extend_from_slice(&salt);
out.extend_from_slice(&sealed);
Ok(base64::engine::general_purpose::STANDARD.encode(out))
}
fn decrypt(&self, entry_key: &str, encoded: &str) -> Result<Option<String>> {
let combined = base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|e| ProtonError::invalid_operation(format!("cache base64: {e}")))?;
if combined.len() < SALT_LEN + TAG_LEN {
return Err(ProtonError::invalid_operation("cache value too short"));
}
let (salt, sealed) = combined.split_at(SALT_LEN);
let (key, nonce) = self.derive(salt, entry_key)?;
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|e| ProtonError::invalid_operation(format!("cache cipher: {e}")))?;
match cipher.decrypt(
&Nonce::from(nonce),
Payload {
msg: sealed,
aad: &[],
},
) {
Ok(plaintext) => {
let text = String::from_utf8(plaintext)
.map_err(|e| ProtonError::invalid_operation(format!("cache utf8: {e}")))?;
Ok(Some(text))
}
Err(_) => Ok(None),
}
}
}
#[async_trait]
impl CacheRepository for EncryptedCacheRepository {
async fn set(&self, key: &str, value: &str, tags: &[String]) -> Result<()> {
let encrypted = self.encrypt(key, value)?;
self.inner.set(key, &encrypted, tags).await
}
async fn get(&self, key: &str) -> Result<Option<String>> {
let Some(encrypted) = self.inner.get(key).await? else {
return Ok(None);
};
match self.decrypt(key, &encrypted)? {
Some(value) => Ok(Some(value)),
None => {
self.inner.clear().await?;
Ok(None)
}
}
}
async fn remove(&self, key: &str) -> Result<()> {
self.inner.remove(key).await
}
async fn remove_by_tag(&self, tag: &str) -> Result<()> {
self.inner.remove_by_tag(tag).await
}
async fn clear(&self) -> Result<()> {
self.inner.clear().await
}
async fn get_by_tags(&self, tags: &[String]) -> Result<Vec<(String, String)>> {
let entries = self.inner.get_by_tags(tags).await?;
let mut out = Vec::with_capacity(entries.len());
for (key, encrypted) in entries {
match self.decrypt(&key, &encrypted)? {
Some(value) => out.push((key, value)),
None => {
self.inner.clear().await?;
return Ok(Vec::new());
}
}
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tags(values: &[&str]) -> Vec<String> {
values.iter().map(|s| s.to_string()).collect()
}
#[tokio::test]
async fn in_memory_round_trips_and_overwrites() {
let cache = InMemoryCacheRepository::new();
cache.set("k", "v1", &[]).await.unwrap();
assert_eq!(cache.get("k").await.unwrap().as_deref(), Some("v1"));
cache.set("k", "v2", &[]).await.unwrap();
assert_eq!(cache.get("k").await.unwrap().as_deref(), Some("v2"));
cache.remove("k").await.unwrap();
assert_eq!(cache.get("k").await.unwrap(), None);
}
#[tokio::test]
async fn in_memory_get_by_tags_intersects() {
let cache = InMemoryCacheRepository::new();
cache.set("a", "1", &tags(&["x", "y"])).await.unwrap();
cache.set("b", "2", &tags(&["x"])).await.unwrap();
cache.set("c", "3", &tags(&["y"])).await.unwrap();
let mut both = cache.get_by_tags(&tags(&["x", "y"])).await.unwrap();
both.sort();
assert_eq!(both, vec![("a".to_string(), "1".to_string())]);
let mut just_x = cache.get_by_tags(&tags(&["x"])).await.unwrap();
just_x.sort();
assert_eq!(
just_x,
vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string())
]
);
assert!(cache.get_by_tags(&[]).await.unwrap().is_empty());
}
#[tokio::test]
async fn in_memory_remove_by_tag_drops_only_tagged() {
let cache = InMemoryCacheRepository::new();
cache.set("a", "1", &tags(&["x"])).await.unwrap();
cache.set("b", "2", &tags(&["y"])).await.unwrap();
cache.remove_by_tag("x").await.unwrap();
assert_eq!(cache.get("a").await.unwrap(), None);
assert_eq!(cache.get("b").await.unwrap().as_deref(), Some("2"));
assert!(cache.get_by_tags(&tags(&["x"])).await.unwrap().is_empty());
}
#[tokio::test]
async fn encrypted_round_trips_and_hides_plaintext() {
let inner = InMemoryCacheRepository::shared();
let cache = EncryptedCacheRepository::new(inner.clone(), b"hunter2-master-key".to_vec());
cache
.set("share:1", "secret-value", &tags(&["t"]))
.await
.unwrap();
let stored = inner.get("share:1").await.unwrap().unwrap();
assert_ne!(stored, "secret-value");
assert_eq!(
cache.get("share:1").await.unwrap().as_deref(),
Some("secret-value")
);
let by_tag = cache.get_by_tags(&tags(&["t"])).await.unwrap();
assert_eq!(
by_tag,
vec![("share:1".to_string(), "secret-value".to_string())]
);
}
#[tokio::test]
async fn encrypted_wrong_key_is_a_miss_and_clears() {
let inner = InMemoryCacheRepository::shared();
EncryptedCacheRepository::new(inner.clone(), b"key-one".to_vec())
.set("k", "v", &[])
.await
.unwrap();
let other = EncryptedCacheRepository::new(inner.clone(), b"key-two".to_vec());
assert_eq!(other.get("k").await.unwrap(), None);
assert_eq!(inner.get("k").await.unwrap(), None);
}
#[tokio::test]
async fn encrypted_salt_is_random_per_write() {
let inner = InMemoryCacheRepository::shared();
let cache = EncryptedCacheRepository::new(inner.clone(), b"k".to_vec());
cache.set("k", "same", &[]).await.unwrap();
let first = inner.get("k").await.unwrap().unwrap();
cache.set("k", "same", &[]).await.unwrap();
let second = inner.get("k").await.unwrap().unwrap();
assert_ne!(first, second);
}
}