wisegate_core/auth/
hash.rs1use base64::{Engine, engine::general_purpose::STANDARD};
10use md5::{Digest, Md5};
11use sha1::Sha1;
12
13pub fn verify(password: &str, hash: &str) -> bool {
16 if hash.starts_with("$2y$") || hash.starts_with("$2a$") || hash.starts_with("$2b$") {
17 verify_bcrypt(password, hash)
18 } else if hash.starts_with("$apr1$") {
19 verify_apr1(password, hash)
20 } else if hash.starts_with("{SHA}") {
21 verify_sha1(password, hash)
22 } else {
23 constant_time_eq(password.as_bytes(), hash.as_bytes())
25 }
26}
27
28pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
31 let len_eq = a.len() == b.len();
32 let max_len = a.len().max(b.len());
33 let mut result = 0u8;
34
35 for i in 0..max_len {
36 let x = a.get(i).copied().unwrap_or(0);
37 let y = b.get(i).copied().unwrap_or(0);
38 result |= x ^ y;
39 }
40
41 len_eq && result == 0
42}
43
44fn verify_bcrypt(password: &str, hash: &str) -> bool {
46 bcrypt::verify(password, hash).unwrap_or(false)
47}
48
49fn verify_sha1(password: &str, hash: &str) -> bool {
51 let Some(encoded) = hash.strip_prefix("{SHA}") else {
52 return false;
53 };
54
55 let Ok(stored_digest) = STANDARD.decode(encoded) else {
56 return false;
57 };
58
59 let computed_digest = Sha1::digest(password.as_bytes());
60 constant_time_eq(computed_digest.as_slice(), &stored_digest)
61}
62
63fn verify_apr1(password: &str, hash: &str) -> bool {
66 let Some(rest) = hash.strip_prefix("$apr1$") else {
67 return false;
68 };
69
70 let Some((salt, _)) = rest.split_once('$') else {
71 return false;
72 };
73
74 let computed = apr1_hash(password, salt);
75 constant_time_eq(computed.as_bytes(), hash.as_bytes())
76}
77
78fn apr1_hash(password: &str, salt: &str) -> String {
81 let password = password.as_bytes();
82 let salt = salt.as_bytes();
83
84 let mut ctx = Md5::new();
86 ctx.update(password);
87 ctx.update(b"$apr1$");
88 ctx.update(salt);
89
90 let mut ctx1 = Md5::new();
92 ctx1.update(password);
93 ctx1.update(salt);
94 ctx1.update(password);
95 let fin = ctx1.finalize();
96
97 let mut pl = password.len();
99 let mut i = 0;
100 while pl > 0 {
101 let len = if pl > 16 { 16 } else { pl };
102 ctx.update(&fin[i..i + len]);
103 pl -= len;
104 i += len;
105 if i >= 16 {
106 i = 0;
107 }
108 }
109
110 let mut pl = password.len();
112 while pl > 0 {
113 if pl & 1 != 0 {
114 ctx.update([0u8]);
115 } else {
116 ctx.update(&password[0..1]);
117 }
118 pl >>= 1;
119 }
120
121 let mut fin = ctx.finalize();
122
123 for i in 0..1000 {
125 let mut ctx1 = Md5::new();
126
127 if i & 1 != 0 {
128 ctx1.update(password);
129 } else {
130 ctx1.update(fin.as_slice());
131 }
132
133 if i % 3 != 0 {
134 ctx1.update(salt);
135 }
136
137 if i % 7 != 0 {
138 ctx1.update(password);
139 }
140
141 if i & 1 != 0 {
142 ctx1.update(fin.as_slice());
143 } else {
144 ctx1.update(password);
145 }
146
147 fin = ctx1.finalize();
148 }
149
150 let fin_arr: [u8; 16] = fin.into();
152 let encoded = apr1_encode(&fin_arr);
153 format!(
154 "$apr1${salt}${encoded}",
155 salt = std::str::from_utf8(salt).unwrap_or("")
156 )
157}
158
159fn apr1_encode(digest: &[u8; 16]) -> String {
162 const ITOA64: &[u8] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
163
164 let mut result = String::with_capacity(22);
165
166 let encode_triple = |a: u8, b: u8, c: u8| -> [char; 4] {
168 let v = (u32::from(a) << 16) | (u32::from(b) << 8) | u32::from(c);
169 [
170 ITOA64[(v & 0x3f) as usize] as char,
171 ITOA64[((v >> 6) & 0x3f) as usize] as char,
172 ITOA64[((v >> 12) & 0x3f) as usize] as char,
173 ITOA64[((v >> 18) & 0x3f) as usize] as char,
174 ]
175 };
176
177 for chars in encode_triple(digest[0], digest[6], digest[12]) {
179 result.push(chars);
180 }
181 for chars in encode_triple(digest[1], digest[7], digest[13]) {
182 result.push(chars);
183 }
184 for chars in encode_triple(digest[2], digest[8], digest[14]) {
185 result.push(chars);
186 }
187 for chars in encode_triple(digest[3], digest[9], digest[15]) {
188 result.push(chars);
189 }
190 for chars in encode_triple(digest[4], digest[10], digest[5]) {
191 result.push(chars);
192 }
193
194 let v = u32::from(digest[11]);
196 result.push(ITOA64[(v & 0x3f) as usize] as char);
197 result.push(ITOA64[((v >> 6) & 0x3f) as usize] as char);
198
199 result
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_verify_plain_text() {
208 assert!(verify("secret", "secret"));
209 assert!(!verify("secret", "wrong"));
210 }
211
212 #[test]
213 fn test_verify_sha1_password() {
214 let hash = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
216 assert!(verify("password", hash));
217 assert!(!verify("wrong", hash));
218 }
219
220 #[test]
221 fn test_verify_bcrypt_password() {
222 let hash = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe";
224 assert!(verify("password", hash));
225 assert!(!verify("wrong", hash));
226 }
227
228 #[test]
229 fn test_verify_apr1_password() {
230 let hash = "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00";
232 assert!(verify("password", hash));
233 assert!(!verify("wrong", hash));
234 }
235
236 #[test]
237 fn test_apr1_known_hash() {
238 let computed = apr1_hash("password", "lZL6V/ci");
240 assert_eq!(computed, "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00");
241 }
242
243 #[test]
244 fn test_constant_time_eq() {
245 assert!(constant_time_eq(b"hello", b"hello"));
246 assert!(!constant_time_eq(b"hello", b"world"));
247 assert!(!constant_time_eq(b"hello", b"hell"));
248 assert!(!constant_time_eq(b"", b"a"));
249 assert!(constant_time_eq(b"", b""));
250 }
251}