Skip to main content

peat_protocol/security/
user_auth.rs

1//! User Authentication for Human Operators.
2//!
3//! Implements ADR-006 Layer 2: User Authentication.
4//!
5//! # Overview
6//!
7//! This module provides authentication for human operators using C2 tablets
8//! and mission planning tools. It supports:
9//!
10//! - Password + TOTP (Time-based One-Time Password) for tactical environments
11//! - Session management with configurable expiry (default 8 hours)
12//! - Local user database for offline operation
13//!
14//! # Security Properties
15//!
16//! - Passwords are stored using Argon2id (memory-hard, resistant to GPU attacks)
17//! - TOTP uses HMAC-SHA256 with 6-digit codes
18//! - Sessions are identified by cryptographically random UUIDs
19//! - Failed login attempts are logged (not rate-limited in this implementation)
20//!
21//! # Example
22//!
23//! ```ignore
24//! use peat_protocol::security::{UserAuthenticator, LocalUserStore, Credential};
25//!
26//! // Create authenticator with local user store
27//! let store = LocalUserStore::new();
28//! let authenticator = UserAuthenticator::new(Box::new(store));
29//!
30//! // Register a new user
31//! authenticator.register_user(
32//!     "alpha_6",
33//!     "password123",
34//!     UserIdentity {
35//!         username: "alpha_6".to_string(),
36//!         display_name: "CPT Smith".to_string(),
37//!         rank: MilitaryRank::Captain,
38//!         clearance: SecurityClearance::Secret,
39//!         unit: OrganizationUnit::new("1st Plt", "A Co"),
40//!         roles: HashSet::from([Role::Commander]),
41//!     },
42//! ).await?;
43//!
44//! // Authenticate user
45//! let session = authenticator.authenticate(
46//!     "alpha_6",
47//!     &Credential::PasswordMfa {
48//!         password: "password123".to_string(),
49//!         totp_code: "123456".to_string(),
50//!     },
51//! ).await?;
52//!
53//! // Validate session
54//! let identity = authenticator.validate_session(&session.session_id).await?;
55//! ```
56
57use super::authorization::Role;
58use super::device_id::DeviceId;
59use super::error::SecurityError;
60use argon2::{
61    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
62    Argon2,
63};
64use hmac::{Hmac, Mac};
65use rand_core::RngCore;
66use serde::{Deserialize, Serialize};
67use sha2::Sha256;
68use std::collections::{HashMap, HashSet};
69use std::sync::{Arc, RwLock};
70use std::time::{Duration, SystemTime, UNIX_EPOCH};
71use uuid::Uuid;
72
73/// Default session expiry in hours.
74pub const DEFAULT_SESSION_EXPIRY_HOURS: u64 = 8;
75
76/// TOTP time step in seconds (standard is 30).
77pub const TOTP_TIME_STEP_SECS: u64 = 30;
78
79/// TOTP code length (standard is 6 digits).
80pub const TOTP_CODE_LENGTH: usize = 6;
81
82/// Number of time steps to allow for clock drift (1 step = 30 seconds).
83pub const TOTP_CLOCK_DRIFT_STEPS: i64 = 1;
84
85/// Military ranks (simplified for Peat Protocol).
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87pub enum MilitaryRank {
88    // Enlisted
89    Private,
90    Specialist,
91    Corporal,
92    Sergeant,
93    StaffSergeant,
94    SergeantFirstClass,
95    MasterSergeant,
96    FirstSergeant,
97    SergeantMajor,
98
99    // Warrant Officers
100    WarrantOfficer1,
101    ChiefWarrantOfficer2,
102    ChiefWarrantOfficer3,
103    ChiefWarrantOfficer4,
104    ChiefWarrantOfficer5,
105
106    // Commissioned Officers
107    SecondLieutenant,
108    FirstLieutenant,
109    Captain,
110    Major,
111    LieutenantColonel,
112    Colonel,
113    BrigadierGeneral,
114    MajorGeneral,
115    LieutenantGeneral,
116    General,
117
118    // Civilian
119    Civilian,
120}
121
122impl std::fmt::Display for MilitaryRank {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            MilitaryRank::Private => write!(f, "PVT"),
126            MilitaryRank::Specialist => write!(f, "SPC"),
127            MilitaryRank::Corporal => write!(f, "CPL"),
128            MilitaryRank::Sergeant => write!(f, "SGT"),
129            MilitaryRank::StaffSergeant => write!(f, "SSG"),
130            MilitaryRank::SergeantFirstClass => write!(f, "SFC"),
131            MilitaryRank::MasterSergeant => write!(f, "MSG"),
132            MilitaryRank::FirstSergeant => write!(f, "1SG"),
133            MilitaryRank::SergeantMajor => write!(f, "SGM"),
134            MilitaryRank::WarrantOfficer1 => write!(f, "WO1"),
135            MilitaryRank::ChiefWarrantOfficer2 => write!(f, "CW2"),
136            MilitaryRank::ChiefWarrantOfficer3 => write!(f, "CW3"),
137            MilitaryRank::ChiefWarrantOfficer4 => write!(f, "CW4"),
138            MilitaryRank::ChiefWarrantOfficer5 => write!(f, "CW5"),
139            MilitaryRank::SecondLieutenant => write!(f, "2LT"),
140            MilitaryRank::FirstLieutenant => write!(f, "1LT"),
141            MilitaryRank::Captain => write!(f, "CPT"),
142            MilitaryRank::Major => write!(f, "MAJ"),
143            MilitaryRank::LieutenantColonel => write!(f, "LTC"),
144            MilitaryRank::Colonel => write!(f, "COL"),
145            MilitaryRank::BrigadierGeneral => write!(f, "BG"),
146            MilitaryRank::MajorGeneral => write!(f, "MG"),
147            MilitaryRank::LieutenantGeneral => write!(f, "LTG"),
148            MilitaryRank::General => write!(f, "GEN"),
149            MilitaryRank::Civilian => write!(f, "CIV"),
150        }
151    }
152}
153
154/// Security clearance levels.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
156pub enum SecurityClearance {
157    /// Unclassified - no clearance required
158    Unclassified,
159    /// Controlled Unclassified Information
160    Cui,
161    /// Confidential clearance
162    Confidential,
163    /// Secret clearance
164    Secret,
165    /// Top Secret clearance
166    TopSecret,
167    /// Top Secret / Sensitive Compartmented Information
168    TopSecretSci,
169}
170
171impl std::fmt::Display for SecurityClearance {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        match self {
174            SecurityClearance::Unclassified => write!(f, "UNCLASSIFIED"),
175            SecurityClearance::Cui => write!(f, "CUI"),
176            SecurityClearance::Confidential => write!(f, "CONFIDENTIAL"),
177            SecurityClearance::Secret => write!(f, "SECRET"),
178            SecurityClearance::TopSecret => write!(f, "TOP SECRET"),
179            SecurityClearance::TopSecretSci => write!(f, "TS/SCI"),
180        }
181    }
182}
183
184/// Organizational unit information.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct OrganizationUnit {
187    /// Primary unit (e.g., "1st Platoon", "Alpha Company")
188    pub name: String,
189
190    /// Parent organization (e.g., "2nd Battalion", "1st Brigade")
191    pub parent: Option<String>,
192
193    /// Unit identifier code (UIC)
194    pub uic: Option<String>,
195}
196
197impl OrganizationUnit {
198    /// Create a new organization unit.
199    pub fn new(name: impl Into<String>, parent: impl Into<String>) -> Self {
200        Self {
201            name: name.into(),
202            parent: Some(parent.into()),
203            uic: None,
204        }
205    }
206
207    /// Create a top-level organization unit.
208    pub fn top_level(name: impl Into<String>) -> Self {
209        Self {
210            name: name.into(),
211            parent: None,
212            uic: None,
213        }
214    }
215
216    /// Set the Unit Identifier Code.
217    pub fn with_uic(mut self, uic: impl Into<String>) -> Self {
218        self.uic = Some(uic.into());
219        self
220    }
221}
222
223impl std::fmt::Display for OrganizationUnit {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        if let Some(parent) = &self.parent {
226            write!(f, "{}, {}", self.name, parent)
227        } else {
228            write!(f, "{}", self.name)
229        }
230    }
231}
232
233/// Complete user identity for human operators.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct UserIdentity {
236    /// Username (e.g., call sign like "alpha_6")
237    pub username: String,
238
239    /// Display name (e.g., "CPT John Smith")
240    pub display_name: String,
241
242    /// Military rank
243    pub rank: MilitaryRank,
244
245    /// Security clearance level
246    pub clearance: SecurityClearance,
247
248    /// Organizational unit
249    pub unit: OrganizationUnit,
250
251    /// Roles for RBAC authorization
252    pub roles: HashSet<Role>,
253}
254
255impl UserIdentity {
256    /// Create a builder for UserIdentity.
257    pub fn builder(username: impl Into<String>) -> UserIdentityBuilder {
258        UserIdentityBuilder::new(username)
259    }
260}
261
262/// Builder for UserIdentity.
263pub struct UserIdentityBuilder {
264    username: String,
265    display_name: Option<String>,
266    rank: MilitaryRank,
267    clearance: SecurityClearance,
268    unit: Option<OrganizationUnit>,
269    roles: HashSet<Role>,
270}
271
272impl UserIdentityBuilder {
273    /// Create a new builder.
274    pub fn new(username: impl Into<String>) -> Self {
275        Self {
276            username: username.into(),
277            display_name: None,
278            rank: MilitaryRank::Civilian,
279            clearance: SecurityClearance::Unclassified,
280            unit: None,
281            roles: HashSet::new(),
282        }
283    }
284
285    /// Set the display name.
286    pub fn display_name(mut self, name: impl Into<String>) -> Self {
287        self.display_name = Some(name.into());
288        self
289    }
290
291    /// Set the rank.
292    pub fn rank(mut self, rank: MilitaryRank) -> Self {
293        self.rank = rank;
294        self
295    }
296
297    /// Set the clearance.
298    pub fn clearance(mut self, clearance: SecurityClearance) -> Self {
299        self.clearance = clearance;
300        self
301    }
302
303    /// Set the organizational unit.
304    pub fn unit(mut self, unit: OrganizationUnit) -> Self {
305        self.unit = Some(unit);
306        self
307    }
308
309    /// Add a role.
310    pub fn role(mut self, role: Role) -> Self {
311        self.roles.insert(role);
312        self
313    }
314
315    /// Add multiple roles.
316    pub fn roles(mut self, roles: impl IntoIterator<Item = Role>) -> Self {
317        self.roles.extend(roles);
318        self
319    }
320
321    /// Build the UserIdentity.
322    pub fn build(self) -> UserIdentity {
323        UserIdentity {
324            username: self.username.clone(),
325            display_name: self.display_name.unwrap_or_else(|| self.username.clone()),
326            rank: self.rank,
327            clearance: self.clearance,
328            unit: self
329                .unit
330                .unwrap_or_else(|| OrganizationUnit::top_level("Unknown")),
331            roles: self.roles,
332        }
333    }
334}
335
336/// Stored user record with authentication credentials.
337#[derive(Debug, Clone)]
338pub struct UserRecord {
339    /// User identity
340    pub identity: UserIdentity,
341
342    /// Authentication method
343    pub auth_method: AuthMethod,
344
345    /// Account status
346    pub status: AccountStatus,
347
348    /// Account creation time
349    pub created_at: SystemTime,
350
351    /// Last login time
352    pub last_login: Option<SystemTime>,
353
354    /// Failed login attempt count (reset on successful login)
355    pub failed_attempts: u32,
356}
357
358/// Account status.
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
360pub enum AccountStatus {
361    /// Account is active
362    Active,
363    /// Account is locked (too many failed attempts)
364    Locked,
365    /// Account is disabled by admin
366    Disabled,
367    /// Account is pending activation
368    Pending,
369}
370
371/// Authentication methods supported.
372#[derive(Debug, Clone)]
373pub enum AuthMethod {
374    /// Password + TOTP (Time-based One-Time Password)
375    PasswordMfa {
376        /// Argon2id password hash
377        password_hash: String,
378        /// TOTP secret (base32 encoded)
379        totp_secret: Vec<u8>,
380    },
381
382    /// Smart card (CAC/PIV) - placeholder for future implementation
383    SmartCard {
384        /// Card serial number
385        card_id: String,
386        /// PIN hash
387        pin_hash: String,
388    },
389
390    /// Certificate-based authentication - placeholder for future implementation
391    Certificate {
392        /// X.509 certificate fingerprint
393        certificate_fingerprint: String,
394    },
395}
396
397/// Credential provided during authentication.
398#[derive(Debug, Clone)]
399pub enum Credential {
400    /// Password + TOTP code
401    PasswordMfa {
402        /// Plain text password
403        password: String,
404        /// 6-digit TOTP code
405        totp_code: String,
406    },
407
408    /// Smart card PIN
409    SmartCard {
410        /// Card serial number
411        card_id: String,
412        /// PIN
413        pin: String,
414    },
415
416    /// Certificate-based (certificate presented during TLS handshake)
417    Certificate {
418        /// Certificate fingerprint
419        fingerprint: String,
420    },
421}
422
423/// User session token.
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct UserSession {
426    /// Unique session identifier
427    pub session_id: SessionId,
428
429    /// Authenticated user identity
430    pub identity: UserIdentity,
431
432    /// Device this session is bound to (if any)
433    pub device_id: Option<DeviceId>,
434
435    /// Session creation time
436    pub created_at: SystemTime,
437
438    /// Session expiration time
439    pub expires_at: SystemTime,
440}
441
442impl UserSession {
443    /// Check if the session is expired.
444    pub fn is_expired(&self) -> bool {
445        SystemTime::now() > self.expires_at
446    }
447
448    /// Get remaining session time.
449    pub fn remaining_time(&self) -> Option<Duration> {
450        self.expires_at.duration_since(SystemTime::now()).ok()
451    }
452}
453
454/// Session identifier (UUID v4).
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
456pub struct SessionId(Uuid);
457
458impl SessionId {
459    /// Generate a new random session ID.
460    pub fn new() -> Self {
461        Self(Uuid::new_v4())
462    }
463}
464
465impl Default for SessionId {
466    fn default() -> Self {
467        Self::new()
468    }
469}
470
471impl std::fmt::Display for SessionId {
472    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473        write!(f, "{}", self.0)
474    }
475}
476
477/// User store trait for database abstraction.
478pub trait UserStore: Send + Sync {
479    /// Get a user by username.
480    fn get_user(&self, username: &str) -> Option<UserRecord>;
481
482    /// Store a user record.
483    fn store_user(&self, record: UserRecord) -> Result<(), SecurityError>;
484
485    /// Update a user record.
486    fn update_user(&self, record: UserRecord) -> Result<(), SecurityError>;
487
488    /// Delete a user.
489    fn delete_user(&self, username: &str) -> Result<(), SecurityError>;
490
491    /// List all usernames.
492    fn list_users(&self) -> Vec<String>;
493}
494
495/// Local in-memory user store for offline tactical use.
496#[derive(Debug, Default)]
497pub struct LocalUserStore {
498    users: RwLock<HashMap<String, UserRecord>>,
499}
500
501impl LocalUserStore {
502    /// Create a new empty local user store.
503    pub fn new() -> Self {
504        Self {
505            users: RwLock::new(HashMap::new()),
506        }
507    }
508
509    /// Create a local user store with pre-provisioned users.
510    pub fn with_users(users: Vec<UserRecord>) -> Self {
511        let store = Self::new();
512        {
513            let mut map = store.users.write().expect("users lock poisoned");
514            for user in users {
515                map.insert(user.identity.username.clone(), user);
516            }
517        }
518        store
519    }
520}
521
522impl UserStore for LocalUserStore {
523    fn get_user(&self, username: &str) -> Option<UserRecord> {
524        self.users.read().ok()?.get(username).cloned()
525    }
526
527    fn store_user(&self, record: UserRecord) -> Result<(), SecurityError> {
528        let mut users = self
529            .users
530            .write()
531            .map_err(|e| SecurityError::Internal(format!("users lock poisoned: {e}")))?;
532        if users.contains_key(&record.identity.username) {
533            return Err(SecurityError::UserAlreadyExists {
534                username: record.identity.username,
535            });
536        }
537        users.insert(record.identity.username.clone(), record);
538        Ok(())
539    }
540
541    fn update_user(&self, record: UserRecord) -> Result<(), SecurityError> {
542        let mut users = self
543            .users
544            .write()
545            .map_err(|e| SecurityError::Internal(format!("users lock poisoned: {e}")))?;
546        if !users.contains_key(&record.identity.username) {
547            return Err(SecurityError::UserNotFound {
548                username: record.identity.username,
549            });
550        }
551        users.insert(record.identity.username.clone(), record);
552        Ok(())
553    }
554
555    fn delete_user(&self, username: &str) -> Result<(), SecurityError> {
556        let mut users = self
557            .users
558            .write()
559            .map_err(|e| SecurityError::Internal(format!("users lock poisoned: {e}")))?;
560        if users.remove(username).is_none() {
561            return Err(SecurityError::UserNotFound {
562                username: username.to_string(),
563            });
564        }
565        Ok(())
566    }
567
568    fn list_users(&self) -> Vec<String> {
569        self.users
570            .read()
571            .map(|u| u.keys().cloned().collect())
572            .unwrap_or_default()
573    }
574}
575
576/// User authentication manager.
577pub struct UserAuthenticator {
578    /// User database
579    user_store: Box<dyn UserStore>,
580
581    /// Active sessions
582    sessions: Arc<RwLock<HashMap<SessionId, UserSession>>>,
583
584    /// Session expiry duration
585    session_expiry: Duration,
586
587    /// Maximum failed login attempts before lockout
588    max_failed_attempts: u32,
589}
590
591impl UserAuthenticator {
592    /// Create a new user authenticator.
593    pub fn new(user_store: Box<dyn UserStore>) -> Self {
594        Self {
595            user_store,
596            sessions: Arc::new(RwLock::new(HashMap::new())),
597            session_expiry: Duration::from_secs(DEFAULT_SESSION_EXPIRY_HOURS * 3600),
598            max_failed_attempts: 5,
599        }
600    }
601
602    /// Create authenticator with custom session expiry.
603    pub fn with_session_expiry(mut self, expiry: Duration) -> Self {
604        self.session_expiry = expiry;
605        self
606    }
607
608    /// Create authenticator with custom max failed attempts.
609    pub fn with_max_failed_attempts(mut self, max: u32) -> Self {
610        self.max_failed_attempts = max;
611        self
612    }
613
614    /// Register a new user with Password + TOTP authentication.
615    ///
616    /// Returns the TOTP secret (base32 encoded) for the user to configure their authenticator app.
617    /// The `username` parameter is used as the lookup key - it should match identity.username.
618    pub fn register_user(
619        &self,
620        _username: &str,
621        password: &str,
622        identity: UserIdentity,
623    ) -> Result<String, SecurityError> {
624        // Hash password with Argon2id
625        let salt = SaltString::generate(&mut OsRng);
626        let argon2 = Argon2::default();
627        let password_hash = argon2
628            .hash_password(password.as_bytes(), &salt)
629            .map_err(|e| SecurityError::PasswordHashError {
630                message: e.to_string(),
631            })?
632            .to_string();
633
634        // Generate TOTP secret (20 bytes = 160 bits, standard for TOTP)
635        let totp_secret: Vec<u8> = (0..20).map(|_| rand_core::OsRng.next_u64() as u8).collect();
636
637        // Base32 encode the secret for display
638        let totp_secret_b32 = base32_encode(&totp_secret);
639
640        let record = UserRecord {
641            identity,
642            auth_method: AuthMethod::PasswordMfa {
643                password_hash,
644                totp_secret,
645            },
646            status: AccountStatus::Active,
647            created_at: SystemTime::now(),
648            last_login: None,
649            failed_attempts: 0,
650        };
651
652        self.user_store.store_user(record)?;
653
654        Ok(totp_secret_b32)
655    }
656
657    /// Authenticate a user and create a session.
658    pub fn authenticate(
659        &self,
660        username: &str,
661        credential: &Credential,
662    ) -> Result<UserSession, SecurityError> {
663        // Look up user
664        let mut user =
665            self.user_store
666                .get_user(username)
667                .ok_or_else(|| SecurityError::UserNotFound {
668                    username: username.to_string(),
669                })?;
670
671        // Check account status
672        match user.status {
673            AccountStatus::Locked => {
674                return Err(SecurityError::AccountLocked {
675                    username: username.to_string(),
676                })
677            }
678            AccountStatus::Disabled => {
679                return Err(SecurityError::AccountDisabled {
680                    username: username.to_string(),
681                })
682            }
683            AccountStatus::Pending => {
684                return Err(SecurityError::AccountPending {
685                    username: username.to_string(),
686                })
687            }
688            AccountStatus::Active => {}
689        }
690
691        // Verify credentials
692        let verified = self.verify_credentials(&user.auth_method, credential)?;
693
694        if !verified {
695            // Increment failed attempts
696            user.failed_attempts += 1;
697            if user.failed_attempts >= self.max_failed_attempts {
698                user.status = AccountStatus::Locked;
699            }
700            let _ = self.user_store.update_user(user);
701
702            return Err(SecurityError::InvalidCredential {
703                username: username.to_string(),
704            });
705        }
706
707        // Reset failed attempts and update last login
708        user.failed_attempts = 0;
709        user.last_login = Some(SystemTime::now());
710        let _ = self.user_store.update_user(user.clone());
711
712        // Create session
713        let now = SystemTime::now();
714        let session = UserSession {
715            session_id: SessionId::new(),
716            identity: user.identity,
717            device_id: None,
718            created_at: now,
719            expires_at: now + self.session_expiry,
720        };
721
722        // Store session
723        self.sessions
724            .write()
725            .map_err(|e| SecurityError::Internal(format!("sessions lock poisoned: {e}")))?
726            .insert(session.session_id, session.clone());
727
728        Ok(session)
729    }
730
731    /// Authenticate without TOTP (for testing or initial setup).
732    ///
733    /// WARNING: This should only be used in development/testing or when TOTP
734    /// is being initially configured.
735    pub fn authenticate_password_only(
736        &self,
737        username: &str,
738        password: &str,
739    ) -> Result<UserSession, SecurityError> {
740        // Look up user
741        let mut user =
742            self.user_store
743                .get_user(username)
744                .ok_or_else(|| SecurityError::UserNotFound {
745                    username: username.to_string(),
746                })?;
747
748        // Check account status
749        match user.status {
750            AccountStatus::Locked => {
751                return Err(SecurityError::AccountLocked {
752                    username: username.to_string(),
753                })
754            }
755            AccountStatus::Disabled => {
756                return Err(SecurityError::AccountDisabled {
757                    username: username.to_string(),
758                })
759            }
760            AccountStatus::Pending => {
761                return Err(SecurityError::AccountPending {
762                    username: username.to_string(),
763                })
764            }
765            AccountStatus::Active => {}
766        }
767
768        // Verify password only
769        match &user.auth_method {
770            AuthMethod::PasswordMfa { password_hash, .. } => {
771                let parsed_hash = PasswordHash::new(password_hash).map_err(|e| {
772                    SecurityError::PasswordHashError {
773                        message: e.to_string(),
774                    }
775                })?;
776
777                if Argon2::default()
778                    .verify_password(password.as_bytes(), &parsed_hash)
779                    .is_err()
780                {
781                    user.failed_attempts += 1;
782                    if user.failed_attempts >= self.max_failed_attempts {
783                        user.status = AccountStatus::Locked;
784                    }
785                    let _ = self.user_store.update_user(user);
786
787                    return Err(SecurityError::InvalidCredential {
788                        username: username.to_string(),
789                    });
790                }
791            }
792            _ => {
793                return Err(SecurityError::UnsupportedAuthMethod {
794                    method: "non-password".to_string(),
795                })
796            }
797        }
798
799        // Reset failed attempts and update last login
800        user.failed_attempts = 0;
801        user.last_login = Some(SystemTime::now());
802        let _ = self.user_store.update_user(user.clone());
803
804        // Create session
805        let now = SystemTime::now();
806        let session = UserSession {
807            session_id: SessionId::new(),
808            identity: user.identity,
809            device_id: None,
810            created_at: now,
811            expires_at: now + self.session_expiry,
812        };
813
814        // Store session
815        self.sessions
816            .write()
817            .map_err(|e| SecurityError::Internal(format!("sessions lock poisoned: {e}")))?
818            .insert(session.session_id, session.clone());
819
820        Ok(session)
821    }
822
823    /// Validate a session and return the user identity.
824    pub fn validate_session(&self, session_id: &SessionId) -> Result<UserIdentity, SecurityError> {
825        let sessions = self
826            .sessions
827            .read()
828            .map_err(|e| SecurityError::Internal(format!("sessions lock poisoned: {e}")))?;
829        let session = sessions
830            .get(session_id)
831            .ok_or(SecurityError::SessionNotFound)?;
832
833        if session.is_expired() {
834            drop(sessions);
835            self.invalidate_session(session_id);
836            return Err(SecurityError::SessionExpired);
837        }
838
839        Ok(session.identity.clone())
840    }
841
842    /// Get a session by ID.
843    pub fn get_session(&self, session_id: &SessionId) -> Option<UserSession> {
844        self.sessions.read().ok()?.get(session_id).cloned()
845    }
846
847    /// Invalidate (logout) a session.
848    pub fn invalidate_session(&self, session_id: &SessionId) {
849        if let Ok(mut sessions) = self.sessions.write() {
850            sessions.remove(session_id);
851        }
852    }
853
854    /// Invalidate all sessions for a user.
855    pub fn invalidate_user_sessions(&self, username: &str) {
856        if let Ok(mut sessions) = self.sessions.write() {
857            sessions.retain(|_, session| session.identity.username != username);
858        }
859    }
860
861    /// Clean up expired sessions.
862    pub fn cleanup_expired_sessions(&self) {
863        let now = SystemTime::now();
864        if let Ok(mut sessions) = self.sessions.write() {
865            sessions.retain(|_, session| session.expires_at > now);
866        }
867    }
868
869    /// Get count of active sessions.
870    pub fn active_session_count(&self) -> usize {
871        self.sessions.read().map(|s| s.len()).unwrap_or(0)
872    }
873
874    /// Bind a session to a device.
875    pub fn bind_session_to_device(
876        &self,
877        session_id: &SessionId,
878        device_id: DeviceId,
879    ) -> Result<(), SecurityError> {
880        let mut sessions = self
881            .sessions
882            .write()
883            .map_err(|e| SecurityError::Internal(format!("sessions lock poisoned: {e}")))?;
884        let session = sessions
885            .get_mut(session_id)
886            .ok_or(SecurityError::SessionNotFound)?;
887        session.device_id = Some(device_id);
888        Ok(())
889    }
890
891    /// Verify credentials against the stored auth method.
892    fn verify_credentials(
893        &self,
894        auth_method: &AuthMethod,
895        credential: &Credential,
896    ) -> Result<bool, SecurityError> {
897        match (auth_method, credential) {
898            (
899                AuthMethod::PasswordMfa {
900                    password_hash,
901                    totp_secret,
902                },
903                Credential::PasswordMfa {
904                    password,
905                    totp_code,
906                },
907            ) => {
908                // Verify password
909                let parsed_hash = PasswordHash::new(password_hash).map_err(|e| {
910                    SecurityError::PasswordHashError {
911                        message: e.to_string(),
912                    }
913                })?;
914
915                if Argon2::default()
916                    .verify_password(password.as_bytes(), &parsed_hash)
917                    .is_err()
918                {
919                    return Ok(false);
920                }
921
922                // Verify TOTP
923                if !verify_totp(totp_secret, totp_code)? {
924                    return Err(SecurityError::InvalidMfaCode);
925                }
926
927                Ok(true)
928            }
929
930            (
931                AuthMethod::SmartCard {
932                    card_id: stored_id,
933                    pin_hash,
934                },
935                Credential::SmartCard { card_id, pin },
936            ) => {
937                // Verify card ID matches
938                if stored_id != card_id {
939                    return Ok(false);
940                }
941
942                // Verify PIN
943                let parsed_hash =
944                    PasswordHash::new(pin_hash).map_err(|e| SecurityError::PasswordHashError {
945                        message: e.to_string(),
946                    })?;
947
948                Ok(Argon2::default()
949                    .verify_password(pin.as_bytes(), &parsed_hash)
950                    .is_ok())
951            }
952
953            (
954                AuthMethod::Certificate {
955                    certificate_fingerprint: stored_fp,
956                },
957                Credential::Certificate { fingerprint },
958            ) => {
959                // Compare certificate fingerprints
960                Ok(stored_fp == fingerprint)
961            }
962
963            _ => Err(SecurityError::UnsupportedAuthMethod {
964                method: "mismatched auth method and credential".to_string(),
965            }),
966        }
967    }
968
969    /// Unlock a locked account (admin operation).
970    pub fn unlock_account(&self, username: &str) -> Result<(), SecurityError> {
971        let mut user =
972            self.user_store
973                .get_user(username)
974                .ok_or_else(|| SecurityError::UserNotFound {
975                    username: username.to_string(),
976                })?;
977
978        user.status = AccountStatus::Active;
979        user.failed_attempts = 0;
980        self.user_store.update_user(user)
981    }
982
983    /// Disable an account (admin operation).
984    pub fn disable_account(&self, username: &str) -> Result<(), SecurityError> {
985        let mut user =
986            self.user_store
987                .get_user(username)
988                .ok_or_else(|| SecurityError::UserNotFound {
989                    username: username.to_string(),
990                })?;
991
992        user.status = AccountStatus::Disabled;
993        self.user_store.update_user(user)?;
994
995        // Also invalidate all sessions
996        self.invalidate_user_sessions(username);
997        Ok(())
998    }
999
1000    /// Change a user's password.
1001    pub fn change_password(
1002        &self,
1003        user_name: &str,
1004        old_password: &str,
1005        new_password: &str,
1006    ) -> Result<(), SecurityError> {
1007        let mut user =
1008            self.user_store
1009                .get_user(user_name)
1010                .ok_or_else(|| SecurityError::UserNotFound {
1011                    username: user_name.to_string(),
1012                })?;
1013
1014        // Verify old password
1015        match &user.auth_method {
1016            AuthMethod::PasswordMfa {
1017                password_hash,
1018                totp_secret,
1019            } => {
1020                let parsed_hash = PasswordHash::new(password_hash).map_err(|e| {
1021                    SecurityError::PasswordHashError {
1022                        message: e.to_string(),
1023                    }
1024                })?;
1025
1026                if Argon2::default()
1027                    .verify_password(old_password.as_bytes(), &parsed_hash)
1028                    .is_err()
1029                {
1030                    return Err(SecurityError::InvalidCredential {
1031                        username: user_name.to_string(),
1032                    });
1033                }
1034
1035                // Hash new password
1036                let salt = SaltString::generate(&mut OsRng);
1037                let argon2 = Argon2::default();
1038                let new_hash = argon2
1039                    .hash_password(new_password.as_bytes(), &salt)
1040                    .map_err(|e| SecurityError::PasswordHashError {
1041                        message: e.to_string(),
1042                    })?
1043                    .to_string();
1044
1045                user.auth_method = AuthMethod::PasswordMfa {
1046                    password_hash: new_hash,
1047                    totp_secret: totp_secret.clone(),
1048                };
1049            }
1050            _ => {
1051                return Err(SecurityError::UnsupportedAuthMethod {
1052                    method: "non-password".to_string(),
1053                })
1054            }
1055        }
1056
1057        self.user_store.update_user(user)?;
1058
1059        // Invalidate all sessions after password change
1060        self.invalidate_user_sessions(user_name);
1061        Ok(())
1062    }
1063}
1064
1065/// Generate a TOTP code for the current time.
1066#[allow(dead_code)]
1067pub fn generate_totp(secret: &[u8]) -> Result<String, SecurityError> {
1068    let time = SystemTime::now()
1069        .duration_since(UNIX_EPOCH)
1070        .map_err(|_| SecurityError::TotpError {
1071            message: "System time before UNIX epoch".to_string(),
1072        })?
1073        .as_secs();
1074
1075    let counter = time / TOTP_TIME_STEP_SECS;
1076    generate_hotp(secret, counter)
1077}
1078
1079/// Generate an HOTP code for a given counter.
1080fn generate_hotp(secret: &[u8], counter: u64) -> Result<String, SecurityError> {
1081    let mut mac = Hmac::<Sha256>::new_from_slice(secret).map_err(|e| SecurityError::TotpError {
1082        message: format!("HMAC error: {}", e),
1083    })?;
1084
1085    mac.update(&counter.to_be_bytes());
1086    let result = mac.finalize().into_bytes();
1087
1088    // Dynamic truncation (RFC 4226)
1089    let offset = (result[result.len() - 1] & 0x0f) as usize;
1090    let binary = ((result[offset] & 0x7f) as u32) << 24
1091        | (result[offset + 1] as u32) << 16
1092        | (result[offset + 2] as u32) << 8
1093        | (result[offset + 3] as u32);
1094
1095    let otp = binary % 10u32.pow(TOTP_CODE_LENGTH as u32);
1096    Ok(format!("{:0width$}", otp, width = TOTP_CODE_LENGTH))
1097}
1098
1099/// Verify a TOTP code (allows for clock drift).
1100pub fn verify_totp(secret: &[u8], code: &str) -> Result<bool, SecurityError> {
1101    let time = SystemTime::now()
1102        .duration_since(UNIX_EPOCH)
1103        .map_err(|_| SecurityError::TotpError {
1104            message: "System time before UNIX epoch".to_string(),
1105        })?
1106        .as_secs();
1107
1108    let counter = (time / TOTP_TIME_STEP_SECS) as i64;
1109
1110    // Check current time step and adjacent steps for clock drift
1111    for offset in -TOTP_CLOCK_DRIFT_STEPS..=TOTP_CLOCK_DRIFT_STEPS {
1112        let check_counter = (counter + offset) as u64;
1113        let expected = generate_hotp(secret, check_counter)?;
1114        if constant_time_compare(code.as_bytes(), expected.as_bytes()) {
1115            return Ok(true);
1116        }
1117    }
1118
1119    Ok(false)
1120}
1121
1122/// Constant-time string comparison to prevent timing attacks.
1123fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
1124    if a.len() != b.len() {
1125        return false;
1126    }
1127
1128    let mut result = 0u8;
1129    for (x, y) in a.iter().zip(b.iter()) {
1130        result |= x ^ y;
1131    }
1132    result == 0
1133}
1134
1135/// Base32 encode bytes (RFC 4648).
1136fn base32_encode(data: &[u8]) -> String {
1137    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1138
1139    let mut result = String::new();
1140    let mut buffer: u64 = 0;
1141    let mut bits_left = 0;
1142
1143    for &byte in data {
1144        buffer = (buffer << 8) | (byte as u64);
1145        bits_left += 8;
1146
1147        while bits_left >= 5 {
1148            bits_left -= 5;
1149            let index = ((buffer >> bits_left) & 0x1f) as usize;
1150            result.push(ALPHABET[index] as char);
1151        }
1152    }
1153
1154    if bits_left > 0 {
1155        let index = ((buffer << (5 - bits_left)) & 0x1f) as usize;
1156        result.push(ALPHABET[index] as char);
1157    }
1158
1159    result
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165
1166    #[test]
1167    fn test_military_rank_display() {
1168        assert_eq!(MilitaryRank::Captain.to_string(), "CPT");
1169        assert_eq!(MilitaryRank::Sergeant.to_string(), "SGT");
1170        assert_eq!(MilitaryRank::Colonel.to_string(), "COL");
1171    }
1172
1173    #[test]
1174    fn test_security_clearance_ordering() {
1175        assert!(SecurityClearance::Secret > SecurityClearance::Confidential);
1176        assert!(SecurityClearance::TopSecret > SecurityClearance::Secret);
1177        assert!(SecurityClearance::TopSecretSci > SecurityClearance::TopSecret);
1178    }
1179
1180    #[test]
1181    fn test_organization_unit() {
1182        let unit = OrganizationUnit::new("1st Platoon", "Alpha Company");
1183        assert_eq!(unit.to_string(), "1st Platoon, Alpha Company");
1184
1185        let top = OrganizationUnit::top_level("Battalion HQ");
1186        assert_eq!(top.to_string(), "Battalion HQ");
1187    }
1188
1189    #[test]
1190    fn test_user_identity_builder() {
1191        let identity = UserIdentity::builder("alpha_6")
1192            .display_name("CPT John Smith")
1193            .rank(MilitaryRank::Captain)
1194            .clearance(SecurityClearance::Secret)
1195            .unit(OrganizationUnit::new("1st Plt", "A Co"))
1196            .role(Role::Commander)
1197            .build();
1198
1199        assert_eq!(identity.username, "alpha_6");
1200        assert_eq!(identity.rank, MilitaryRank::Captain);
1201        assert!(identity.roles.contains(&Role::Commander));
1202    }
1203
1204    #[test]
1205    fn test_session_id_generation() {
1206        let id1 = SessionId::new();
1207        let id2 = SessionId::new();
1208        assert_ne!(id1, id2);
1209    }
1210
1211    #[test]
1212    fn test_local_user_store() {
1213        let store = LocalUserStore::new();
1214
1215        let identity = UserIdentity::builder("test_user")
1216            .rank(MilitaryRank::Sergeant)
1217            .build();
1218
1219        let record = UserRecord {
1220            identity,
1221            auth_method: AuthMethod::PasswordMfa {
1222                password_hash: "hash".to_string(),
1223                totp_secret: vec![1, 2, 3],
1224            },
1225            status: AccountStatus::Active,
1226            created_at: SystemTime::now(),
1227            last_login: None,
1228            failed_attempts: 0,
1229        };
1230
1231        // Store user
1232        store.store_user(record.clone()).unwrap();
1233
1234        // Retrieve user
1235        let retrieved = store.get_user("test_user").unwrap();
1236        assert_eq!(retrieved.identity.username, "test_user");
1237
1238        // List users
1239        let users = store.list_users();
1240        assert_eq!(users.len(), 1);
1241        assert!(users.contains(&"test_user".to_string()));
1242
1243        // Delete user
1244        store.delete_user("test_user").unwrap();
1245        assert!(store.get_user("test_user").is_none());
1246    }
1247
1248    #[test]
1249    fn test_user_store_duplicate_prevention() {
1250        let store = LocalUserStore::new();
1251
1252        let identity = UserIdentity::builder("test_user").build();
1253        let record = UserRecord {
1254            identity: identity.clone(),
1255            auth_method: AuthMethod::PasswordMfa {
1256                password_hash: "hash".to_string(),
1257                totp_secret: vec![1, 2, 3],
1258            },
1259            status: AccountStatus::Active,
1260            created_at: SystemTime::now(),
1261            last_login: None,
1262            failed_attempts: 0,
1263        };
1264
1265        store.store_user(record.clone()).unwrap();
1266
1267        // Attempt to store duplicate
1268        let result = store.store_user(record);
1269        assert!(matches!(
1270            result,
1271            Err(SecurityError::UserAlreadyExists { .. })
1272        ));
1273    }
1274
1275    #[test]
1276    fn test_hotp_generation() {
1277        // Test vector from RFC 4226
1278        let secret = b"12345678901234567890";
1279        let code = generate_hotp(secret, 0).unwrap();
1280        assert_eq!(code.len(), 6);
1281    }
1282
1283    #[test]
1284    fn test_totp_generation_and_verification() {
1285        let secret = b"test_secret_key_1234";
1286
1287        // Generate a code
1288        let code = generate_totp(secret).unwrap();
1289        assert_eq!(code.len(), 6);
1290
1291        // Verify the code
1292        assert!(verify_totp(secret, &code).unwrap());
1293
1294        // Wrong code should fail
1295        assert!(!verify_totp(secret, "000000").unwrap());
1296    }
1297
1298    #[test]
1299    fn test_base32_encode() {
1300        // Test vectors
1301        assert_eq!(base32_encode(b""), "");
1302        assert_eq!(base32_encode(b"f"), "MY");
1303        assert_eq!(base32_encode(b"fo"), "MZXQ");
1304        assert_eq!(base32_encode(b"foo"), "MZXW6");
1305        assert_eq!(base32_encode(b"foob"), "MZXW6YQ");
1306        assert_eq!(base32_encode(b"fooba"), "MZXW6YTB");
1307        assert_eq!(base32_encode(b"foobar"), "MZXW6YTBOI");
1308    }
1309
1310    #[test]
1311    fn test_constant_time_compare() {
1312        assert!(constant_time_compare(b"abc", b"abc"));
1313        assert!(!constant_time_compare(b"abc", b"abd"));
1314        assert!(!constant_time_compare(b"abc", b"ab"));
1315    }
1316
1317    #[test]
1318    fn test_user_registration_and_password_auth() {
1319        let store = LocalUserStore::new();
1320        let authenticator = UserAuthenticator::new(Box::new(store));
1321
1322        let identity = UserIdentity::builder("test_commander")
1323            .display_name("MAJ Test")
1324            .rank(MilitaryRank::Major)
1325            .clearance(SecurityClearance::Secret)
1326            .role(Role::Commander)
1327            .build();
1328
1329        // Register user
1330        let totp_secret = authenticator
1331            .register_user("test_commander", "secure_password_123", identity)
1332            .unwrap();
1333
1334        assert!(!totp_secret.is_empty());
1335
1336        // Password-only auth should work
1337        let session = authenticator
1338            .authenticate_password_only("test_commander", "secure_password_123")
1339            .unwrap();
1340
1341        assert_eq!(session.identity.username, "test_commander");
1342        assert!(!session.is_expired());
1343    }
1344
1345    #[test]
1346    fn test_session_validation() {
1347        let store = LocalUserStore::new();
1348        let authenticator = UserAuthenticator::new(Box::new(store));
1349
1350        let identity = UserIdentity::builder("session_test")
1351            .role(Role::Observer)
1352            .build();
1353
1354        authenticator
1355            .register_user("session_test", "password123", identity)
1356            .unwrap();
1357
1358        let session = authenticator
1359            .authenticate_password_only("session_test", "password123")
1360            .unwrap();
1361
1362        // Validate session
1363        let validated = authenticator.validate_session(&session.session_id).unwrap();
1364        assert_eq!(validated.username, "session_test");
1365
1366        // Invalidate session
1367        authenticator.invalidate_session(&session.session_id);
1368
1369        // Should fail now
1370        assert!(matches!(
1371            authenticator.validate_session(&session.session_id),
1372            Err(SecurityError::SessionNotFound)
1373        ));
1374    }
1375
1376    #[test]
1377    fn test_account_lockout() {
1378        let store = LocalUserStore::new();
1379        let authenticator = UserAuthenticator::new(Box::new(store)).with_max_failed_attempts(3);
1380
1381        let identity = UserIdentity::builder("lockout_test").build();
1382
1383        authenticator
1384            .register_user("lockout_test", "correct_password", identity)
1385            .unwrap();
1386
1387        // Fail 3 times
1388        for _ in 0..3 {
1389            let _ = authenticator.authenticate_password_only("lockout_test", "wrong_password");
1390        }
1391
1392        // Now even correct password should fail
1393        let result = authenticator.authenticate_password_only("lockout_test", "correct_password");
1394        assert!(matches!(result, Err(SecurityError::AccountLocked { .. })));
1395
1396        // Unlock account
1397        authenticator.unlock_account("lockout_test").unwrap();
1398
1399        // Should work now
1400        let session = authenticator
1401            .authenticate_password_only("lockout_test", "correct_password")
1402            .unwrap();
1403        assert_eq!(session.identity.username, "lockout_test");
1404    }
1405
1406    #[test]
1407    fn test_password_change() {
1408        let store = LocalUserStore::new();
1409        let authenticator = UserAuthenticator::new(Box::new(store));
1410
1411        let identity = UserIdentity::builder("pwd_change_test").build();
1412
1413        authenticator
1414            .register_user("pwd_change_test", "old_password", identity)
1415            .unwrap();
1416
1417        // Change password
1418        authenticator
1419            .change_password("pwd_change_test", "old_password", "new_password")
1420            .unwrap();
1421
1422        // Old password should fail
1423        let result = authenticator.authenticate_password_only("pwd_change_test", "old_password");
1424        assert!(result.is_err());
1425
1426        // New password should work
1427        let session = authenticator
1428            .authenticate_password_only("pwd_change_test", "new_password")
1429            .unwrap();
1430        assert_eq!(session.identity.username, "pwd_change_test");
1431    }
1432
1433    #[test]
1434    fn test_session_count_and_cleanup() {
1435        let store = LocalUserStore::new();
1436        let authenticator =
1437            UserAuthenticator::new(Box::new(store)).with_session_expiry(Duration::from_millis(10)); // Very short expiry
1438
1439        let identity = UserIdentity::builder("cleanup_test").build();
1440
1441        authenticator
1442            .register_user("cleanup_test", "password", identity)
1443            .unwrap();
1444
1445        // Create a session
1446        let _session = authenticator
1447            .authenticate_password_only("cleanup_test", "password")
1448            .unwrap();
1449
1450        assert_eq!(authenticator.active_session_count(), 1);
1451
1452        // Wait for expiry
1453        std::thread::sleep(Duration::from_millis(20));
1454
1455        // Cleanup should remove it
1456        authenticator.cleanup_expired_sessions();
1457        assert_eq!(authenticator.active_session_count(), 0);
1458    }
1459
1460    #[test]
1461    fn test_disable_account() {
1462        let store = LocalUserStore::new();
1463        let authenticator = UserAuthenticator::new(Box::new(store));
1464
1465        let identity = UserIdentity::builder("disable_test").build();
1466
1467        authenticator
1468            .register_user("disable_test", "password", identity)
1469            .unwrap();
1470
1471        // Create session
1472        let session = authenticator
1473            .authenticate_password_only("disable_test", "password")
1474            .unwrap();
1475
1476        // Disable account
1477        authenticator.disable_account("disable_test").unwrap();
1478
1479        // Session should be invalidated
1480        assert!(authenticator.validate_session(&session.session_id).is_err());
1481
1482        // New login should fail
1483        let result = authenticator.authenticate_password_only("disable_test", "password");
1484        assert!(matches!(result, Err(SecurityError::AccountDisabled { .. })));
1485    }
1486
1487    #[test]
1488    fn test_bind_session_to_device() {
1489        let store = LocalUserStore::new();
1490        let authenticator = UserAuthenticator::new(Box::new(store));
1491
1492        let identity = UserIdentity::builder("device_bind_test").build();
1493
1494        authenticator
1495            .register_user("device_bind_test", "password", identity)
1496            .unwrap();
1497
1498        let session = authenticator
1499            .authenticate_password_only("device_bind_test", "password")
1500            .unwrap();
1501
1502        assert!(session.device_id.is_none());
1503
1504        // Bind to device
1505        let keypair = crate::security::DeviceKeypair::generate();
1506        let device_id = keypair.device_id();
1507
1508        authenticator
1509            .bind_session_to_device(&session.session_id, device_id)
1510            .unwrap();
1511
1512        // Verify binding
1513        let updated_session = authenticator.get_session(&session.session_id).unwrap();
1514        assert!(updated_session.device_id.is_some());
1515    }
1516}