Files
Oxicloud/src/infrastructure/services/password_hasher.rs
T

150 lines
4.6 KiB
Rust
Raw Normal View History

2026-02-14 01:29:34 +01:00
//! Argon2-based password hasher implementation.
//!
//! This module provides a secure password hashing implementation using the Argon2id
//! algorithm, which is the recommended choice for password hashing as of 2023+.
//!
//! Both `hash_password` and `verify_password` are CPU-intensive (~300-500 ms with
//! default parameters) so they run inside `spawn_blocking` to avoid blocking Tokio
//! worker threads.
2026-02-14 01:29:34 +01:00
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use async_trait::async_trait;
2026-02-14 01:29:34 +01:00
use rand_core::OsRng;
use crate::application::ports::auth_ports::PasswordHasherPort;
use crate::common::errors::{DomainError, ErrorKind};
/// Argon2-based implementation of the PasswordHasherPort.
///
/// Uses Argon2id algorithm which provides resistance against both side-channel
/// and GPU-based attacks. This is the recommended algorithm for password hashing.
///
/// The struct is stateless — `Argon2::default()` is constructed per call inside
/// `spawn_blocking` so it is `Send` without extra synchronisation.
2026-02-14 01:29:34 +01:00
#[derive(Debug, Clone)]
pub struct Argon2PasswordHasher {
_private: (),
}
impl Argon2PasswordHasher {
/// Create a new Argon2PasswordHasher with default secure parameters.
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for Argon2PasswordHasher {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
2026-02-14 01:29:34 +01:00
impl PasswordHasherPort for Argon2PasswordHasher {
async fn hash_password(&self, password: &str) -> Result<String, DomainError> {
let pwd = password.to_owned();
tokio::task::spawn_blocking(move || {
let salt = SaltString::generate(&mut OsRng);
Argon2::default()
.hash_password(pwd.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"PasswordHasher",
format!("Error generating password hash: {}", e),
)
})
})
.await
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"PasswordHasher",
format!("Task join error: {}", e),
)
})?
}
2026-02-14 01:29:34 +01:00
async fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError> {
let pwd = password.to_owned();
let hash = hash.to_owned();
tokio::task::spawn_blocking(move || {
let parsed_hash = PasswordHash::new(&hash).map_err(|e| {
2026-02-14 01:29:34 +01:00
DomainError::new(
ErrorKind::InternalError,
"PasswordHasher",
format!("Error processing password hash: {}", e),
2026-02-14 01:29:34 +01:00
)
})?;
Ok(Argon2::default()
.verify_password(pwd.as_bytes(), &parsed_hash)
.is_ok())
})
.await
.map_err(|e| {
2026-02-14 01:29:34 +01:00
DomainError::new(
ErrorKind::InternalError,
"PasswordHasher",
format!("Task join error: {}", e),
2026-02-14 01:29:34 +01:00
)
})?
2026-02-14 01:29:34 +01:00
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_hash_and_verify_password() {
2026-02-14 01:29:34 +01:00
let hasher = Argon2PasswordHasher::new();
let password = "test_password_123";
let hash = hasher
.hash_password(password)
.await
2026-02-14 01:29:34 +01:00
.expect("Should hash password");
assert!(
hasher
.verify_password(password, &hash)
.await
2026-02-14 01:29:34 +01:00
.expect("Should verify")
);
assert!(
!hasher
.verify_password("wrong_password", &hash)
.await
2026-02-14 01:29:34 +01:00
.expect("Should verify")
);
}
#[tokio::test]
async fn test_different_hashes_for_same_password() {
2026-02-14 01:29:34 +01:00
let hasher = Argon2PasswordHasher::new();
let password = "same_password";
let hash1 = hasher.hash_password(password).await.expect("Should hash");
let hash2 = hasher.hash_password(password).await.expect("Should hash");
2026-02-14 01:29:34 +01:00
// Hashes should be different due to random salt
assert_ne!(hash1, hash2);
// But both should verify correctly
assert!(
hasher
.verify_password(password, &hash1)
.await
2026-02-14 01:29:34 +01:00
.expect("Should verify")
);
assert!(
hasher
.verify_password(password, &hash2)
.await
2026-02-14 01:29:34 +01:00
.expect("Should verify")
);
}
}