communitas-core 0.1.19

Core business logic for Communitas - PQC collaboration with virtual disks
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
//! Shared Authentication Service
//!
//! This module provides a unified authentication service that can be used by all UI frontends
//! (Tauri Desktop, TUI, Web, etc.). It encapsulates all business logic for:
//! - Multi-identity management
//! - Vault creation and authentication
//! - Passkey/biometric support
//! - Session management
//! - Auto-login functionality

use crate::encrypted_storage::{
    EncryptedStorageManager, PasskeyInfo, RecentIdentity, Session, VaultInfo,
};
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};

/// Session information for active authenticated user
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
    pub session_id: String,
    pub four_words: String,
    pub display_name: String,
}

impl From<Session> for SessionInfo {
    fn from(session: Session) -> Self {
        Self {
            session_id: session.id,
            four_words: session.four_words,
            display_name: session.display_name,
        }
    }
}

/// Unified authentication service for all UI frontends
pub struct AuthService {
    storage_manager: EncryptedStorageManager,
    active_session: Option<Session>,
}

impl AuthService {
    /// Create new auth service with storage manager
    pub fn new(storage_manager: EncryptedStorageManager) -> Self {
        Self {
            storage_manager,
            active_session: None,
        }
    }

    /// Get reference to storage manager
    pub fn storage_manager(&self) -> &EncryptedStorageManager {
        &self.storage_manager
    }

    /// Get mutable reference to storage manager
    pub fn storage_manager_mut(&mut self) -> &mut EncryptedStorageManager {
        &mut self.storage_manager
    }

    /// Create a new vault for a four-word identity
    ///
    /// This creates an encrypted vault with PBKDF2 key derivation (100,000 iterations)
    /// and ChaCha20-Poly1305 encryption.
    pub async fn create_vault(
        &mut self,
        four_words: &str,
        password: &str,
        display_name: &str,
    ) -> Result<String> {
        tracing::info!("AuthService: Creating vault for {}", four_words);

        let vault_id = self
            .storage_manager
            .create_vault(four_words, password, display_name)
            .await?;

        tracing::info!("AuthService: Vault created with ID: {}", vault_id);
        Ok(vault_id)
    }

    /// Login with four-word identity and password
    ///
    /// On success, stores session and optionally saves password to keyring for auto-login.
    pub async fn login(
        &mut self,
        four_words: &str,
        password: &str,
        _device_name: Option<&str>,
    ) -> Result<SessionInfo> {
        tracing::info!("AuthService: Login attempt for {}", four_words);

        // Note: EncryptedStorageManager::login expects Option<Vec<u8>> for passkey, we pass None
        let session = self
            .storage_manager
            .login(four_words, password, None)
            .await?;

        let session_info = SessionInfo::from(session.clone());
        self.active_session = Some(session);

        tracing::info!("AuthService: Login successful for {}", four_words);
        Ok(session_info)
    }

    /// Logout current session
    pub async fn logout(&mut self) -> Result<()> {
        if let Some(session) = &self.active_session {
            tracing::info!("AuthService: Logging out {}", session.four_words);
            self.storage_manager.logout(&session.id).await?;
            self.active_session = None;
            tracing::info!("AuthService: Logout successful");
            Ok(())
        } else {
            Err(anyhow!("No active session to logout"))
        }
    }

    /// Get current active session
    pub fn get_current_session(&self) -> Option<SessionInfo> {
        self.active_session.as_ref().map(|s| SessionInfo {
            session_id: s.id.clone(),
            four_words: s.four_words.clone(),
            display_name: s.display_name.clone(),
        })
    }

    /// Check if user is currently logged in
    pub fn is_logged_in(&self) -> bool {
        self.active_session.is_some()
    }

    /// List all available vaults
    pub async fn list_vaults(&self) -> Result<Vec<VaultInfo>> {
        self.storage_manager.list_vaults().await
    }

    /// Get recent identities (sorted by last used, max 10)
    pub async fn get_recent_identities(&self) -> Result<Vec<RecentIdentity>> {
        // Note: storage_manager returns Vec directly, not Result
        Ok(self.storage_manager.get_recent_identities().await)
    }

    /// Remove a recent identity from the list (does not delete the vault)
    pub async fn remove_recent_identity(&mut self, four_words: &str) -> Result<()> {
        self.storage_manager
            .remove_recent_identity(four_words)
            .await
    }

    /// Check if vault exists for four-word identity
    pub async fn vault_exists(&self, four_words: &str) -> Result<bool> {
        self.storage_manager.vault_exists(four_words).await
    }

    /// Delete a vault (requires password confirmation)
    pub async fn delete_vault(&mut self, four_words: &str, password: &str) -> Result<()> {
        tracing::warn!("AuthService: Deleting vault for {}", four_words);

        // Verify password before deletion
        let _ = self.login(four_words, password, None).await?;

        self.storage_manager.delete_vault(four_words).await?;

        // Logout if this was the active session
        if let Some(session) = &self.active_session
            && session.four_words == four_words
        {
            self.active_session = None;
        }

        tracing::warn!("AuthService: Vault deleted for {}", four_words);
        Ok(())
    }

    // ========================================================================
    // Passkey / Biometric Authentication Methods
    // ========================================================================

    /// Register a passkey for biometric authentication (legacy - without WebAuthn)
    ///
    /// This enables Touch ID, Face ID, or Windows Hello for the identity.
    /// The password is stored in the platform keyring for secure retrieval.
    pub async fn passkey_register(
        &mut self,
        four_words: &str,
        device_name: &str,
    ) -> Result<PasskeyInfo> {
        tracing::info!(
            "AuthService: Registering passkey for {} on {}",
            four_words,
            device_name
        );

        let info = self
            .storage_manager
            .passkey_register(four_words, device_name)
            .await?;

        tracing::info!("AuthService: Passkey registered successfully");
        Ok(info)
    }

    /// Register a passkey with WebAuthn credential
    ///
    /// This stores the WebAuthn credential for true biometric authentication.
    pub async fn passkey_register_webauthn(
        &mut self,
        four_words: &str,
        device_name: &str,
        credential: crate::encrypted_storage::passkey::WebAuthnCredential,
    ) -> Result<PasskeyInfo> {
        tracing::info!(
            "AuthService: Registering WebAuthn passkey for {} on {}",
            four_words,
            device_name
        );

        let info = self
            .storage_manager
            .passkey_register_webauthn(four_words, device_name, credential)
            .await?;

        tracing::info!("AuthService: WebAuthn passkey registered successfully");
        Ok(info)
    }

    /// Authenticate using passkey/biometric
    ///
    /// This retrieves the password from keyring and performs standard vault login.
    pub async fn passkey_authenticate(&mut self, four_words: &str) -> Result<SessionInfo> {
        tracing::info!("AuthService: Passkey authentication for {}", four_words);

        let session = self
            .storage_manager
            .passkey_authenticate(four_words)
            .await?;

        let session_info = SessionInfo::from(session.clone());
        self.active_session = Some(session);

        tracing::info!("AuthService: Passkey authentication successful");
        Ok(session_info)
    }

    /// Check if identity has a registered passkey
    pub async fn passkey_has_passkey(&self, four_words: &str) -> Result<bool> {
        Ok(self.storage_manager.passkey_has_passkey(four_words).await)
    }

    /// Get passkey information for an identity
    pub async fn passkey_get_info(&self, four_words: &str) -> Result<PasskeyInfo> {
        self.storage_manager.passkey_get_info(four_words).await
    }

    /// Delete passkey for an identity
    pub async fn passkey_delete(&mut self, four_words: &str) -> Result<()> {
        tracing::warn!("AuthService: Deleting passkey for {}", four_words);
        self.storage_manager.passkey_delete(four_words).await?;
        tracing::warn!("AuthService: Passkey deleted");
        Ok(())
    }

    // ========================================================================
    // Auto-Login Methods
    // ========================================================================

    /// Attempt auto-login using last-used identity
    ///
    /// Returns session info if successful, None if no auto-login available.
    pub async fn try_auto_login(&mut self) -> Result<Option<SessionInfo>> {
        tracing::info!("AuthService: Attempting auto-login");

        // Get last used identity from app config (returns Vec directly, not Result)
        let recent = self.storage_manager.get_recent_identities().await;

        if recent.is_empty() {
            tracing::info!("AuthService: No recent identities for auto-login");
            return Ok(None);
        }

        let last_identity = &recent[0];
        tracing::info!(
            "AuthService: Attempting auto-login for {}",
            last_identity.four_words
        );

        // Check if passkey is available
        if last_identity.has_passkey {
            match self.passkey_authenticate(&last_identity.four_words).await {
                Ok(session_info) => {
                    tracing::info!("AuthService: Auto-login successful via passkey");
                    return Ok(Some(session_info));
                }
                Err(e) => {
                    tracing::warn!("AuthService: Passkey auto-login failed: {}", e);
                    // Fall through to return None
                }
            }
        }

        tracing::info!("AuthService: No auto-login available");
        Ok(None)
    }

    /// Enable auto-login for current session
    ///
    /// Stores password in keyring so passkey authentication can work.
    pub async fn enable_auto_login(&mut self, password: &str) -> Result<()> {
        let session = self
            .active_session
            .as_ref()
            .ok_or_else(|| anyhow!("No active session"))?;

        tracing::info!(
            "AuthService: Enabling auto-login for {}",
            session.four_words
        );

        // Store password in keyring via storage manager
        self.storage_manager
            .store_password_in_keyring(&session.four_words, password)
            .await?;

        tracing::info!("AuthService: Auto-login enabled");
        Ok(())
    }

    /// Disable auto-login for an identity
    pub async fn disable_auto_login(&mut self, four_words: &str) -> Result<()> {
        tracing::info!("AuthService: Disabling auto-login for {}", four_words);

        // Remove password from keyring
        self.storage_manager
            .remove_password_from_keyring(four_words)
            .await?;

        // Delete passkey if exists
        if self.passkey_has_passkey(four_words).await? {
            self.passkey_delete(four_words).await?;
        }

        tracing::info!("AuthService: Auto-login disabled");
        Ok(())
    }

    // ========================================================================
    // Identity Switching Methods
    // ========================================================================

    /// Switch to another identity (logout current, login new)
    pub async fn switch_identity(&mut self, four_words: &str) -> Result<SessionInfo> {
        tracing::info!("AuthService: Switching to identity {}", four_words);

        // Logout current session if exists
        if self.active_session.is_some() {
            self.logout().await.ok(); // Ignore logout errors
        }

        // Try passkey authentication first
        if self.passkey_has_passkey(four_words).await? {
            match self.passkey_authenticate(four_words).await {
                Ok(session_info) => {
                    tracing::info!("AuthService: Identity switch successful via passkey");
                    return Ok(session_info);
                }
                Err(e) => {
                    tracing::warn!("AuthService: Passkey switch failed: {}", e);
                    return Err(anyhow!("Passkey authentication required but failed"));
                }
            }
        }

        Err(anyhow!(
            "Cannot switch to identity without password or passkey"
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::encrypted_storage::StorageConfig;
    use tempfile::TempDir;

    #[tokio::test]
    async fn test_auth_service_basic_flow() {
        let temp_dir = TempDir::new().expect("Failed to create temp dir");
        let config = StorageConfig {
            vault_dir: temp_dir.path().to_path_buf(),
            use_keyring: false,
            ..Default::default()
        };

        let storage_manager = EncryptedStorageManager::new(config)
            .await
            .expect("Failed to create storage manager");

        let mut auth_service = AuthService::new(storage_manager);

        // Create vault
        let vault_id = auth_service
            .create_vault("ocean-forest-moon-star", "test-password", "Test User")
            .await
            .expect("Failed to create vault");

        assert!(!vault_id.is_empty());

        // Login
        let session_info = auth_service
            .login(
                "ocean-forest-moon-star",
                "test-password",
                Some("Test Device"),
            )
            .await
            .expect("Failed to login");

        assert_eq!(session_info.four_words, "ocean-forest-moon-star");
        assert_eq!(session_info.display_name, "Test User");
        assert!(auth_service.is_logged_in());

        // Get current session
        let current = auth_service.get_current_session();
        assert!(current.is_some());
        assert_eq!(current.unwrap().four_words, "ocean-forest-moon-star");

        // Logout
        auth_service.logout().await.expect("Failed to logout");
        assert!(!auth_service.is_logged_in());
    }

    #[tokio::test]
    async fn test_auth_service_recent_identities() {
        let temp_dir = TempDir::new().expect("Failed to create temp dir");
        let config = StorageConfig {
            vault_dir: temp_dir.path().to_path_buf(),
            use_keyring: false,
            ..Default::default()
        };

        let storage_manager = EncryptedStorageManager::new(config)
            .await
            .expect("Failed to create storage manager");

        let mut auth_service = AuthService::new(storage_manager);

        // Create and login with first identity
        auth_service
            .create_vault("ocean-forest-moon-star", "pass1", "User 1")
            .await
            .expect("Failed to create vault 1");

        auth_service
            .login("ocean-forest-moon-star", "pass1", Some("Device 1"))
            .await
            .expect("Failed to login 1");

        auth_service.logout().await.expect("Failed to logout 1");

        // Create and login with second identity
        auth_service
            .create_vault("river-cloud-stone-tree", "pass2", "User 2")
            .await
            .expect("Failed to create vault 2");

        auth_service
            .login("river-cloud-stone-tree", "pass2", Some("Device 2"))
            .await
            .expect("Failed to login 2");

        // Get recent identities
        let recent = auth_service
            .get_recent_identities()
            .await
            .expect("Failed to get recent");

        assert_eq!(recent.len(), 2);
        // Most recent should be first
        assert_eq!(recent[0].four_words, "river-cloud-stone-tree");
        assert_eq!(recent[1].four_words, "ocean-forest-moon-star");
    }
}