← Back to all articles

Never Store Passwords Directly: Salting, Slow Hashing and Rainbow Tables

PasswordsHashing

Three problems with naive hashing

  • Too fast: SHA-256 computes hundreds of millions of times per second, so attackers brute-force at scale;
  • No salt → rainbow tables: identical passwords produce identical digests, so one precomputed table recovers many at once;
  • Correlated leaks: two users with the same password are visibly identical in the database.

What salting fixes

A salt is a per-user random value concatenated with the password before hashing and stored alongside the digest. It defeats rainbow tables (each salt needs its own precomputation) and makes identical passwords hash differently. Note: a salt stops batch precomputation, not brute force against one account — that is the next layer.

What slow hashing fixes

bcrypt, scrypt and Argon2 use a tunable work factor to deliberately slow computation, cutting attempts per second from billions to thousands. Login stays imperceptible while attacker cost rises by orders of magnitude.

AlgorithmSuitable for passwordsNotes
MD5 / SHA-1NeverCollision-broken and far too fast
SHA-256 (raw)NoUse for integrity checks, not passwords
bcryptRecommendedSalt built in, adjustable cost
Argon2idRecommended (first choice)GPU/ASIC resistant; memory and time tunable
PBKDF2AcceptableCommon for compliance; weaker against GPUs than Argon2

Choosing parameters

  1. Aim for 50–200ms per verification — the practical ceiling for a server;
  2. Start bcrypt at cost 10 and raise it as hardware improves;
  3. Typical Argon2id starting point: 19–64 MB memory, 2–4 iterations, parallelism 1–2, then tune to the timing target;
  4. Store the parameters with the digest so you can upgrade smoothly later.

Common questions

Must the salt be secret? No — it only needs to be unique and random, not confidential. Can I use one fixed salt? That is equivalent to no salt. Can I encrypt passwords instead? No — a leaked key decrypts everything at once; one-way hashing is the point.

Migrating a legacy system

  1. Do not overwrite the password column: add algorithm, salt and hash columns instead;
  2. Verify on both paths at login: legacy users still check against the old algorithm; on success, rehash with the new one and store it;
  3. Migrate lazily: dormant accounts migrate at their next login, or reset after a deadline;
  4. Clean up: drop the old hash once migration completes, and log the migration ratio so you can verify it.

Other things to get right

  • Rate limiting and progressive lockout: throttle attempts per account and per IP to stop online brute force;
  • Never log plaintext passwords: filter them from logs, analytics and error reports;
  • Short-lived, single-use reset links: 15–30 minutes, invalidated immediately after use;
  • Do not hand-roll parameters: use mature libraries with recommended settings and raise the work factor as guidance evolves.

Real-world cases: three postmortems

  1. Plaintext / reversible storage: a dump leaks every password outright — the worst case; attackers log in directly.
  2. Unsalted SHA-256: identical passwords share a digest, so rainbow tables recover common ones in bulk.
  3. Salted but single-round: the salt defeats rainbow tables but not billions of guesses per second — still "seconds to crack" on a GPU.

FAQ

How do I size bcrypt / Argon2? Target roughly 100–250ms per verification and tune cost/rounds and memory accordingly, rather than copying a fixed number. Can I SHA-256 then bcrypt? Modern implementations allow it (bcrypt has a 72-byte limit), but Argon2 directly is simpler for most cases. Do I store the salt separately? No — bcrypt/Argon2 hashes embed the salt, so store the whole string. How do I migrate algorithms? Rehash with the new algorithm on the next successful login and overwrite.

Try them: hashing, password generator

Parameter comparison

  • bcrypt: the classic; each +1 in cost factor doubles the time, typically 10–12, and input is capped at 72 bytes so long passphrases need pre-hashing;
  • scrypt: memory plus CPU cost, stronger against GPUs, tuned by N, r and p;
  • Argon2: the current recommendation — time, memory and parallelism tuned separately, with Argon2id balancing side-channel and GPU resistance;
  • PBKDF2: best standards compatibility (common in FIPS contexts) but weaker GPU resistance than scrypt or Argon2.