pub mod app_config;
pub mod fec_storage;
pub mod key_management;
pub mod passkey;
pub mod platform_storage;
pub mod session;
pub mod vault;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
pub use app_config::*;
pub use fec_storage::*;
pub use key_management::*;
pub use passkey::*;
pub use platform_storage::*;
pub use session::*;
pub use vault::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub vault_dir: PathBuf,
pub pbkdf2_iterations: u32,
pub enable_fec: bool,
pub fec_redundancy: f32,
pub max_vault_size: u64,
pub use_keyring: bool,
pub cache_timeout: u64,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
vault_dir: get_vault_directory(),
pbkdf2_iterations: 100_000, enable_fec: true,
fec_redundancy: 1.5,
max_vault_size: 0, use_keyring: true,
cache_timeout: 300, }
}
}
pub struct EncryptedStorageManager {
config: StorageConfig,
vaults: Arc<RwLock<HashMap<String, Arc<EncryptedVault>>>>,
active_sessions: Arc<RwLock<HashMap<String, Session>>>,
key_manager: Arc<KeyManager>,
platform_storage: Arc<PlatformStorage>,
app_config: Arc<RwLock<AppConfigManager>>,
passkey_manager: Arc<PasskeyManager>,
}
impl EncryptedStorageManager {
pub async fn new(config: StorageConfig) -> Result<Self> {
let platform_storage = Arc::new(
PlatformStorage::new(&config.vault_dir)
.context("Failed to initialize platform storage")?,
);
let key_manager = Arc::new(
KeyManager::new(config.pbkdf2_iterations, config.use_keyring)
.await
.context("Failed to initialize key manager")?,
);
let config_dir = config
.vault_dir
.parent()
.unwrap_or(&config.vault_dir)
.to_path_buf();
let app_config = Arc::new(RwLock::new(
AppConfigManager::new(config_dir.clone())
.await
.context("Failed to initialize app config")?,
));
let passkey_storage_dir = config_dir.join("passkeys");
let passkey_manager = Arc::new(
PasskeyManager::new(&passkey_storage_dir)
.context("Failed to initialize passkey manager")?,
);
Ok(Self {
config,
vaults: Arc::new(RwLock::new(HashMap::new())),
active_sessions: Arc::new(RwLock::new(HashMap::new())),
key_manager,
platform_storage,
app_config,
passkey_manager,
})
}
pub async fn create_vault(
&self,
four_words: &str,
password: &str,
display_name: &str,
) -> Result<String> {
let normalized = self.normalize_four_words(four_words);
if self.vault_exists(&normalized).await? {
return Err(anyhow::anyhow!("Vault already exists for {}", four_words));
}
let salt = generate_salt();
let key = self
.key_manager
.derive_key(password, &salt)
.await
.context("Failed to derive encryption key")?;
let vault = EncryptedVault::create(
normalized.clone(),
display_name.to_string(),
key.clone(),
salt,
&self.config,
)
.await
.context("Failed to create vault")?;
let mut vaults = self.vaults.write().await;
vaults.insert(normalized.clone(), Arc::new(vault));
self.store_password_locator(&normalized, password).await?;
if self.config.use_keyring {
self.key_manager
.store_in_keyring(&normalized, &key)
.await
.ok(); }
Ok(normalized)
}
pub async fn login_password_only(&self, password: &str) -> Result<Session> {
let password_hash = self.key_manager.hash_password(password).await?;
let four_words = self
.platform_storage
.find_vault_by_password_hash(&password_hash)
.await
.context("No vault found for this password")?;
self.login(&four_words, password, None).await
}
pub async fn login(
&self,
four_words: &str,
password: &str,
_passkey: Option<Vec<u8>>,
) -> Result<Session> {
let normalized = self.normalize_four_words(four_words);
let vault = self.load_vault(&normalized, password).await?;
let session = Session::new(
normalized.clone(),
vault.display_name.clone(),
self.config.cache_timeout,
);
let mut sessions = self.active_sessions.write().await;
sessions.insert(session.id.clone(), session.clone());
let mut app_config = self.app_config.write().await;
app_config
.set_last_identity(normalized.clone(), vault.display_name.clone())
.await
.ok();
if self.config.use_keyring && app_config.get_config().keyring_enabled {
tracing::info!(
"🔑 LOGIN: Attempting to store password in keyring for '{}'",
normalized
);
match self
.key_manager
.store_in_keyring(&normalized, password.as_bytes())
.await
{
Ok(()) => {
tracing::info!(
"✅ LOGIN: Password stored in keyring successfully for '{}'",
normalized
);
}
Err(e) => {
tracing::error!(
"❌ LOGIN: Failed to store password in keyring for '{}': {}",
normalized,
e
);
tracing::error!(
"⚠️ LOGIN: This means passkey/Touch ID authentication will fail later!"
);
}
}
} else {
tracing::warn!(
"⚠️ LOGIN: Keyring storage skipped - use_keyring={}, keyring_enabled={}",
self.config.use_keyring,
app_config.get_config().keyring_enabled
);
}
Ok(session)
}
pub async fn store(
&self,
session_id: &str,
key: &str,
data: &[u8],
use_fec: bool,
) -> Result<()> {
let session = self.validate_session(session_id).await?;
let vaults = self.vaults.read().await;
let vault = vaults
.get(&session.four_words)
.ok_or_else(|| anyhow::anyhow!("Vault not loaded"))?;
if use_fec && self.config.enable_fec {
vault
.store_with_fec(key, data, self.config.fec_redundancy)
.await
} else {
vault.store(key, data).await
}
}
pub async fn retrieve(&self, session_id: &str, key: &str) -> Result<Vec<u8>> {
let session = self.validate_session(session_id).await?;
let vaults = self.vaults.read().await;
let vault = vaults
.get(&session.four_words)
.ok_or_else(|| anyhow::anyhow!("Vault not loaded"))?;
vault.retrieve(key).await
}
pub async fn list_vaults(&self) -> Result<Vec<VaultInfo>> {
self.platform_storage.list_vaults().await
}
pub async fn switch_account(&self, session_id: &str, four_words: &str) -> Result<Session> {
self.logout(session_id).await?;
if let Some(vault) = self.vaults.read().await.get(four_words) {
let session = Session::new(
four_words.to_string(),
vault.display_name.clone(),
self.config.cache_timeout,
);
let mut sessions = self.active_sessions.write().await;
sessions.insert(session.id.clone(), session.clone());
return Ok(session);
}
Err(anyhow::anyhow!("Vault not cached, password required"))
}
pub async fn logout(&self, session_id: &str) -> Result<()> {
let mut sessions = self.active_sessions.write().await;
sessions.remove(session_id);
Ok(())
}
pub async fn export_vault(&self, session_id: &str, include_data: bool) -> Result<Vec<u8>> {
let session = self.validate_session(session_id).await?;
let vaults = self.vaults.read().await;
let vault = vaults
.get(&session.four_words)
.ok_or_else(|| anyhow::anyhow!("Vault not loaded"))?;
vault.export(include_data).await
}
pub async fn import_vault(&self, backup_data: &[u8], password: &str) -> Result<String> {
let vault = EncryptedVault::import(backup_data, password, &self.config).await?;
let four_words = vault.four_words.clone();
let mut vaults = self.vaults.write().await;
vaults.insert(four_words.clone(), Arc::new(vault));
Ok(four_words)
}
pub async fn get_app_config(&self) -> AppConfig {
self.app_config.read().await.get_config().clone()
}
pub async fn try_auto_login(&self) -> Result<Option<Session>> {
let app_config = self.app_config.read().await;
let config = app_config.get_config();
if !config.auto_login_enabled {
return Ok(None);
}
let four_words = match &config.last_identity {
Some(fw) => fw.clone(),
None => return Ok(None),
};
drop(app_config);
if self.config.use_keyring
&& let Ok(password_bytes) = self.key_manager.get_from_keyring(&four_words).await
&& let Ok(password) = String::from_utf8(password_bytes.to_vec())
{
match self.login(&four_words, &password, None).await {
Ok(session) => return Ok(Some(session)),
Err(_) => {
self.key_manager.delete_from_keyring(&four_words).await.ok();
}
}
}
Ok(None)
}
pub async fn set_auto_login_enabled(&self, enabled: bool) -> Result<()> {
let mut app_config = self.app_config.write().await;
app_config.set_auto_login(enabled).await
}
pub async fn set_keyring_enabled(&self, enabled: bool) -> Result<()> {
let mut app_config = self.app_config.write().await;
app_config.set_keyring_enabled(enabled).await
}
pub async fn get_recent_identities(&self) -> Vec<RecentIdentity> {
self.app_config
.read()
.await
.get_config()
.recent_identities
.clone()
}
pub async fn remove_recent_identity(&self, four_words: &str) -> Result<()> {
self.app_config
.write()
.await
.remove_recent_identity(four_words)
.await
}
pub async fn passkey_register(
&self,
four_words: &str,
device_name: &str,
) -> Result<PasskeyInfo> {
let normalized = self.normalize_four_words(four_words);
let info = self
.passkey_manager
.register_passkey(&normalized, device_name)
.await?;
let mut app_config = self.app_config.write().await;
app_config
.set_identity_has_passkey(&normalized, true)
.await?;
Ok(info)
}
pub async fn passkey_register_webauthn(
&self,
four_words: &str,
device_name: &str,
credential: WebAuthnCredential,
) -> Result<PasskeyInfo> {
let normalized = self.normalize_four_words(four_words);
let info = self
.passkey_manager
.register_passkey_webauthn(&normalized, device_name, credential)
.await?;
let mut app_config = self.app_config.write().await;
app_config
.set_identity_has_passkey(&normalized, true)
.await?;
Ok(info)
}
pub async fn passkey_authenticate(&self, four_words: &str) -> Result<Session> {
let normalized = self.normalize_four_words(four_words);
tracing::info!(
"🔍 RETRIEVAL: Attempting passkey auth for four_words='{}' -> normalized='{}'",
four_words,
normalized
);
if !self.passkey_manager.has_passkey(&normalized).await {
tracing::error!("❌ RETRIEVAL: No passkey registered for '{}'", normalized);
anyhow::bail!("No passkey registered for this identity");
}
tracing::info!("✅ RETRIEVAL: Passkey IS registered for '{}'", normalized);
self.passkey_manager
.mark_passkey_used(&normalized)
.await
.ok();
tracing::info!(
"🔍 RETRIEVAL: Checking keyring config - use_keyring={}",
self.config.use_keyring
);
if self.config.use_keyring {
tracing::info!(
"🔍 RETRIEVAL: Attempting to get password from keyring for '{}'",
normalized
);
match self.key_manager.get_from_keyring(&normalized).await {
Ok(password_bytes) => {
tracing::info!(
"✅ RETRIEVAL: Password bytes retrieved from keyring for '{}'",
normalized
);
match String::from_utf8(password_bytes.to_vec()) {
Ok(password) => {
tracing::info!(
"✅ RETRIEVAL: Password successfully decoded, attempting login for '{}'",
normalized
);
return self.login(&normalized, &password, None).await;
}
Err(e) => {
tracing::error!(
"❌ RETRIEVAL: Failed to decode password bytes for '{}': {}",
normalized,
e
);
}
}
}
Err(e) => {
tracing::error!(
"❌ RETRIEVAL: Failed to get password from keyring for '{}': {}",
normalized,
e
);
}
}
} else {
tracing::warn!("⚠️ RETRIEVAL: Keyring is disabled in config");
}
tracing::error!(
"❌ RETRIEVAL: No password found in keyring for '{}'",
normalized
);
anyhow::bail!(
"Passkey registered but vault password not found in keyring. Please login with password first."
)
}
pub async fn passkey_has_passkey(&self, four_words: &str) -> bool {
let normalized = self.normalize_four_words(four_words);
self.passkey_manager.has_passkey(&normalized).await
}
pub async fn passkey_get_info(&self, four_words: &str) -> Result<PasskeyInfo> {
let normalized = self.normalize_four_words(four_words);
self.passkey_manager.get_passkey_info(&normalized).await
}
pub async fn passkey_delete(&self, four_words: &str) -> Result<()> {
let normalized = self.normalize_four_words(four_words);
self.passkey_manager.delete_passkey(&normalized).await?;
let mut app_config = self.app_config.write().await;
app_config
.set_identity_has_passkey(&normalized, false)
.await?;
Ok(())
}
pub async fn delete_vault(&self, four_words: &str) -> Result<()> {
let normalized = self.normalize_four_words(four_words);
let mut vaults = self.vaults.write().await;
vaults.remove(&normalized);
drop(vaults);
let vault_path = self.config.vault_dir.join(&normalized);
if vault_path.exists() {
tokio::fs::remove_dir_all(&vault_path)
.await
.map_err(|e| anyhow::anyhow!("Failed to delete vault directory: {}", e))?;
}
self.key_manager.delete_from_keyring(&normalized).await.ok();
self.passkey_manager.delete_passkey(&normalized).await.ok();
let mut app_config = self.app_config.write().await;
app_config.remove_recent_identity(&normalized).await?;
Ok(())
}
pub async fn store_password_in_keyring(&self, four_words: &str, password: &str) -> Result<()> {
let normalized = self.normalize_four_words(four_words);
tracing::info!(
"🔑 STORAGE: Storing password in keyring for four_words='{}' -> normalized='{}'",
four_words,
normalized
);
let result = self
.key_manager
.store_in_keyring(&normalized, password.as_bytes())
.await;
if result.is_ok() {
tracing::info!(
"✅ STORAGE: Password successfully stored in keyring for '{}'",
normalized
);
} else {
tracing::error!(
"❌ STORAGE: Failed to store password in keyring for '{}': {:?}",
normalized,
result
);
}
result
}
pub async fn remove_password_from_keyring(&self, four_words: &str) -> Result<()> {
let normalized = self.normalize_four_words(four_words);
self.key_manager.delete_from_keyring(&normalized).await
}
fn normalize_four_words(&self, four_words: &str) -> String {
four_words.trim().to_lowercase().replace([' ', '_'], "-")
}
pub async fn vault_exists(&self, four_words: &str) -> Result<bool> {
self.platform_storage.vault_exists(four_words).await
}
async fn load_vault(&self, four_words: &str, password: &str) -> Result<Arc<EncryptedVault>> {
let vault = EncryptedVault::load(four_words, password, &self.config).await?;
let mut vaults = self.vaults.write().await;
let vault_arc = Arc::new(vault);
vaults.insert(four_words.to_string(), vault_arc.clone());
Ok(vault_arc)
}
async fn store_password_locator(&self, four_words: &str, password: &str) -> Result<()> {
let password_hash = self.key_manager.hash_password(password).await?;
self.platform_storage
.store_password_locator(&password_hash, four_words)
.await
}
async fn validate_session(&self, session_id: &str) -> Result<Session> {
let sessions = self.active_sessions.read().await;
let session = sessions
.get(session_id)
.ok_or_else(|| anyhow::anyhow!("Invalid or expired session"))?;
if session.is_expired() {
return Err(anyhow::anyhow!("Session expired"));
}
Ok(session.clone())
}
}
fn get_vault_directory() -> PathBuf {
#[cfg(target_os = "macos")]
{
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("Library")
.join("Application Support")
.join("com.saorsalabs.communitas")
.join("vaults")
}
#[cfg(target_os = "windows")]
{
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
.join("communitas")
.join("vaults")
}
#[cfg(target_os = "linux")]
{
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("communitas")
.join("vaults")
}
#[cfg(target_os = "ios")]
{
dirs::document_dir()
.unwrap_or_else(|| PathBuf::from("/var/mobile/Containers/Data/Application"))
.join("communitas")
.join("vaults")
}
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
target_os = "linux",
target_os = "ios"
)))]
{
PathBuf::from("/tmp").join("communitas").join("vaults")
}
}
fn generate_salt() -> Vec<u8> {
use rand::{Rng, SeedableRng};
let mut salt = vec![0u8; 32];
rand::rngs::StdRng::from_entropy().fill(&mut salt[..]);
salt
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultInfo {
pub four_words: String,
pub display_name: String,
pub created_at: u64,
pub last_accessed: u64,
pub size_bytes: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_vault_creation_and_login() {
let temp_dir = TempDir::new().unwrap();
let config = StorageConfig {
vault_dir: temp_dir.path().to_path_buf(),
use_keyring: false, ..Default::default()
};
let manager = EncryptedStorageManager::new(config).await.unwrap();
let four_words = manager
.create_vault("ocean-forest-moon-star", "test_password", "Alice")
.await
.unwrap();
assert_eq!(four_words, "ocean-forest-moon-star");
let session = manager
.login("ocean-forest-moon-star", "test_password", None)
.await
.unwrap();
assert_eq!(session.four_words, "ocean-forest-moon-star");
let test_data = b"Hello, encrypted world!";
manager
.store(&session.id, "test_key", test_data, false)
.await
.unwrap();
let retrieved = manager.retrieve(&session.id, "test_key").await.unwrap();
assert_eq!(retrieved, test_data);
}
#[tokio::test]
async fn test_password_only_login() {
let temp_dir = TempDir::new().unwrap();
let config = StorageConfig {
vault_dir: temp_dir.path().to_path_buf(),
use_keyring: false,
..Default::default()
};
let manager = EncryptedStorageManager::new(config).await.unwrap();
manager
.create_vault("river-mountain-sun-cloud", "unique_password", "Bob")
.await
.unwrap();
let session = manager
.login_password_only("unique_password")
.await
.unwrap();
assert_eq!(session.four_words, "river-mountain-sun-cloud");
}
#[tokio::test]
async fn test_fec_storage() {
let temp_dir = TempDir::new().unwrap();
let config = StorageConfig {
vault_dir: temp_dir.path().to_path_buf(),
use_keyring: false,
enable_fec: true,
fec_redundancy: 2.0, ..Default::default()
};
let manager = EncryptedStorageManager::new(config).await.unwrap();
manager
.create_vault("test-fec-vault-storage", "password", "FEC Test")
.await
.unwrap();
let session = manager
.login("test-fec-vault-storage", "password", None)
.await
.unwrap();
let large_data = vec![42u8; 1024 * 1024]; manager
.store(&session.id, "large_file", &large_data, true)
.await
.unwrap();
let retrieved = manager.retrieve(&session.id, "large_file").await.unwrap();
assert_eq!(retrieved, large_data);
}
}