Security for Legal SaaS

Episode 17 · Module 5 · Authentication & Identity

Password Hashing Done Right

18 May 2026 · 9:52 · Security for Legal SaaS

9:52 9:52

Encryption is reversible — hashing is not. Alice and Dan cover why “encrypted passwords” is the wrong answer, the modern algorithm hierarchy (Argon2id, bcrypt, scrypt), salts and rainbow table attacks, timing attacks and constant-time comparison, NIST 800-63B’s password policy revolution, credential stuffing defence with haveibeenpwned, and work factor calibration — all through the lens of legal SaaS where a breached password database has compounding consequences.

Today’s Lesson

Security for Legal SaaS — Episode 17: Password Hashing Done Right

Why “Encrypted Passwords” Is the Wrong Answer

When someone says their passwords are stored “encrypted,” alarm bells should ring. Encryption is reversible — if you can decrypt, so can an attacker who obtains your key. The 2012 LinkedIn breach exposed 117 million passwords hashed with unsalted SHA-1 — a hash function designed for speed, not resistance. Within days, the majority were cracked.

Key distinction: Hashing is a one-way function — you cannot reverse it to obtain the original password. Encryption is two-way — anyone with the key can recover the plaintext. Password storage requires hashing, never encryption. OWASP’s Password Storage Cheat Sheet is unambiguous on this point.

For legal SaaS platforms, a password database breach has compounding consequences. Lawyers reuse passwords across systems — a 2023 Bitwarden survey found 68% of internet users manage passwords for 10+ sites, with significant reuse. A cracked password from your platform becomes a credential stuffing weapon against court filing systems, client portals, and bar association accounts.

The Modern Password Hashing Algorithms

Argon2 — The Current Standard

Argon2 won the Password Hashing Competition in 2015, selected from 24 submissions by a panel of cryptographers. It comes in three variants:

Variant Optimised Against Use Case
Argon2idBoth GPU and side-channel attacksRecommended default
Argon2iSide-channel attacksWhen timing attacks are the primary threat
Argon2dGPU crackingWhen side-channels are not a concern

OWASP recommends Argon2id with these minimum parameters:

The key insight is memory-hardness. Unlike bcrypt or PBKDF2, Argon2 requires a configurable amount of RAM per hash computation. GPUs have limited per-core memory — this makes massively parallel cracking economically infeasible.

bcrypt — Battle-Tested but Aging

bcrypt (1999) introduced the concept of an adaptive cost factor. Each increment doubles the computation time. It’s been the workhorse of password hashing for 25 years.

Cost Factor Approximate Time (2024 hardware) Suitable For
10~100msDevelopment/testing
12~400msProduction minimum
14~1.6sHigh-security applications

Limitations: bcrypt truncates input at 72 bytes. Passwords longer than 72 characters are silently truncated — which means a 100-character passphrase provides no more security than its first 72 characters. It also cannot leverage more than a fixed amount of memory, making it less resistant to modern GPU attacks than Argon2.

scrypt — Memory-Hard Pioneer

scrypt (2009) introduced memory-hardness before Argon2. Designed by Colin Percival for the Tarsnap backup service. It’s still a solid choice, but Argon2id is preferred because scrypt’s memory-hardness parameter (N) is less granular and it lacks Argon2’s hybrid resistance to both GPU and side-channel attacks.

Salts — Why They’re Non-Negotiable

A salt is a random value prepended to the password before hashing, unique per user. Without salts:

Requirements:

Modern algorithms (bcrypt, scrypt, Argon2) generate and embed salts automatically. If you’re implementing salt management manually, you’re probably using the wrong library.

Timing Attacks and Constant-Time Comparison

When verifying a password, naive string comparison (==) leaks information through timing. If the comparison fails on the first byte, it returns faster than if it fails on the last byte. An attacker making thousands of requests can statistically determine the correct hash byte-by-byte.

The defence is constant-time comparison — functions that always take the same amount of time regardless of where the mismatch occurs. Every major framework provides this:

Implementation note: Even with constant-time comparison, the hashing step itself has variable timing based on the input. This is acceptable — the timing variation reveals nothing about the stored hash, only about the submitted password (which the attacker already knows).

Password Policies — Length Over Complexity

NIST Special Publication 800-63B (Digital Identity Guidelines) overturned decades of conventional wisdom in 2017:

Old Policy (Deprecated) NIST 800-63B Recommendation
Minimum 8 characters with uppercase, lowercase, number, symbolMinimum 8 characters (15+ recommended), no composition rules
Forced rotation every 90 daysNo periodic rotation unless compromise evidence
Security questions for recoveryProhibited (answers are guessable/social-engineerable)
Password hintsProhibited

Why? Composition rules produce predictable patterns (P@ssw0rd1!). Forced rotation produces incremental changes (Summer2024!Autumn2024!). Length provides exponential entropy growth — a 20-character passphrase of random words defeats any brute-force attack regardless of character composition.

NCSC (UK National Cyber Security Centre) guidance aligns: “Help users cope with password overload” — allow password managers, stop penalising length, stop requiring rotation.

Credential Stuffing Defence

Credential stuffing attacks use leaked username/password pairs from other breaches against your platform. The haveibeenpwned database contains over 12 billion compromised accounts.

Defence Layers

Layer Mechanism Effectiveness
Compromised password checkCheck against haveibeenpwned API (k-anonymity model) at registration and loginBlocks known-compromised passwords
Rate limitingMaximum 5 failed attempts per account per 15 minutesSlows automated attacks
IP reputationBlock/challenge requests from known botnet IPsReduces attack volume
Device fingerprintingChallenge logins from unrecognised devicesDetects credential stuffing from new origins
CAPTCHA on thresholdTrigger after 3 failuresBlocks automated tooling

Troy Hunt’s haveibeenpwned Pwned Passwords API uses k-anonymity — you send only the first 5 characters of the SHA-1 hash of the password, receive all matching suffixes, and check locally. The full password never leaves your server. NIST 800-63B specifically requires checking passwords against known-compromised lists.

Implementation Checklist for Legal SaaS

Production checklist:

  • Use Argon2id (preferred) or bcrypt with cost factor ≥12
  • Never implement your own hashing — use established libraries (argon2-cffi, bcrypt, passlib)
  • Check passwords against haveibeenpwned at registration and periodic login
  • Enforce minimum 12 characters, no maximum below 128, no composition rules
  • Allow and encourage paste (password managers rely on it)
  • No forced rotation without evidence of compromise
  • Constant-time comparison for all credential verification
  • Log authentication failures with IP/user-agent (but NEVER log the attempted password)
  • Rate limit failed attempts per account and per IP

The Dropbox breach (2012, disclosed 2016) exposed 68 million bcrypt hashes. Despite the breach, bcrypt’s work factor meant mass cracking was economically impractical. Proper hashing doesn’t prevent breaches — it ensures breached data is useless.

Work Factor Calibration

Your hashing work factor should be calibrated to your hardware, targeting 200–500ms per hash on your authentication servers. OWASP’s guidance: “Err on the side of longer computation time.”

Recalibrate annually as hardware improves. When you increase the work factor, rehash existing passwords on next successful login — verify against the old hash, then silently upgrade to the new parameters. The user never notices; your security improves continuously.

Conclusion

Password hashing is a solved problem — but only if you use the solution. Argon2id with appropriate memory and iteration parameters. Salts generated automatically. Constant-time verification. Length-based policies without composition rules. Compromised credential checking via haveibeenpwned. And the work factor recalibrated annually.

The cost of getting this right is one afternoon of implementation. The cost of getting it wrong is 117 million cracked passwords and a breach notification to every client your platform serves.

Sources & references

  1. Troy Hunt, “Observations and Thoughts on the LinkedIn Data Breach,” 2016. 117 million SHA-1 hashes exposed
  2. OWASP, “Password Storage Cheat Sheet.” Argon2id recommended with minimum parameters
  3. Bitwarden, “2023 Password Decisions Survey.” 68% of users manage 10+ site passwords
  4. Password Hashing Competition, “Argon2 — Winner,” 2015
  5. Provos & Mazieres, “A Future-Adaptable Password Scheme,” USENIX 1999. Original bcrypt paper
  6. Auth0, “Hashing in Action: Understanding bcrypt.” 72-byte input limit
  7. Colin Percival, “scrypt: A new key derivation function,” 2009
  8. Wikipedia, “RockYou Data Breach,” 2009. 32 million plaintext passwords
  9. Coda Hale, “A Lesson in Timing Attacks,” 2009
  10. NIST, SP 800-63B: Digital Identity Guidelines — Authentication and Lifecycle Management
  11. UK NCSC, “Updating Your Approach to Passwords”
  12. Troy Hunt, Have I Been Pwned. 12+ billion compromised accounts
  13. Troy Hunt, “Pwned Passwords API v3.” K-anonymity model
  14. Troy Hunt, “The Dropbox Hack Is Real,” 2016. 68 million bcrypt hashes