use anyhow::{anyhow, Result}; use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::Argon2; use rand_core::OsRng; use crate::prelude::log; use crate::repo::Storage; pub enum Verdict { Authenticated, UserNotFound, InvalidPassword, } pub enum UpdatePasswordResult { PasswordUpdated, UserNotFound, } #[derive(Clone)] pub struct Authenticator { storage: Storage, } impl Authenticator { pub fn new(storage: Storage) -> Self { Self { storage } } #[tracing::instrument(skip(self, provided_password), name = "Authenticator::authenticate")] pub async fn authenticate(&self, login: &str, provided_password: &str) -> Result { let Some(stored_user) = self.storage.retrieve_user_by_name(login).await? else { return Ok(Verdict::UserNotFound); }; if let Some(argon2_hash) = stored_user.argon2_hash { let argon2 = Argon2::default(); let password_hash = PasswordHash::new(&argon2_hash).map_err(|e| anyhow!("Failed to parse password hash: {e:?}"))?; let password_verifier = argon2.verify_password(provided_password.as_bytes(), &password_hash); if password_verifier.is_ok() { return Ok(Verdict::Authenticated); } } if let Some(expected_password) = stored_user.password { if expected_password == provided_password { return Ok(Verdict::Authenticated); } } Ok(Verdict::InvalidPassword) } #[tracing::instrument(skip(self, provided_password), name = "Authenticator::set_password")] pub async fn set_password(&self, login: &str, provided_password: &str) -> Result { let Some(u) = self.storage.retrieve_user_by_name(login).await? else { return Ok(UpdatePasswordResult::UserNotFound); }; let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let password_hash = argon2 .hash_password(provided_password.as_bytes(), &salt) .map_err(|e| anyhow!("Failed to hash password: {e:?}"))?; self.storage.set_argon2_challenge(u.id, password_hash.to_string().as_str()).await?; log::info!("Password changed for player {login}"); Ok(UpdatePasswordResult::PasswordUpdated) } }