1use aes_gcm::aead::{Aead, KeyInit, OsRng};
10use aes_gcm::{Aes256Gcm, Nonce};
11use keyring::Entry;
12use std::collections::HashMap;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::Mutex;
15use tracing::warn;
16use zeroize::Zeroize;
17
18const SERVICE_NAME: &str = "com.dscode.secrets";
19
20const CACHE_TTL_SECS: u64 = 300;
22
23struct CachedSecret {
25 ciphertext: Vec<u8>,
27 cached_at: u64,
29}
30
31pub struct SecretStorage {
32 cache: Mutex<HashMap<String, CachedSecret>>,
36 enc_key: Mutex<Vec<u8>>,
38 nonce_counter: AtomicU64,
40}
41
42impl SecretStorage {
43 pub fn new() -> Self {
44 let key = Aes256Gcm::generate_key(OsRng);
46 Self {
47 cache: Mutex::new(HashMap::new()),
48 enc_key: Mutex::new(key.to_vec()),
49 nonce_counter: AtomicU64::new(0),
50 }
51 }
52
53 fn encrypt(&self, plaintext: &str) -> Result<Vec<u8>, String> {
56 let cipher = self.cipher()?;
57 let counter = self.nonce_counter.fetch_add(1, Ordering::Relaxed);
59 let nonce_bytes = Self::derive_nonce(counter);
60 let nonce = Nonce::from_slice(&nonce_bytes);
61 let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())
62 .map_err(|e| format!("Encryption failed: {}", e))?;
63 let mut out = Vec::with_capacity(12 + ciphertext.len());
65 out.extend_from_slice(&nonce_bytes);
66 out.extend_from_slice(&ciphertext);
67 Ok(out)
68 }
69
70 fn decrypt(&self, blob: &[u8]) -> Result<String, String> {
72 if blob.len() < 13 {
73 return Err("Ciphertext too short".into());
74 }
75 let cipher = self.cipher()?;
76 let (nonce_bytes, ciphertext) = blob.split_at(12);
77 let nonce = Nonce::from_slice(nonce_bytes);
78 let plaintext = cipher.decrypt(nonce, ciphertext)
79 .map_err(|_| String::from("Decryption failed (tampered or wrong key)"))?;
80 String::from_utf8(plaintext)
81 .map_err(|e| format!("Decrypted value is not valid UTF-8: {}", e))
82 }
83
84 fn cipher(&self) -> Result<Aes256Gcm, String> {
86 let key_guard = self.enc_key.lock().unwrap_or_else(|e| {
87 warn!("Encryption key lock poisoned, recovering");
88 e.into_inner()
89 });
90 if key_guard.len() != 32 {
91 return Err("Invalid encryption key length".into());
92 }
93 let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&key_guard);
94 Ok(Aes256Gcm::new(key))
95 }
96
97 fn derive_nonce(counter: u64) -> [u8; 12] {
99 let mut nonce = [0u8; 12];
100 nonce[4..12].copy_from_slice(&counter.to_be_bytes());
101 nonce
102 }
103
104 fn now_secs() -> u64 {
106 std::time::SystemTime::now()
107 .duration_since(std::time::UNIX_EPOCH)
108 .unwrap_or_default()
109 .as_secs()
110 }
111
112 fn evict_expired(cache: &mut HashMap<String, CachedSecret>) {
114 let now = Self::now_secs();
115 cache.retain(|_, entry| now.saturating_sub(entry.cached_at) < CACHE_TTL_SECS);
116 }
117
118 pub fn get(&self, extension_id: &str, key: &str) -> Result<Option<String>, String> {
120 let full_key = Self::make_key(extension_id, key);
121
122 {
124 let mut cache = self.cache.lock().unwrap_or_else(|e| {
125 warn!("Secrets cache lock poisoned, recovering");
126 e.into_inner()
127 });
128 Self::evict_expired(&mut cache);
129 if let Some(entry) = cache.get(&full_key) {
130 let plaintext = self.decrypt(&entry.ciphertext)?;
131 return Ok(Some(plaintext));
132 }
133 }
134
135 match Entry::new(SERVICE_NAME, &full_key) {
137 Ok(entry) => {
138 match entry.get_password() {
139 Ok(password) => {
140 let ciphertext = self.encrypt(&password)?;
142 let mut cache = self.cache.lock().unwrap_or_else(|e| {
143 warn!("Secrets cache lock poisoned, recovering");
144 e.into_inner()
145 });
146 cache.insert(full_key, CachedSecret {
147 ciphertext,
148 cached_at: Self::now_secs(),
149 });
150 Ok(Some(password))
151 }
152 Err(keyring::Error::NoEntry) => Ok(None),
153 Err(e) => Err(format!("Failed to retrieve secret: {}", e)),
154 }
155 }
156 Err(e) => Err(format!("Failed to access keychain: {}", e)),
157 }
158 }
159
160 pub fn set(&self, extension_id: &str, key: &str, value: &str) -> Result<(), String> {
162 let full_key = Self::make_key(extension_id, key);
163
164 match Entry::new(SERVICE_NAME, &full_key) {
166 Ok(entry) => {
167 entry.set_password(value).map_err(|e| format!("Failed to store secret: {}", e))?;
168
169 let ciphertext = self.encrypt(value)?;
171 let mut cache = self.cache.lock().unwrap_or_else(|e| {
172 warn!("Secrets cache lock poisoned, recovering");
173 e.into_inner()
174 });
175 cache.insert(full_key, CachedSecret {
176 ciphertext,
177 cached_at: Self::now_secs(),
178 });
179
180 Ok(())
181 }
182 Err(e) => Err(format!("Failed to access keychain: {}", e)),
183 }
184 }
185
186 pub fn delete(&self, extension_id: &str, key: &str) -> Result<(), String> {
188 let full_key = Self::make_key(extension_id, key);
189
190 match Entry::new(SERVICE_NAME, &full_key) {
192 Ok(entry) => {
193 match entry.delete_password() {
194 Ok(()) | Err(keyring::Error::NoEntry) => {
195 let mut cache = self.cache.lock().unwrap_or_else(|e| {
197 warn!("Secrets cache lock poisoned, recovering");
198 e.into_inner()
199 });
200 cache.remove(&full_key);
201 Ok(())
202 }
203 Err(e) => Err(format!("Failed to delete secret: {}", e)),
204 }
205 }
206 Err(e) => Err(format!("Failed to access keychain: {}", e)),
207 }
208 }
209
210 pub fn delete_all_for_extension(&self, extension_id: &str) -> Result<(), String> {
212 let mut cache = self.cache.lock().unwrap_or_else(|e| {
214 warn!("Secrets cache lock poisoned, recovering");
215 e.into_inner()
216 });
217 cache.retain(|k, _| !k.starts_with(&format!("{}:", extension_id)));
218 drop(cache);
219
220 Ok(())
225 }
226
227 pub fn clear_cache(&self) {
230 let mut cache = self.cache.lock().unwrap_or_else(|e| {
231 warn!("Secrets cache lock poisoned, recovering");
232 e.into_inner()
233 });
234 cache.clear();
235 }
236
237 fn make_key(extension_id: &str, key: &str) -> String {
239 format!("{}:{}", extension_id, key)
240 }
241}
242
243impl Default for SecretStorage {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249impl Drop for SecretStorage {
250 fn drop(&mut self) {
251 if let Ok(mut key) = self.enc_key.lock() {
253 key.zeroize();
254 }
255 if let Ok(mut cache) = self.cache.lock() {
258 for entry in cache.values_mut() {
260 entry.ciphertext.zeroize();
261 }
262 cache.clear();
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_secret_storage() {
273 let storage = SecretStorage::new();
274 let ext_id = "test.extension";
275 let key = "test_secret";
276 let value = "secret_value_123";
277
278 storage.set(ext_id, key, value).unwrap();
280
281 let retrieved = storage.get(ext_id, key).unwrap();
283 assert_eq!(retrieved, Some(value.to_string()));
284
285 storage.delete(ext_id, key).unwrap();
287
288 let after_delete = storage.get(ext_id, key).unwrap();
290 assert_eq!(after_delete, None);
291 }
292
293 #[test]
294 fn test_clear_cache() {
295 let storage = SecretStorage::new();
296 storage.set("ext", "key", "value").unwrap();
297
298 {
300 let cache = storage.cache.lock().unwrap();
301 assert!(!cache.is_empty());
302 }
303
304 storage.clear_cache();
305
306 {
308 let cache = storage.cache.lock().unwrap();
309 assert!(cache.is_empty());
310 }
311
312 storage.delete("ext", "key").ok();
314 }
315
316 #[test]
317 fn test_encryption_roundtrip() {
318 let storage = SecretStorage::new();
319 let plaintext = "my_secret_password";
320 let encrypted = storage.encrypt(plaintext).unwrap();
321 assert_ne!(&encrypted[12..], plaintext.as_bytes());
323 let decrypted = storage.decrypt(&encrypted).unwrap();
325 assert_eq!(decrypted, plaintext);
326 }
327
328 #[test]
329 fn test_cache_stores_encrypted() {
330 let storage = SecretStorage::new();
331 storage.set("ext2", "k2", "plaintext_value").unwrap();
332
333 let cache = storage.cache.lock().unwrap();
335 let entry = cache.get("ext2:k2").unwrap();
336 assert!(!String::from_utf8_lossy(&entry.ciphertext).contains("plaintext_value"));
338 }
339
340 #[test]
341 fn test_nonce_uniqueness() {
342 let storage = SecretStorage::new();
343 let ct1 = storage.encrypt("aaa").unwrap();
344 let ct2 = storage.encrypt("aaa").unwrap();
345 assert_ne!(&ct1[..12], &ct2[..12]);
347 }
348
349 #[test]
350 fn test_ttl_eviction() {
351 let storage = SecretStorage::new();
352 storage.set("ext3", "k3", "val").unwrap();
353
354 {
356 let mut cache = storage.cache.lock().unwrap();
357 if let Some(entry) = cache.get_mut("ext3:k3") {
358 entry.cached_at = 0; }
360 }
361
362 {
364 let mut cache = storage.cache.lock().unwrap();
365 SecretStorage::evict_expired(&mut cache);
366 }
367
368 {
370 let cache = storage.cache.lock().unwrap();
371 assert!(cache.get("ext3:k3").is_none());
372 }
373
374 storage.delete("ext3", "k3").ok();
376 }
377}