use crate::randombytes::{randombytes_buf, RandomError};
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::{Algorithm, Argon2, Params, Version};
use core::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Strength {
Interactive,
Moderate,
Sensitive,
}
impl Strength {
const fn m_and_t_cost(self) -> (u32, u32) {
match self {
Strength::Interactive => (65536, 2), Strength::Moderate => (262_144, 3), Strength::Sensitive => (1_048_576, 4), }
}
const fn params(self) -> Params {
let (m_cost, t_cost) = self.m_and_t_cost();
match Params::new(m_cost, t_cost, 1, None) {
Ok(p) => p,
Err(_) => panic!("Strength's own hardcoded (m_cost, t_cost, 1) is always valid"),
}
}
}
#[derive(Debug)]
pub enum PwHashError {
Random(RandomError),
Hash(argon2::password_hash::Error),
}
impl fmt::Display for PwHashError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PwHashError::Random(e) => write!(f, "{e}"),
PwHashError::Hash(e) => write!(f, "Argon2 hashing failed: {e}"),
}
}
}
impl core::error::Error for PwHashError {}
impl From<RandomError> for PwHashError {
fn from(e: RandomError) -> Self {
PwHashError::Random(e)
}
}
impl From<argon2::password_hash::Error> for PwHashError {
fn from(e: argon2::password_hash::Error) -> Self {
PwHashError::Hash(e)
}
}
pub fn hash_password(password: &[u8], strength: Strength) -> Result<String, PwHashError> {
let mut salt_bytes = [0u8; 16];
randombytes_buf(&mut salt_bytes)?;
let salt = SaltString::encode_b64(&salt_bytes)?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, strength.params());
let hash = argon2.hash_password(password, &salt)?;
Ok(hash.to_string())
}
#[must_use]
pub fn verify_password(password: &[u8], hash: &str) -> bool {
let Ok(parsed) = PasswordHash::new(hash) else {
return false;
};
Argon2::default().verify_password(password, &parsed).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use argon2::AssociatedData;
#[test]
fn argon2_dependency_matches_rfc9106_argon2id_vector() -> Result<(), argon2::Error> {
let password = [0x01u8; 32];
let salt = [0x02u8; 16];
let secret = [0x03u8; 8];
let associated_data = AssociatedData::new(&[0x04u8; 12])?;
let mut builder = argon2::ParamsBuilder::new();
builder
.m_cost(32)
.t_cost(3)
.p_cost(4)
.data(associated_data)
.output_len(32);
let params = builder.build()?;
let argon2 = Argon2::new_with_secret(&secret, Algorithm::Argon2id, Version::V0x13, params)?;
let mut out = [0u8; 32];
argon2.hash_password_into(&password, &salt, &mut out)?;
assert_eq!(
out,
[
0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, 0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b,
0x53, 0xc9, 0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e, 0xb5, 0x25, 0x20, 0xe9,
0x6b, 0x01, 0xe6, 0x59,
]
);
Ok(())
}
#[test]
fn sensitive_preset_has_libsodiums_sensitive_params() {
let params = Strength::Sensitive.params();
assert_eq!(params.m_cost(), 1_048_576);
assert_eq!(params.t_cost(), 4);
assert_eq!(params.p_cost(), 1);
}
}