use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use argon2::password_hash::{rand_core::OsRng, SaltString};
use argon2::{Argon2, PasswordHasher, PasswordVerifier};
use crate::Plugin;
fn dummy_hash() -> &'static str {
static CELL: OnceLock<String> = OnceLock::new();
CELL.get_or_init(|| {
hash_password("dummy-password-for-timing-equalization")
})
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct PasswordEntry {
user_id: String,
email: String,
hash: String,
}
pub struct PasswordAuthPlugin {
entries: Mutex<HashMap<String, PasswordEntry>>,
}
impl PasswordAuthPlugin {
pub fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
pub fn register(&self, email: &str, password: &str, user_id: &str) -> Result<(), String> {
let mut entries = self.entries.lock().unwrap();
if entries.contains_key(email) {
return Err("Email already registered".into());
}
let hash = hash_password(password);
entries.insert(
email.to_string(),
PasswordEntry {
user_id: user_id.to_string(),
email: email.to_string(),
hash,
},
);
Ok(())
}
pub fn verify(&self, email: &str, password: &str) -> Option<String> {
let entries = self.entries.lock().unwrap();
match entries.get(email) {
Some(entry) => {
if verify_password(password, &entry.hash) {
Some(entry.user_id.clone())
} else {
None
}
}
None => {
let _ = verify_password(password, dummy_hash());
None
}
}
}
pub fn change_password(
&self,
email: &str,
old_password: &str,
new_password: &str,
) -> Result<(), String> {
let mut entries = self.entries.lock().unwrap();
let entry = entries.get_mut(email).ok_or("User not found")?;
if !verify_password(old_password, &entry.hash) {
return Err("Incorrect password".into());
}
entry.hash = hash_password(new_password);
Ok(())
}
pub fn is_registered(&self, email: &str) -> bool {
self.entries.lock().unwrap().contains_key(email)
}
pub fn reset_password(&self, email: &str, new_password: &str) -> Result<(), String> {
let mut entries = self.entries.lock().unwrap();
let entry = entries.get_mut(email).ok_or("User not found")?;
entry.hash = hash_password(new_password);
Ok(())
}
}
impl Plugin for PasswordAuthPlugin {
fn name(&self) -> &str {
"password-auth"
}
}
pub fn hash_password(password: &str) -> String {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
.hash_password(password.as_bytes(), &salt)
.expect("Argon2 hash should succeed")
.to_string()
}
pub fn verify_password(password: &str, hash: &str) -> bool {
use argon2::PasswordHash;
let parsed = match PasswordHash::new(hash) {
Ok(h) => h,
Err(_) => return false,
};
Argon2::default()
.verify_password(password.as_bytes(), &parsed)
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_and_verify() {
let plugin = PasswordAuthPlugin::new();
plugin
.register("alice@test.com", "password123", "user-1")
.unwrap();
let user_id = plugin.verify("alice@test.com", "password123").unwrap();
assert_eq!(user_id, "user-1");
}
#[test]
fn wrong_password_rejected() {
let plugin = PasswordAuthPlugin::new();
plugin
.register("alice@test.com", "password123", "user-1")
.unwrap();
assert!(plugin.verify("alice@test.com", "wrong").is_none());
}
#[test]
fn unknown_email_rejected() {
let plugin = PasswordAuthPlugin::new();
assert!(plugin.verify("nobody@test.com", "password").is_none());
}
#[test]
fn duplicate_email_rejected() {
let plugin = PasswordAuthPlugin::new();
plugin
.register("alice@test.com", "pass1", "user-1")
.unwrap();
let result = plugin.register("alice@test.com", "pass2", "user-2");
assert!(result.is_err());
}
#[test]
fn change_password() {
let plugin = PasswordAuthPlugin::new();
plugin
.register("alice@test.com", "old-pass", "user-1")
.unwrap();
plugin
.change_password("alice@test.com", "old-pass", "new-pass")
.unwrap();
assert!(plugin.verify("alice@test.com", "old-pass").is_none());
assert!(plugin.verify("alice@test.com", "new-pass").is_some());
}
#[test]
fn change_password_wrong_old() {
let plugin = PasswordAuthPlugin::new();
plugin
.register("alice@test.com", "password", "user-1")
.unwrap();
let result = plugin.change_password("alice@test.com", "wrong", "new");
assert!(result.is_err());
}
#[test]
fn reset_password() {
let plugin = PasswordAuthPlugin::new();
plugin
.register("alice@test.com", "old-pass", "user-1")
.unwrap();
plugin
.reset_password("alice@test.com", "reset-pass")
.unwrap();
assert!(plugin.verify("alice@test.com", "reset-pass").is_some());
}
#[test]
fn is_registered() {
let plugin = PasswordAuthPlugin::new();
assert!(!plugin.is_registered("alice@test.com"));
plugin.register("alice@test.com", "pass", "user-1").unwrap();
assert!(plugin.is_registered("alice@test.com"));
}
#[test]
fn hash_is_phc_format() {
let h = hash_password("test-password");
assert!(h.starts_with("$argon2"), "Expected PHC format, got: {}", h);
}
#[test]
fn same_password_different_hashes() {
let h1 = hash_password("password");
let h2 = hash_password("password");
assert_ne!(h1, h2);
assert!(verify_password("password", &h1));
assert!(verify_password("password", &h2));
}
}