auth_framework/auth_modular/mfa/
totp.rs1use crate::errors::{AuthError, Result};
4use crate::storage::AuthStorage;
5use std::sync::Arc;
6use subtle::ConstantTimeEq;
7use tracing::{debug, info, warn};
8
9pub struct TotpManager {
11 storage: Arc<dyn AuthStorage>,
12}
13
14impl TotpManager {
15 pub fn new(storage: Arc<dyn AuthStorage>) -> Self {
17 Self { storage }
18 }
19
20 pub async fn generate_secret(&self, user_id: &str) -> Result<String> {
22 debug!("Generating TOTP secret for user '{}'", user_id);
23
24 let rng = ring::rand::SystemRandom::new();
27 let mut raw_bytes = [0u8; 20];
28 ring::rand::SecureRandom::fill(&rng, &mut raw_bytes)
29 .map_err(|_| AuthError::internal("Failed to generate random bytes for TOTP secret"))?;
30 let secret = base32::encode(base32::Alphabet::Rfc4648 { padding: true }, &raw_bytes);
31
32 let key = format!("user:{}:totp_secret", user_id);
34 self.storage.store_kv(&key, secret.as_bytes(), None).await?;
35
36 info!("TOTP secret generated for user '{}'", user_id);
37 Ok(secret)
38 }
39
40 pub async fn generate_qr_code(
42 &self,
43 user_id: &str,
44 app_name: &str,
45 secret: &str,
46 ) -> Result<String> {
47 let qr_url =
48 format!("otpauth://totp/{app_name}:{user_id}?secret={secret}&issuer={app_name}");
49
50 info!("TOTP QR code generated for user '{}'", user_id);
51 Ok(qr_url)
52 }
53
54 pub async fn generate_code(&self, secret: &str) -> Result<String> {
56 self.generate_code_for_window(secret, None).await
57 }
58
59 pub async fn generate_code_for_window(
61 &self,
62 secret: &str,
63 time_window: Option<u64>,
64 ) -> Result<String> {
65 if secret.is_empty() {
66 return Err(AuthError::validation("TOTP secret cannot be empty"));
67 }
68
69 let window = time_window.unwrap_or_else(|| {
70 std::time::SystemTime::now()
71 .duration_since(std::time::UNIX_EPOCH)
72 .unwrap_or_default()
73 .as_secs()
74 / 30
75 });
76
77 use ring::hmac;
79
80 let secret_bytes = base32::decode(base32::Alphabet::Rfc4648 { padding: true }, secret)
82 .ok_or_else(|| AuthError::InvalidRequest("Invalid TOTP secret format".to_string()))?;
83
84 let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, &secret_bytes);
86
87 let time_bytes = window.to_be_bytes();
89
90 let signature = hmac::sign(&key, &time_bytes);
92 let hmac_result = signature.as_ref();
93
94 let offset = (hmac_result[19] & 0xf) as usize;
96 let code = ((hmac_result[offset] as u32 & 0x7f) << 24)
97 | ((hmac_result[offset + 1] as u32) << 16)
98 | ((hmac_result[offset + 2] as u32) << 8)
99 | (hmac_result[offset + 3] as u32);
100
101 let totp_code = code % 1_000_000;
103 Ok(format!("{:06}", totp_code))
104 }
105
106 pub async fn verify_code(&self, user_id: &str, code: &str) -> Result<bool> {
108 debug!("Verifying TOTP code for user '{}'", user_id);
109
110 if code.len() != 6 || !code.chars().all(|c| c.is_ascii_digit()) {
111 return Ok(false);
112 }
113
114 let user_secret = match self.get_user_secret(user_id).await {
116 Ok(secret) => secret,
117 Err(_) => {
118 warn!("No TOTP secret found for user '{}'", user_id);
119 return Ok(false);
120 }
121 };
122
123 let current_time = std::time::SystemTime::now()
125 .duration_since(std::time::UNIX_EPOCH)
126 .unwrap_or_default()
127 .as_secs();
128
129 let time_step = 30;
131 let current_window = current_time / time_step;
132
133 for window in (current_window.saturating_sub(1))..=(current_window + 1) {
135 if let Ok(expected_code) = self
136 .generate_code_for_window(&user_secret, Some(window))
137 .await
138 && bool::from(code.as_bytes().ct_eq(expected_code.as_bytes()))
139 {
140 info!("TOTP code verification successful for user '{}'", user_id);
141 return Ok(true);
142 }
143 }
144
145 info!("TOTP code verification failed for user '{}'", user_id);
146 Ok(false)
147 }
148
149 async fn get_user_secret(&self, user_id: &str) -> Result<String> {
151 let key = format!("user:{}:totp_secret", user_id);
152
153 if let Some(secret_data) = self.storage.get_kv(&key).await? {
154 Ok(String::from_utf8(secret_data)
155 .map_err(|e| AuthError::internal(format!("Failed to parse TOTP secret: {}", e)))?)
156 } else {
157 use sha2::{Digest, Sha256};
159 let mut hasher = Sha256::new();
160 hasher.update(user_id.as_bytes());
161 hasher.update(b"totp_secret_salt_2024");
162 let hash = hasher.finalize();
163
164 let secret = base32::encode(
166 base32::Alphabet::Rfc4648 { padding: true },
167 &hash[0..20], );
169
170 self.storage.store_kv(&key, secret.as_bytes(), None).await?;
172 Ok(secret)
173 }
174 }
175
176 pub async fn has_totp_secret(&self, user_id: &str) -> Result<bool> {
178 let api_key = format!("mfa_secret:{}", user_id);
179 match self.storage.get_kv(&api_key).await {
180 Ok(Some(_)) => Ok(true),
181 Ok(None) => {
182 let modular_key = format!("user:{}:totp_secret", user_id);
183 match self.storage.get_kv(&modular_key).await {
184 Ok(Some(_)) => Ok(true),
185 Ok(None) => Ok(false),
186 Err(_) => Ok(false),
187 }
188 }
189 Err(_) => Ok(false), }
191 }
192
193 pub async fn verify_login_code(&self, user_id: &str, code: &str) -> Result<bool> {
196 use crate::security::secure_utils::constant_time_compare;
197
198 if code.len() != 6 || !code.chars().all(|c| c.is_ascii_digit()) {
199 return Ok(false);
200 }
201
202 let secret_b32 = match self
204 .storage
205 .get_kv(&format!("mfa_secret:{}", user_id))
206 .await?
207 {
208 Some(data) => String::from_utf8_lossy(&data).to_string(),
209 None => match self
210 .storage
211 .get_kv(&format!("user:{}:totp_secret", user_id))
212 .await?
213 {
214 Some(data) => String::from_utf8_lossy(&data).to_string(),
215 None => return Ok(false),
216 },
217 };
218
219 let secret_bytes = match base32::decode(
220 base32::Alphabet::Rfc4648 { padding: false },
221 &secret_b32,
222 )
223 .or_else(|| base32::decode(base32::Alphabet::Rfc4648 { padding: true }, &secret_b32))
224 {
225 Some(bytes) => bytes,
226 None => return Ok(false),
227 };
228
229 let now = chrono::Utc::now().timestamp() as u64;
230 const STEP: u64 = 30;
231 const DIGITS: u32 = 6;
232 let mut matched = false;
233
234 for offset in [0u64, STEP, STEP.wrapping_neg()] {
235 let timestamp = now.wrapping_add(offset);
236 let expected =
237 totp_lite::totp_custom::<totp_lite::Sha1>(STEP, DIGITS, &secret_bytes, timestamp);
238 if constant_time_compare(expected.as_bytes(), code.as_bytes()) {
239 matched = true;
240 }
241 }
242
243 Ok(matched)
244 }
245}