use std::collections::HashMap;
use std::sync::Mutex;
use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::{Algorithm, Argon2, Params, Version};
use tokio::sync::Semaphore;
fn argon2() -> Argon2<'static> {
Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
Params::new(8 * 1024, 2, 1, None).expect("valid params"),
)
}
pub fn hash_password(password: &[u8]) -> String {
let salt = SaltString::generate(&mut OsRng);
argon2()
.hash_password(password, &salt)
.expect("argon2 hashing")
.to_string()
}
fn verify_one(password: &[u8], phc: &str) -> bool {
PasswordHash::new(phc).is_ok_and(|h| argon2().verify_password(password, &h).is_ok())
}
fn hex_decode(s: &[u8]) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
let v = |c: u8| (c as char).to_digit(16).map(|d| d as u8);
s.chunks(2)
.map(|p| Some(v(p[0])? << 4 | v(p[1])?))
.collect()
}
fn hex_encode(b: &[u8], upper: bool) -> Vec<u8> {
b.iter()
.flat_map(|x| {
if upper {
format!("{x:02X}")
} else {
format!("{x:02x}")
}
.into_bytes()
})
.collect()
}
fn verify_password(password: &[u8], phc: &str) -> bool {
if verify_one(password, phc) {
return true;
}
if password.len() == 40
&& let Some(raw) = hex_decode(password)
{
return verify_one(&raw, phc);
}
if password.len() == 20 {
return verify_one(&hex_encode(password, true), phc)
|| verify_one(&hex_encode(password, false), phc);
}
false
}
pub fn parse_basic(header: &str) -> Option<(String, Vec<u8>)> {
let (scheme, value) = header.trim().split_once(' ')?;
if !scheme.eq_ignore_ascii_case("basic") {
return None;
}
let decoded = ocpp_ws::base64::decode(value.trim())?;
let colon = decoded.iter().position(|&b| b == b':')?;
let user = String::from_utf8(decoded[..colon].to_vec()).ok()?;
Some((user, decoded[colon + 1..].to_vec()))
}
pub struct Authenticator {
permits: Semaphore,
verified: Mutex<HashMap<String, (Vec<u8>, String)>>,
}
impl Authenticator {
pub fn new(concurrency: usize) -> Self {
Authenticator {
permits: Semaphore::new(concurrency.max(1)),
verified: Mutex::new(HashMap::new()),
}
}
pub async fn verify(&self, identity: &str, password: &[u8], phc: &str) -> bool {
{
let cache = self.verified.lock().unwrap_or_else(|e| e.into_inner());
if let Some((pw, hash)) = cache.get(identity)
&& pw.as_slice() == password
&& hash == phc
{
return true;
}
}
let Ok(_permit) = self.permits.acquire().await else {
return false;
};
let (pw, h) = (password.to_vec(), phc.to_owned());
let ok = tokio::task::spawn_blocking(move || verify_password(&pw, &h))
.await
.unwrap_or(false);
let mut cache = self.verified.lock().unwrap_or_else(|e| e.into_inner());
if ok {
cache.insert(identity.to_owned(), (password.to_vec(), phc.to_owned()));
} else {
cache.remove(identity);
}
ok
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_header_with_binary_key() {
let (u, p) = parse_basic("Basic QUwxMDAwOgABAgMEBQYH////////////////").unwrap();
assert_eq!(u, "AL1000");
assert_eq!(p.len(), 20);
assert_eq!(&p[..8], &[0, 1, 2, 3, 4, 5, 6, 7]);
assert!(parse_basic("Bearer abc").is_none());
}
#[tokio::test]
async fn raw_and_hex_forms_match() {
let key: Vec<u8> = (0u8..20).collect();
let phc = hash_password(&key);
let a = Authenticator::new(2);
assert!(a.verify("CS1", &key, &phc).await);
assert!(
a.verify("CS1", b"000102030405060708090a0b0c0d0e0f10111213", &phc)
.await
);
assert!(!a.verify("CS1", b"wrong", &phc).await);
let phc2 = hash_password(b"a-utf8-password-of-2.0.1");
assert!(a.verify("CS2", b"a-utf8-password-of-2.0.1", &phc2).await);
assert!(a.verify("CS2", b"a-utf8-password-of-2.0.1", &phc2).await);
}
}