Skip to main content

krabka_security/
plain.rs

1use std::{collections::HashMap, hash::BuildHasher};
2
3use subtle::ConstantTimeEq;
4
5use crate::{AuthError, AuthMethod, Principal};
6
7/// Verifies a SASL/PLAIN auth attempt against a static credential map.
8///
9/// On a known user, the password comparison is constant-time. On an unknown
10/// user, this function returns `UnknownUser`. The wire response upstream
11/// collapses both outcomes to `SASL_AUTHENTICATION_FAILED`.
12///
13/// `skip_all` keeps `creds` and the raw `password` bytes out of the span
14/// fields. Only the non-sensitive `user` and mechanism name are recorded. `err`
15/// surfaces `AuthError` (Debug) without leaking the password.
16#[tracing::instrument(
17    level = "debug",
18    skip_all,
19    fields(mechanism = "PLAIN", user = %user),
20    err
21)]
22/// # Errors
23/// Returns an error when credentials or key material are invalid, cryptographic verification fails, or the TLS, SASL, or Kerberos exchange is rejected.
24pub fn verify_plain<S: BuildHasher>(
25    creds: &HashMap<String, String, S>,
26    user: &str,
27    password: &[u8],
28) -> Result<Principal, AuthError> {
29    let Some(expected) = creds.get(user) else {
30        return Err(AuthError::UnknownUser);
31    };
32    if expected.as_bytes().ct_eq(password).unwrap_u8() == 1 {
33        Ok(Principal {
34            name: user.to_owned(),
35            auth_method: AuthMethod::SaslPlain,
36            groups: vec![],
37        })
38    } else {
39        Err(AuthError::BadPassword)
40    }
41}
42
43#[cfg(test)]
44mod tests {
45
46    use super::*;
47
48    fn creds() -> HashMap<String, String> {
49        let mut m = HashMap::new();
50        m.insert("alice".into(), "wonderland".into());
51        m
52    }
53
54    #[test]
55    fn correct_creds_pass() {
56        let p = verify_plain(&creds(), "alice", b"wonderland").unwrap();
57        assert2::assert!(p.name.as_str() == "alice");
58        assert2::assert!(p.auth_method == AuthMethod::SaslPlain);
59    }
60
61    #[test]
62    fn wrong_password_fails() {
63        assert2::assert!(
64            verify_plain(&creds(), "alice", b"hunter2") == Err(AuthError::BadPassword)
65        );
66    }
67
68    #[test]
69    fn unknown_user_fails() {
70        assert2::assert!(verify_plain(&creds(), "bob", b"anything") == Err(AuthError::UnknownUser));
71    }
72}