Skip to main content

communitas_core/encrypted_storage/
mod.rs

1//! Encrypted Local Storage System for Communitas
2//!
3//! This module implements a sophisticated multi-layered encrypted storage system that:
4//! - Supports multiple accounts per device with secure switching
5//! - Uses PBKDF2 for key derivation (100,000 iterations as per DESIGN.md)
6//! - Implements ChaCha20-Poly1305 for encryption (superior to AES-GCM on most CPUs)
7//! - Provides Forward Error Correction via Reed-Solomon for resilience
8//! - Integrates with platform-specific secure storage (keyring)
9//! - Enables password-only login on familiar devices
10//!
11//! Architecture:
12//! ```
13//! ┌─────────────────────────────────────────────────────────┐
14//! │                  User Authentication                    │
15//! │         (Password / Passkey / Four-Word Address)       │
16//! └──────────────────────┬──────────────────────────────────┘
17//!                        ▼
18//! ┌─────────────────────────────────────────────────────────┐
19//! │              Key Derivation Layer (PBKDF2)             │
20//! │            100,000 iterations, per-vault salt          │
21//! └──────────────────────┬──────────────────────────────────┘
22//!                        ▼
23//! ┌─────────────────────────────────────────────────────────┐
24//! │            Encryption Layer (ChaCha20-Poly1305)        │
25//! │              Per-file IV, authenticated encryption     │
26//! └──────────────────────┬──────────────────────────────────┘
27//!                        ▼
28//! ┌─────────────────────────────────────────────────────────┐
29//! │          Forward Error Correction (Reed-Solomon)       │
30//! │            Data sharding with redundancy               │
31//! └──────────────────────┬──────────────────────────────────┘
32//!                        ▼
33//! ┌─────────────────────────────────────────────────────────┐
34//! │               Platform Storage Layer                    │
35//! │     (macOS Keychain / Windows DPAPI / Linux Secret)    │
36//! └─────────────────────────────────────────────────────────┘
37//! ```
38
39pub mod app_config;
40pub mod fec_storage;
41pub mod key_management;
42pub mod passkey;
43pub mod platform_storage;
44pub mod session;
45pub mod vault;
46
47use anyhow::{Context, Result};
48use serde::{Deserialize, Serialize};
49use std::collections::HashMap;
50use std::path::PathBuf;
51use std::sync::Arc;
52use tokio::sync::RwLock;
53
54pub use app_config::*;
55pub use fec_storage::*;
56pub use key_management::*;
57pub use passkey::*;
58pub use platform_storage::*;
59pub use session::*;
60pub use vault::*;
61
62/// Configuration for the encrypted storage system
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct StorageConfig {
65    /// Base directory for encrypted vaults
66    pub vault_dir: PathBuf,
67
68    /// PBKDF2 iteration count (default: 100,000 as per DESIGN.md)
69    pub pbkdf2_iterations: u32,
70
71    /// Enable Forward Error Correction for stored files
72    pub enable_fec: bool,
73
74    /// FEC redundancy factor (e.g., 1.5 = 50% redundancy)
75    pub fec_redundancy: f32,
76
77    /// Maximum vault size in bytes (0 = unlimited)
78    pub max_vault_size: u64,
79
80    /// Enable platform keyring integration
81    pub use_keyring: bool,
82
83    /// Cache timeout for decrypted data (seconds)
84    pub cache_timeout: u64,
85}
86
87impl Default for StorageConfig {
88    fn default() -> Self {
89        Self {
90            vault_dir: get_vault_directory(),
91            pbkdf2_iterations: 100_000, // As specified in DESIGN.md
92            enable_fec: true,
93            fec_redundancy: 1.5,
94            max_vault_size: 0, // Unlimited
95            use_keyring: true,
96            cache_timeout: 300, // 5 minutes
97        }
98    }
99}
100
101/// Main encrypted storage manager
102pub struct EncryptedStorageManager {
103    config: StorageConfig,
104    vaults: Arc<RwLock<HashMap<String, Arc<EncryptedVault>>>>,
105    active_sessions: Arc<RwLock<HashMap<String, Session>>>,
106    key_manager: Arc<KeyManager>,
107    platform_storage: Arc<PlatformStorage>,
108    app_config: Arc<RwLock<AppConfigManager>>,
109    passkey_manager: Arc<PasskeyManager>,
110}
111
112impl EncryptedStorageManager {
113    /// Create a new encrypted storage manager
114    pub async fn new(config: StorageConfig) -> Result<Self> {
115        // Initialize platform-specific storage
116        let platform_storage = Arc::new(
117            PlatformStorage::new(&config.vault_dir)
118                .context("Failed to initialize platform storage")?,
119        );
120
121        // Initialize key manager with PBKDF2
122        let key_manager = Arc::new(
123            KeyManager::new(config.pbkdf2_iterations, config.use_keyring)
124                .await
125                .context("Failed to initialize key manager")?,
126        );
127
128        // Initialize app config manager (config stored in parent of vault_dir)
129        let config_dir = config
130            .vault_dir
131            .parent()
132            .unwrap_or(&config.vault_dir)
133            .to_path_buf();
134        let app_config = Arc::new(RwLock::new(
135            AppConfigManager::new(config_dir.clone())
136                .await
137                .context("Failed to initialize app config")?,
138        ));
139
140        // Initialize passkey manager (passkeys stored in config dir)
141        let passkey_storage_dir = config_dir.join("passkeys");
142        let passkey_manager = Arc::new(
143            PasskeyManager::new(&passkey_storage_dir)
144                .context("Failed to initialize passkey manager")?,
145        );
146
147        Ok(Self {
148            config,
149            vaults: Arc::new(RwLock::new(HashMap::new())),
150            active_sessions: Arc::new(RwLock::new(HashMap::new())),
151            key_manager,
152            platform_storage,
153            app_config,
154            passkey_manager,
155        })
156    }
157
158    /// Create a new vault for a four-word identity
159    pub async fn create_vault(
160        &self,
161        four_words: &str,
162        password: &str,
163        display_name: &str,
164    ) -> Result<String> {
165        // Validate four-word address
166        let normalized = self.normalize_four_words(four_words);
167
168        // Check if vault already exists
169        if self.vault_exists(&normalized).await? {
170            return Err(anyhow::anyhow!("Vault already exists for {}", four_words));
171        }
172
173        // Derive encryption key from password
174        let salt = generate_salt();
175        let key = self
176            .key_manager
177            .derive_key(password, &salt)
178            .await
179            .context("Failed to derive encryption key")?;
180
181        // Create vault
182        let vault = EncryptedVault::create(
183            normalized.clone(),
184            display_name.to_string(),
185            key.clone(),
186            salt,
187            &self.config,
188        )
189        .await
190        .context("Failed to create vault")?;
191
192        // Store vault
193        let mut vaults = self.vaults.write().await;
194        vaults.insert(normalized.clone(), Arc::new(vault));
195
196        // Store password hash for password-only login
197        self.store_password_locator(&normalized, password).await?;
198
199        // If keyring is enabled, store master key
200        if self.config.use_keyring {
201            self.key_manager
202                .store_in_keyring(&normalized, &key)
203                .await
204                .ok(); // Non-fatal if keyring fails
205        }
206
207        Ok(normalized)
208    }
209
210    /// Login with password only (searches all vaults)
211    pub async fn login_password_only(&self, password: &str) -> Result<Session> {
212        // Generate password hash for lookup
213        let password_hash = self.key_manager.hash_password(password).await?;
214
215        // Find matching vault
216        let four_words = self
217            .platform_storage
218            .find_vault_by_password_hash(&password_hash)
219            .await
220            .context("No vault found for this password")?;
221
222        // Login with found four-words
223        self.login(&four_words, password, None).await
224    }
225
226    /// Login with four-word address and password
227    pub async fn login(
228        &self,
229        four_words: &str,
230        password: &str,
231        _passkey: Option<Vec<u8>>,
232    ) -> Result<Session> {
233        let normalized = self.normalize_four_words(four_words);
234
235        // Load or open vault
236        let vault = self.load_vault(&normalized, password).await?;
237
238        // Create session
239        let session = Session::new(
240            normalized.clone(),
241            vault.display_name.clone(),
242            self.config.cache_timeout,
243        );
244
245        // Store active session
246        let mut sessions = self.active_sessions.write().await;
247        sessions.insert(session.id.clone(), session.clone());
248
249        // Update app config with last used identity
250        let mut app_config = self.app_config.write().await;
251        app_config
252            .set_last_identity(normalized.clone(), vault.display_name.clone())
253            .await
254            .ok(); // Non-fatal if config update fails
255
256        // Store password in keyring if enabled
257        if self.config.use_keyring && app_config.get_config().keyring_enabled {
258            tracing::info!(
259                "🔑 LOGIN: Attempting to store password in keyring for '{}'",
260                normalized
261            );
262            match self
263                .key_manager
264                .store_in_keyring(&normalized, password.as_bytes())
265                .await
266            {
267                Ok(()) => {
268                    tracing::info!(
269                        "✅ LOGIN: Password stored in keyring successfully for '{}'",
270                        normalized
271                    );
272                }
273                Err(e) => {
274                    tracing::error!(
275                        "❌ LOGIN: Failed to store password in keyring for '{}': {}",
276                        normalized,
277                        e
278                    );
279                    tracing::error!(
280                        "⚠️ LOGIN: This means passkey/Touch ID authentication will fail later!"
281                    );
282                }
283            }
284        } else {
285            tracing::warn!(
286                "⚠️ LOGIN: Keyring storage skipped - use_keyring={}, keyring_enabled={}",
287                self.config.use_keyring,
288                app_config.get_config().keyring_enabled
289            );
290        }
291
292        Ok(session)
293    }
294
295    /// Store encrypted data in a vault
296    pub async fn store(
297        &self,
298        session_id: &str,
299        key: &str,
300        data: &[u8],
301        use_fec: bool,
302    ) -> Result<()> {
303        // Validate session
304        let session = self.validate_session(session_id).await?;
305
306        // Get vault
307        let vaults = self.vaults.read().await;
308        let vault = vaults
309            .get(&session.four_words)
310            .ok_or_else(|| anyhow::anyhow!("Vault not loaded"))?;
311
312        // Store data
313        if use_fec && self.config.enable_fec {
314            // Use Forward Error Correction for important data
315            vault
316                .store_with_fec(key, data, self.config.fec_redundancy)
317                .await
318        } else {
319            // Simple encrypted storage
320            vault.store(key, data).await
321        }
322    }
323
324    /// Retrieve encrypted data from a vault
325    pub async fn retrieve(&self, session_id: &str, key: &str) -> Result<Vec<u8>> {
326        // Validate session
327        let session = self.validate_session(session_id).await?;
328
329        // Get vault
330        let vaults = self.vaults.read().await;
331        let vault = vaults
332            .get(&session.four_words)
333            .ok_or_else(|| anyhow::anyhow!("Vault not loaded"))?;
334
335        // Retrieve data
336        vault.retrieve(key).await
337    }
338
339    /// List all available vaults on this device
340    pub async fn list_vaults(&self) -> Result<Vec<VaultInfo>> {
341        self.platform_storage.list_vaults().await
342    }
343
344    /// Switch to a different account
345    pub async fn switch_account(&self, session_id: &str, four_words: &str) -> Result<Session> {
346        // End current session
347        self.logout(session_id).await?;
348
349        // Try password-less switch if vault is cached
350        if let Some(vault) = self.vaults.read().await.get(four_words) {
351            // Create new session for cached vault
352            let session = Session::new(
353                four_words.to_string(),
354                vault.display_name.clone(),
355                self.config.cache_timeout,
356            );
357
358            let mut sessions = self.active_sessions.write().await;
359            sessions.insert(session.id.clone(), session.clone());
360
361            return Ok(session);
362        }
363
364        Err(anyhow::anyhow!("Vault not cached, password required"))
365    }
366
367    /// Logout and clear session
368    pub async fn logout(&self, session_id: &str) -> Result<()> {
369        let mut sessions = self.active_sessions.write().await;
370        sessions.remove(session_id);
371        Ok(())
372    }
373
374    /// Export vault for backup
375    pub async fn export_vault(&self, session_id: &str, include_data: bool) -> Result<Vec<u8>> {
376        let session = self.validate_session(session_id).await?;
377
378        let vaults = self.vaults.read().await;
379        let vault = vaults
380            .get(&session.four_words)
381            .ok_or_else(|| anyhow::anyhow!("Vault not loaded"))?;
382
383        vault.export(include_data).await
384    }
385
386    /// Import vault from backup
387    pub async fn import_vault(&self, backup_data: &[u8], password: &str) -> Result<String> {
388        let vault = EncryptedVault::import(backup_data, password, &self.config).await?;
389        let four_words = vault.four_words.clone();
390
391        let mut vaults = self.vaults.write().await;
392        vaults.insert(four_words.clone(), Arc::new(vault));
393
394        Ok(four_words)
395    }
396
397    /// Get app configuration
398    pub async fn get_app_config(&self) -> AppConfig {
399        self.app_config.read().await.get_config().clone()
400    }
401
402    /// Try auto-login with last used identity
403    pub async fn try_auto_login(&self) -> Result<Option<Session>> {
404        let app_config = self.app_config.read().await;
405        let config = app_config.get_config();
406
407        // Check if auto-login is enabled
408        if !config.auto_login_enabled {
409            return Ok(None);
410        }
411
412        // Get last identity
413        let four_words = match &config.last_identity {
414            Some(fw) => fw.clone(),
415            None => return Ok(None),
416        };
417
418        drop(app_config); // Release lock
419
420        // Try to get password from keyring
421        if self.config.use_keyring
422            && let Ok(password_bytes) = self.key_manager.get_from_keyring(&four_words).await
423            && let Ok(password) = String::from_utf8(password_bytes.to_vec())
424        {
425            // Attempt login
426            match self.login(&four_words, &password, None).await {
427                Ok(session) => return Ok(Some(session)),
428                Err(_) => {
429                    // Login failed - possibly password changed
430                    // Remove from keyring
431                    self.key_manager.delete_from_keyring(&four_words).await.ok();
432                }
433            }
434        }
435
436        Ok(None)
437    }
438
439    /// Update app configuration setting
440    pub async fn set_auto_login_enabled(&self, enabled: bool) -> Result<()> {
441        let mut app_config = self.app_config.write().await;
442        app_config.set_auto_login(enabled).await
443    }
444
445    /// Update keyring setting
446    pub async fn set_keyring_enabled(&self, enabled: bool) -> Result<()> {
447        let mut app_config = self.app_config.write().await;
448        app_config.set_keyring_enabled(enabled).await
449    }
450
451    /// Get recent identities
452    pub async fn get_recent_identities(&self) -> Vec<RecentIdentity> {
453        self.app_config
454            .read()
455            .await
456            .get_config()
457            .recent_identities
458            .clone()
459    }
460
461    /// Remove a recent identity from the list (does not delete the vault)
462    pub async fn remove_recent_identity(&self, four_words: &str) -> Result<()> {
463        self.app_config
464            .write()
465            .await
466            .remove_recent_identity(four_words)
467            .await
468    }
469
470    // Passkey (biometric) methods
471
472    /// Register passkey/biometric for an identity
473    pub async fn passkey_register(
474        &self,
475        four_words: &str,
476        device_name: &str,
477    ) -> Result<PasskeyInfo> {
478        let normalized = self.normalize_four_words(four_words);
479
480        // Register passkey
481        let info = self
482            .passkey_manager
483            .register_passkey(&normalized, device_name)
484            .await?;
485
486        // Update app config to mark passkey as available
487        let mut app_config = self.app_config.write().await;
488        app_config
489            .set_identity_has_passkey(&normalized, true)
490            .await?;
491
492        Ok(info)
493    }
494
495    /// Register passkey with WebAuthn credential
496    ///
497    /// This stores the WebAuthn credential for true biometric authentication.
498    pub async fn passkey_register_webauthn(
499        &self,
500        four_words: &str,
501        device_name: &str,
502        credential: WebAuthnCredential,
503    ) -> Result<PasskeyInfo> {
504        let normalized = self.normalize_four_words(four_words);
505
506        // Register passkey with WebAuthn credential
507        let info = self
508            .passkey_manager
509            .register_passkey_webauthn(&normalized, device_name, credential)
510            .await?;
511
512        // Update app config to mark passkey as available
513        let mut app_config = self.app_config.write().await;
514        app_config
515            .set_identity_has_passkey(&normalized, true)
516            .await?;
517
518        Ok(info)
519    }
520
521    /// Authenticate with passkey/biometric and create session
522    ///
523    /// This uses the password stored in keyring after biometric verification
524    pub async fn passkey_authenticate(&self, four_words: &str) -> Result<Session> {
525        let normalized = self.normalize_four_words(four_words);
526
527        tracing::info!(
528            "🔍 RETRIEVAL: Attempting passkey auth for four_words='{}' -> normalized='{}'",
529            four_words,
530            normalized
531        );
532
533        // Check passkey is registered
534        if !self.passkey_manager.has_passkey(&normalized).await {
535            tracing::error!("❌ RETRIEVAL: No passkey registered for '{}'", normalized);
536            anyhow::bail!("No passkey registered for this identity");
537        }
538
539        tracing::info!("✅ RETRIEVAL: Passkey IS registered for '{}'", normalized);
540
541        // Mark passkey as used
542        self.passkey_manager
543            .mark_passkey_used(&normalized)
544            .await
545            .ok();
546
547        // Load vault using password from keyring
548        tracing::info!(
549            "🔍 RETRIEVAL: Checking keyring config - use_keyring={}",
550            self.config.use_keyring
551        );
552
553        if self.config.use_keyring {
554            tracing::info!(
555                "🔍 RETRIEVAL: Attempting to get password from keyring for '{}'",
556                normalized
557            );
558
559            match self.key_manager.get_from_keyring(&normalized).await {
560                Ok(password_bytes) => {
561                    tracing::info!(
562                        "✅ RETRIEVAL: Password bytes retrieved from keyring for '{}'",
563                        normalized
564                    );
565
566                    match String::from_utf8(password_bytes.to_vec()) {
567                        Ok(password) => {
568                            tracing::info!(
569                                "✅ RETRIEVAL: Password successfully decoded, attempting login for '{}'",
570                                normalized
571                            );
572                            // Login with stored password
573                            return self.login(&normalized, &password, None).await;
574                        }
575                        Err(e) => {
576                            tracing::error!(
577                                "❌ RETRIEVAL: Failed to decode password bytes for '{}': {}",
578                                normalized,
579                                e
580                            );
581                        }
582                    }
583                }
584                Err(e) => {
585                    tracing::error!(
586                        "❌ RETRIEVAL: Failed to get password from keyring for '{}': {}",
587                        normalized,
588                        e
589                    );
590                }
591            }
592        } else {
593            tracing::warn!("⚠️ RETRIEVAL: Keyring is disabled in config");
594        }
595
596        // If no password in keyring, cannot proceed
597        tracing::error!(
598            "❌ RETRIEVAL: No password found in keyring for '{}'",
599            normalized
600        );
601        anyhow::bail!(
602            "Passkey registered but vault password not found in keyring. Please login with password first."
603        )
604    }
605
606    /// Check if passkey is registered for identity
607    pub async fn passkey_has_passkey(&self, four_words: &str) -> bool {
608        let normalized = self.normalize_four_words(four_words);
609        self.passkey_manager.has_passkey(&normalized).await
610    }
611
612    /// Get passkey information
613    pub async fn passkey_get_info(&self, four_words: &str) -> Result<PasskeyInfo> {
614        let normalized = self.normalize_four_words(four_words);
615        self.passkey_manager.get_passkey_info(&normalized).await
616    }
617
618    /// Delete passkey for identity
619    pub async fn passkey_delete(&self, four_words: &str) -> Result<()> {
620        let normalized = self.normalize_four_words(four_words);
621        self.passkey_manager.delete_passkey(&normalized).await?;
622
623        // Update app config
624        let mut app_config = self.app_config.write().await;
625        app_config
626            .set_identity_has_passkey(&normalized, false)
627            .await?;
628
629        Ok(())
630    }
631
632    /// Delete a vault permanently
633    ///
634    /// WARNING: This permanently deletes all encrypted data for this identity.
635    pub async fn delete_vault(&self, four_words: &str) -> Result<()> {
636        let normalized = self.normalize_four_words(four_words);
637
638        // Remove from cache
639        let mut vaults = self.vaults.write().await;
640        vaults.remove(&normalized);
641        drop(vaults);
642
643        // Delete vault directory from filesystem
644        let vault_path = self.config.vault_dir.join(&normalized);
645        if vault_path.exists() {
646            tokio::fs::remove_dir_all(&vault_path)
647                .await
648                .map_err(|e| anyhow::anyhow!("Failed to delete vault directory: {}", e))?;
649        }
650
651        // Remove password from keyring if exists
652        self.key_manager.delete_from_keyring(&normalized).await.ok();
653
654        // Remove passkey if exists
655        self.passkey_manager.delete_passkey(&normalized).await.ok();
656
657        // Remove from app config recent identities
658        let mut app_config = self.app_config.write().await;
659        app_config.remove_recent_identity(&normalized).await?;
660
661        Ok(())
662    }
663
664    /// Store password in platform keyring (for auto-login)
665    pub async fn store_password_in_keyring(&self, four_words: &str, password: &str) -> Result<()> {
666        let normalized = self.normalize_four_words(four_words);
667        tracing::info!(
668            "🔑 STORAGE: Storing password in keyring for four_words='{}' -> normalized='{}'",
669            four_words,
670            normalized
671        );
672        let result = self
673            .key_manager
674            .store_in_keyring(&normalized, password.as_bytes())
675            .await;
676
677        if result.is_ok() {
678            tracing::info!(
679                "✅ STORAGE: Password successfully stored in keyring for '{}'",
680                normalized
681            );
682        } else {
683            tracing::error!(
684                "❌ STORAGE: Failed to store password in keyring for '{}': {:?}",
685                normalized,
686                result
687            );
688        }
689
690        result
691    }
692
693    /// Remove password from platform keyring
694    pub async fn remove_password_from_keyring(&self, four_words: &str) -> Result<()> {
695        let normalized = self.normalize_four_words(four_words);
696        self.key_manager.delete_from_keyring(&normalized).await
697    }
698
699    // Helper methods
700
701    fn normalize_four_words(&self, four_words: &str) -> String {
702        four_words.trim().to_lowercase().replace([' ', '_'], "-")
703    }
704
705    pub async fn vault_exists(&self, four_words: &str) -> Result<bool> {
706        self.platform_storage.vault_exists(four_words).await
707    }
708
709    async fn load_vault(&self, four_words: &str, password: &str) -> Result<Arc<EncryptedVault>> {
710        // 🔒 SECURITY FIX: Always validate password by loading vault from disk
711        // Never trust cached vaults during authentication - they bypass password validation
712        // ChaCha20-Poly1305 AEAD will automatically fail decryption with wrong password
713
714        // Load from disk (this validates password via AEAD decryption)
715        let vault = EncryptedVault::load(four_words, password, &self.config).await?;
716
717        // Cache only AFTER successful password validation
718        let mut vaults = self.vaults.write().await;
719        let vault_arc = Arc::new(vault);
720        vaults.insert(four_words.to_string(), vault_arc.clone());
721
722        Ok(vault_arc)
723    }
724
725    async fn store_password_locator(&self, four_words: &str, password: &str) -> Result<()> {
726        let password_hash = self.key_manager.hash_password(password).await?;
727        self.platform_storage
728            .store_password_locator(&password_hash, four_words)
729            .await
730    }
731
732    async fn validate_session(&self, session_id: &str) -> Result<Session> {
733        let sessions = self.active_sessions.read().await;
734        let session = sessions
735            .get(session_id)
736            .ok_or_else(|| anyhow::anyhow!("Invalid or expired session"))?;
737
738        if session.is_expired() {
739            return Err(anyhow::anyhow!("Session expired"));
740        }
741
742        Ok(session.clone())
743    }
744}
745
746/// Get platform-specific vault directory
747fn get_vault_directory() -> PathBuf {
748    #[cfg(target_os = "macos")]
749    {
750        dirs::home_dir()
751            .unwrap_or_else(|| PathBuf::from("/tmp"))
752            .join("Library")
753            .join("Application Support")
754            .join("com.saorsalabs.communitas")
755            .join("vaults")
756    }
757
758    #[cfg(target_os = "windows")]
759    {
760        dirs::config_dir()
761            .unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
762            .join("communitas")
763            .join("vaults")
764    }
765
766    #[cfg(target_os = "linux")]
767    {
768        dirs::config_dir()
769            .unwrap_or_else(|| PathBuf::from("/tmp"))
770            .join("communitas")
771            .join("vaults")
772    }
773
774    #[cfg(target_os = "ios")]
775    {
776        // On iOS, use the app's document directory
777        dirs::document_dir()
778            .unwrap_or_else(|| PathBuf::from("/var/mobile/Containers/Data/Application"))
779            .join("communitas")
780            .join("vaults")
781    }
782
783    #[cfg(not(any(
784        target_os = "macos",
785        target_os = "windows",
786        target_os = "linux",
787        target_os = "ios"
788    )))]
789    {
790        // Fallback for other platforms
791        PathBuf::from("/tmp").join("communitas").join("vaults")
792    }
793}
794
795/// Generate a cryptographically secure salt
796fn generate_salt() -> Vec<u8> {
797    use rand::{Rng, SeedableRng};
798    let mut salt = vec![0u8; 32];
799    rand::rngs::StdRng::from_entropy().fill(&mut salt[..]);
800    salt
801}
802
803/// Vault metadata
804#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct VaultInfo {
806    pub four_words: String,
807    pub display_name: String,
808    pub created_at: u64,
809    pub last_accessed: u64,
810    pub size_bytes: u64,
811}
812
813#[cfg(test)]
814mod tests {
815    use super::*;
816    use tempfile::TempDir;
817
818    #[tokio::test]
819    async fn test_vault_creation_and_login() {
820        let temp_dir = TempDir::new().unwrap();
821        let config = StorageConfig {
822            vault_dir: temp_dir.path().to_path_buf(),
823            use_keyring: false, // Disable for tests
824            ..Default::default()
825        };
826
827        let manager = EncryptedStorageManager::new(config).await.unwrap();
828
829        // Create vault
830        let four_words = manager
831            .create_vault("ocean-forest-moon-star", "test_password", "Alice")
832            .await
833            .unwrap();
834
835        assert_eq!(four_words, "ocean-forest-moon-star");
836
837        // Login with four-words
838        let session = manager
839            .login("ocean-forest-moon-star", "test_password", None)
840            .await
841            .unwrap();
842
843        assert_eq!(session.four_words, "ocean-forest-moon-star");
844
845        // Store and retrieve data
846        let test_data = b"Hello, encrypted world!";
847        manager
848            .store(&session.id, "test_key", test_data, false)
849            .await
850            .unwrap();
851
852        let retrieved = manager.retrieve(&session.id, "test_key").await.unwrap();
853        assert_eq!(retrieved, test_data);
854    }
855
856    #[tokio::test]
857    async fn test_password_only_login() {
858        let temp_dir = TempDir::new().unwrap();
859        let config = StorageConfig {
860            vault_dir: temp_dir.path().to_path_buf(),
861            use_keyring: false,
862            ..Default::default()
863        };
864
865        let manager = EncryptedStorageManager::new(config).await.unwrap();
866
867        // Create vault
868        manager
869            .create_vault("river-mountain-sun-cloud", "unique_password", "Bob")
870            .await
871            .unwrap();
872
873        // Logout (simulate app restart)
874        // ...
875
876        // Login with password only
877        let session = manager
878            .login_password_only("unique_password")
879            .await
880            .unwrap();
881
882        assert_eq!(session.four_words, "river-mountain-sun-cloud");
883    }
884
885    #[tokio::test]
886    async fn test_fec_storage() {
887        let temp_dir = TempDir::new().unwrap();
888        let config = StorageConfig {
889            vault_dir: temp_dir.path().to_path_buf(),
890            use_keyring: false,
891            enable_fec: true,
892            fec_redundancy: 2.0, // 100% redundancy
893            ..Default::default()
894        };
895
896        let manager = EncryptedStorageManager::new(config).await.unwrap();
897
898        // Create vault and login
899        manager
900            .create_vault("test-fec-vault-storage", "password", "FEC Test")
901            .await
902            .unwrap();
903
904        let session = manager
905            .login("test-fec-vault-storage", "password", None)
906            .await
907            .unwrap();
908
909        // Store large data with FEC
910        let large_data = vec![42u8; 1024 * 1024]; // 1MB
911        manager
912            .store(&session.id, "large_file", &large_data, true)
913            .await
914            .unwrap();
915
916        // Retrieve should work even if some shards are corrupted
917        let retrieved = manager.retrieve(&session.id, "large_file").await.unwrap();
918        assert_eq!(retrieved, large_data);
919    }
920}