Skip to main content

securitydept_creds/
basic.rs

1use 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
19/// Parse a basic auth header value ("Basic base64(user:pass)").
20pub 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
32/// Parse a basic auth header value ("Basic base64(user:pass)") with error
33/// handling.
34pub 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
65/// Hash a plaintext password with argon2.
66pub 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
77/// Verify a plaintext password against an argon2 hash.
78pub 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        // admin:secret123 encoded in base64
158        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}