1use super::{BoxedAuditSink, Secret, SecretAuditEvent, SecretError, SecretStore};
7use crate::crypto::{Aes256GcmCrypto, CryptoError, EncryptedData, KeyUtils};
8use crate::secrets::config::{FileConfig, FileFormat};
9use async_trait::async_trait;
10use serde_json::Value;
11use std::collections::HashMap;
12use std::time::SystemTime;
13use tokio::sync::RwLock;
14
15pub struct FileSecretStore {
17 config: FileConfig,
18 audit_sink: Option<BoxedAuditSink>,
19 agent_id: String,
20 cache: RwLock<Option<(SystemTime, HashMap<String, String>)>>,
21}
22
23impl FileSecretStore {
24 pub async fn new(
26 config: FileConfig,
27 audit_sink: Option<BoxedAuditSink>,
28 agent_id: String,
29 ) -> Result<Self, SecretError> {
30 Ok(Self {
31 config,
32 audit_sink,
33 agent_id,
34 cache: RwLock::new(None),
35 })
36 }
37
38 async fn log_audit_event(&self, event: SecretAuditEvent) -> Result<(), SecretError> {
42 if let Some(audit_sink) = &self.audit_sink {
43 if let Err(e) = audit_sink.log_event(event).await {
44 match audit_sink.failure_mode() {
45 crate::secrets::auditing::AuditFailureMode::Strict => {
46 return Err(SecretError::AuditFailed {
47 message: format!("Audit logging failed (strict mode): {}", e),
48 });
49 }
50 crate::secrets::auditing::AuditFailureMode::Permissive => {
51 tracing::warn!("Audit logging failed (permissive mode): {}", e);
52 }
53 }
54 }
55 }
56 Ok(())
57 }
58
59 async fn load_secrets_cached(&self) -> Result<HashMap<String, String>, SecretError> {
68 let path = self.config.path.clone();
69
70 let mtime = tokio::task::spawn_blocking(move || -> Result<SystemTime, SecretError> {
73 let file = std::fs::File::open(&path).map_err(|e| SecretError::IoError {
74 message: format!("Failed to open secrets file for mtime check: {}", e),
75 })?;
76 file.metadata()
77 .and_then(|m| m.modified())
78 .map_err(|e| SecretError::IoError {
79 message: format!("Failed to get mtime from open file handle: {}", e),
80 })
81 })
82 .await
83 .map_err(|e| SecretError::IoError {
84 message: format!("Blocking task panicked: {}", e),
85 })??;
86
87 {
89 let guard = self.cache.read().await;
90 if let Some((cached_mtime, ref secrets)) = *guard {
91 if cached_mtime == mtime {
92 return Ok(secrets.clone());
93 }
94 }
95 }
96
97 let secrets = self.load_secrets().await?;
99
100 {
102 let mut guard = self.cache.write().await;
103 *guard = Some((mtime, secrets.clone()));
104 }
105
106 Ok(secrets)
107 }
108
109 async fn load_secrets(&self) -> Result<HashMap<String, String>, SecretError> {
115 let path = self.config.path.clone();
116 let encryption_enabled = self.config.encryption.enabled;
117
118 let file_content = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, SecretError> {
120 let file = std::fs::File::open(&path).map_err(|e| SecretError::IoError {
121 message: format!("Failed to open secrets file: {}", e),
122 })?;
123
124 let lock = fd_lock::RwLock::new(file);
125 let guard = lock.read().map_err(|e| SecretError::IoError {
126 message: format!("Failed to acquire read lock on secrets file: {}", e),
127 })?;
128
129 use std::io::Read;
131 let mut buf = Vec::new();
132 (&*guard)
133 .read_to_end(&mut buf)
134 .map_err(|e| SecretError::IoError {
135 message: format!("Failed to read secrets file: {}", e),
136 })?;
137 Ok(buf)
139 })
140 .await
141 .map_err(|e| SecretError::IoError {
142 message: format!("Blocking task panicked: {}", e),
143 })??;
144
145 let secrets_data = if encryption_enabled {
146 self.decrypt_content(&file_content).await?
148 } else {
149 String::from_utf8(file_content).map_err(|e| SecretError::ParseError {
151 message: format!("Invalid UTF-8 in secrets file: {}", e),
152 })?
153 };
154
155 self.parse_secrets_data(&secrets_data)
157 }
158
159 async fn decrypt_content(&self, encrypted_content: &[u8]) -> Result<String, SecretError> {
161 let key = self.get_decryption_key().await?;
163
164 let encrypted_data: EncryptedData =
166 serde_json::from_slice(encrypted_content).map_err(|e| SecretError::ParseError {
167 message: format!("Failed to parse encrypted data: {}", e),
168 })?;
169
170 if encrypted_data.algorithm != self.config.encryption.algorithm {
172 return Err(SecretError::CryptoError {
173 message: format!(
174 "Algorithm mismatch: expected {}, found {}",
175 self.config.encryption.algorithm, encrypted_data.algorithm
176 ),
177 });
178 }
179
180 let decrypted_bytes = Aes256GcmCrypto::decrypt_with_password(&encrypted_data, &key)
182 .map_err(|e| self.map_crypto_error(e))?;
183
184 String::from_utf8(decrypted_bytes).map_err(|e| SecretError::ParseError {
185 message: format!("Decrypted content is not valid UTF-8: {}", e),
186 })
187 }
188
189 async fn get_decryption_key(&self) -> Result<String, SecretError> {
191 match self.config.encryption.key.provider.as_str() {
192 "env" => {
193 let env_var = self.config.encryption.key.env_var.as_ref().ok_or_else(|| {
194 SecretError::ConfigurationError {
195 message: "Environment variable name not specified for 'env' key provider"
196 .to_string(),
197 }
198 })?;
199
200 KeyUtils::get_key_from_env(env_var).map_err(|e| self.map_crypto_error(e))
201 }
202 "os_keychain" => {
203 let service = self.config.encryption.key.service.as_ref().ok_or_else(|| {
204 SecretError::ConfigurationError {
205 message: "Service name not specified for 'os_keychain' key provider"
206 .to_string(),
207 }
208 })?;
209
210 let account = self.config.encryption.key.account.as_ref().ok_or_else(|| {
211 SecretError::ConfigurationError {
212 message: "Account name not specified for 'os_keychain' key provider"
213 .to_string(),
214 }
215 })?;
216
217 let key_utils = KeyUtils::new();
218 key_utils
219 .get_key_from_keychain(service, account)
220 .map_err(|e| self.map_crypto_error(e))
221 }
222 "file" => {
223 let file_path = self
224 .config
225 .encryption
226 .key
227 .file_path
228 .as_ref()
229 .ok_or_else(|| SecretError::ConfigurationError {
230 message: "File path not specified for 'file' key provider".to_string(),
231 })?;
232
233 tokio::fs::read_to_string(file_path)
234 .await
235 .map(|content| content.trim().to_string())
236 .map_err(|e| SecretError::IoError {
237 message: format!("Failed to read key file: {}", e),
238 })
239 }
240 _ => Err(SecretError::ConfigurationError {
241 message: format!(
242 "Unsupported key provider: {}",
243 self.config.encryption.key.provider
244 ),
245 }),
246 }
247 }
248
249 fn parse_secrets_data(&self, data: &str) -> Result<HashMap<String, String>, SecretError> {
251 match self.config.format {
252 FileFormat::Json => self.parse_json_secrets(data),
253 FileFormat::Yaml => self.parse_yaml_secrets(data),
254 FileFormat::Toml => self.parse_toml_secrets(data),
255 FileFormat::Env => self.parse_env_secrets(data),
256 }
257 }
258
259 fn parse_json_secrets(&self, data: &str) -> Result<HashMap<String, String>, SecretError> {
261 let value: Value = serde_json::from_str(data).map_err(|e| SecretError::ParseError {
262 message: format!("Failed to parse JSON: {}", e),
263 })?;
264
265 let mut secrets = HashMap::new();
266 if let Value::Object(map) = value {
267 for (key, value) in map {
268 let secret_value = match value {
269 Value::String(s) => s,
270 _ => value.to_string(),
271 };
272 secrets.insert(key, secret_value);
273 }
274 } else {
275 return Err(SecretError::ParseError {
276 message: "JSON root must be an object".to_string(),
277 });
278 }
279
280 Ok(secrets)
281 }
282
283 const MAX_SECRETS_FILE_SIZE: usize = 1_048_576;
285
286 fn parse_yaml_secrets(&self, data: &str) -> Result<HashMap<String, String>, SecretError> {
288 if data.len() > Self::MAX_SECRETS_FILE_SIZE {
289 return Err(SecretError::ParseError {
290 message: format!(
291 "Secrets file exceeds maximum size ({} bytes > {} byte limit)",
292 data.len(),
293 Self::MAX_SECRETS_FILE_SIZE
294 ),
295 });
296 }
297 let value: serde_yaml::Value =
298 serde_yaml::from_str(data).map_err(|e| SecretError::ParseError {
299 message: format!("Failed to parse YAML: {}", e),
300 })?;
301
302 let mut secrets = HashMap::new();
303 if let serde_yaml::Value::Mapping(map) = value {
304 for (key, value) in map {
305 if let serde_yaml::Value::String(key_str) = key {
306 let secret_value = match value {
307 serde_yaml::Value::String(s) => s,
308 _ => {
309 serde_yaml::to_string(&value).map_err(|e| SecretError::ParseError {
310 message: format!("Failed to serialize YAML value: {}", e),
311 })?
312 }
313 };
314 secrets.insert(key_str, secret_value);
315 }
316 }
317 } else {
318 return Err(SecretError::ParseError {
319 message: "YAML root must be a mapping".to_string(),
320 });
321 }
322
323 Ok(secrets)
324 }
325
326 fn parse_toml_secrets(&self, data: &str) -> Result<HashMap<String, String>, SecretError> {
328 if data.len() > Self::MAX_SECRETS_FILE_SIZE {
329 return Err(SecretError::ParseError {
330 message: format!(
331 "Secrets file exceeds maximum size ({} bytes > {} byte limit)",
332 data.len(),
333 Self::MAX_SECRETS_FILE_SIZE
334 ),
335 });
336 }
337 let value: toml::Value = toml::from_str(data).map_err(|e| SecretError::ParseError {
338 message: format!("Failed to parse TOML: {}", e),
339 })?;
340
341 let mut secrets = HashMap::new();
342 if let toml::Value::Table(table) = value {
343 for (key, value) in table {
344 let secret_value = match value {
345 toml::Value::String(s) => s,
346 _ => value.to_string(),
347 };
348 secrets.insert(key, secret_value);
349 }
350 } else {
351 return Err(SecretError::ParseError {
352 message: "TOML root must be a table".to_string(),
353 });
354 }
355
356 Ok(secrets)
357 }
358
359 fn parse_env_secrets(&self, data: &str) -> Result<HashMap<String, String>, SecretError> {
362 let mut secrets = HashMap::new();
363 for item in dotenvy::from_read_iter(data.as_bytes()) {
364 match item {
365 Ok((key, value)) => {
366 secrets.insert(key, value);
367 }
368 Err(e) => {
369 return Err(SecretError::ParseError {
370 message: format!("Failed to parse env file: {}", e),
371 });
372 }
373 }
374 }
375 Ok(secrets)
376 }
377
378 fn map_crypto_error(&self, error: CryptoError) -> SecretError {
380 SecretError::CryptoError {
381 message: error.to_string(),
382 }
383 }
384}
385
386#[async_trait]
387impl SecretStore for FileSecretStore {
388 async fn get_secret(&self, key: &str) -> Result<Secret, SecretError> {
390 self.log_audit_event(SecretAuditEvent::attempt(
393 self.agent_id.clone(),
394 "get_secret".to_string(),
395 Some(key.to_string()),
396 ))
397 .await?;
398
399 let result: Result<Secret, SecretError> = async {
400 let secrets = self.load_secrets_cached().await?;
401
402 match secrets.get(key) {
403 Some(value) => Ok(Secret::new(key.to_string(), value.clone())),
404 None => Err(SecretError::NotFound {
405 key: key.to_string(),
406 }),
407 }
408 }
409 .await;
410
411 let audit_event = match &result {
413 Ok(_) => SecretAuditEvent::success(
414 self.agent_id.clone(),
415 "get_secret".to_string(),
416 Some(key.to_string()),
417 ),
418 Err(e) => SecretAuditEvent::failure(
419 self.agent_id.clone(),
420 "get_secret".to_string(),
421 Some(key.to_string()),
422 e.to_string(),
423 ),
424 };
425 self.log_audit_event(audit_event).await?;
426
427 result
428 }
429
430 async fn list_secrets(&self) -> Result<Vec<String>, SecretError> {
432 self.log_audit_event(SecretAuditEvent::attempt(
433 self.agent_id.clone(),
434 "list_secrets".to_string(),
435 None,
436 ))
437 .await?;
438
439 let result: Result<Vec<String>, SecretError> = async {
440 let secrets = self.load_secrets_cached().await?;
441 Ok(secrets.keys().cloned().collect())
442 }
443 .await;
444
445 let audit_event = match &result {
447 Ok(keys) => {
448 SecretAuditEvent::success(self.agent_id.clone(), "list_secrets".to_string(), None)
449 .with_metadata(serde_json::json!({
450 "secrets_count": keys.len()
451 }))
452 }
453 Err(e) => SecretAuditEvent::failure(
454 self.agent_id.clone(),
455 "list_secrets".to_string(),
456 None,
457 e.to_string(),
458 ),
459 };
460 self.log_audit_event(audit_event).await?;
461
462 result
463 }
464}
465
466impl FileSecretStore {
468 pub async fn list_secrets_with_prefix(&self, prefix: &str) -> Result<Vec<String>, SecretError> {
470 let secrets = self.load_secrets_cached().await?;
471 Ok(secrets
472 .keys()
473 .filter(|key| key.starts_with(prefix))
474 .cloned()
475 .collect())
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use std::io::Write;
483 use std::path::PathBuf;
484 use tempfile::NamedTempFile;
485
486 fn create_test_config(path: PathBuf) -> FileConfig {
487 FileConfig {
488 path,
489 format: FileFormat::Json,
490 encryption: crate::secrets::config::FileEncryptionConfig {
491 enabled: false,
492 algorithm: "AES-256-GCM".to_string(),
493 kdf: "Argon2".to_string(),
494 key: crate::secrets::config::FileKeyConfig {
495 provider: "env".to_string(),
496 env_var: Some("TEST_KEY".to_string()),
497 service: None,
498 account: None,
499 file_path: None,
500 },
501 },
502 permissions: Some(0o600),
503 watch_for_changes: false,
504 backup: crate::secrets::config::FileBackupConfig::default(),
505 }
506 }
507
508 #[tokio::test]
509 async fn test_parse_json_secrets() {
510 let mut temp_file = NamedTempFile::new().unwrap();
511 writeln!(temp_file, r#"{{"key1": "value1", "key2": "value2"}}"#).unwrap();
512
513 let config = create_test_config(temp_file.path().to_path_buf());
514 let store = FileSecretStore::new(config, None, "test-agent".to_string())
515 .await
516 .unwrap();
517
518 let secret = store.get_secret("key1").await.unwrap();
519 assert_eq!(secret.value(), "value1");
520
521 let keys = store.list_secrets().await.unwrap();
522 assert!(keys.contains(&"key1".to_string()));
523 assert!(keys.contains(&"key2".to_string()));
524 }
525
526 #[tokio::test]
527 async fn test_secret_not_found() {
528 let mut temp_file = NamedTempFile::new().unwrap();
529 writeln!(temp_file, r#"{{"key1": "value1"}}"#).unwrap();
530
531 let config = create_test_config(temp_file.path().to_path_buf());
532 let store = FileSecretStore::new(config, None, "test-agent".to_string())
533 .await
534 .unwrap();
535
536 let result = store.get_secret("nonexistent").await;
537 assert!(matches!(result, Err(SecretError::NotFound { .. })));
538 }
539
540 #[tokio::test]
541 async fn test_list_secrets_with_prefix() {
542 let mut temp_file = NamedTempFile::new().unwrap();
543 writeln!(
544 temp_file,
545 r#"{{"app_key1": "value1", "app_key2": "value2", "other_key": "value3"}}"#
546 )
547 .unwrap();
548
549 let config = create_test_config(temp_file.path().to_path_buf());
550 let store = FileSecretStore::new(config, None, "test-agent".to_string())
551 .await
552 .unwrap();
553
554 let keys = store.list_secrets_with_prefix("app_").await.unwrap();
555 assert_eq!(keys.len(), 2);
556 assert!(keys.contains(&"app_key1".to_string()));
557 assert!(keys.contains(&"app_key2".to_string()));
558 assert!(!keys.contains(&"other_key".to_string()));
559 }
560
561 #[tokio::test]
562 async fn test_concurrent_reads_no_deadlock() {
563 let mut temp_file = NamedTempFile::new().unwrap();
564 writeln!(temp_file, r#"{{"secret_a": "val_a", "secret_b": "val_b"}}"#).unwrap();
565
566 let config = create_test_config(temp_file.path().to_path_buf());
567 let store = std::sync::Arc::new(
568 FileSecretStore::new(config, None, "test-agent".to_string())
569 .await
570 .unwrap(),
571 );
572
573 let mut handles = Vec::new();
575 for _ in 0..10 {
576 let s = store.clone();
577 handles.push(tokio::spawn(async move {
578 let secret = s.get_secret("secret_a").await.unwrap();
579 assert_eq!(secret.value(), "val_a");
580 }));
581 }
582
583 for h in handles {
584 h.await.unwrap();
585 }
586 }
587}