perf: findings 6.1, 6.2, 2.6 — async Argon2, moka cache, full streaming migration
- 6.1: PasswordHasherPort now async_trait with spawn_blocking for Argon2
- 6.2: OIDC pending maps migrated from std::sync::Mutex to moka::sync::Cache with TTL
- 2.6: All file download paths migrated to 64KB streaming (get_file_stream / read_blob_stream)
- WOPI, dedup, batch ZIP, file_retrieval_service consumers migrated
- WebDAV COPY uses zero-copy dedup (copy_file)
- Removed dead code: get_file_content, get_file_mmap, read_blob, read_blob_bytes
from traits, impls, stubs, and mocks (18 files touched)
This commit is contained in:
@@ -2,9 +2,14 @@
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use async_trait::async_trait;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use crate::application::ports::auth_ports::PasswordHasherPort;
|
||||
@@ -14,9 +19,11 @@ use crate::common::errors::{DomainError, ErrorKind};
|
||||
///
|
||||
/// 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.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Argon2PasswordHasher {
|
||||
/// Argon2 hasher instance - uses default secure parameters
|
||||
_private: (),
|
||||
}
|
||||
|
||||
@@ -33,35 +40,57 @@ impl Default for Argon2PasswordHasher {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PasswordHasherPort for Argon2PasswordHasher {
|
||||
fn hash_password(&self, password: &str) -> Result<String, DomainError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
argon2
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map(|hash| hash.to_string())
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"PasswordHasher",
|
||||
format!("Error generating password hash: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError> {
|
||||
let parsed_hash = PasswordHash::new(hash).map_err(|e| {
|
||||
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!("Error processing password hash: {}", e),
|
||||
format!("Task join error: {}", e),
|
||||
)
|
||||
})?;
|
||||
})?
|
||||
}
|
||||
|
||||
Ok(Argon2::default()
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok())
|
||||
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| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"PasswordHasher",
|
||||
format!("Error processing password hash: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Argon2::default()
|
||||
.verify_password(pwd.as_bytes(), &parsed_hash)
|
||||
.is_ok())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"PasswordHasher",
|
||||
format!("Task join error: {}", e),
|
||||
)
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,33 +98,36 @@ impl PasswordHasherPort for Argon2PasswordHasher {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hash_and_verify_password() {
|
||||
#[tokio::test]
|
||||
async fn test_hash_and_verify_password() {
|
||||
let hasher = Argon2PasswordHasher::new();
|
||||
let password = "test_password_123";
|
||||
|
||||
let hash = hasher
|
||||
.hash_password(password)
|
||||
.await
|
||||
.expect("Should hash password");
|
||||
assert!(
|
||||
hasher
|
||||
.verify_password(password, &hash)
|
||||
.await
|
||||
.expect("Should verify")
|
||||
);
|
||||
assert!(
|
||||
!hasher
|
||||
.verify_password("wrong_password", &hash)
|
||||
.await
|
||||
.expect("Should verify")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_hashes_for_same_password() {
|
||||
#[tokio::test]
|
||||
async fn test_different_hashes_for_same_password() {
|
||||
let hasher = Argon2PasswordHasher::new();
|
||||
let password = "same_password";
|
||||
|
||||
let hash1 = hasher.hash_password(password).expect("Should hash");
|
||||
let hash2 = hasher.hash_password(password).expect("Should hash");
|
||||
let hash1 = hasher.hash_password(password).await.expect("Should hash");
|
||||
let hash2 = hasher.hash_password(password).await.expect("Should hash");
|
||||
|
||||
// Hashes should be different due to random salt
|
||||
assert_ne!(hash1, hash2);
|
||||
@@ -104,11 +136,13 @@ mod tests {
|
||||
assert!(
|
||||
hasher
|
||||
.verify_password(password, &hash1)
|
||||
.await
|
||||
.expect("Should verify")
|
||||
);
|
||||
assert!(
|
||||
hasher
|
||||
.verify_password(password, &hash2)
|
||||
.await
|
||||
.expect("Should verify")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user