auth_framework/auth_modular/mfa/
totp.rs1use crate::errors::{AuthError, Result};
4use crate::storage::AuthStorage;
5use std::sync::Arc;
6use tracing::{debug, info, warn};
7
8pub struct TotpManager {
10 storage: Arc<dyn AuthStorage>,
11}
12
13impl TotpManager {
14 pub fn new(storage: Arc<dyn AuthStorage>) -> Self {
16 Self { storage }
17 }
18
19 pub async fn generate_secret(&self, user_id: &str) -> Result<String> {
21 debug!("Generating TOTP secret for user '{}'", user_id);
22
23 let secret = crate::utils::crypto::generate_token(20);
24
25 let key = format!("user:{}:totp_secret", user_id);
27 self.storage.store_kv(&key, secret.as_bytes(), None).await?;
28
29 info!("TOTP secret generated for user '{}'", user_id);
30 Ok(secret)
31 }
32
33 pub async fn generate_qr_code(
35 &self,
36 user_id: &str,
37 app_name: &str,
38 secret: &str,
39 ) -> Result<String> {
40 let qr_url =
41 format!("otpauth://totp/{app_name}:{user_id}?secret={secret}&issuer={app_name}");
42
43 info!("TOTP QR code generated for user '{}'", user_id);
44 Ok(qr_url)
45 }
46
47 pub async fn generate_code(&self, secret: &str) -> Result<String> {
49 self.generate_code_for_window(secret, None).await
50 }
51
52 pub async fn generate_code_for_window(
54 &self,
55 secret: &str,
56 time_window: Option<u64>,
57 ) -> Result<String> {
58 if secret.is_empty() {
59 return Err(AuthError::validation("TOTP secret cannot be empty"));
60 }
61
62 let window = time_window.unwrap_or_else(|| {
63 std::time::SystemTime::now()
64 .duration_since(std::time::UNIX_EPOCH)
65 .unwrap()
66 .as_secs()
67 / 30
68 });
69
70 use ring::hmac;
72
73 let secret_bytes = base32::decode(base32::Alphabet::Rfc4648 { padding: true }, secret)
75 .ok_or_else(|| AuthError::InvalidRequest("Invalid TOTP secret format".to_string()))?;
76
77 let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, &secret_bytes);
79
80 let time_bytes = window.to_be_bytes();
82
83 let signature = hmac::sign(&key, &time_bytes);
85 let hmac_result = signature.as_ref();
86
87 let offset = (hmac_result[19] & 0xf) as usize;
89 let code = ((hmac_result[offset] as u32 & 0x7f) << 24)
90 | ((hmac_result[offset + 1] as u32) << 16)
91 | ((hmac_result[offset + 2] as u32) << 8)
92 | (hmac_result[offset + 3] as u32);
93
94 let totp_code = code % 1_000_000;
96 Ok(format!("{:06}", totp_code))
97 }
98
99 pub async fn verify_code(&self, user_id: &str, code: &str) -> Result<bool> {
101 debug!("Verifying TOTP code for user '{}'", user_id);
102
103 if code.len() != 6 || !code.chars().all(|c| c.is_ascii_digit()) {
104 return Ok(false);
105 }
106
107 let user_secret = match self.get_user_secret(user_id).await {
109 Ok(secret) => secret,
110 Err(_) => {
111 warn!("No TOTP secret found for user '{}'", user_id);
112 return Ok(false);
113 }
114 };
115
116 let current_time = std::time::SystemTime::now()
118 .duration_since(std::time::UNIX_EPOCH)
119 .unwrap()
120 .as_secs();
121
122 let time_step = 30;
124 let current_window = current_time / time_step;
125
126 for window in (current_window.saturating_sub(1))..=(current_window + 1) {
128 if let Ok(expected_code) = self
129 .generate_code_for_window(&user_secret, Some(window))
130 .await
131 && code == expected_code
132 {
133 info!("TOTP code verification successful for user '{}'", user_id);
134 return Ok(true);
135 }
136 }
137
138 info!("TOTP code verification failed for user '{}'", user_id);
139 Ok(false)
140 }
141
142 async fn get_user_secret(&self, user_id: &str) -> Result<String> {
144 let key = format!("user:{}:totp_secret", user_id);
145
146 if let Some(secret_data) = self.storage.get_kv(&key).await? {
147 Ok(String::from_utf8(secret_data)
148 .map_err(|e| AuthError::internal(format!("Failed to parse TOTP secret: {}", e)))?)
149 } else {
150 use sha2::{Digest, Sha256};
152 let mut hasher = Sha256::new();
153 hasher.update(user_id.as_bytes());
154 hasher.update(b"totp_secret_salt_2024");
155 let hash = hasher.finalize();
156
157 let secret = base32::encode(
159 base32::Alphabet::Rfc4648 { padding: true },
160 &hash[0..20], );
162
163 self.storage.store_kv(&key, secret.as_bytes(), None).await?;
165 Ok(secret)
166 }
167 }
168
169 pub async fn has_totp_secret(&self, user_id: &str) -> Result<bool> {
171 let key = format!("totp_secret:{}", user_id);
172 match self.storage.get_kv(&key).await {
173 Ok(Some(_)) => Ok(true),
174 Ok(None) => Ok(false),
175 Err(_) => Ok(false), }
177 }
178}