use super::HashedValue;
use crate::errors::{Error, Result};
use crate::hashing::HashingService;
use crate::hashing::{HashingError, HashingOperation};
use crate::verification_result::VerificationResult;
use argon2::password_hash::{PasswordHasher, SaltString, rand_core::OsRng};
use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordVerifier, Version};
#[derive(Debug, Clone, Copy)]
pub struct Argon2Config {
pub memory_kib: u32,
pub time_cost: u32,
pub parallelism: u32,
}
impl Argon2Config {
pub fn high_security() -> Self {
Self {
memory_kib: 64 * 1024, time_cost: 3,
parallelism: 1,
}
}
pub fn interactive() -> Self {
Self {
memory_kib: 32 * 1024,
time_cost: 2,
parallelism: 1,
}
}
#[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
pub fn dev_fast() -> Self {
Self {
memory_kib: 4 * 1024,
time_cost: 1,
parallelism: 1,
}
}
pub fn with_memory_kib(mut self, v: u32) -> Self {
self.memory_kib = v;
self
}
pub fn with_time_cost(mut self, v: u32) -> Self {
self.time_cost = v;
self
}
pub fn with_parallelism(mut self, v: u32) -> Self {
self.parallelism = v;
self
}
}
impl Default for Argon2Config {
fn default() -> Self {
Argon2Config::high_security()
}
}
#[derive(Debug, Clone, Copy)]
pub enum Argon2Preset {
HighSecurity,
Interactive,
#[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
DevFast,
}
impl Argon2Preset {
pub fn to_config(self) -> Argon2Config {
match self {
Self::HighSecurity => Argon2Config::high_security(),
Self::Interactive => Argon2Config::interactive(),
#[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
Self::DevFast => Argon2Config::dev_fast(),
}
}
}
#[derive(Clone)]
pub struct Argon2Hasher {
config: Argon2Config,
engine: Argon2<'static>,
}
impl Argon2Hasher {
pub fn new_recommended() -> Result<Self> {
if cfg!(debug_assertions) {
#[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
{
return Self::dev_fast();
}
}
Self::high_security()
}
pub fn from_config(config: Argon2Config) -> Result<Self> {
let params = Params::new(
config.memory_kib,
config.time_cost,
config.parallelism,
None,
)?;
let engine = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
Ok(Self { config, engine })
}
pub fn from_preset(preset: Argon2Preset) -> Result<Self> {
Self::from_config(preset.to_config())
}
pub fn config(&self) -> &Argon2Config {
&self.config
}
pub fn high_security() -> Result<Self> {
Self::from_preset(Argon2Preset::HighSecurity)
}
pub fn interactive() -> Result<Self> {
Self::from_preset(Argon2Preset::Interactive)
}
#[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
pub fn dev_fast() -> Result<Self> {
Self::from_preset(Argon2Preset::DevFast)
}
}
impl HashingService for Argon2Hasher {
fn hash_value(&self, plain_value: &str) -> Result<HashedValue> {
let salt = SaltString::generate(&mut OsRng);
Ok(self
.engine
.hash_password(plain_value.as_bytes(), &salt)
.map_err(|e| {
Error::Hashing(HashingError::with_context(
HashingOperation::Hash,
format!("Could not hash secret: {e}"),
Some("Argon2id".to_string()),
Some("PHC".to_string()),
))
})?
.to_string())
}
fn verify_value(&self, plain_value: &str, hashed_value: &str) -> Result<VerificationResult> {
let hash = PasswordHash::new(hashed_value).map_err(|e| {
Error::Hashing(HashingError::with_context(
HashingOperation::Verify,
format!("Could not parse stored hash: {e}"),
Some("Argon2id".to_string()),
Some("PHC".to_string()),
))
})?;
Ok(VerificationResult::from(
self.engine
.verify_password(plain_value.as_bytes(), &hash)
.is_ok(),
))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::hashing::HashingService;
#[test]
fn default_build_mode() {
let hasher = Argon2Hasher::new_recommended().unwrap();
let hash = hasher.hash_value("pw").unwrap();
assert!(matches!(
hasher.verify_value("pw", &hash),
Ok(VerificationResult::Ok)
));
}
#[test]
fn presets_work() {
for preset in [
Argon2Preset::HighSecurity,
Argon2Preset::Interactive,
#[cfg(any(feature = "insecure-fast-hash", debug_assertions))]
Argon2Preset::DevFast,
] {
let hasher = Argon2Hasher::from_preset(preset).unwrap();
let h = hasher.hash_value("secret").unwrap();
assert_eq!(
VerificationResult::Ok,
hasher.verify_value("secret", &h).unwrap()
);
assert_eq!(
VerificationResult::Unauthorized,
hasher.verify_value("other", &h).unwrap()
);
}
}
#[test]
fn custom_config() {
let cfg = Argon2Config::default()
.with_memory_kib(48 * 1024)
.with_time_cost(2)
.with_parallelism(1);
let hasher = Argon2Hasher::from_config(cfg).unwrap();
let h = hasher.hash_value("abc").unwrap();
assert!(matches!(
hasher.verify_value("abc", &h),
Ok(VerificationResult::Ok)
));
}
}