use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::{Algorithm, Argon2, Params, Version};
use rand_core::OsRng;
use webx_core::error::{Error, Result};
use webx_core::security::IPasswordHasher;
const ARGON2ID_IDENT: &str = "argon2id";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Argon2Params {
pub memory_kib: u32,
pub iterations: u32,
pub parallelism: u32,
}
impl Argon2Params {
pub const fn new(memory_kib: u32, iterations: u32, parallelism: u32) -> Self {
Self {
memory_kib,
iterations,
parallelism,
}
}
pub const fn owasp_default() -> Self {
Self::new(19 * 1024, 2, 1)
}
fn to_argon2_params(self) -> Result<Params> {
Params::new(self.memory_kib, self.iterations, self.parallelism, None).map_err(|error| {
Error::Internal(format!(
"invalid Argon2 parameters (m={}, t={}, p={}): {error}",
self.memory_kib, self.iterations, self.parallelism
))
})
}
}
impl Default for Argon2Params {
fn default() -> Self {
Self::owasp_default()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Argon2PasswordHasher {
params: Argon2Params,
}
impl Argon2PasswordHasher {
pub fn new() -> Self {
Self::default()
}
pub fn with_params(params: Argon2Params) -> Self {
Self { params }
}
pub fn params(&self) -> Argon2Params {
self.params
}
}
fn verify_blocking(password: &str, stored_hash: &str) -> Result<bool> {
let parsed = PasswordHash::new(stored_hash)
.map_err(|error| Error::Internal(format!("stored password hash is malformed: {error}")))?;
if parsed.salt.is_none() || parsed.hash.is_none() {
return Err(Error::Internal(
"stored password hash is malformed: missing salt or hash output".to_string(),
));
}
Params::try_from(&parsed).map_err(|error| {
Error::Internal(format!(
"stored password hash parameters are invalid: {error}"
))
})?;
let algorithm = match parsed.algorithm.as_str() {
ARGON2ID_IDENT => Algorithm::Argon2id,
"argon2i" => Algorithm::Argon2i,
"argon2d" => Algorithm::Argon2d,
other => {
return Err(Error::Internal(format!(
"stored password hash uses an unsupported algorithm '{other}'"
)))
}
};
let argon2 = Argon2::new(algorithm, Version::V0x13, Params::DEFAULT);
match argon2.verify_password(password.as_bytes(), &parsed) {
Ok(()) => Ok(true),
Err(argon2::password_hash::Error::Password) => Ok(false),
Err(error) => Err(Error::Internal(format!(
"password verification failed: {error}"
))),
}
}
#[async_trait::async_trait]
impl IPasswordHasher for Argon2PasswordHasher {
async fn hash(&self, password: &str) -> Result<String> {
let params = self.params;
let password = password.to_owned();
params.to_argon2_params()?;
tokio::task::spawn_blocking(move || {
let argon2 = Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
params.to_argon2_params()?,
);
let salt = SaltString::generate(&mut OsRng);
argon2
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|error| Error::Internal(format!("password hashing failed: {error}")))
})
.await
.map_err(|error| Error::Internal(format!("password hashing task failed: {error}")))?
}
async fn verify(&self, password: &str, stored_hash: &str) -> Result<bool> {
let password = password.to_owned();
let stored_hash = stored_hash.to_owned();
tokio::task::spawn_blocking(move || verify_blocking(&password, &stored_hash))
.await
.map_err(|error| {
Error::Internal(format!("password verification task failed: {error}"))
})?
}
fn needs_rehash(&self, stored_hash: &str) -> bool {
let Ok(parsed) = PasswordHash::new(stored_hash) else {
return true;
};
if parsed.salt.is_none()
|| parsed.hash.is_none()
|| parsed.algorithm.as_str() != ARGON2ID_IDENT
|| parsed.version != Some(0x13)
{
return true;
}
let Ok(stored) = Params::try_from(&parsed) else {
return true;
};
stored.m_cost() != self.params.memory_kib
|| stored.t_cost() != self.params.iterations
|| stored.p_cost() != self.params.parallelism
}
}