bcrypt: still acceptable, with caveats
bcrypt is a 25+ year-old password hash based on a modified Blowfish key schedule. Its defining feature is a cost parameter (logarithmic, typically 10 to 14) that doubles the work for each increment. A cost of 12 means 4,096 internal iterations of the expensive key setup.
Two quirks worth knowing:
- 72-byte input limit. bcrypt silently truncates anything longer. Pre-hash with SHA-256 if you support longer passphrases.
- Null-byte truncation in some implementations. Any null byte ends the password early. Modern libraries fix this; older PHP versions did not.
Tune the cost so a single hash takes 250 to 500 ms on your slowest production server. Re-tune when you move to faster hardware.
scrypt and Argon2id: memory hardness
Pure CPU-bound hashes lost a factor of 1,000+ to GPUs over the last decade. Memory-hard functions push the bottleneck into RAM, which is much more expensive to provision in parallel.
- scrypt (2009) takes parameters
N, r, p: iteration count, block size, parallelism. Memory cost is proportional toN * r. - Argon2id (2015, PHC winner) takes
t(time cost),m(memory in KiB), andp(parallelism). It is the current default for new systems.
OWASP's reference parameters as of writing: Argon2id with m=19 MiB, t=2, p=1 for
most servers; bump m to 47 MiB and t to 1 on hardware that can
handle it.
Salt and pepper
Salt is a per-user random value stored alongside the hash. It defeats precomputed rainbow tables and ensures two users with the same password get different hashes.
Pepper is a server-wide secret stored separately from the database (in an HSM, KMS, or config not committed to git). It is mixed into the input before hashing. If the database leaks but the pepper does not, the attacker cannot crack hashes offline.
Pepper is useful but not a substitute for proper hashing. Implementation: append pepper to password before passing to bcrypt/Argon2id, rotate the pepper occasionally, plan for graceful rotation (typically by storing a pepper ID alongside each hash).
Breach-list checks: the k-anonymity trick
Have I Been Pwned exposes an API that lets you check whether a password has appeared in a public breach without sending the password. The trick:
- SHA-1 the candidate password.
- Send the first 5 hex characters of the hash to the API.
- The API returns every breached hash that begins with those 5 characters (typically a few hundred).
- The client searches that list for the full hash locally.
The API learns only a 5-character prefix (about 1 in a million possible prefixes), which is not enough to identify the candidate. The client gets a yes/no answer with near-zero information leakage. Use this at signup and at password change.
Order of operations at signup
(1) validate length and basic shape (Unicode normalization, no length cap), (2) check against the breach list with k-anonymity, (3) pepper, (4) Argon2id, (5) store hash and salt. Same flow at password change, plus invalidate other sessions.
Credential stuffing defenses
Attackers feed leaked email/password pairs from one site into login forms on hundreds of others. Defenses, roughly in order of effectiveness:
- Phishing-resistant MFA: makes the password almost irrelevant once it leaks.
- Detect credential reuse: maintain your own breach list (or use commercial feeds); on every login, check the submitted password against it.
- Device-aware risk: log in from a known device with a known geolocation? frictionless. New device + new geo? challenge.
- Rate limiting per username and per IP: blunt but reduces unattended attack volume.
- Account lockouts with care: aggressive lockouts can themselves be abused for denial of service.
Account recovery is the new attack surface
Once MFA and breach checks are in place, the weakest link becomes account recovery. Common failure modes:
- Email-based reset is only as strong as the email account. If email has weaker MFA than the target, that is the attack path.
- Security questions are public records for most people. Treat them as another password the user must remember.
- Support-team override: social-engineering a human is often easier than breaking any technical control. Train and test.