fix(manager-core): F-P-002 move Argon2id verify off the Tokio async worker

verify_password (Argon2id, OWASP defaults m=19456 KiB, t=2) is CPU-bound
at tens-to-hundreds of ms and was invoked synchronously on the Tokio
worker. Worse, verify_api_key Argon2-verifies every candidate sharing
the 8-char prefix — a hot user with N keys serialized every admin
request behind N×Argon2.

Wrap each call site in tokio::task::spawn_blocking:
- auth_middleware::verify_api_key (per request carrying a Bearer key)
- auth_api::login (admin login)
- users_service::login (data-plane app-user login, both real-hash and
  TIMING_FLAT_DUMMY_HASH branches)

Cold-cache login is now ~2× current latency due to one spawn_blocking
hop, but the worker no longer parks on Argon2 so steady-state under
load is dramatically better. The LRU cache for the hot-path (token →
principal) is finding F-P-009 — separate commit.

AUDIT.md anchor: F-P-002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 20:10:54 +02:00
parent 5303419eec
commit 1cd8791bff
3 changed files with 35 additions and 6 deletions

View File

@@ -590,10 +590,15 @@ impl UsersService for UsersServiceImpl {
.await?;
// Both branches normalize the email so an attacker can't probe
// existence via "" / "x" / very-long-string differences.
let password_owned = password.to_string();
let Ok(normalized) = validate_email(email) else {
// Still run a dummy verify so the timing of an invalid
// email shape matches the timing of a legitimate miss.
let _ = verify_password(TIMING_FLAT_DUMMY_HASH, password);
// F-P-002: off the async worker — Argon2 is CPU-bound.
let _ = tokio::task::spawn_blocking(move || {
verify_password(TIMING_FLAT_DUMMY_HASH, &password_owned)
})
.await;
return Ok(None);
};
let creds = self
@@ -604,7 +609,10 @@ impl UsersService for UsersServiceImpl {
Some(c) => (c.password_hash, Some(c.id), Some(c.app_id)),
None => (TIMING_FLAT_DUMMY_HASH.to_string(), None, None),
};
let password_ok = verify_password(&hash, password);
// F-P-002: Argon2id verify off the async worker.
let password_ok = tokio::task::spawn_blocking(move || verify_password(&hash, &password_owned))
.await
.map_err(|e| UsersError::Backend(format!("verify spawn_blocking join: {e}")))?;
let (Some(user_id), Some(app_id)) = (user_id, app_id) else {
return Ok(None);
};