use md5::{Digest, Md5};
use rand::Rng;
#[derive(Debug, Clone)]
pub enum Auth {
ApiKey {
api_key: String,
},
Token {
username: String,
password: String,
},
Plain {
username: String,
password: String,
},
}
impl Auth {
pub fn api_key(api_key: impl Into<String>) -> Self {
Auth::ApiKey {
api_key: api_key.into(),
}
}
pub fn token(username: impl Into<String>, password: impl Into<String>) -> Self {
Auth::Token {
username: username.into(),
password: password.into(),
}
}
pub fn plain(username: impl Into<String>, password: impl Into<String>) -> Self {
Auth::Plain {
username: username.into(),
password: password.into(),
}
}
pub fn username(&self) -> Option<&str> {
match self {
Auth::ApiKey { .. } => None,
Auth::Token { username, .. } | Auth::Plain { username, .. } => Some(username),
}
}
pub fn params(&self) -> Vec<(&'static str, String)> {
match self {
Auth::ApiKey { api_key } => {
vec![("apiKey", api_key.clone())]
}
Auth::Token { password, .. } => {
let salt = generate_salt();
let token = compute_token(password, &salt);
vec![("t", token), ("s", salt)]
}
Auth::Plain { password, .. } => {
let hex_password = hex_encode(password.as_bytes());
vec![("p", format!("enc:{hex_password}"))]
}
}
}
}
fn generate_salt() -> String {
let mut rng = rand::rng();
let bytes: [u8; 6] = rng.random(); hex_encode(&bytes)
}
fn compute_token(password: &str, salt: &str) -> String {
let mut hasher = Md5::new();
hasher.update(password.as_bytes());
hasher.update(salt.as_bytes());
let result = hasher.finalize();
hex_encode(&result)
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_auth_produces_correct_params() {
let auth = Auth::token("admin", "sesame");
let params = auth.params();
assert_eq!(params.len(), 2);
assert_eq!(params[0].0, "t");
assert_eq!(params[1].0, "s");
assert_eq!(params[1].1.len(), 12);
assert!(params[1].1.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(params[0].1.len(), 32);
assert!(params[0].1.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn token_auth_matches_known_md5() {
let salt = "abcdef012345";
let token = compute_token("sesame", salt);
let mut hasher = Md5::new();
hasher.update(b"sesameabcdef012345");
let expected = hex_encode(&hasher.finalize());
assert_eq!(token, expected);
}
#[test]
fn plain_auth_hex_encodes_password() {
let auth = Auth::plain("admin", "sesame");
let params = auth.params();
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "p");
assert_eq!(params[0].1, "enc:736573616d65");
}
#[test]
fn api_key_auth_produces_correct_params() {
let auth = Auth::api_key("my-secret-key-123");
let params = auth.params();
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "apiKey");
assert_eq!(params[0].1, "my-secret-key-123");
}
#[test]
fn username_accessor() {
assert_eq!(Auth::token("alice", "pass").username(), Some("alice"));
assert_eq!(Auth::plain("bob", "pass").username(), Some("bob"));
assert_eq!(Auth::api_key("key").username(), None);
}
#[test]
fn salt_is_random_per_call() {
let auth = Auth::token("user", "password");
let p1 = auth.params();
let p2 = auth.params();
assert_ne!(p1[1].1, p2[1].1);
}
}