avl_auth/
mfa.rs

1//! Multi-factor authentication with TOTP and WebAuthn
2
3use crate::error::{AuthError, Result};
4use crate::models::{TotpAlgorithm, TotpConfig, WebAuthnCredential};
5use rand::RngCore;
6use serde::{Deserialize, Serialize};
7use std::time::{SystemTime, UNIX_EPOCH};
8use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
9
10pub struct MfaManager {
11    totp_issuer: String,
12    totp_period: u32,
13    totp_digits: u32,
14}
15
16impl MfaManager {
17    pub fn new(issuer: String, period: u32, digits: u32) -> Self {
18        Self {
19            totp_issuer: issuer,
20            totp_period: period,
21            totp_digits: digits,
22        }
23    }
24
25    // ==================== TOTP Implementation ====================
26
27    pub fn generate_totp_secret(&self) -> String {
28        let mut secret = vec![0u8; 20]; // 160 bits
29        rand::thread_rng().fill_bytes(&mut secret);
30        base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret)
31    }
32
33    pub fn generate_totp_config(&self, account_name: &str, secret: Option<String>) -> TotpConfig {
34        TotpConfig {
35            secret: secret.unwrap_or_else(|| self.generate_totp_secret()),
36            algorithm: TotpAlgorithm::SHA1,
37            digits: self.totp_digits,
38            period: self.totp_period,
39            issuer: self.totp_issuer.clone(),
40            account_name: account_name.to_string(),
41        }
42    }
43
44    pub fn generate_totp_uri(&self, config: &TotpConfig) -> String {
45        format!(
46            "otpauth://totp/{}:{}?secret={}&issuer={}&algorithm={:?}&digits={}&period={}",
47            urlencoding::encode(&config.issuer),
48            urlencoding::encode(&config.account_name),
49            config.secret,
50            urlencoding::encode(&config.issuer),
51            config.algorithm,
52            config.digits,
53            config.period
54        )
55    }
56
57    pub fn verify_totp(&self, secret: &str, code: &str, tolerance: u64) -> Result<bool> {
58        let current_time = SystemTime::now()
59            .duration_since(UNIX_EPOCH)
60            .map_err(|e| AuthError::Internal(e.to_string()))?
61            .as_secs();
62
63        // Check current time window and adjacent windows (tolerance)
64        for i in 0..=tolerance {
65            for sign in &[-1i64, 1i64] {
66                let offset = (*sign as i64) * (i as i64);
67                let time = (current_time as i64 + offset * self.totp_period as i64) as u64;
68                let expected_code = self.generate_totp_code(secret, time)?;
69
70                if expected_code == code {
71                    return Ok(true);
72                }
73            }
74        }
75
76        Ok(false)
77    }
78
79    fn generate_totp_code(&self, secret: &str, time: u64) -> Result<String> {
80        let decoded = base32::decode(base32::Alphabet::Rfc4648 { padding: false }, secret)
81            .ok_or_else(|| AuthError::CryptoError("Invalid TOTP secret".to_string()))?;
82
83        let counter = time / self.totp_period as u64;
84        let code = self.generate_hotp(&decoded, counter)?;
85
86        Ok(format!("{:0width$}", code, width = self.totp_digits as usize))
87    }
88
89    fn generate_hotp(&self, key: &[u8], counter: u64) -> Result<u32> {
90        use hmac::{Hmac, Mac};
91        use sha1::Sha1;
92
93        type HmacSha1 = Hmac<Sha1>;
94
95        let mut mac = HmacSha1::new_from_slice(key)
96            .map_err(|e| AuthError::CryptoError(e.to_string()))?;
97
98        mac.update(&counter.to_be_bytes());
99        let result = mac.finalize();
100        let code = result.into_bytes();
101
102        let offset = (code[19] & 0xf) as usize;
103        let binary = ((code[offset] & 0x7f) as u32) << 24
104            | ((code[offset + 1] & 0xff) as u32) << 16
105            | ((code[offset + 2] & 0xff) as u32) << 8
106            | ((code[offset + 3] & 0xff) as u32);
107
108        let modulo = 10u32.pow(self.totp_digits);
109        Ok(binary % modulo)
110    }
111
112    pub fn generate_backup_codes(&self, count: usize) -> Vec<String> {
113        use rand::Rng;
114        let mut rng = rand::thread_rng();
115
116        (0..count)
117            .map(|_| {
118                format!(
119                    "{:04}-{:04}",
120                    rng.gen_range(0..10000),
121                    rng.gen_range(0..10000)
122                )
123            })
124            .collect()
125    }
126
127    // ==================== WebAuthn Implementation ====================
128    // Note: This is a simplified WebAuthn implementation
129    // For production, use a dedicated crate like `webauthn-rs`
130
131    pub fn generate_webauthn_challenge(&self) -> String {
132        let mut challenge = vec![0u8; 32];
133        rand::thread_rng().fill_bytes(&mut challenge);
134        URL_SAFE_NO_PAD.encode(&challenge)
135    }
136
137    pub fn create_webauthn_credential(
138        &self,
139        credential_id: String,
140        public_key: Vec<u8>,
141        name: String,
142    ) -> WebAuthnCredential {
143        WebAuthnCredential {
144            id: credential_id,
145            public_key,
146            counter: 0,
147            name,
148            created_at: chrono::Utc::now(),
149            last_used_at: None,
150        }
151    }
152
153    pub fn verify_webauthn_signature(
154        &self,
155        _credential: &WebAuthnCredential,
156        authenticator_data: &[u8],
157        _client_data_json: &[u8],
158        _signature: &[u8],
159    ) -> Result<bool> {
160        // This is a placeholder for WebAuthn signature verification
161        // In production, implement full FIDO2/WebAuthn spec
162
163        // Verify authenticator data flags
164        if authenticator_data.len() < 37 {
165            return Ok(false);
166        }
167
168        let flags = authenticator_data[32];
169        let user_present = (flags & 0x01) != 0;
170        let user_verified = (flags & 0x04) != 0;
171
172        if !user_present {
173            return Ok(false);
174        }
175
176        // In real implementation:
177        // 1. Parse and verify authenticator data
178        // 2. Hash client data JSON
179        // 3. Verify signature using public key
180        // 4. Check and update counter
181
182        Ok(user_verified)
183    }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct MfaChallenge {
188    pub challenge_id: String,
189    pub challenge: String,
190    pub expires_at: chrono::DateTime<chrono::Utc>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct BackupCodes {
195    pub codes: Vec<String>,
196    pub created_at: chrono::DateTime<chrono::Utc>,
197}