use argon2::password_hash::{PasswordHasher as _, PasswordVerifier as _};
use argon2::{Algorithm, Argon2, Params, Version};
use rand_core::CryptoRng;
use zeroize::Zeroize;
use crate::Error;
pub const MIN_LEN: usize = 12;
pub const MAX_LEN: usize = 1024;
const SALT_LEN: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PasswordParams {
memory_kib: u32,
iterations: u32,
parallelism: u32,
}
impl PasswordParams {
pub fn new(memory_kib: u32, iterations: u32, parallelism: u32) -> Result<Self, Error> {
let params = Self {
memory_kib,
iterations,
parallelism,
};
params.to_argon2()?;
Ok(params)
}
#[must_use]
pub const fn memory_kib(&self) -> u32 {
self.memory_kib
}
#[must_use]
pub const fn iterations(&self) -> u32 {
self.iterations
}
#[must_use]
pub const fn parallelism(&self) -> u32 {
self.parallelism
}
fn to_argon2(self) -> Result<Params, Error> {
Params::new(self.memory_kib, self.iterations, self.parallelism, None)
.map_err(|_| Error::Config("argon2 parameters"))
}
}
impl Default for PasswordParams {
fn default() -> Self {
Self {
memory_kib: 64 * 1024,
iterations: 3,
parallelism: 2,
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct PasswordHash(String);
impl PasswordHash {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(mut self) -> String {
std::mem::take(&mut self.0)
}
}
impl TryFrom<String> for PasswordHash {
type Error = Error;
fn try_from(phc: String) -> Result<Self, Error> {
argon2::PasswordHash::new(&phc)
.map_err(|_| Error::Crypto("stored password hash is not a PHC string"))?;
Ok(Self(phc))
}
}
impl TryFrom<&str> for PasswordHash {
type Error = Error;
fn try_from(phc: &str) -> Result<Self, Error> {
Self::try_from(phc.to_owned())
}
}
impl std::fmt::Debug for PasswordHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("PasswordHash(<redacted>)")
}
}
#[derive(Clone)]
pub struct PasswordHasher {
params: Params,
dummy: PasswordHash,
}
impl std::fmt::Debug for PasswordHasher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PasswordHasher")
.field("params", &self.params)
.finish_non_exhaustive()
}
}
impl PasswordHasher {
pub fn new<R: CryptoRng + ?Sized>(params: PasswordParams, rng: &mut R) -> Result<Self, Error> {
let params = params.to_argon2()?;
let mut filler = [0u8; 32];
rng.fill_bytes(&mut filler);
let dummy = hash_with(¶ms, &crate::hex(&filler), rng)?;
filler.zeroize();
Ok(Self { params, dummy })
}
pub fn hash<R: CryptoRng + ?Sized>(
&self,
password: &str,
rng: &mut R,
) -> Result<PasswordHash, Error> {
hash_with(&self.params, password, rng)
}
pub fn hash_new_password<R: CryptoRng + ?Sized>(
&self,
password: &str,
rng: &mut R,
) -> Result<PasswordHash, Error> {
check_policy(password)?;
self.hash(password, rng)
}
#[must_use]
pub fn verify(&self, password: &str, stored: Option<&PasswordHash>) -> bool {
let argon2 = argon2(self.params.clone());
let too_long = password.len() > MAX_LEN * 4;
let (candidate, real) = match stored {
Some(stored) if !too_long => (stored, true),
_ => (&self.dummy, false),
};
let password = if too_long { "" } else { password };
let matched = argon2
.verify_password(password.as_bytes(), candidate.as_str())
.is_ok();
matched && real
}
}
fn argon2(params: Params) -> Argon2<'static> {
Argon2::new(Algorithm::Argon2id, Version::V0x13, params)
}
fn hash_with<R: CryptoRng + ?Sized>(
params: &Params,
password: &str,
rng: &mut R,
) -> Result<PasswordHash, Error> {
let mut salt = [0u8; SALT_LEN];
rng.fill_bytes(&mut salt);
argon2(params.clone())
.hash_password_with_salt(password.as_bytes(), &salt)
.map(|hash| PasswordHash(hash.to_string()))
.map_err(|_| Error::Crypto("argon2 hashing failed"))
}
pub fn check_policy(password: &str) -> Result<(), Error> {
let len = password.chars().count();
if len < MIN_LEN {
return Err(Error::InvalidRequest("password_too_short"));
}
if len > MAX_LEN {
return Err(Error::InvalidRequest("password_too_long"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
pub(crate) fn cheap() -> PasswordParams {
PasswordParams {
memory_kib: 64,
iterations: 1,
parallelism: 1,
}
}
#[test]
fn hashes_verify_and_reject() -> Result<(), Error> {
let mut rng = crate::test_rng();
let hasher = PasswordHasher::new(cheap(), &mut rng)?;
let hash = hasher.hash("correct horse battery", &mut rng)?;
assert!(hash.as_str().starts_with("$argon2id$v=19$m=64,t=1,p=1$"));
assert!(hasher.verify("correct horse battery", Some(&hash)));
assert!(!hasher.verify("wrong horse battery", Some(&hash)));
Ok(())
}
#[test]
fn missing_hash_never_verifies() -> Result<(), Error> {
let mut rng = crate::test_rng();
let hasher = PasswordHasher::new(cheap(), &mut rng)?;
assert!(!hasher.verify("anything at all", None));
assert!(!hasher.verify("", None));
Ok(())
}
#[test]
fn oversized_passwords_never_verify() -> Result<(), Error> {
let mut rng = crate::test_rng();
let hasher = PasswordHasher::new(cheap(), &mut rng)?;
let long = "x".repeat(MAX_LEN * 4 + 1);
let hash = hasher.hash(&long, &mut rng)?;
assert!(!hasher.verify(&long, Some(&hash)));
Ok(())
}
#[test]
fn salts_differ_per_hash() -> Result<(), Error> {
let mut rng = crate::test_rng();
let hasher = PasswordHasher::new(cheap(), &mut rng)?;
let a = hasher.hash("correct horse battery", &mut rng)?;
let b = hasher.hash("correct horse battery", &mut rng)?;
assert_ne!(a, b);
Ok(())
}
#[test]
fn stored_hashes_parse_and_garbage_does_not() -> Result<(), Error> {
let mut rng = crate::test_rng();
let hasher = PasswordHasher::new(cheap(), &mut rng)?;
let hash = hasher.hash("correct horse battery", &mut rng)?;
let reloaded = PasswordHash::try_from(hash.as_str())?;
assert!(hasher.verify("correct horse battery", Some(&reloaded)));
assert!(PasswordHash::try_from("not a hash").is_err());
assert!(!format!("{hash:?}").contains("argon2id"));
Ok(())
}
#[test]
fn default_params_match_policy() {
let params = PasswordParams::default();
assert_eq!(
(
params.memory_kib(),
params.iterations(),
params.parallelism()
),
(65536, 3, 2)
);
assert!(PasswordParams::new(0, 0, 0).is_err());
}
#[test]
fn length_policy() {
assert!(check_policy("short").is_err());
assert!(check_policy("twelve chars").is_ok());
assert!(check_policy(&"x".repeat(1025)).is_err());
}
}