Skip to main content

ADR-0004: Argon2id password hashing with transparent upgrade from BCrypt

Status: Accepted (retroactive) Date: 2026-07-24

Context

The system originally hashed passwords with BCrypt, and real user rows with BCrypt hashes existed in every deployed database. Two problems:

  1. BCrypt is CPU-hard but memory-cheap, so GPU/ASIC cracking rigs scale against it; OWASP's current first choice is Argon2id.
  2. A hash-algorithm change must not require resetting anyone's password — forcing resets across per-client production stacks (ADR-0007) for a purely internal crypto upgrade would be user-hostile and operationally noisy.

A password hash can only be recomputed when the plaintext is available — i.e. at login. Any migration therefore has to be lazy.

Decision

Implemented in PasswordEncoderConfiguration:

  • Argon2id becomes the default encoder: new Argon2PasswordEncoder(16, 32, 1, 19456, 2) — 16-byte salt, 32-byte hash, parallelism 1, 19456 KiB (~19 MiB) memory, 2 iterations — the OWASP-recommended parameter set.
  • Wrapped in Spring Security's DelegatingPasswordEncoder with argon2 as the default id and BCrypt registered as a legacy delegate, so {bcrypt}... hashes still verify.
  • Transparent rehash on login: upgradeEncoding() reports true for BCrypt hashes, and the login flow re-encodes the just-verified plaintext as {argon2} and saves it. No forced resets, no batch migration, no user-visible change. (Documented in security/request-pipeline.md, "Password Security".)

Consequences

Positive

  • New and active users end up on a memory-hard hash with zero support burden — the fleet converges to Argon2id one login at a time.
  • The {id} prefix scheme makes any future algorithm migration (e.g. tuning Argon2 parameters) the same trivial pattern again.
  • No flag-day: old and new hashes coexist indefinitely and both verify.

Negative

  • Dormant accounts keep BCrypt hashes forever — the weakest hashes belong to exactly the accounts nobody is watching. A DB leak exposes those rows at BCrypt strength until (unless) the user logs in again.
  • ~19 MiB of memory per login verification. A burst of concurrent logins multiplies that; this is one reason login is rate-limited (ADR-0006) — the hash itself is a resource-exhaustion vector.
  • The saving write on login makes authentication a read-write operation, which is easy to forget when reasoning about "read-only" login load.

References