use alloc::string::{String, ToString};
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::{Algorithm, Argon2, Params, Version};
use crate::error::{Error, Result};
pub const ARGON2_DEFAULT_OUTPUT_LEN: usize = 32;
pub const ARGON2_DEFAULT_SALT_LEN: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Argon2Params {
pub m_cost: u32,
pub t_cost: u32,
pub p_cost: u32,
pub output_len: usize,
}
impl Argon2Params {
#[must_use]
pub const fn new(m_cost: u32, t_cost: u32, p_cost: u32, output_len: usize) -> Self {
Self {
m_cost,
t_cost,
p_cost,
output_len,
}
}
}
impl Default for Argon2Params {
fn default() -> Self {
Self {
m_cost: 19 * 1024,
t_cost: 2,
p_cost: 1,
output_len: ARGON2_DEFAULT_OUTPUT_LEN,
}
}
}
pub fn argon2_hash(password: &[u8]) -> Result<String> {
argon2_hash_with_params(password, Argon2Params::default())
}
pub fn argon2_hash_with_params(password: &[u8], params: Argon2Params) -> Result<String> {
let mut salt_bytes = [0u8; ARGON2_DEFAULT_SALT_LEN];
mod_rand::tier3::fill_bytes(&mut salt_bytes)
.map_err(|_| Error::RandomFailure("mod_rand::tier3::fill_bytes"))?;
let salt =
SaltString::encode_b64(&salt_bytes).map_err(|_| Error::Kdf("argon2 salt encoding"))?;
let argon2_params = Params::new(
params.m_cost,
params.t_cost,
params.p_cost,
Some(params.output_len),
)
.map_err(|_| Error::Kdf("argon2 invalid params"))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
let hash = argon2
.hash_password(password, &salt)
.map_err(|_| Error::Kdf("argon2 hash"))?;
Ok(hash.to_string())
}
pub fn argon2_verify(phc: &str, password: &[u8]) -> Result<bool> {
let parsed = PasswordHash::new(phc).map_err(|_| Error::Kdf("argon2 phc parse"))?;
let argon2 = Argon2::default();
Ok(argon2.verify_password(password, &parsed).is_ok())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, unused_results)]
mod tests {
use super::*;
use alloc::format;
fn fast_params() -> Argon2Params {
Argon2Params {
m_cost: 8,
t_cost: 1,
p_cost: 1,
output_len: 32,
}
}
#[test]
fn hash_then_verify_round_trip() {
let phc = argon2_hash_with_params(b"hunter2", fast_params()).unwrap();
assert!(phc.starts_with("$argon2id$"));
assert!(argon2_verify(&phc, b"hunter2").unwrap());
}
#[test]
fn verify_rejects_wrong_password() {
let phc = argon2_hash_with_params(b"correct", fast_params()).unwrap();
assert!(!argon2_verify(&phc, b"wrong").unwrap());
}
#[test]
fn two_hashes_of_same_password_differ() {
let p = fast_params();
let a = argon2_hash_with_params(b"same", p).unwrap();
let b = argon2_hash_with_params(b"same", p).unwrap();
assert_ne!(a, b);
assert!(argon2_verify(&a, b"same").unwrap());
assert!(argon2_verify(&b, b"same").unwrap());
}
#[test]
fn verify_rejects_unparseable_phc() {
let err = argon2_verify("not-a-valid-phc-string", b"password").unwrap_err();
assert!(matches!(err, Error::Kdf(_)), "{err:?}");
}
#[test]
fn verify_rejects_tampered_phc() {
let phc = argon2_hash_with_params(b"hunter2", fast_params()).unwrap();
let mut chars: alloc::vec::Vec<char> = phc.chars().collect();
let last = chars.len() - 1;
chars[last] = if chars[last] == 'A' { 'B' } else { 'A' };
let tampered: String = chars.into_iter().collect();
assert!(!argon2_verify(&tampered, b"hunter2").unwrap());
}
#[test]
fn empty_password_round_trips() {
let phc = argon2_hash_with_params(b"", fast_params()).unwrap();
assert!(argon2_verify(&phc, b"").unwrap());
assert!(!argon2_verify(&phc, b"not-empty").unwrap());
}
#[test]
fn long_password_round_trips() {
let password = [b'x'; 1024];
let phc = argon2_hash_with_params(&password, fast_params()).unwrap();
assert!(argon2_verify(&phc, &password).unwrap());
}
#[test]
fn custom_params_are_honoured() {
let params = Argon2Params::new(16, 2, 1, 32);
let phc = argon2_hash_with_params(b"pw", params).unwrap();
assert!(phc.contains("m=16"));
assert!(phc.contains("t=2"));
assert!(phc.contains("p=1"));
}
#[test]
fn default_params_use_owasp_recommendations() {
let d = Argon2Params::default();
assert_eq!(d.m_cost, 19 * 1024);
assert_eq!(d.t_cost, 2);
assert_eq!(d.p_cost, 1);
assert_eq!(d.output_len, ARGON2_DEFAULT_OUTPUT_LEN);
}
#[test]
fn invalid_params_rejected() {
let bad = Argon2Params::new(0, 1, 1, 32);
let err = argon2_hash_with_params(b"pw", bad).unwrap_err();
assert!(matches!(err, Error::Kdf(_)), "{err:?}");
}
#[test]
fn error_messages_redact_password() {
let secret = "my-super-secret-password";
let err = argon2_verify("not-a-phc", secret.as_bytes()).unwrap_err();
let rendered = format!("{err}");
assert!(!rendered.contains(secret));
let rendered_dbg = format!("{err:?}");
assert!(!rendered_dbg.contains(secret));
}
}