use super::HashedValue;
use crate::Result;
use crate::hashing::errors::{HashingError, HashingOperation};
use crate::hashing::hashing_service::HashingService;
use argon2::password_hash::{PasswordHasher, SaltString, rand_core::OsRng};
use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordVerifier, Version};
use webgates_core::verification_result::VerificationResult;
#[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,
}
}
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,
}
impl Argon2Preset {
pub fn to_config(self) -> Argon2Config {
match self {
Self::HighSecurity => Argon2Config::high_security(),
Self::Interactive => Argon2Config::interactive(),
}
}
}
#[derive(Clone)]
pub struct Argon2Hasher {
config: Argon2Config,
engine: Argon2<'static>,
}
impl Argon2Hasher {
pub fn new_recommended() -> Result<Self> {
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)
}
}
impl HashingService for Argon2Hasher {
fn hash_value(&self, plain_value: &str) -> Result<HashedValue, HashingError> {
let salt = SaltString::generate(&mut OsRng);
Ok(self
.engine
.hash_password(plain_value.as_bytes(), &salt)
.map_err(|e| {
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, HashingError> {
let hash = PasswordHash::new(hashed_value).map_err(|e| {
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::hashing_service::HashingService;
#[test]
fn recommended_hasher_verifies_matching_secret() {
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_verify_matching_and_non_matching_secrets() {
for preset in [Argon2Preset::HighSecurity, Argon2Preset::Interactive] {
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_verifies_matching_secret() {
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)
));
}
}