use crate::secret::SecretString;
use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::{PasswordHash as Argon2PasswordHash, PasswordVerifier, SaltString};
use argon2::{Argon2, PasswordHasher as Argon2PasswordHasher};
use serde::Serializer;
use zeroize::Zeroizing;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PasswordError {
#[error("empty password")]
EmptyPassword,
#[error("hashing failed: {reason}")]
HashingFailed {
reason: String,
},
#[error("invalid hash format: {reason}")]
InvalidHashFormat {
reason: String,
},
}
#[must_use]
pub struct PasswordHash {
inner: Zeroizing<String>,
}
impl PasswordHash {
pub(crate) fn new(phc_string: String) -> Self {
Self {
inner: Zeroizing::new(phc_string),
}
}
#[must_use]
pub fn expose_hash(&self) -> &str {
&self.inner
}
}
impl std::fmt::Debug for PasswordHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PasswordHash([REDACTED])")
}
}
impl Clone for PasswordHash {
fn clone(&self) -> Self {
Self {
inner: Zeroizing::new((*self.inner).clone()),
}
}
}
impl serde::Serialize for PasswordHash {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str("[REDACTED]")
}
}
pub trait PasswordHasher: Send + Sync {
fn hash_password(&self, password: &SecretString) -> Result<PasswordHash, PasswordError>;
fn verify_password(
&self,
password: &SecretString,
hash: &PasswordHash,
) -> Result<bool, PasswordError>;
}
#[derive(Debug, Clone, Default)]
pub struct Argon2Hasher {
_private: (),
}
impl PasswordHasher for Argon2Hasher {
fn hash_password(&self, password: &SecretString) -> Result<PasswordHash, PasswordError> {
let plaintext = password.expose_secret();
if plaintext.is_empty() {
return Err(PasswordError::EmptyPassword);
}
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2
.hash_password(plaintext.as_bytes(), &salt)
.map_err(|e| PasswordError::HashingFailed {
reason: e.to_string(),
})?;
Ok(PasswordHash::new(hash.to_string()))
}
fn verify_password(
&self,
password: &SecretString,
hash: &PasswordHash,
) -> Result<bool, PasswordError> {
let parsed = Argon2PasswordHash::new(hash.expose_hash()).map_err(|e| {
PasswordError::InvalidHashFormat {
reason: e.to_string(),
}
})?;
let argon2 = Argon2::default();
match argon2.verify_password(password.expose_secret().as_bytes(), &parsed) {
Ok(()) => Ok(true),
Err(argon2::password_hash::Error::Password) => Ok(false),
Err(e) => Err(PasswordError::HashingFailed {
reason: e.to_string(),
}),
}
}
}
pub fn hash_password(password: &SecretString) -> Result<PasswordHash, PasswordError> {
Argon2Hasher::default().hash_password(password)
}
pub fn verify_password(
password: &SecretString,
hash: &PasswordHash,
) -> Result<bool, PasswordError> {
Argon2Hasher::default().verify_password(password, hash)
}