Skip to main content

auth_framework/security/
mod.rs

1//! Security primitives and hardened implementations.
2//!
3//! This module contains:
4//!
5//! - **[`secure_jwt`]** — Production-grade JWT creation and validation with
6//!   key-rotation and revocation support.
7//! - **[`secure_mfa`]** — Hardened TOTP, backup-code, and recovery-flow logic.
8//! - **[`secure_session`]** — Session management with abuse-detection guards.
9//! - **[`timing_protection`]** — Constant-time comparison utilities to prevent
10//!   timing side-channel attacks.
11//! - **[`presets`]** — One-call security presets (`HighSecurity`, `Standard`,
12//!   `Development`) and an automated security-audit report generator.
13//!
14//! Most users interact with these through [`AuthFramework`](crate::auth::AuthFramework)
15//! rather than importing security types directly.
16
17use serde::{Deserialize, Serialize};
18
19// Timing protection utilities
20pub mod timing_protection;
21
22// Secure implementations
23pub mod secure_jwt;
24pub mod secure_mfa;
25pub mod secure_session;
26pub mod secure_session_config;
27pub mod secure_utils;
28
29// Security presets for easy configuration
30pub mod presets;
31
32// Enhanced cryptography (ChaCha20-Poly1305, Ed25519, X25519)
33#[cfg(feature = "enhanced-crypto")]
34pub mod enhanced_crypto;
35
36// Re-export presets for convenience
37pub use presets::{
38    SecurityAuditReport, SecurityAuditStatus, SecurityIssue, SecurityPreset, SecuritySeverity,
39};
40
41/// Multi-Factor Authentication configuration
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct MfaConfig {
44    /// TOTP configuration
45    pub totp_config: TotpConfig,
46    /// SMS configuration (optional)
47    pub sms_config: Option<SmsConfig>,
48    /// Email configuration (optional)
49    pub email_config: Option<EmailConfig>,
50    /// Backup codes configuration
51    pub backup_codes_config: BackupCodesConfig,
52    /// Challenge timeout in seconds
53    pub challenge_timeout_seconds: u64,
54    /// Maximum verification attempts
55    pub max_verification_attempts: u32,
56}
57
58impl Default for MfaConfig {
59    fn default() -> Self {
60        Self {
61            totp_config: TotpConfig::default(),
62            sms_config: None,
63            email_config: None,
64            backup_codes_config: BackupCodesConfig::default(),
65            challenge_timeout_seconds: 300, // 5 minutes
66            max_verification_attempts: 3,
67        }
68    }
69}
70
71/// TOTP (Time-based One-Time Password) configuration
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TotpConfig {
74    /// Issuer name for TOTP apps
75    pub issuer: String,
76    /// Number of digits in the TOTP code
77    pub digits: u8,
78    /// Time period in seconds
79    pub period: u64,
80    /// Number of time windows to allow (for clock skew)
81    pub skew: u8,
82}
83
84impl Default for TotpConfig {
85    fn default() -> Self {
86        Self {
87            issuer: "Auth Framework".to_string(),
88            digits: 6,
89            period: 30,
90            skew: 1,
91        }
92    }
93}
94
95/// Backup codes configuration
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct BackupCodesConfig {
98    /// Number of backup codes to generate
99    pub count: usize,
100    /// Length of each backup code
101    pub length: usize,
102}
103
104impl Default for BackupCodesConfig {
105    fn default() -> Self {
106        Self {
107            count: 8,
108            length: 8,
109        }
110    }
111}
112
113/// Password strength assessment result
114#[derive(Debug, Clone, PartialEq)]
115pub enum PasswordStrength {
116    VeryWeak,
117    Weak,
118    Fair,
119    Good,
120    Strong,
121    VeryStrong,
122}
123
124/// Password validation result
125#[derive(Debug, Clone)]
126pub struct PasswordValidation {
127    pub is_valid: bool,
128    pub strength: PasswordStrength,
129    pub issues: Vec<String>,
130    pub suggestions: Vec<String>,
131}
132
133/// Audit logging configuration
134#[derive(Debug, Clone, Serialize, Deserialize, Default)]
135pub struct AuditConfig {
136    pub enabled: bool,
137    pub log_success: bool,
138    pub log_failures: bool,
139    pub log_permission_changes: bool,
140    pub log_admin_actions: bool,
141    pub retention_days: u32,
142    pub include_metadata: bool,
143}
144
145/// Account lockout configuration
146#[derive(Debug, Clone, Serialize, Deserialize, Default)]
147pub struct LockoutConfig {
148    pub enabled: bool,
149    pub max_failed_attempts: u32,
150    pub lockout_duration_seconds: u64,
151    pub progressive_lockout: bool,
152    pub max_lockout_duration_seconds: u64,
153}
154
155/// SMS configuration
156///
157/// **Security note:** `api_key` is never serialized to prevent accidental
158/// secret leakage in logs or config dumps.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct SmsConfig {
161    pub provider: String,
162    #[serde(skip_serializing, default)]
163    pub api_key: String,
164    pub from_number: String,
165}
166
167impl Default for SmsConfig {
168    fn default() -> Self {
169        Self {
170            provider: "twilio".to_string(),
171            api_key: String::new(),
172            from_number: String::new(),
173        }
174    }
175}
176
177/// Email configuration
178///
179/// **Security note:** `password` is never serialized to prevent accidental
180/// secret leakage in logs or config dumps.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct EmailConfig {
183    pub smtp_server: String,
184    pub smtp_port: u16,
185    pub username: String,
186    #[serde(skip_serializing, default)]
187    pub password: String,
188    pub from_email: String,
189    pub use_tls: bool,
190}
191
192impl Default for EmailConfig {
193    fn default() -> Self {
194        Self {
195            smtp_server: "smtp.gmail.com".to_string(),
196            smtp_port: 587,
197            username: String::new(),
198            password: String::new(),
199            from_email: String::new(),
200            use_tls: true,
201        }
202    }
203}
204
205/// Security context for authentication operations
206#[derive(Debug, Clone, Default)]
207pub struct SecurityContext {
208    pub ip_address: Option<String>,
209    pub user_agent: Option<String>,
210    pub session_id: Option<String>,
211    pub request_id: Option<String>,
212}
213
214/// Password validator
215pub struct PasswordValidator {
216    pub min_length: usize,
217    pub require_uppercase: bool,
218    pub require_lowercase: bool,
219    pub require_digits: bool,
220    pub require_special_chars: bool,
221    pub min_special_chars: usize,
222    pub forbidden_patterns: Vec<String>,
223}
224
225impl Default for PasswordValidator {
226    fn default() -> Self {
227        Self {
228            min_length: 8,
229            require_uppercase: true,
230            require_lowercase: true,
231            require_digits: true,
232            require_special_chars: true,
233            min_special_chars: 1,
234            forbidden_patterns: vec![
235                "password".to_string(),
236                "123456".to_string(),
237                "qwerty".to_string(),
238                "admin".to_string(),
239            ],
240        }
241    }
242}
243
244impl PasswordValidator {
245    /// Validate password strength and return detailed results
246    pub fn validate(&self, password: &str) -> PasswordValidation {
247        let mut is_valid = true;
248        let mut issues = Vec::new();
249        let mut suggestions = Vec::new();
250
251        // Check minimum length
252        if password.len() < self.min_length {
253            is_valid = false;
254            issues.push(format!(
255                "Password must be at least {} characters long",
256                self.min_length
257            ));
258            suggestions.push("Use a longer password".to_string());
259        }
260
261        // Check character requirements
262        if self.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
263            is_valid = false;
264            issues.push("Password must contain at least one uppercase letter".to_string());
265            suggestions.push("Add uppercase letters".to_string());
266        }
267
268        if self.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
269            is_valid = false;
270            issues.push("Password must contain at least one lowercase letter".to_string());
271            suggestions.push("Add lowercase letters".to_string());
272        }
273
274        if self.require_digits && !password.chars().any(|c| c.is_numeric()) {
275            is_valid = false;
276            issues.push("Password must contain at least one digit".to_string());
277            suggestions.push("Add numbers".to_string());
278        }
279
280        if self.require_special_chars {
281            let special_count = password.chars().filter(|&c| !c.is_alphanumeric()).count();
282            if special_count < self.min_special_chars {
283                is_valid = false;
284                issues.push(format!(
285                    "Password must contain at least {} special characters",
286                    self.min_special_chars
287                ));
288                suggestions.push("Add special characters like !@#$%^&*".to_string());
289            }
290        }
291
292        // Check forbidden patterns
293        for pattern in &self.forbidden_patterns {
294            if password.to_lowercase().contains(&pattern.to_lowercase()) {
295                is_valid = false;
296                issues.push(format!("Password contains forbidden pattern: {}", pattern));
297                suggestions.push("Avoid common passwords and patterns".to_string());
298            }
299        }
300
301        // Assess strength
302        let strength = self.assess_strength(password);
303
304        PasswordValidation {
305            is_valid,
306            strength,
307            issues,
308            suggestions,
309        }
310    }
311
312    /// Assess password strength
313    fn assess_strength(&self, password: &str) -> PasswordStrength {
314        let mut score = 0;
315
316        // Length scoring
317        if password.len() >= 8 {
318            score += 1;
319        }
320        if password.len() >= 12 {
321            score += 1;
322        }
323        if password.len() >= 16 {
324            score += 1;
325        }
326
327        // Character variety scoring
328        if password.chars().any(|c| c.is_lowercase()) {
329            score += 1;
330        }
331        if password.chars().any(|c| c.is_uppercase()) {
332            score += 1;
333        }
334        if password.chars().any(|c| c.is_numeric()) {
335            score += 1;
336        }
337        if password.chars().any(|c| !c.is_alphanumeric()) {
338            score += 1;
339        }
340
341        // Complexity bonus
342        if password.len() >= 20 {
343            score += 1;
344        }
345
346        match score {
347            0..=2 => PasswordStrength::VeryWeak,
348            3..=4 => PasswordStrength::Weak,
349            5 => PasswordStrength::Fair,
350            6 => PasswordStrength::Good,
351            7 => PasswordStrength::Strong,
352            _ => PasswordStrength::VeryStrong,
353        }
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_password_validation() {
363        let validator = PasswordValidator::default();
364
365        // Test weak password
366        let result = validator.validate("weak");
367        assert!(!result.is_valid);
368        assert_eq!(result.strength, PasswordStrength::VeryWeak);
369        assert!(!result.issues.is_empty());
370
371        // Test strong password
372        let result = validator.validate("Strong@Secure123!");
373        assert!(result.is_valid);
374        assert!(matches!(
375            result.strength,
376            PasswordStrength::Strong | PasswordStrength::VeryStrong
377        ));
378        assert!(result.issues.is_empty());
379    }
380
381    #[test]
382    fn test_forbidden_patterns() {
383        let validator = PasswordValidator::default();
384        let result = validator.validate("Password123!");
385        assert!(!result.is_valid);
386        assert!(
387            result
388                .issues
389                .iter()
390                .any(|issue| issue.contains("forbidden pattern"))
391        );
392    }
393}