Skip to main content

auth_framework/auth_modular/mfa/
totp.rs

1//! TOTP (Time-based One-Time Password) manager
2
3use crate::errors::{AuthError, Result};
4use crate::storage::AuthStorage;
5use std::sync::Arc;
6use subtle::ConstantTimeEq;
7use tracing::{debug, info, warn};
8
9/// TOTP manager for handling time-based one-time passwords
10pub struct TotpManager {
11    storage: Arc<dyn AuthStorage>,
12}
13
14impl TotpManager {
15    /// Create a new TOTP manager
16    pub fn new(storage: Arc<dyn AuthStorage>) -> Self {
17        Self { storage }
18    }
19
20    /// Generate TOTP secret for a user
21    pub async fn generate_secret(&self, user_id: &str) -> Result<String> {
22        debug!("Generating TOTP secret for user '{}'", user_id);
23
24        // Generate 20 cryptographically-secure random bytes encoded as RFC 4648 Base32,
25        // compatible with generate_code_for_window() which base32-decodes the secret.
26        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        // Store the secret securely
33        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    /// Generate TOTP QR code URL
41    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    /// Generate current TOTP code using provided secret
55    pub async fn generate_code(&self, secret: &str) -> Result<String> {
56        self.generate_code_for_window(secret, None).await
57    }
58
59    /// Generate TOTP code for given secret and optional specific time window
60    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        // Generate TOTP code using ring/sha2 for production cryptographic implementation
78        use ring::hmac;
79
80        // Decode base32 secret
81        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        // Create HMAC key for TOTP (using SHA1 as per RFC)
85        let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, &secret_bytes);
86
87        // Convert time window to 8-byte big-endian
88        let time_bytes = window.to_be_bytes();
89
90        // Compute HMAC
91        let signature = hmac::sign(&key, &time_bytes);
92        let hmac_result = signature.as_ref();
93
94        // Dynamic truncation (RFC 4226)
95        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        // Generate 6-digit code
102        let totp_code = code % 1_000_000;
103        Ok(format!("{:06}", totp_code))
104    }
105
106    /// Verify TOTP code for a user
107    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        // Get user's TOTP secret
115        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        // Generate expected TOTP codes for current and adjacent time windows
124        let current_time = std::time::SystemTime::now()
125            .duration_since(std::time::UNIX_EPOCH)
126            .unwrap_or_default()
127            .as_secs();
128
129        // TOTP uses 30-second time steps
130        let time_step = 30;
131        let current_window = current_time / time_step;
132
133        // Check current window and ±1 window for clock drift tolerance
134        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    /// Get user's TOTP secret from secure storage
150    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            // Generate a consistent secret per user for testing if none exists
158            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            // Convert to base32 for TOTP compatibility
165            let secret = base32::encode(
166                base32::Alphabet::Rfc4648 { padding: true },
167                &hash[0..20], // Use first 160 bits (20 bytes)
168            );
169
170            // Store it for future use
171            self.storage.store_kv(&key, secret.as_bytes(), None).await?;
172            Ok(secret)
173        }
174    }
175
176    /// Check if user has TOTP secret configured
177    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), // Assume false on error
190        }
191    }
192
193    /// Verify a TOTP code during the login MFA flow.
194    /// Reads from `mfa_secret:{user_id}` (API setup) or `user:{user_id}:totp_secret` (modular setup).
195    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        // Try the API MFA key first, then fall back to the modular TOTP key.
203        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}