this repo has no description
1use base32::Alphabet;
2use rand::RngCore;
3use subtle::ConstantTimeEq;
4use totp_rs::{Algorithm, TOTP};
5
6const TOTP_DIGITS: usize = 6;
7const TOTP_STEP: u64 = 30;
8const TOTP_SECRET_LENGTH: usize = 20;
9
10pub fn generate_totp_secret() -> Vec<u8> {
11 let mut secret = vec![0u8; TOTP_SECRET_LENGTH];
12 rand::thread_rng().fill_bytes(&mut secret);
13 secret
14}
15
16pub fn encrypt_totp_secret(secret: &[u8]) -> Result<Vec<u8>, String> {
17 crate::config::encrypt_key(secret)
18}
19
20pub fn decrypt_totp_secret(encrypted: &[u8], version: i32) -> Result<Vec<u8>, String> {
21 crate::config::decrypt_key(encrypted, Some(version))
22}
23
24fn create_totp(
25 secret: Vec<u8>,
26 issuer: Option<String>,
27 account_name: String,
28) -> Result<TOTP, String> {
29 TOTP::new(
30 Algorithm::SHA1,
31 TOTP_DIGITS,
32 1,
33 TOTP_STEP,
34 secret,
35 issuer,
36 account_name,
37 )
38 .map_err(|e| format!("Failed to create TOTP: {}", e))
39}
40
41pub fn verify_totp_code(secret: &[u8], code: &str) -> bool {
42 let code = code.trim();
43 if code.len() != TOTP_DIGITS {
44 return false;
45 }
46
47 let Ok(totp) = create_totp(secret.to_vec(), None, String::new()) else {
48 return false;
49 };
50
51 let now = std::time::SystemTime::now()
52 .duration_since(std::time::UNIX_EPOCH)
53 .map(|d| d.as_secs())
54 .unwrap_or(0);
55
56 for offset in [-1i64, 0, 1] {
57 let time = (now as i64 + offset * TOTP_STEP as i64) as u64;
58 let expected = totp.generate(time);
59 let is_valid: bool = code.as_bytes().ct_eq(expected.as_bytes()).into();
60 if is_valid {
61 return true;
62 }
63 }
64
65 false
66}
67
68pub fn generate_totp_uri(secret: &[u8], account_name: &str, issuer: &str) -> String {
69 let secret_base32 = base32::encode(Alphabet::Rfc4648 { padding: false }, secret);
70 format!(
71 "otpauth://totp/{}:{}?secret={}&issuer={}&algorithm=SHA1&digits={}&period={}",
72 urlencoding::encode(issuer),
73 urlencoding::encode(account_name),
74 secret_base32,
75 urlencoding::encode(issuer),
76 TOTP_DIGITS,
77 TOTP_STEP
78 )
79}
80
81pub fn generate_qr_png_base64(
82 secret: &[u8],
83 account_name: &str,
84 issuer: &str,
85) -> Result<String, String> {
86 use base64::{Engine, engine::general_purpose::STANDARD};
87
88 let totp = create_totp(
89 secret.to_vec(),
90 Some(issuer.to_string()),
91 account_name.to_string(),
92 )?;
93
94 let qr_png = totp
95 .get_qr_png()
96 .map_err(|e| format!("Failed to generate QR code: {}", e))?;
97
98 Ok(STANDARD.encode(qr_png))
99}
100
101const BACKUP_CODE_ALPHABET: &[u8] = b"23456789ABCDEFGHJKMNPQRSTUVWXYZ";
102const BACKUP_CODE_LENGTH: usize = 8;
103const BACKUP_CODE_COUNT: usize = 10;
104const BACKUP_CODE_BCRYPT_COST: u32 = 10;
105
106pub fn generate_backup_codes() -> Vec<String> {
107 let mut codes = Vec::with_capacity(BACKUP_CODE_COUNT);
108 let mut rng = rand::thread_rng();
109
110 for _ in 0..BACKUP_CODE_COUNT {
111 let mut code = String::with_capacity(BACKUP_CODE_LENGTH);
112 for _ in 0..BACKUP_CODE_LENGTH {
113 let idx = (rng.next_u32() as usize) % BACKUP_CODE_ALPHABET.len();
114 code.push(BACKUP_CODE_ALPHABET[idx] as char);
115 }
116 codes.push(code);
117 }
118
119 codes
120}
121
122pub fn hash_backup_code(code: &str) -> Result<String, String> {
123 bcrypt::hash(code, BACKUP_CODE_BCRYPT_COST).map_err(|e| format!("Failed to hash code: {}", e))
124}
125
126pub fn verify_backup_code(code: &str, hash: &str) -> bool {
127 bcrypt::verify(code, hash).unwrap_or(false)
128}
129
130pub fn is_backup_code_format(code: &str) -> bool {
131 let code = code.trim().to_uppercase();
132 code.len() == BACKUP_CODE_LENGTH
133 && code
134 .chars()
135 .all(|c| BACKUP_CODE_ALPHABET.contains(&(c as u8)))
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_generate_totp_secret() {
144 let secret = generate_totp_secret();
145 assert_eq!(secret.len(), TOTP_SECRET_LENGTH);
146 }
147
148 #[test]
149 fn test_verify_totp_code() {
150 let secret = generate_totp_secret();
151 let totp = create_totp(secret.clone(), None, String::new()).unwrap();
152 let code = totp.generate_current().unwrap();
153 assert!(verify_totp_code(&secret, &code));
154 assert!(!verify_totp_code(&secret, "000000"));
155 }
156
157 #[test]
158 fn test_generate_totp_uri() {
159 let secret = vec![0u8; 20];
160 let uri = generate_totp_uri(&secret, "test@example.com", "TestPDS");
161 assert!(uri.starts_with("otpauth://totp/"));
162 assert!(uri.contains("secret="));
163 assert!(uri.contains("issuer=TestPDS"));
164 }
165
166 #[test]
167 fn test_backup_codes() {
168 let codes = generate_backup_codes();
169 assert_eq!(codes.len(), BACKUP_CODE_COUNT);
170 for code in &codes {
171 assert_eq!(code.len(), BACKUP_CODE_LENGTH);
172 assert!(is_backup_code_format(code));
173 }
174 }
175
176 #[test]
177 fn test_backup_code_hash_verify() {
178 let codes = generate_backup_codes();
179 let code = &codes[0];
180 let hash = hash_backup_code(code).unwrap();
181 assert!(verify_backup_code(code, &hash));
182 assert!(!verify_backup_code("WRONGCOD", &hash));
183 }
184
185 #[test]
186 fn test_is_backup_code_format() {
187 assert!(is_backup_code_format("ABCD2345"));
188 assert!(is_backup_code_format(" abcd2345 "));
189 assert!(!is_backup_code_format("ABCD234"));
190 assert!(!is_backup_code_format("ABCD23456"));
191 assert!(!is_backup_code_format("ABCD234O"));
192 assert!(!is_backup_code_format("ABCD2341"));
193 }
194}