pulseengine_mcp_auth/
storage.rs

1//! Storage backend for authentication data
2
3use crate::{
4    config::StorageConfig,
5    models::{ApiKey, SecureApiKey},
6};
7use async_trait::async_trait;
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::sync::Arc;
11use thiserror::Error;
12use tokio::fs;
13use tracing::{debug, error, info, warn};
14
15#[derive(Debug, Error)]
16pub enum StorageError {
17    #[error("Storage error: {0}")]
18    General(String),
19
20    #[error("File I/O error: {0}")]
21    Io(#[from] std::io::Error),
22
23    #[error("Serialization error: {0}")]
24    Serialization(#[from] serde_json::Error),
25
26    #[error("Permission error: {0}")]
27    Permission(String),
28
29    #[error("Encryption error: {0}")]
30    Encryption(#[from] crate::crypto::encryption::EncryptionError),
31}
32
33/// Storage backend trait
34#[async_trait]
35pub trait StorageBackend: Send + Sync {
36    async fn load_keys(&self) -> Result<HashMap<String, ApiKey>, StorageError>;
37    async fn save_key(&self, key: &ApiKey) -> Result<(), StorageError>;
38    async fn delete_key(&self, key_id: &str) -> Result<(), StorageError>;
39    async fn save_all_keys(&self, keys: &HashMap<String, ApiKey>) -> Result<(), StorageError>;
40}
41
42/// Create a storage backend from configuration
43pub async fn create_storage_backend(
44    config: &StorageConfig,
45) -> Result<Arc<dyn StorageBackend>, StorageError> {
46    match config {
47        StorageConfig::File {
48            path,
49            file_permissions,
50            dir_permissions,
51            require_secure_filesystem,
52            enable_filesystem_monitoring,
53        } => {
54            let storage = FileStorage::new(
55                path.clone(),
56                *file_permissions,
57                *dir_permissions,
58                *require_secure_filesystem,
59                *enable_filesystem_monitoring,
60            )
61            .await?;
62            Ok(Arc::new(storage))
63        }
64        StorageConfig::Environment { prefix } => {
65            let storage = EnvironmentStorage::new(prefix.clone());
66            Ok(Arc::new(storage))
67        }
68        StorageConfig::Memory => {
69            let storage = MemoryStorage::new();
70            Ok(Arc::new(storage))
71        }
72    }
73}
74
75/// File-based storage backend with atomic operations and encryption
76pub struct FileStorage {
77    path: PathBuf,
78    encryption_key: [u8; 32],
79    file_permissions: u32,
80    #[allow(dead_code)]
81    dir_permissions: u32,
82    #[allow(dead_code)]
83    require_secure_filesystem: bool,
84    enable_filesystem_monitoring: bool,
85}
86
87impl FileStorage {
88    pub async fn new(
89        path: PathBuf,
90        file_permissions: u32,
91        dir_permissions: u32,
92        require_secure_filesystem: bool,
93        enable_filesystem_monitoring: bool,
94    ) -> Result<Self, StorageError> {
95        use crate::crypto::encryption::derive_encryption_key;
96        use crate::crypto::keys::generate_master_key;
97
98        // Validate filesystem security if required
99        if require_secure_filesystem {
100            Self::validate_filesystem_security(&path).await?;
101        }
102
103        // Ensure parent directory exists with secure permissions
104        if let Some(parent) = path.parent() {
105            fs::create_dir_all(parent).await?;
106
107            // Set secure permissions on Unix
108            #[cfg(unix)]
109            {
110                use std::os::unix::fs::PermissionsExt;
111                let mut perms = fs::metadata(parent).await?.permissions();
112                perms.set_mode(dir_permissions); // Use configured directory permissions
113                fs::set_permissions(parent, perms).await?;
114
115                // Verify no other users have access
116                Self::verify_directory_security(parent, dir_permissions).await?;
117            }
118        }
119
120        // Generate or load master key, then derive storage key
121        let master_key = generate_master_key().map_err(|e| StorageError::General(e.to_string()))?;
122        let encryption_key = derive_encryption_key(&master_key, "api-key-storage");
123
124        let storage = Self {
125            path,
126            encryption_key,
127            file_permissions,
128            dir_permissions,
129            require_secure_filesystem,
130            enable_filesystem_monitoring,
131        };
132
133        // Initialize empty file if it doesn't exist
134        if !storage.path.exists() {
135            storage.save_all_keys(&HashMap::new()).await?;
136        } else {
137            // Verify existing file security
138            storage.ensure_secure_permissions().await?;
139        }
140
141        Ok(storage)
142    }
143
144    async fn ensure_secure_permissions(&self) -> Result<(), StorageError> {
145        #[cfg(unix)]
146        {
147            use std::os::unix::fs::PermissionsExt;
148
149            if self.path.exists() {
150                let metadata = fs::metadata(&self.path).await?;
151                let mode = metadata.permissions().mode() & 0o777;
152
153                // Check if permissions are more permissive than configured
154                if mode != self.file_permissions {
155                    warn!(
156                        "Incorrect permissions on key file: {:o}, fixing to {:o}",
157                        mode, self.file_permissions
158                    );
159                    let mut perms = metadata.permissions();
160                    perms.set_mode(self.file_permissions);
161                    fs::set_permissions(&self.path, perms).await?;
162                }
163
164                // Verify file ownership (only owner should have access)
165                Self::verify_file_ownership(&self.path).await?;
166            }
167        }
168        Ok(())
169    }
170
171    /// Validate that the filesystem is secure (not network/shared)
172    async fn validate_filesystem_security(path: &PathBuf) -> Result<(), StorageError> {
173        #[cfg(unix)]
174        {
175            use std::os::unix::fs::MetadataExt;
176
177            if let Some(parent) = path.parent() {
178                if parent.exists() {
179                    let metadata = fs::metadata(parent).await?;
180
181                    // Check if this is a network filesystem (basic check)
182                    let _dev = metadata.dev();
183
184                    // On many Unix systems, network filesystems have device IDs that indicate remote storage
185                    // This is a basic check - in production you might want more sophisticated detection
186                    if let Ok(mount_info) = fs::read_to_string("/proc/mounts").await {
187                        let path_str = parent.to_string_lossy();
188                        for line in mount_info.lines() {
189                            let parts: Vec<&str> = line.split_whitespace().collect();
190                            if parts.len() >= 3 {
191                                let mount_point = parts[1];
192                                let fs_type = parts[2];
193
194                                if path_str.starts_with(mount_point) {
195                                    // Check for network filesystem types
196                                    match fs_type {
197                                        "nfs" | "nfs4" | "cifs" | "smb" | "smbfs"
198                                        | "fuse.sshfs" => {
199                                            return Err(StorageError::Permission(format!(
200                                                "Storage path {} is on insecure network filesystem: {}", 
201                                                path_str, fs_type
202                                            )));
203                                        }
204                                        _ => {}
205                                    }
206                                }
207                            }
208                        }
209                    }
210                }
211            }
212        }
213
214        Ok(())
215    }
216
217    /// Verify directory security and ownership
218    async fn verify_directory_security(
219        dir: &std::path::Path,
220        expected_perms: u32,
221    ) -> Result<(), StorageError> {
222        #[cfg(unix)]
223        {
224            use std::os::unix::fs::{MetadataExt, PermissionsExt};
225
226            let metadata = fs::metadata(dir).await?;
227            let mode = metadata.permissions().mode() & 0o777;
228
229            // Verify permissions are not more permissive than expected
230            if (mode & !expected_perms) != 0 {
231                return Err(StorageError::Permission(format!(
232                    "Directory {} has insecure permissions: {:o} (expected: {:o})",
233                    dir.display(),
234                    mode,
235                    expected_perms
236                )));
237            }
238
239            // Verify ownership (should be current user)
240            let current_uid = unsafe { libc::getuid() };
241            if metadata.uid() != current_uid {
242                return Err(StorageError::Permission(format!(
243                    "Directory {} is not owned by current user (uid: {} vs {})",
244                    dir.display(),
245                    metadata.uid(),
246                    current_uid
247                )));
248            }
249        }
250
251        Ok(())
252    }
253
254    /// Verify file ownership
255    async fn verify_file_ownership(file: &std::path::Path) -> Result<(), StorageError> {
256        #[cfg(unix)]
257        {
258            use std::os::unix::fs::MetadataExt;
259
260            let metadata = fs::metadata(file).await?;
261            let current_uid = unsafe { libc::getuid() };
262
263            if metadata.uid() != current_uid {
264                return Err(StorageError::Permission(format!(
265                    "File {} is not owned by current user (uid: {} vs {})",
266                    file.display(),
267                    metadata.uid(),
268                    current_uid
269                )));
270            }
271        }
272
273        Ok(())
274    }
275
276    /// Save secure keys with encryption
277    async fn save_secure_keys(
278        &self,
279        keys: &HashMap<String, SecureApiKey>,
280    ) -> Result<(), StorageError> {
281        use crate::crypto::encryption::encrypt_data;
282
283        let content = serde_json::to_string_pretty(keys)?;
284        let encrypted_data = encrypt_data(content.as_bytes(), &self.encryption_key)?;
285        let encrypted_content = serde_json::to_string_pretty(&encrypted_data)?;
286
287        // Atomic write using temp file
288        let temp_path = self.path.with_extension("tmp");
289        fs::write(&temp_path, encrypted_content).await?;
290
291        // Set secure permissions before moving
292        #[cfg(unix)]
293        {
294            use std::os::unix::fs::PermissionsExt;
295            let mut perms = fs::metadata(&temp_path).await?.permissions();
296            perms.set_mode(self.file_permissions); // Use configured file permissions
297            fs::set_permissions(&temp_path, perms).await?;
298        }
299
300        // Atomic move
301        fs::rename(&temp_path, &self.path).await?;
302
303        debug!("Saved {} keys to encrypted file storage", keys.len());
304        Ok(())
305    }
306
307    /// Create a secure backup of the storage file
308    pub async fn create_backup(&self) -> Result<PathBuf, StorageError> {
309        if !self.path.exists() {
310            return Err(StorageError::General(
311                "Storage file does not exist".to_string(),
312            ));
313        }
314
315        let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
316        let backup_path = self
317            .path
318            .with_extension(format!("backup_{}.enc", timestamp));
319
320        // Copy with secure permissions
321        fs::copy(&self.path, &backup_path).await?;
322
323        #[cfg(unix)]
324        {
325            use std::os::unix::fs::PermissionsExt;
326            let mut perms = fs::metadata(&backup_path).await?.permissions();
327            perms.set_mode(self.file_permissions);
328            fs::set_permissions(&backup_path, perms).await?;
329        }
330
331        debug!("Created secure backup: {}", backup_path.display());
332        Ok(backup_path)
333    }
334
335    /// Restore from a backup file
336    pub async fn restore_from_backup(&self, backup_path: &PathBuf) -> Result<(), StorageError> {
337        if !backup_path.exists() {
338            return Err(StorageError::General(
339                "Backup file does not exist".to_string(),
340            ));
341        }
342
343        // Verify backup file security
344        Self::verify_file_ownership(backup_path).await?;
345
346        // Create temp file for atomic restore
347        let temp_path = self.path.with_extension("restore_tmp");
348        fs::copy(backup_path, &temp_path).await?;
349
350        #[cfg(unix)]
351        {
352            use std::os::unix::fs::PermissionsExt;
353            let mut perms = fs::metadata(&temp_path).await?.permissions();
354            perms.set_mode(self.file_permissions);
355            fs::set_permissions(&temp_path, perms).await?;
356        }
357
358        // Atomic move
359        fs::rename(&temp_path, &self.path).await?;
360
361        info!("Restored from backup: {}", backup_path.display());
362        Ok(())
363    }
364
365    /// Clean up old backup files (keep only last N backups)
366    pub async fn cleanup_backups(&self, keep_count: usize) -> Result<(), StorageError> {
367        if let Some(parent) = self.path.parent() {
368            let filename_stem = self
369                .path
370                .file_stem()
371                .and_then(|s| s.to_str())
372                .unwrap_or("keys");
373
374            let mut backups = Vec::new();
375            let mut entries = fs::read_dir(parent).await?;
376
377            while let Some(entry) = entries.next_entry().await? {
378                let path = entry.path();
379                if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
380                    if filename.starts_with(&format!("{}.backup_", filename_stem)) {
381                        if let Ok(metadata) = entry.metadata().await {
382                            backups.push((
383                                path,
384                                metadata
385                                    .modified()
386                                    .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
387                            ));
388                        }
389                    }
390                }
391            }
392
393            // Sort by modification time (newest first)
394            backups.sort_by(|a, b| b.1.cmp(&a.1));
395
396            // Remove old backups
397            for (backup_path, _) in backups.iter().skip(keep_count) {
398                if let Err(e) = fs::remove_file(backup_path).await {
399                    warn!(
400                        "Failed to remove old backup {}: {}",
401                        backup_path.display(),
402                        e
403                    );
404                } else {
405                    debug!("Removed old backup: {}", backup_path.display());
406                }
407            }
408        }
409
410        Ok(())
411    }
412
413    /// Start filesystem monitoring for unauthorized changes (Linux only)
414    #[cfg(target_os = "linux")]
415    pub async fn start_filesystem_monitoring(&self) -> Result<(), StorageError> {
416        if !self.enable_filesystem_monitoring {
417            return Ok(());
418        }
419
420        use inotify::{Inotify, WatchMask};
421
422        let mut inotify = Inotify::init()
423            .map_err(|e| StorageError::General(format!("Failed to initialize inotify: {}", e)))?;
424
425        // Watch the directory for changes
426        if let Some(parent) = self.path.parent() {
427            inotify
428                .watches()
429                .add(
430                    parent,
431                    WatchMask::MODIFY | WatchMask::ATTRIB | WatchMask::MOVED_TO | WatchMask::DELETE,
432                )
433                .map_err(|e| {
434                    StorageError::General(format!("Failed to add inotify watch: {}", e))
435                })?;
436
437            info!("Started filesystem monitoring for: {}", parent.display());
438
439            // Spawn background task to monitor changes
440            let path = self.path.clone();
441            let file_permissions = self.file_permissions;
442
443            tokio::spawn(async move {
444                use tracing::{error, warn};
445                let mut buffer = [0; 1024];
446                loop {
447                    match inotify.read_events_blocking(&mut buffer) {
448                        Ok(events) => {
449                            for event in events {
450                                if let Some(name) = event.name {
451                                    if name.to_string_lossy().contains("keys") {
452                                        warn!(
453                                            "Detected unauthorized change to auth storage: {:?} (mask: {:?})",
454                                            name, event.mask
455                                        );
456
457                                        // Verify file permissions haven't been changed
458                                        if path.exists() {
459                                            #[cfg(unix)]
460                                            {
461                                                use std::os::unix::fs::PermissionsExt;
462                                                if let Ok(metadata) = std::fs::metadata(&path) {
463                                                    let mode =
464                                                        metadata.permissions().mode() & 0o777;
465                                                    if mode != file_permissions {
466                                                        error!(
467                                                            "Security violation: File permissions changed from {:o} to {:o}",
468                                                            file_permissions, mode
469                                                        );
470                                                    }
471                                                }
472                                            }
473                                        }
474                                    }
475                                }
476                            }
477                        }
478                        Err(e) => {
479                            error!("Error reading inotify events: {}", e);
480                            break;
481                        }
482                    }
483                }
484            });
485        }
486
487        Ok(())
488    }
489
490    /// Start filesystem monitoring (no-op on non-Linux systems)
491    #[cfg(not(target_os = "linux"))]
492    pub async fn start_filesystem_monitoring(&self) -> Result<(), StorageError> {
493        if self.enable_filesystem_monitoring {
494            warn!("Filesystem monitoring is only supported on Linux systems");
495        }
496        Ok(())
497    }
498}
499
500#[async_trait]
501impl StorageBackend for FileStorage {
502    async fn load_keys(&self) -> Result<HashMap<String, ApiKey>, StorageError> {
503        use crate::crypto::encryption::decrypt_data;
504
505        self.ensure_secure_permissions().await?;
506
507        if !self.path.exists() {
508            return Ok(HashMap::new());
509        }
510
511        let content = fs::read(&self.path).await?;
512        if content.is_empty() {
513            return Ok(HashMap::new());
514        }
515
516        // Try to decrypt the content (new format)
517        let decrypted_content = if let Ok(encrypted_data) = serde_json::from_slice(&content) {
518            // Encrypted format
519            let decrypted_bytes = decrypt_data(&encrypted_data, &self.encryption_key)?;
520            String::from_utf8(decrypted_bytes)
521                .map_err(|e| StorageError::General(format!("Invalid UTF-8: {}", e)))?
522        } else {
523            // Legacy plain text format - convert to secure format
524            let plain_text = String::from_utf8(content)
525                .map_err(|e| StorageError::General(format!("Invalid UTF-8: {}", e)))?;
526            warn!("Found legacy plain text keys, converting to secure format");
527
528            // Load legacy keys and convert them
529            let legacy_keys: HashMap<String, ApiKey> = serde_json::from_str(&plain_text)?;
530            let secure_keys: HashMap<String, SecureApiKey> = legacy_keys
531                .into_iter()
532                .map(|(id, key)| (id, key.to_secure_storage()))
533                .collect();
534
535            // Save in secure format
536            self.save_secure_keys(&secure_keys).await?;
537
538            // Return the decrypted content for this load
539            plain_text
540        };
541
542        // Parse secure keys from decrypted content
543        let secure_keys: HashMap<String, SecureApiKey> = serde_json::from_str(&decrypted_content)?;
544
545        // Convert secure keys back to API keys (without plain text)
546        let keys: HashMap<String, ApiKey> = secure_keys
547            .into_iter()
548            .map(|(id, secure_key)| (id, secure_key.to_api_key()))
549            .collect();
550
551        debug!("Loaded {} keys from encrypted file storage", keys.len());
552        Ok(keys)
553    }
554
555    async fn save_key(&self, key: &ApiKey) -> Result<(), StorageError> {
556        let mut keys = self.load_keys().await?;
557        keys.insert(key.id.clone(), key.clone());
558        self.save_all_keys(&keys).await
559    }
560
561    async fn delete_key(&self, key_id: &str) -> Result<(), StorageError> {
562        let mut keys = self.load_keys().await?;
563        keys.remove(key_id);
564        self.save_all_keys(&keys).await
565    }
566
567    async fn save_all_keys(&self, keys: &HashMap<String, ApiKey>) -> Result<(), StorageError> {
568        // Convert to secure keys for storage
569        let secure_keys: HashMap<String, SecureApiKey> = keys
570            .iter()
571            .map(|(id, key)| (id.clone(), key.to_secure_storage()))
572            .collect();
573
574        self.save_secure_keys(&secure_keys).await
575    }
576}
577
578/// Environment variable storage backend
579pub struct EnvironmentStorage {
580    var_name: String,
581}
582
583impl EnvironmentStorage {
584    pub fn new(var_name: String) -> Self {
585        Self { var_name }
586    }
587}
588
589#[async_trait]
590impl StorageBackend for EnvironmentStorage {
591    async fn load_keys(&self) -> Result<HashMap<String, ApiKey>, StorageError> {
592        match std::env::var(&self.var_name) {
593            Ok(content) => {
594                if content.trim().is_empty() {
595                    return Ok(HashMap::new());
596                }
597                let keys: HashMap<String, ApiKey> = serde_json::from_str(&content)?;
598                debug!("Loaded {} keys from environment storage", keys.len());
599                Ok(keys)
600            }
601            Err(_) => {
602                debug!(
603                    "Environment variable {} not found, returning empty keys",
604                    self.var_name
605                );
606                Ok(HashMap::new())
607            }
608        }
609    }
610
611    async fn save_key(&self, key: &ApiKey) -> Result<(), StorageError> {
612        let mut keys = self.load_keys().await?;
613        keys.insert(key.id.clone(), key.clone());
614        self.save_all_keys(&keys).await
615    }
616
617    async fn delete_key(&self, key_id: &str) -> Result<(), StorageError> {
618        let mut keys = self.load_keys().await?;
619        keys.remove(key_id);
620        self.save_all_keys(&keys).await
621    }
622
623    async fn save_all_keys(&self, keys: &HashMap<String, ApiKey>) -> Result<(), StorageError> {
624        let content = serde_json::to_string(keys)?;
625        std::env::set_var(&self.var_name, content);
626
627        debug!("Saved {} keys to environment storage", keys.len());
628        Ok(())
629    }
630}
631
632/// In-memory storage backend (for testing)
633pub struct MemoryStorage {
634    keys: tokio::sync::RwLock<HashMap<String, ApiKey>>,
635}
636
637impl MemoryStorage {
638    pub fn new() -> Self {
639        Self {
640            keys: tokio::sync::RwLock::new(HashMap::new()),
641        }
642    }
643}
644
645#[async_trait]
646impl StorageBackend for MemoryStorage {
647    async fn load_keys(&self) -> Result<HashMap<String, ApiKey>, StorageError> {
648        let keys = self.keys.read().await;
649        debug!("Loaded {} keys from memory storage", keys.len());
650        Ok(keys.clone())
651    }
652
653    async fn save_key(&self, key: &ApiKey) -> Result<(), StorageError> {
654        let mut keys = self.keys.write().await;
655        keys.insert(key.id.clone(), key.clone());
656        debug!("Saved key {} to memory storage", key.id);
657        Ok(())
658    }
659
660    async fn delete_key(&self, key_id: &str) -> Result<(), StorageError> {
661        let mut keys = self.keys.write().await;
662        keys.remove(key_id);
663        debug!("Deleted key {} from memory storage", key_id);
664        Ok(())
665    }
666
667    async fn save_all_keys(&self, new_keys: &HashMap<String, ApiKey>) -> Result<(), StorageError> {
668        let mut keys = self.keys.write().await;
669        *keys = new_keys.clone();
670        debug!(
671            "Replaced all keys in memory storage with {} keys",
672            new_keys.len()
673        );
674        Ok(())
675    }
676}