securitydept_creds/
basic.rs1use std::fmt::{Debug, Formatter};
2
3use argon2::{
4 Argon2,
5 password_hash::{
6 PasswordHasher, PasswordVerifier,
7 phc::{PasswordHash, SaltString},
8 },
9};
10use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
11use serde::{Deserialize, Serialize};
12
13use crate::{CredsError, CredsResult};
14
15pub fn is_basic_auth_header(header_value: &str) -> bool {
16 header_value.len() >= 6 && header_value[..6].eq_ignore_ascii_case("Basic ")
17}
18
19pub fn parse_basic_auth_header_opt(header_value: &str) -> Option<(String, String)> {
21 if is_basic_auth_header(header_value) {
22 let encoded = header_value[6..].trim();
23 let decoded = BASE64.decode(encoded).ok()?;
24 let decoded_str = String::from_utf8(decoded).ok()?;
25 let (user, pass) = decoded_str.split_once(':')?;
26 Some((user.to_string(), pass.to_string()))
27 } else {
28 None
29 }
30}
31
32pub fn parse_basic_auth_header(header_value: &str) -> Result<(String, String), CredsError> {
35 if !is_basic_auth_header(header_value) {
36 return Err(CredsError::InvalidCredentialsFormat {
37 message: "Authorization header must have 'Basic' scheme and credentials for basic auth"
38 .to_string(),
39 });
40 }
41
42 let encoded = header_value[6..].trim();
43
44 let decoded = BASE64
45 .decode(encoded)
46 .map_err(|e| CredsError::InvalidCredentialsFormat {
47 message: format!("Failed to decode credentials: {}", e),
48 })?;
49
50 let decoded_str =
51 String::from_utf8(decoded).map_err(|e| CredsError::InvalidCredentialsFormat {
52 message: format!("Credentials contain invalid UTF-8: {}", e),
53 })?;
54
55 let (username, password) =
56 decoded_str
57 .split_once(':')
58 .ok_or_else(|| CredsError::InvalidCredentialsFormat {
59 message: "Missing username or password".to_string(),
60 })?;
61
62 Ok((username.to_string(), password.to_string()))
63}
64
65pub fn hash_password_argon2(password: &str) -> CredsResult<String> {
67 let salt = SaltString::generate();
68 let argon2 = Argon2::default();
69 let hash = argon2
70 .hash_password_with_salt(password.as_bytes(), salt.as_bytes())
71 .map_err(|e| CredsError::PasswordHash {
72 message: e.to_string(),
73 })?;
74 Ok(hash.to_string())
75}
76
77pub fn verify_password_argon2(password: &str, password_hash: &str) -> CredsResult<bool> {
79 let parsed = PasswordHash::new(password_hash).map_err(|e| CredsError::PasswordHash {
80 message: e.to_string(),
81 })?;
82 Ok(Argon2::default()
83 .verify_password(password.as_bytes(), &parsed)
84 .is_ok())
85}
86
87pub trait BasicAuthCred: Clone {
88 fn username(&self) -> &str;
89 fn display_name(&self) -> &str {
90 self.username()
91 }
92 fn verify_password(&self, password: &str) -> CredsResult<bool>;
93}
94
95#[derive(Clone, Serialize, Deserialize)]
96pub struct Argon2BasicAuthCred {
97 pub username: String,
98 pub password_hash: String,
99}
100
101impl Argon2BasicAuthCred {
102 pub fn new(username: String, password: String) -> CredsResult<Self> {
103 let password_hash = hash_password_argon2(&password)?;
104 Ok(Self {
105 username,
106 password_hash,
107 })
108 }
109
110 pub fn update_password(&mut self, password: String) -> CredsResult<()> {
111 self.password_hash = hash_password_argon2(&password)?;
112 Ok(())
113 }
114}
115
116impl Debug for Argon2BasicAuthCred {
117 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("Argon2BasicAuthCreds")
119 .field("username", &self.username)
120 .finish()
121 }
122}
123
124impl BasicAuthCred for Argon2BasicAuthCred {
125 fn username(&self) -> &str {
126 &self.username
127 }
128
129 fn verify_password(&self, password: &str) -> CredsResult<bool> {
130 verify_password_argon2(password, &self.password_hash)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_hash_and_verify_password() {
140 let password = "test_password_123";
141 let hash = hash_password_argon2(password).unwrap();
142 assert!(verify_password_argon2(password, &hash).unwrap());
143 assert!(!verify_password_argon2("wrong_password", &hash).unwrap());
144 }
145
146 #[test]
147 fn test_parse_basic_auth_header() {
148 let credentials = BASE64.encode("username:password");
149 let header = format!("Basic {}", credentials);
150 let (user, pass) = parse_basic_auth_header_opt(&header).unwrap();
151 assert_eq!(user, "username");
152 assert_eq!(pass, "password");
153 }
154
155 #[test]
156 fn test_parse_authorization_header() -> CredsResult<()> {
157 let header = "Basic YWRtaW46c2VjcmV0MTIz";
159 let (username, password) = parse_basic_auth_header(header)?;
160 assert_eq!(username, "admin");
161 assert_eq!(password, "secret123");
162 Ok(())
163 }
164
165 #[test]
166 fn test_parse_invalid_header() {
167 assert!(parse_basic_auth_header("invalid").is_err());
168 assert!(parse_basic_auth_header("Bearer token").is_err());
169 assert!(parse_basic_auth_header("Basic invalid-base64").is_err());
170 }
171}