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:
@@ -28,6 +28,25 @@ pub fn verify_password(plain: &str, phc: &str) -> bool {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Async wrapper around [`hash_password`] that offloads the CPU- and
|
||||
/// memory-heavy Argon2 work to the blocking pool. Async handlers must call
|
||||
/// this rather than the sync primitive: at ~15-50 ms per hash, running it
|
||||
/// inline stalls every other task sharing the runtime worker thread.
|
||||
pub async fn hash_password_async(plain: String) -> AppResult<String> {
|
||||
tokio::task::spawn_blocking(move || hash_password(&plain))
|
||||
.await
|
||||
.map_err(|e| AppError::Other(anyhow::anyhow!("password hash task join: {e}")))?
|
||||
}
|
||||
|
||||
/// Async wrapper around [`verify_password`]. A join failure (blocking pool
|
||||
/// gone at shutdown, panic) resolves to `false` — the caller only ever uses
|
||||
/// the result as a boolean gate, and denying auth is the safe default.
|
||||
pub async fn verify_password_async(plain: String, phc: String) -> bool {
|
||||
tokio::task::spawn_blocking(move || verify_password(&plain, &phc))
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -56,4 +75,24 @@ mod tests {
|
||||
let b = hash_password("same").unwrap();
|
||||
assert_ne!(a, b, "two hashes of the same password must differ (salt)");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn async_hash_then_verify_roundtrip() {
|
||||
let phc = hash_password_async("correct horse battery staple".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(phc.starts_with("$argon2id$"));
|
||||
assert!(verify_password_async("correct horse battery staple".to_string(), phc).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn async_verify_rejects_wrong_password() {
|
||||
let phc = hash_password_async("hunter2".to_string()).await.unwrap();
|
||||
assert!(!verify_password_async("hunter3".to_string(), phc).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn async_verify_rejects_malformed_hash() {
|
||||
assert!(!verify_password_async("anything".to_string(), "not a real phc".to_string()).await);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user