1#[cfg(feature = "secret-store")]
4use std::collections::hash_map::Entry;
5
6#[cfg(feature = "secret-store")]
7use chrono::Utc;
8#[cfg(feature = "secret-store")]
9use uuid::Uuid;
10
11#[cfg(feature = "secret-store")]
12use crate::crypto::{decrypt, encrypt};
13use crate::entities::{Page, Secret, SecretMetadata};
14use crate::error::StoreError;
15use crate::secret_store::SecretStore;
16use crate::store::StoreFuture;
17
18use super::InMemoryStore;
19
20impl SecretStore for InMemoryStore {
21 fn get_secret(&self, key: &str) -> StoreFuture<'_, Option<Secret>> {
22 let key = key.to_string();
23 Box::pin(async move {
24 #[cfg(feature = "secret-store")]
25 {
26 let master_key = self
27 .master_key
28 .as_ref()
29 .ok_or_else(|| StoreError::Crypto("no master key configured".to_string()))?;
30
31 let state = self.state.read().await;
32 let Some(encrypted) = state.secrets.get(&key) else {
33 return Ok(None);
34 };
35
36 let plaintext = decrypt(master_key, &encrypted.encrypted_value, &encrypted.nonce)
37 .map_err(|e| StoreError::Crypto(e.to_string()))?;
38
39 let value = String::from_utf8(plaintext)
40 .map_err(|e| StoreError::Crypto(format!("invalid UTF-8: {e}")))?;
41
42 Ok(Some(Secret {
43 id: encrypted.id,
44 key: encrypted.key.clone(),
45 value,
46 created_at: encrypted.created_at,
47 updated_at: encrypted.updated_at,
48 }))
49 }
50 #[cfg(not(feature = "secret-store"))]
51 {
52 let _ = key;
53 Err(StoreError::Crypto(
54 "secret-store feature not enabled".to_string(),
55 ))
56 }
57 })
58 }
59
60 fn set_secret(&self, key: &str, value: &str) -> StoreFuture<'_, Secret> {
61 let key = key.to_string();
62 let value = value.to_string();
63 Box::pin(async move {
64 #[cfg(feature = "secret-store")]
65 {
66 let master_key = self
67 .master_key
68 .as_ref()
69 .ok_or_else(|| StoreError::Crypto("no master key configured".to_string()))?;
70
71 let (encrypted_value, nonce) = encrypt(master_key, value.as_bytes())
72 .map_err(|e| StoreError::Crypto(e.to_string()))?;
73
74 let now = Utc::now();
75 let mut state = self.state.write().await;
76
77 let entry = state.secrets.entry(key.clone());
78 let encrypted = match entry {
79 Entry::Occupied(mut occ) => {
80 let existing = occ.get_mut();
81 existing.encrypted_value = encrypted_value;
82 existing.nonce = nonce;
83 existing.updated_at = now;
84 existing.clone()
85 }
86 Entry::Vacant(vac) => {
87 let new = super::EncryptedSecret {
88 id: Uuid::now_v7(),
89 key: key.clone(),
90 encrypted_value,
91 nonce,
92 created_at: now,
93 updated_at: now,
94 };
95 vac.insert(new.clone());
96 new
97 }
98 };
99
100 Ok(Secret {
101 id: encrypted.id,
102 key: encrypted.key,
103 value,
104 created_at: encrypted.created_at,
105 updated_at: encrypted.updated_at,
106 })
107 }
108 #[cfg(not(feature = "secret-store"))]
109 {
110 let _ = (key, value);
111 Err(StoreError::Crypto(
112 "secret-store feature not enabled".to_string(),
113 ))
114 }
115 })
116 }
117
118 fn delete_secret(&self, key: &str) -> StoreFuture<'_, bool> {
119 let key = key.to_string();
120 Box::pin(async move {
121 let mut state = self.state.write().await;
122 Ok(state.secrets.remove(&key).is_some())
123 })
124 }
125
126 fn list_secret_keys(&self, prefix: &str) -> StoreFuture<'_, Vec<String>> {
127 let prefix = prefix.to_string();
128 Box::pin(async move {
129 let state = self.state.read().await;
130 let keys: Vec<String> = state
131 .secrets
132 .keys()
133 .filter(|k| k.starts_with(&prefix))
134 .cloned()
135 .collect();
136 Ok(keys)
137 })
138 }
139
140 fn list_secrets(
141 &self,
142 prefix: &str,
143 page: u32,
144 per_page: u32,
145 ) -> StoreFuture<'_, Page<SecretMetadata>> {
146 let prefix = prefix.to_string();
147 Box::pin(async move {
148 let state = self.state.read().await;
149 let mut metadata: Vec<SecretMetadata> = state
150 .secrets
151 .values()
152 .filter(|s| s.key.starts_with(&prefix))
153 .map(|s| SecretMetadata {
154 id: s.id,
155 key: s.key.clone(),
156 created_at: s.created_at,
157 updated_at: s.updated_at,
158 })
159 .collect();
160
161 metadata.sort_by(|a, b| a.key.cmp(&b.key));
162
163 let total = metadata.len() as u64;
164 let offset = ((page.saturating_sub(1)) as usize) * (per_page as usize);
165 let items: Vec<SecretMetadata> = metadata
166 .into_iter()
167 .skip(offset)
168 .take(per_page as usize)
169 .collect();
170
171 Ok(Page {
172 items,
173 total,
174 page,
175 per_page,
176 })
177 })
178 }
179}
180
181#[cfg(all(test, feature = "secret-store"))]
182mod tests {
183 use crate::crypto::MasterKey;
184 use crate::memory::InMemoryStore;
185 use crate::secret_store::SecretStore;
186
187 fn test_store() -> InMemoryStore {
188 let key = MasterKey::from_bytes(&[42u8; 32]).unwrap();
189 let mut store = InMemoryStore::new();
190 store.set_master_key(key);
191 store
192 }
193
194 #[tokio::test]
195 async fn set_and_get_secret() {
196 let store = test_store();
197 let secret = store.set_secret("my/key", "my-value").await.unwrap();
198 assert_eq!(secret.key, "my/key");
199 assert_eq!(secret.value, "my-value");
200
201 let fetched = store.get_secret("my/key").await.unwrap().unwrap();
202 assert_eq!(fetched.value, "my-value");
203 assert_eq!(fetched.id, secret.id);
204 }
205
206 #[tokio::test]
207 async fn get_missing_secret_returns_none() {
208 let store = test_store();
209 let result = store.get_secret("does/not/exist").await.unwrap();
210 assert!(result.is_none());
211 }
212
213 #[tokio::test]
214 async fn set_secret_updates_existing() {
215 let store = test_store();
216 let first = store.set_secret("token", "v1").await.unwrap();
217 let second = store.set_secret("token", "v2").await.unwrap();
218
219 assert_eq!(first.id, second.id);
220 assert_eq!(second.value, "v2");
221
222 let fetched = store.get_secret("token").await.unwrap().unwrap();
223 assert_eq!(fetched.value, "v2");
224 }
225
226 #[tokio::test]
227 async fn delete_existing_secret() {
228 let store = test_store();
229 store.set_secret("to-delete", "val").await.unwrap();
230
231 let deleted = store.delete_secret("to-delete").await.unwrap();
232 assert!(deleted);
233
234 let fetched = store.get_secret("to-delete").await.unwrap();
235 assert!(fetched.is_none());
236 }
237
238 #[tokio::test]
239 async fn delete_missing_secret_returns_false() {
240 let store = test_store();
241 let deleted = store.delete_secret("nope").await.unwrap();
242 assert!(!deleted);
243 }
244
245 #[tokio::test]
246 async fn list_keys_with_prefix() {
247 let store = test_store();
248 store.set_secret("wf/inbox/token_a", "a").await.unwrap();
249 store.set_secret("wf/inbox/token_b", "b").await.unwrap();
250 store.set_secret("wf/veille/token_c", "c").await.unwrap();
251
252 let mut keys = store.list_secret_keys("wf/inbox/").await.unwrap();
253 keys.sort();
254 assert_eq!(keys, vec!["wf/inbox/token_a", "wf/inbox/token_b"]);
255 }
256
257 #[tokio::test]
258 async fn list_keys_empty_prefix_returns_all() {
259 let store = test_store();
260 store.set_secret("a", "1").await.unwrap();
261 store.set_secret("b", "2").await.unwrap();
262
263 let keys = store.list_secret_keys("").await.unwrap();
264 assert_eq!(keys.len(), 2);
265 }
266
267 #[tokio::test]
268 async fn operations_without_master_key_fail() {
269 let store = InMemoryStore::new();
270 let err = store.get_secret("key").await.unwrap_err();
271 assert!(err.to_string().contains("no master key"));
272
273 let err = store.set_secret("key", "val").await.unwrap_err();
274 assert!(err.to_string().contains("no master key"));
275 }
276
277 #[tokio::test]
278 async fn secret_value_is_encrypted_at_rest() {
279 let store = test_store();
280 store
281 .set_secret("sensitive", "plaintext-value")
282 .await
283 .unwrap();
284
285 let state = store.state.read().await;
286 let encrypted = state.secrets.get("sensitive").unwrap();
287 let as_str = String::from_utf8(encrypted.encrypted_value.clone());
288 assert!(
289 as_str.is_err() || as_str.unwrap() != "plaintext-value",
290 "value must be encrypted at rest"
291 );
292 }
293
294 #[tokio::test]
295 async fn set_secret_with_empty_value() {
296 let store = test_store();
297 let secret = store.set_secret("empty", "").await.unwrap();
298 assert_eq!(secret.value, "");
299
300 let fetched = store.get_secret("empty").await.unwrap().unwrap();
301 assert_eq!(fetched.value, "");
302 }
303
304 #[tokio::test]
305 async fn list_secrets_paginated() {
306 let store = test_store();
307 store.set_secret("a/1", "v").await.unwrap();
308 store.set_secret("a/2", "v").await.unwrap();
309 store.set_secret("a/3", "v").await.unwrap();
310 store.set_secret("b/1", "v").await.unwrap();
311
312 let page = store.list_secrets("a/", 1, 2).await.unwrap();
313 assert_eq!(page.total, 3);
314 assert_eq!(page.items.len(), 2);
315 assert_eq!(page.items[0].key, "a/1");
316 assert_eq!(page.items[1].key, "a/2");
317
318 let page2 = store.list_secrets("a/", 2, 2).await.unwrap();
319 assert_eq!(page2.items.len(), 1);
320 assert_eq!(page2.items[0].key, "a/3");
321 }
322}