codex_memory/security/
secrets.rs

1use crate::security::{Result, SecretsConfig, SecurityError};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7use tracing::{debug, info, warn};
8
9/// Secrets management system
10pub struct SecretsManager {
11    config: SecretsConfig,
12    secrets_cache: Arc<RwLock<HashMap<String, SecretValue>>>,
13}
14
15/// Secret value with metadata
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SecretValue {
18    pub value: String,
19    pub version: u32,
20    pub created_at: chrono::DateTime<chrono::Utc>,
21    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
22    pub metadata: HashMap<String, String>,
23}
24
25/// Secret metadata only (without the actual value)
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SecretMetadata {
28    pub key: String,
29    pub version: u32,
30    pub created_at: chrono::DateTime<chrono::Utc>,
31    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
32    pub metadata: HashMap<String, String>,
33}
34
35impl SecretsManager {
36    pub fn new(config: SecretsConfig) -> Result<Self> {
37        let manager = Self {
38            config,
39            secrets_cache: Arc::new(RwLock::new(HashMap::new())),
40        };
41
42        Ok(manager)
43    }
44
45    /// Initialize secrets management system
46    pub async fn initialize(&self) -> Result<()> {
47        info!("Initializing secrets management system");
48
49        if self.config.vault_enabled {
50            self.initialize_vault().await?;
51        } else if self.config.env_fallback {
52            self.load_from_environment().await?;
53        }
54
55        info!("Secrets management system initialized");
56        Ok(())
57    }
58
59    /// Initialize HashiCorp Vault connection (mock implementation)
60    async fn initialize_vault(&self) -> Result<()> {
61        if let Some(vault_address) = &self.config.vault_address {
62            info!("Connecting to Vault at: {}", vault_address);
63
64            // In a real implementation, this would:
65            // 1. Load the Vault token from the token path
66            // 2. Create a Vault client
67            // 3. Test the connection
68            // 4. Set up token renewal if needed
69
70            if let Some(token_path) = &self.config.vault_token_path {
71                if !token_path.exists() {
72                    return Err(SecurityError::SecretsError {
73                        message: format!("Vault token file not found: {token_path:?}"),
74                    });
75                }
76
77                debug!("Vault token found at: {:?}", token_path);
78
79                // Mock: Load token from file
80                let _token =
81                    fs::read_to_string(token_path).map_err(|e| SecurityError::SecretsError {
82                        message: format!("Failed to read Vault token: {e}"),
83                    })?;
84
85                info!("Connected to Vault successfully");
86            } else {
87                warn!("Vault enabled but no token path configured");
88            }
89        } else {
90            return Err(SecurityError::SecretsError {
91                message: "Vault enabled but no address configured".to_string(),
92            });
93        }
94
95        Ok(())
96    }
97
98    /// Load secrets from environment variables
99    async fn load_from_environment(&self) -> Result<()> {
100        debug!("Loading secrets from environment variables");
101
102        let mut cache = self.secrets_cache.write().await;
103
104        // Load common secret environment variables
105        let env_secrets = vec![
106            "DATABASE_URL",
107            "OPENAI_API_KEY",
108            "JWT_SECRET",
109            "ENCRYPTION_KEY",
110            "VAULT_TOKEN",
111            "API_KEY",
112            "SESSION_SECRET",
113        ];
114
115        for secret_name in env_secrets {
116            if let Ok(secret_value) = std::env::var(secret_name) {
117                let secret = SecretValue {
118                    value: secret_value,
119                    version: 1,
120                    created_at: chrono::Utc::now(),
121                    expires_at: None,
122                    metadata: HashMap::new(),
123                };
124
125                cache.insert(secret_name.to_string(), secret);
126                debug!("Loaded secret from environment: {}", secret_name);
127            }
128        }
129
130        info!("Loaded {} secrets from environment", cache.len());
131        Ok(())
132    }
133
134    /// Get secret value
135    pub async fn get_secret(&self, key: &str) -> Result<String> {
136        // First check cache
137        {
138            let cache = self.secrets_cache.read().await;
139            if let Some(secret) = cache.get(key) {
140                // Check if secret is expired
141                if let Some(expires_at) = secret.expires_at {
142                    if chrono::Utc::now() > expires_at {
143                        warn!("Secret '{}' has expired", key);
144                        return Err(SecurityError::SecretsError {
145                            message: format!("Secret '{key}' has expired"),
146                        });
147                    }
148                }
149
150                debug!("Retrieved secret from cache: {}", key);
151                return Ok(secret.value.clone());
152            }
153        }
154
155        // If not in cache, try to load from source
156        if self.config.vault_enabled {
157            self.get_secret_from_vault(key).await
158        } else if self.config.env_fallback {
159            self.get_secret_from_env(key).await
160        } else {
161            Err(SecurityError::SecretsError {
162                message: format!("Secret '{key}' not found and no fallback configured"),
163            })
164        }
165    }
166
167    /// Get secret from HashiCorp Vault (mock implementation)
168    async fn get_secret_from_vault(&self, key: &str) -> Result<String> {
169        // In a real implementation, this would make an HTTP request to Vault
170        // For now, this is a mock that returns an error
171        Err(SecurityError::SecretsError {
172            message: format!("Vault integration not fully implemented for key: {key}"),
173        })
174    }
175
176    /// Get secret from environment variable
177    async fn get_secret_from_env(&self, key: &str) -> Result<String> {
178        match std::env::var(key) {
179            Ok(value) => {
180                // Cache the secret
181                let secret = SecretValue {
182                    value: value.clone(),
183                    version: 1,
184                    created_at: chrono::Utc::now(),
185                    expires_at: None,
186                    metadata: HashMap::new(),
187                };
188
189                let mut cache = self.secrets_cache.write().await;
190                cache.insert(key.to_string(), secret);
191
192                debug!("Retrieved secret from environment: {}", key);
193                Ok(value)
194            }
195            Err(_) => Err(SecurityError::SecretsError {
196                message: format!("Secret '{key}' not found in environment"),
197            }),
198        }
199    }
200
201    /// Set secret value (for testing or manual secret management)
202    pub async fn set_secret(
203        &self,
204        key: &str,
205        value: &str,
206        expires_in_seconds: Option<i64>,
207    ) -> Result<()> {
208        let expires_at = expires_in_seconds
209            .map(|seconds| chrono::Utc::now() + chrono::Duration::seconds(seconds));
210
211        let secret = SecretValue {
212            value: value.to_string(),
213            version: 1,
214            created_at: chrono::Utc::now(),
215            expires_at,
216            metadata: HashMap::new(),
217        };
218
219        let mut cache = self.secrets_cache.write().await;
220        cache.insert(key.to_string(), secret);
221
222        debug!("Set secret: {}", key);
223        Ok(())
224    }
225
226    /// Delete secret from cache
227    pub async fn delete_secret(&self, key: &str) -> Result<()> {
228        let mut cache = self.secrets_cache.write().await;
229
230        if cache.remove(key).is_some() {
231            debug!("Deleted secret from cache: {}", key);
232            Ok(())
233        } else {
234            Err(SecurityError::SecretsError {
235                message: format!("Secret '{key}' not found in cache"),
236            })
237        }
238    }
239
240    /// Get secret metadata (without the actual value)
241    pub async fn get_secret_metadata(&self, key: &str) -> Result<SecretMetadata> {
242        let cache = self.secrets_cache.read().await;
243
244        if let Some(secret) = cache.get(key) {
245            Ok(SecretMetadata {
246                key: key.to_string(),
247                version: secret.version,
248                created_at: secret.created_at,
249                expires_at: secret.expires_at,
250                metadata: secret.metadata.clone(),
251            })
252        } else {
253            Err(SecurityError::SecretsError {
254                message: format!("Secret '{key}' not found"),
255            })
256        }
257    }
258
259    /// List all secret keys (without values)
260    pub async fn list_secrets(&self) -> Vec<String> {
261        let cache = self.secrets_cache.read().await;
262        cache.keys().cloned().collect()
263    }
264
265    /// Rotate secret (create new version)
266    pub async fn rotate_secret(&self, key: &str, new_value: &str) -> Result<u32> {
267        let mut cache = self.secrets_cache.write().await;
268
269        let new_version = if let Some(existing_secret) = cache.get(key) {
270            existing_secret.version + 1
271        } else {
272            1
273        };
274
275        let secret = SecretValue {
276            value: new_value.to_string(),
277            version: new_version,
278            created_at: chrono::Utc::now(),
279            expires_at: None,
280            metadata: HashMap::new(),
281        };
282
283        cache.insert(key.to_string(), secret);
284
285        info!("Rotated secret '{}' to version {}", key, new_version);
286        Ok(new_version)
287    }
288
289    /// Clean up expired secrets
290    pub async fn cleanup_expired_secrets(&self) -> Result<usize> {
291        let mut cache = self.secrets_cache.write().await;
292        let now = chrono::Utc::now();
293
294        let initial_count = cache.len();
295
296        cache.retain(|key, secret| {
297            if let Some(expires_at) = secret.expires_at {
298                if now > expires_at {
299                    debug!("Removing expired secret: {}", key);
300                    return false;
301                }
302            }
303            true
304        });
305
306        let removed_count = initial_count - cache.len();
307
308        if removed_count > 0 {
309            info!("Cleaned up {} expired secrets", removed_count);
310        }
311
312        Ok(removed_count)
313    }
314
315    /// Get secrets statistics
316    pub async fn get_statistics(&self) -> SecretsStatistics {
317        let cache = self.secrets_cache.read().await;
318        let now = chrono::Utc::now();
319
320        let total_secrets = cache.len();
321        let mut expired_secrets = 0;
322        let mut expiring_soon = 0; // Within 24 hours
323
324        for secret in cache.values() {
325            if let Some(expires_at) = secret.expires_at {
326                if now > expires_at {
327                    expired_secrets += 1;
328                } else if expires_at - now < chrono::Duration::hours(24) {
329                    expiring_soon += 1;
330                }
331            }
332        }
333
334        SecretsStatistics {
335            total_secrets,
336            expired_secrets,
337            expiring_soon,
338            vault_enabled: self.config.vault_enabled,
339            env_fallback_enabled: self.config.env_fallback,
340        }
341    }
342
343    /// Test connection to secrets backend
344    pub async fn test_connection(&self) -> Result<()> {
345        if self.config.vault_enabled {
346            // Test Vault connection
347            if let Some(vault_address) = &self.config.vault_address {
348                debug!("Testing Vault connection to: {}", vault_address);
349                // In a real implementation, this would make a health check request
350                Ok(())
351            } else {
352                Err(SecurityError::SecretsError {
353                    message: "Vault address not configured".to_string(),
354                })
355            }
356        } else {
357            // Test environment variable access
358            match std::env::var("PATH") {
359                Ok(_) => {
360                    debug!("Environment variable access test passed");
361                    Ok(())
362                }
363                Err(_) => Err(SecurityError::SecretsError {
364                    message: "Cannot access environment variables".to_string(),
365                }),
366            }
367        }
368    }
369
370    pub fn is_vault_enabled(&self) -> bool {
371        self.config.vault_enabled
372    }
373
374    pub fn is_env_fallback_enabled(&self) -> bool {
375        self.config.env_fallback
376    }
377}
378
379/// Secrets management statistics
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct SecretsStatistics {
382    pub total_secrets: usize,
383    pub expired_secrets: usize,
384    pub expiring_soon: usize,
385    pub vault_enabled: bool,
386    pub env_fallback_enabled: bool,
387}
388
389/// Utility functions for secret management
390impl SecretsManager {
391    /// Generate a random secret key
392    pub fn generate_random_key(length: usize) -> String {
393        use rand::Rng;
394        const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
395
396        let mut rng = rand::thread_rng();
397        (0..length)
398            .map(|_| {
399                let idx = rng.gen_range(0..CHARSET.len());
400                CHARSET[idx] as char
401            })
402            .collect()
403    }
404
405    /// Generate a secure JWT secret
406    pub fn generate_jwt_secret() -> String {
407        Self::generate_random_key(64)
408    }
409
410    /// Generate a secure API key
411    pub fn generate_api_key() -> String {
412        format!("ak_{}", Self::generate_random_key(32))
413    }
414
415    /// Generate a secure encryption key
416    pub fn generate_encryption_key() -> String {
417        Self::generate_random_key(32)
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use std::path::PathBuf;
425    use tempfile::tempdir;
426
427    #[tokio::test]
428    async fn test_secrets_manager_creation() {
429        let config = SecretsConfig::default();
430        let manager = SecretsManager::new(config).unwrap();
431        assert!(!manager.is_vault_enabled());
432        assert!(manager.is_env_fallback_enabled());
433    }
434
435    #[tokio::test]
436    async fn test_secret_operations() {
437        let config = SecretsConfig::default();
438        let manager = SecretsManager::new(config).unwrap();
439
440        // Set a secret
441        let result = manager.set_secret("test_key", "test_value", None).await;
442        assert!(result.is_ok());
443
444        // Get the secret
445        let value = manager.get_secret("test_key").await.unwrap();
446        assert_eq!(value, "test_value");
447
448        // Get secret metadata
449        let metadata = manager.get_secret_metadata("test_key").await.unwrap();
450        assert_eq!(metadata.key, "test_key");
451        assert_eq!(metadata.version, 1);
452
453        // Delete the secret
454        let result = manager.delete_secret("test_key").await;
455        assert!(result.is_ok());
456
457        // Try to get deleted secret
458        let result = manager.get_secret("test_key").await;
459        assert!(result.is_err());
460    }
461
462    #[tokio::test]
463    async fn test_secret_expiration() {
464        let config = SecretsConfig::default();
465        let manager = SecretsManager::new(config).unwrap();
466
467        // Set a secret that expires in 1 second
468        manager
469            .set_secret("expiring_key", "expiring_value", Some(1))
470            .await
471            .unwrap();
472
473        // Should be able to get it immediately
474        let value = manager.get_secret("expiring_key").await.unwrap();
475        assert_eq!(value, "expiring_value");
476
477        // Wait for expiration
478        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
479
480        // Should now be expired
481        let result = manager.get_secret("expiring_key").await;
482        assert!(result.is_err());
483
484        if let Err(SecurityError::SecretsError { message }) = result {
485            assert!(message.contains("expired"));
486        }
487    }
488
489    #[tokio::test]
490    async fn test_secret_rotation() {
491        let config = SecretsConfig::default();
492        let manager = SecretsManager::new(config).unwrap();
493
494        // Set initial secret
495        manager
496            .set_secret("rotating_key", "value_v1", None)
497            .await
498            .unwrap();
499
500        // Rotate the secret
501        let new_version = manager
502            .rotate_secret("rotating_key", "value_v2")
503            .await
504            .unwrap();
505        assert_eq!(new_version, 2);
506
507        // Should get the new value
508        let value = manager.get_secret("rotating_key").await.unwrap();
509        assert_eq!(value, "value_v2");
510
511        // Metadata should show new version
512        let metadata = manager.get_secret_metadata("rotating_key").await.unwrap();
513        assert_eq!(metadata.version, 2);
514    }
515
516    #[tokio::test]
517    async fn test_cleanup_expired_secrets() {
518        let config = SecretsConfig::default();
519        let manager = SecretsManager::new(config).unwrap();
520
521        // Set some secrets with different expiration times
522        manager
523            .set_secret("permanent", "value", None)
524            .await
525            .unwrap();
526        manager
527            .set_secret("short_lived", "value", Some(1))
528            .await
529            .unwrap();
530        manager
531            .set_secret("medium_lived", "value", Some(10))
532            .await
533            .unwrap();
534
535        // Wait for short-lived secret to expire
536        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
537
538        // Cleanup expired secrets
539        let removed_count = manager.cleanup_expired_secrets().await.unwrap();
540        assert_eq!(removed_count, 1);
541
542        // Should still have the other secrets
543        let secrets = manager.list_secrets().await;
544        assert_eq!(secrets.len(), 2);
545        assert!(secrets.contains(&"permanent".to_string()));
546        assert!(secrets.contains(&"medium_lived".to_string()));
547    }
548
549    #[tokio::test]
550    async fn test_get_statistics() {
551        let config = SecretsConfig::default();
552        let manager = SecretsManager::new(config).unwrap();
553
554        // Add some test secrets
555        manager.set_secret("secret1", "value1", None).await.unwrap();
556        manager
557            .set_secret("secret2", "value2", Some(1))
558            .await
559            .unwrap(); // Expires soon
560
561        // Wait for one to expire
562        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
563
564        let stats = manager.get_statistics().await;
565        assert_eq!(stats.total_secrets, 2);
566        assert_eq!(stats.expired_secrets, 1);
567        assert!(stats.env_fallback_enabled);
568        assert!(!stats.vault_enabled);
569    }
570
571    #[tokio::test]
572    async fn test_environment_fallback() {
573        // Set a test environment variable
574        std::env::set_var("TEST_SECRET_KEY", "test_secret_value");
575
576        let config = SecretsConfig {
577            vault_enabled: false,
578            vault_address: None,
579            vault_token_path: None,
580            env_fallback: true,
581        };
582
583        let manager = SecretsManager::new(config).unwrap();
584
585        // Should be able to get the environment variable
586        let value = manager.get_secret("TEST_SECRET_KEY").await.unwrap();
587        assert_eq!(value, "test_secret_value");
588
589        // Clean up
590        std::env::remove_var("TEST_SECRET_KEY");
591    }
592
593    #[tokio::test]
594    async fn test_vault_token_path_validation() {
595        let temp_dir = tempdir().unwrap();
596        let token_path = temp_dir.path().join("vault_token");
597
598        // Create a mock token file
599        std::fs::write(&token_path, "mock_vault_token").unwrap();
600
601        let config = SecretsConfig {
602            vault_enabled: true,
603            vault_address: Some("https://vault.example.com".to_string()),
604            vault_token_path: Some(token_path),
605            env_fallback: false,
606        };
607
608        let manager = SecretsManager::new(config).unwrap();
609
610        // Should successfully initialize (even though it's a mock)
611        let result = manager.initialize().await;
612        assert!(result.is_ok());
613    }
614
615    #[tokio::test]
616    async fn test_vault_missing_token() {
617        let config = SecretsConfig {
618            vault_enabled: true,
619            vault_address: Some("https://vault.example.com".to_string()),
620            vault_token_path: Some(PathBuf::from("/nonexistent/path")),
621            env_fallback: false,
622        };
623
624        let manager = SecretsManager::new(config).unwrap();
625
626        // Should fail to initialize due to missing token
627        let result = manager.initialize().await;
628        assert!(result.is_err());
629
630        if let Err(SecurityError::SecretsError { message }) = result {
631            assert!(message.contains("token file not found"));
632        }
633    }
634
635    #[test]
636    fn test_random_key_generation() {
637        let key1 = SecretsManager::generate_random_key(32);
638        let key2 = SecretsManager::generate_random_key(32);
639
640        assert_eq!(key1.len(), 32);
641        assert_eq!(key2.len(), 32);
642        assert_ne!(key1, key2); // Should be different
643
644        // Should contain only valid characters
645        for ch in key1.chars() {
646            assert!(ch.is_ascii_alphanumeric());
647        }
648    }
649
650    #[test]
651    fn test_jwt_secret_generation() {
652        let secret = SecretsManager::generate_jwt_secret();
653        assert_eq!(secret.len(), 64);
654    }
655
656    #[test]
657    fn test_api_key_generation() {
658        let key = SecretsManager::generate_api_key();
659        assert!(key.starts_with("ak_"));
660        assert_eq!(key.len(), 35); // "ak_" + 32 characters
661    }
662
663    #[test]
664    fn test_encryption_key_generation() {
665        let key = SecretsManager::generate_encryption_key();
666        assert_eq!(key.len(), 32);
667    }
668}