use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use super::error::TenancyError;
pub fn hash(plaintext: &str) -> Result<String, TenancyError> {
if plaintext.is_empty() {
return Err(TenancyError::Validation(
"password must not be empty".into(),
));
}
let salt = SaltString::generate(&mut OsRng);
let hasher = Argon2::default();
let phc = hasher
.hash_password(plaintext.as_bytes(), &salt)
.map_err(|e| TenancyError::Validation(format!("argon2 hash failed: {e}")))?;
Ok(phc.to_string())
}
pub fn verify(plaintext: &str, phc_hash: &str) -> Result<bool, TenancyError> {
let parsed = PasswordHash::new(phc_hash)
.map_err(|e| TenancyError::Validation(format!("malformed password hash: {e}")))?;
Ok(Argon2::default()
.verify_password(plaintext.as_bytes(), &parsed)
.is_ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_and_verify_round_trip() {
let h = hash("hunter2").unwrap();
assert!(h.starts_with("$argon2id$"));
assert!(verify("hunter2", &h).unwrap());
assert!(!verify("wrong", &h).unwrap());
}
#[test]
fn hash_rejects_empty() {
let err = hash("").unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("must not be empty"), "got: {msg}");
}
#[test]
fn verify_rejects_malformed_hash() {
let err = verify("hunter2", "not-a-phc-string").unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("malformed"), "got: {msg}");
}
#[test]
fn two_hashes_of_same_password_differ() {
let h1 = hash("same").unwrap();
let h2 = hash("same").unwrap();
assert_ne!(h1, h2);
assert!(verify("same", &h1).unwrap());
assert!(verify("same", &h2).unwrap());
}
}