wisegate_core/auth/
mod.rs1pub mod hash;
7
8use base64::{Engine, engine::general_purpose::STANDARD};
9
10#[derive(Debug, Clone)]
12pub struct Credential {
13 username: String,
14 password: String,
15}
16
17impl Credential {
18 pub fn new(username: String, password: String) -> Self {
20 Self { username, password }
21 }
22
23 pub fn parse(value: &str) -> Option<Self> {
29 let (user, pass) = value.split_once(':')?;
30 if user.is_empty() {
31 return None;
32 }
33 Some(Self::new(user.to_string(), pass.to_string()))
34 }
35
36 pub fn username(&self) -> &str {
38 &self.username
39 }
40
41 pub fn password(&self) -> &str {
43 &self.password
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct Credentials {
50 entries: Vec<Credential>,
51}
52
53impl Credentials {
54 pub fn new() -> Self {
56 Self {
57 entries: Vec::new(),
58 }
59 }
60
61 pub fn from_entries(entries: Vec<Credential>) -> Self {
63 Self { entries }
64 }
65
66 pub fn is_empty(&self) -> bool {
68 self.entries.is_empty()
69 }
70
71 pub fn len(&self) -> usize {
73 self.entries.len()
74 }
75
76 pub fn iter(&self) -> impl Iterator<Item = &Credential> {
78 self.entries.iter()
79 }
80
81 pub fn verify(&self, auth_header: &str) -> bool {
86 let Some(encoded) = auth_header.strip_prefix("Basic ") else {
87 return false;
88 };
89
90 let Ok(decoded) = STANDARD.decode(encoded.trim()) else {
91 return false;
92 };
93
94 let Ok(decoded_str) = String::from_utf8(decoded) else {
95 return false;
96 };
97
98 let Some((user, password)) = decoded_str.split_once(':') else {
100 return false;
101 };
102
103 self.check(user, password)
104 }
105
106 fn check(&self, username: &str, password: &str) -> bool {
109 let mut found = false;
110 for cred in &self.entries {
111 let user_match = hash::constant_time_eq(cred.username.as_bytes(), username.as_bytes());
113 let pass_match = hash::verify(password, &cred.password);
115 if user_match && pass_match {
116 found = true;
117 }
118 }
119 found
120 }
121}
122
123impl Default for Credentials {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129pub fn check_basic_auth(auth_header: Option<&str>, credentials: &Credentials) -> bool {
139 if credentials.is_empty() {
140 return true;
142 }
143
144 match auth_header {
145 Some(header) => credentials.verify(header),
146 None => false,
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_credential_parse_valid() {
156 let cred = Credential::parse("admin:secret").unwrap();
157 assert_eq!(cred.username(), "admin");
158 assert_eq!(cred.password(), "secret");
159 }
160
161 #[test]
162 fn test_credential_parse_with_colon_in_password() {
163 let cred = Credential::parse("admin:sec:ret").unwrap();
164 assert_eq!(cred.username(), "admin");
165 assert_eq!(cred.password(), "sec:ret");
166 }
167
168 #[test]
169 fn test_credential_parse_no_colon() {
170 assert!(Credential::parse("invalid").is_none());
171 }
172
173 #[test]
174 fn test_credential_parse_empty_user() {
175 assert!(Credential::parse(":password").is_none());
176 }
177
178 #[test]
179 fn test_credentials_empty() {
180 let creds = Credentials::new();
181 assert!(creds.is_empty());
182 assert_eq!(creds.len(), 0);
183 }
184
185 #[test]
186 fn test_credentials_from_entries() {
187 let entries = vec![
188 Credential::new("admin".to_string(), "secret".to_string()),
189 Credential::new("user".to_string(), "pass".to_string()),
190 ];
191 let creds = Credentials::from_entries(entries);
192 assert!(!creds.is_empty());
193 assert_eq!(creds.len(), 2);
194 }
195
196 #[test]
197 fn test_verify_plain_text() {
198 let creds = Credentials::from_entries(vec![Credential::new(
199 "admin".to_string(),
200 "secret".to_string(),
201 )]);
202
203 let header = format!("Basic {}", STANDARD.encode("admin:secret"));
204 assert!(creds.verify(&header));
205 }
206
207 #[test]
208 fn test_verify_wrong_password() {
209 let creds = Credentials::from_entries(vec![Credential::new(
210 "admin".to_string(),
211 "secret".to_string(),
212 )]);
213
214 let header = format!("Basic {}", STANDARD.encode("admin:wrong"));
215 assert!(!creds.verify(&header));
216 }
217
218 #[test]
219 fn test_verify_wrong_user() {
220 let creds = Credentials::from_entries(vec![Credential::new(
221 "admin".to_string(),
222 "secret".to_string(),
223 )]);
224
225 let header = format!("Basic {}", STANDARD.encode("wrong:secret"));
226 assert!(!creds.verify(&header));
227 }
228
229 #[test]
230 fn test_verify_bcrypt() {
231 let hash = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe";
233 let creds =
234 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
235
236 let header = format!("Basic {}", STANDARD.encode("user:password"));
237 assert!(creds.verify(&header));
238 }
239
240 #[test]
241 fn test_verify_sha1() {
242 let hash = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
244 let creds =
245 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
246
247 let header = format!("Basic {}", STANDARD.encode("user:password"));
248 assert!(creds.verify(&header));
249 }
250
251 #[test]
252 fn test_verify_apr1() {
253 let hash = "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00";
255 let creds =
256 Credentials::from_entries(vec![Credential::new("user".to_string(), hash.to_string())]);
257
258 let header = format!("Basic {}", STANDARD.encode("user:password"));
259 assert!(creds.verify(&header));
260 }
261
262 #[test]
263 fn test_verify_multiple_credentials() {
264 let creds = Credentials::from_entries(vec![
265 Credential::new("admin".to_string(), "admin123".to_string()),
266 Credential::new("user1".to_string(), "pass1".to_string()),
267 Credential::new("user2".to_string(), "pass2".to_string()),
268 ]);
269
270 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("admin:admin123"))));
271 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user1:pass1"))));
272 assert!(creds.verify(&format!("Basic {}", STANDARD.encode("user2:pass2"))));
273 assert!(!creds.verify(&format!("Basic {}", STANDARD.encode("unknown:pass"))));
274 }
275
276 #[test]
277 fn test_verify_invalid_base64() {
278 let creds = Credentials::from_entries(vec![Credential::new(
279 "admin".to_string(),
280 "secret".to_string(),
281 )]);
282 assert!(!creds.verify("Basic not-valid-base64!!!"));
283 }
284
285 #[test]
286 fn test_verify_non_basic_auth() {
287 let creds = Credentials::from_entries(vec![Credential::new(
288 "admin".to_string(),
289 "secret".to_string(),
290 )]);
291 assert!(!creds.verify("Bearer some-token"));
292 }
293
294 #[test]
295 fn test_verify_missing_colon_in_decoded() {
296 let creds = Credentials::from_entries(vec![Credential::new(
297 "admin".to_string(),
298 "secret".to_string(),
299 )]);
300 let header = format!("Basic {}", STANDARD.encode("no-colon-here"));
301 assert!(!creds.verify(&header));
302 }
303
304 #[test]
305 fn test_check_basic_auth_no_credentials() {
306 let creds = Credentials::new();
307 assert!(check_basic_auth(None, &creds));
309 assert!(check_basic_auth(Some("Basic anything"), &creds));
310 }
311
312 #[test]
313 fn test_check_basic_auth_with_credentials() {
314 let creds = Credentials::from_entries(vec![Credential::new(
315 "admin".to_string(),
316 "secret".to_string(),
317 )]);
318
319 assert!(!check_basic_auth(None, &creds));
321
322 let valid_header = format!("Basic {}", STANDARD.encode("admin:secret"));
324 assert!(check_basic_auth(Some(&valid_header), &creds));
325
326 let invalid_header = format!("Basic {}", STANDARD.encode("admin:wrong"));
328 assert!(!check_basic_auth(Some(&invalid_header), &creds));
329 }
330}