auth_framework/auth_modular/mfa/
backup_codes.rs1use crate::errors::Result;
4use crate::storage::AuthStorage;
5use std::sync::Arc;
6use subtle::ConstantTimeEq;
7use tracing::{debug, info};
8
9pub struct BackupCodesManager {
11 storage: Arc<dyn AuthStorage>,
12}
13
14impl BackupCodesManager {
15 pub fn new(storage: Arc<dyn AuthStorage>) -> Self {
17 Self { storage }
18 }
19
20 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 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 pub async fn verify_code(&self, user_id: &str, code: &str) -> Result<bool> {
41 debug!("Verifying backup code for user '{}'", user_id);
42
43 if code.len() != 8 || !code.chars().all(|c| c.is_ascii_digit()) {
45 return Ok(false);
46 }
47
48 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 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 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 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 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 self.generate_codes(user_id, count).await
99 }
100
101 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}