Skip to main content

auth_framework/auth_modular/mfa/
backup_codes.rs

1//! Backup codes manager for MFA
2
3use crate::errors::Result;
4use crate::storage::AuthStorage;
5use std::sync::Arc;
6use subtle::ConstantTimeEq;
7use tracing::{debug, info};
8
9/// Backup codes manager for handling backup codes
10pub struct BackupCodesManager {
11    storage: Arc<dyn AuthStorage>,
12}
13
14impl BackupCodesManager {
15    /// Create a new backup codes manager
16    pub fn new(storage: Arc<dyn AuthStorage>) -> Self {
17        Self { storage }
18    }
19
20    /// Generate backup codes for a user
21    pub async fn generate_codes(&self, user_id: &str, count: usize) -> Result<Vec<String>> {
22        debug!("Generating {} backup codes for user '{}'", count, user_id);
23
24        let codes: Vec<String> = (0..count)
25            .map(|_| format!("{:08}", rand::random::<u32>() % 100000000))
26            .collect();
27
28        // Store backup codes for the user
29        let backup_key = format!("user:{}:backup_codes", user_id);
30        let codes_json = serde_json::to_string(&codes).unwrap_or("[]".to_string());
31        self.storage
32            .store_kv(&backup_key, codes_json.as_bytes(), None)
33            .await?;
34
35        info!("Generated {} backup codes for user '{}'", count, user_id);
36        Ok(codes)
37    }
38
39    /// Verify backup code and mark it as used
40    pub async fn verify_code(&self, user_id: &str, code: &str) -> Result<bool> {
41        debug!("Verifying backup code for user '{}'", user_id);
42
43        // Validate code format
44        if code.len() != 8 || !code.chars().all(|c| c.is_ascii_digit()) {
45            return Ok(false);
46        }
47
48        // Get user's backup codes
49        let backup_key = format!("user:{}:backup_codes", user_id);
50        if let Some(codes_data) = self.storage.get_kv(&backup_key).await? {
51            let codes_str = std::str::from_utf8(&codes_data).unwrap_or("[]");
52            let mut backup_codes: Vec<String> = serde_json::from_str(codes_str).unwrap_or_default();
53
54            if let Some(index) = backup_codes.iter().position(|c| bool::from(c.as_bytes().ct_eq(code.as_bytes()))) {
55                // Mark code as used by removing it
56                backup_codes.remove(index);
57                let updated_codes =
58                    serde_json::to_string(&backup_codes).unwrap_or("[]".to_string());
59                self.storage
60                    .store_kv(&backup_key, updated_codes.as_bytes(), None)
61                    .await?;
62
63                info!("Backup code verified and consumed for user '{}'", user_id);
64                Ok(true)
65            } else {
66                Ok(false)
67            }
68        } else {
69            Ok(false)
70        }
71    }
72
73    /// Get remaining backup codes count
74    pub async fn get_remaining_count(&self, user_id: &str) -> Result<usize> {
75        debug!("Getting remaining backup codes for user '{}'", user_id);
76
77        let backup_key = format!("user:{}:backup_codes", user_id);
78        if let Some(codes_data) = self.storage.get_kv(&backup_key).await? {
79            let codes_str = std::str::from_utf8(&codes_data).unwrap_or("[]");
80            let backup_codes: Vec<String> = serde_json::from_str(codes_str).unwrap_or_default();
81            Ok(backup_codes.len())
82        } else {
83            Ok(0)
84        }
85    }
86
87    /// Check if user has backup codes
88    pub async fn has_backup_codes(&self, user_id: &str) -> Result<bool> {
89        let count = self.get_remaining_count(user_id).await?;
90        Ok(count > 0)
91    }
92
93    /// Regenerate backup codes (invalidating old ones)
94    pub async fn regenerate_codes(&self, user_id: &str, count: usize) -> Result<Vec<String>> {
95        info!("Regenerating backup codes for user '{}'", user_id);
96
97        // This will overwrite existing codes
98        self.generate_codes(user_id, count).await
99    }
100
101    /// Verify a backup code during the login MFA flow.
102    /// Reads from `mfa_backup_codes:{user_id}` — the key written by the MFA setup flow.
103    /// Codes are stored as SHA-256 hex strings; comparison is constant-time.
104    pub async fn verify_login_code(&self, user_id: &str, code: &str) -> Result<bool> {
105        use crate::security::secure_utils::constant_time_compare;
106        use sha2::Digest as _;
107
108        if code.trim().is_empty() {
109            return Ok(false);
110        }
111
112        let backup_key = format!("mfa_backup_codes:{}", user_id);
113        let codes: Vec<String> = match self.storage.get_kv(&backup_key).await? {
114            Some(data) => serde_json::from_slice(&data).unwrap_or_default(),
115            None => return Ok(false),
116        };
117
118        let provided_bytes = sha2::Sha256::digest(code.trim().as_bytes()).to_vec();
119
120        let mut found_idx: Option<usize> = None;
121        for (index, stored_hex) in codes.iter().enumerate() {
122            let stored_bytes = hex::decode(stored_hex).unwrap_or_default();
123            if stored_bytes.len() == provided_bytes.len()
124                && constant_time_compare(&stored_bytes, &provided_bytes)
125            {
126                found_idx = Some(index);
127            }
128        }
129
130        match found_idx {
131            Some(index) => {
132                let mut remaining = codes;
133                remaining.remove(index);
134                let updated = serde_json::to_vec(&remaining).unwrap_or_default();
135                self.storage.store_kv(&backup_key, &updated, None).await?;
136                Ok(true)
137            }
138            None => Ok(false),
139        }
140    }
141}