fix: run Argon2 password hashing on the blocking pool
Argon2id hashing (~15-50ms, 19 MiB) ran synchronously inside async handlers, stalling every task sharing those runtime worker threads under concurrent auth load. Add spawn_blocking wrappers hash_password_async / verify_password_async and switch all async call sites; sync primitives stay for the login timing-equaliser and unit tests. Bump to 0.124.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ use crate::api::auth::{validate_password, validate_username};
|
||||
use crate::api::pagination::PagedResponse;
|
||||
use crate::app::AppState;
|
||||
use crate::auth::extractor::RequireAdmin;
|
||||
use crate::auth::password::hash_password;
|
||||
use crate::auth::password::hash_password_async;
|
||||
use crate::domain::User;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::repo;
|
||||
@@ -115,7 +115,7 @@ async fn create_user(
|
||||
// reject (and vice versa).
|
||||
validate_username(username)?;
|
||||
validate_password(&input.password)?;
|
||||
let pwhash = hash_password(&input.password)?;
|
||||
let pwhash = hash_password_async(input.password.clone()).await?;
|
||||
let user = repo::user::admin_create_user(
|
||||
&state.db,
|
||||
actor.id,
|
||||
|
||||
@@ -18,7 +18,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::app::AppState;
|
||||
use crate::auth::extractor::{CurrentUser, SESSION_COOKIE_NAME};
|
||||
use crate::auth::password::{hash_password, verify_password};
|
||||
use crate::auth::password::{hash_password_async, verify_password_async};
|
||||
use crate::auth::token::{generate_token, hash_token};
|
||||
use crate::config::AuthConfig;
|
||||
use crate::domain::user_preferences::{READER_GAPS, READER_MODES};
|
||||
@@ -125,7 +125,7 @@ async fn register(
|
||||
validate_username(username)?;
|
||||
validate_password(&input.password)?;
|
||||
|
||||
let pwhash = hash_password(&input.password)?;
|
||||
let pwhash = hash_password_async(input.password.clone()).await?;
|
||||
let user = repo::user::create(&state.db, username, &pwhash).await?;
|
||||
let jar = start_session(&state, &user, jar).await?;
|
||||
Ok((StatusCode::CREATED, jar, Json(AuthResponse { user })))
|
||||
@@ -154,10 +154,11 @@ async fn login(
|
||||
// response time matches the wrong-password branch — otherwise
|
||||
// an attacker can enumerate usernames by timing the no-user
|
||||
// 401 against the wrong-password 401.
|
||||
let _ = verify_password(&input.password, dummy_password_hash());
|
||||
let _ = verify_password_async(input.password.clone(), dummy_password_hash().to_string())
|
||||
.await;
|
||||
return Err(AppError::Unauthenticated);
|
||||
};
|
||||
if !verify_password(&input.password, &user.password_hash) {
|
||||
if !verify_password_async(input.password.clone(), user.password_hash.clone()).await {
|
||||
return Err(AppError::Unauthenticated);
|
||||
}
|
||||
|
||||
@@ -220,12 +221,12 @@ async fn change_password(
|
||||
// Cap current_password before verify_password runs argon2 (same DoS
|
||||
// vector as login). new_password is bounded by validate_password below.
|
||||
reject_oversized_password(&input.current_password)?;
|
||||
if !verify_password(&input.current_password, &user.password_hash) {
|
||||
if !verify_password_async(input.current_password.clone(), user.password_hash.clone()).await {
|
||||
return Err(AppError::Unauthenticated);
|
||||
}
|
||||
validate_password(&input.new_password)?;
|
||||
|
||||
let new_hash = hash_password(&input.new_password)?;
|
||||
let new_hash = hash_password_async(input.new_password.clone()).await?;
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2")
|
||||
|
||||
Reference in New Issue
Block a user