1use 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 pub fn generate_totp_secret(&self) -> String {
28 let mut secret = vec![0u8; 20]; 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 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 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 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 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}