Hotshard
Open the simulatorSimulator
How a password store keeps secrets safe

Hashing Passwords

Never store a password. Store a salted, deliberately slow hash of it, tuned so every attacker guess is expensive and no two users ever share a digest.

Every login page has the same problem behind it: the server needs to check the password a user types, but it must never keep the password itself. Databases leak. Backups get copied. An engineer runs the wrong query. If the stored form of a password is the password, one leak hands the attacker every account at once, and because people reuse passwords, it hands them accounts on other sites too. This page builds the small pipeline that solves it. It has four moving parts that each answer a specific attack: a one-way hash so the stored value cannot be turned back into the password, a unique per-user salt so identical passwords do not collide and precomputed tables are worthless, a tunable work factor so each guess costs the attacker real time, and a constant-time comparison so the act of checking a password leaks nothing. The simulator on this page lets you store a password, turn the work-factor dial, and run a dictionary attack against a fast unsalted hash and a slow salted one side by side, so you can watch where the time goes.

Open the simulator →~17 min read

Start here: the problem it solves#

TL;DRthe 30-second version
  • A server must verify passwords without storing them. Keeping plaintext means one database leak exposes every account, and password reuse spreads the damage to other sites.
  • Hashing the password one way stops the plaintext leak, but a plain fast hash is not enough: attackers precompute tables of common passwords, and modern hardware tries billions of guesses per second.
  • A unique salt per user makes precomputed tables useless and stops two identical passwords from sharing a digest. A deliberately slow hash makes each guess expensive, so guessing is throttled to a crawl.
  • On login you compare the stored hash to the freshly computed one in constant time, so the reject latency never reveals how close a guess was.

Imagine you run the users table for a service. Someone signs up with the password they use everywhere. You have to be able to check that password every time they log in, but you would rather set the building on fire than keep the password in a column, because sooner or later that column is going to end up somewhere it should not: a leaked backup, an over-broad query, a compromised replica. When it does, whatever is in that column is what the attacker gets.

So the first move is to store something derived from the password instead of the password itself. A hash function turns any input into a fixed-size digest and cannot be run backwards, so storing the digest lets you check a login (hash what the user typed, compare to what you stored) without ever keeping the secret. That is necessary, and it is where a lot of systems stopped in the 2000s, which is exactly why so many of their leaks turned into cracked-account dumps.

The gap is that a plain hash is fast, and an attacker who steals the table does not have to reverse it. They guess. They take a list of common passwords, hash each one, and look for matches. A commodity GPU computes billions of fast hashes per second, so a list of the ten million most common passwords is exhausted in well under a second. Worse, if the hash is unsalted, they compute that list once, as a rainbow table, and reuse it against every leak forever. The rest of this page is the set of defenses that turn that cheap, reusable attack into an expensive, per-user one.

The one ruleThe database will eventually be read by someone you did not intend. Design the stored form of a password so that reading it is not the same as knowing the password. Everything else here follows from taking that sentence literally.

The mechanism: salt it, slow it, compare it carefully#

Start from the plain hash and add defenses one attack at a time. The stored digest is already one-way, so a reader of the table cannot invert it. The remaining problem is guessing, and there are two ways to make guessing cheap that we have to close: sharing work across users, and running fast.

Close the sharing first, with a salt. A salt is a unique random value generated for each user and mixed into the password before hashing. Because the salt differs per user, two people with the same password produce two different digests, so a match on one does not reveal the other. And because the salt is unpredictable, an attacker cannot precompute anything in advance: a rainbow table built for the whole world does not include your user's salt, so it is useless. The salt is not a secret. It is stored in plaintext right next to the hash, because its job is not to be hidden but to be unique, forcing the attacker to redo all their work separately for every single account.

password "correct-horse"
mix salt in
+ salt for aliceunique random
derive
slow hashfold many times
store salt + hash
stored: 3f8a… for alice
One password, two users, two different stored hashes

Now close the speed. The attacker still has to guess, but nothing yet stops them from guessing fast. So make the hash slow on purpose. Instead of hashing once, fold the result back through the hash function many times, thousands or hundreds of thousands of rounds, controlled by a number called the work factor or cost. This is what a key-derivation function such as bcrypt, scrypt, Argon2, or PBKDF2 does. A single login pays this cost once and does not notice a hundred milliseconds. An attacker trying billions of guesses pays it billions of times, so their effective guessing rate drops by the same factor. The dial is the whole point: as hardware gets faster, you raise the cost to keep one hash expensive.

PredictThe salt is stored in plaintext next to the hash. If an attacker steals the database, they have every salt. Why does salting still help?

Hint: Think about what the attacker can no longer reuse.

Because the salt's job is uniqueness, not secrecy. Knowing the salts does not let the attacker reverse anything. What it takes away is sharing: a precomputed table is worthless because it was built without these salts, and cracking one account gives no head start on any other, since each has its own salt. The attacker is forced to run a fresh, full guessing attack against every account separately.

The last part is on the verify path, not the store path. When a user logs in, you hash their attempt with the stored salt and cost, then compare it to the stored digest. Compare it the naive way, character by character stopping at the first difference, and the time the comparison takes depends on how many leading characters matched. An attacker measuring that time learns something about the target. A constant-time comparison always looks at every character regardless of where they differ, so a wrong answer takes the same time whether the first character was wrong or only the last one. The rejection leaks nothing.

  • One-way hash: the stored value cannot be turned back into the password.
  • Per-user salt: identical passwords get different digests, and precomputed tables are useless.
  • Work factor: each guess costs the attacker real compute, tunable upward over time.
  • Constant-time compare: the verify step's timing reveals nothing about the secret.

A walkthrough: store, verify, and crack#

  1. Store: alice signs up with "correct-horse". Generate a unique salt for alice, mix it into the password, and fold the hash the work-factor number of times. Keep only the salt and the final digest. The password is discarded.
  2. Store again: bob signs up with the same password. His salt is different, so his stored digest is different. The store now holds two unrelated-looking hashes for the same secret.
  3. Verify: alice logs in. Hash her attempt with her stored salt and cost, then compare to her stored digest in constant time. A match logs her in; a mismatch is rejected in the same amount of time regardless of how close it was.
  4. Crack: an attacker steals the table and runs a wordlist against alice's hash. Each guess must be hashed with alice's salt and folded the full cost, so the guesses-per-second rate collapses. Because "correct-horse" is a common phrase on the list, it is still found; a random password would not be.

That last step is the honest part most explanations skip. A slow salted hash does not make a weak password safe. If the password is on a common-passwords list, the attacker finds it within the length of that list no matter how slow each guess is, because the list is short. What the slow hash buys is time against strong passwords, the ones not on any list, which force the attacker to grind through an enormous keyspace at the throttled rate. The defense is a partnership: the hash provides the cost per guess, and a strong, unique password provides the number of guesses required.

Try it in the simulatorStore alice and bob with the same password and watch their digests come out different. Turn the work-factor dial and watch the time-to-crack estimate multiply. Then crack a fast unsalted hash and a slow salted one over the same wordlist to see where the cost lands.

The numbers: what the work factor actually buys#

Put real figures on it. A single fast SHA-256 runs at roughly ten billion hashes per second on one commodity GPU, the kind of rate published in Hashcat benchmarks. Suppose a password is eight characters drawn from lowercase letters and digits, so the keyspace is about 36 to the 8th, roughly 2.8 trillion possibilities. Against a plain fast hash, exhausting that space takes about 2.8 trillion divided by ten billion, which is under five minutes.

Now apply a work factor. The cost multiplies the time per guess, so it divides the attacker's rate. At a cost of a few thousand folds, the same keyspace takes days instead of minutes. At the hundreds of thousands of iterations recommended for PBKDF2, it stretches into years. The login server pays this once per attempt and barely notices; the attacker pays it for every one of trillions of guesses. That asymmetry is the entire design.

The table shows how one dial changes the picture for that same 8-character keyspace, holding the attacker's raw hardware fixed. The point is not the exact figures, which move with hardware, but the shape: each step up the work factor multiplies the attacker's time while your per-login cost stays a fraction of a second.

Work factor (folds per hash)Attacker guesses/secTime to exhaust the keyspace
1 (plain fast hash)~10 billion~5 minutes
4,096 (bcrypt cost 12)~2.4 million~2 weeks
100,000~100 thousand~1 year
600,000 (PBKDF2 floor)~17 thousand~5 years

Two caveats keep this honest. First, the cost only helps against passwords an attacker has to brute-force; a password on a wordlist is found in the length of the list, which is short. Second, GPUs and specialized hardware attack simple iterated hashes far more efficiently than they attack memory-hard functions, which is why modern advice favors bcrypt, scrypt, or Argon2 over a plain iterated SHA-256. The dial is necessary, but the choice of function is what keeps the dial effective.

Trade-offs and tuning

The work factor is a cost you pay too. Every login runs the slow hash on your server, so a higher cost means more CPU per authentication and a slower login. The standard target is to tune the cost so one hash takes somewhere around 100 to 250 milliseconds on your production login hardware. That is imperceptible to a user logging in once, and painful to an attacker running it billions of times.

Because the hash is expensive by design, the login endpoint becomes a denial-of-service target: an attacker who floods it with login attempts makes your server do the expensive work. Rate-limit login attempts per account and per source, and consider a cheap pre-check before the expensive hash. The cost that protects stored passwords is the same cost an attacker can try to weaponize against your CPU.

  • Higher cost means stronger against cracking but slower logins and more server CPU. Tune to a target time, not a fixed number, and revisit it as hardware improves.
  • A pepper is a secret value added to every hash and kept outside the database, often in a hardware module or app config. If only the database leaks, the pepper is not in it, so the stolen hashes are still uncrackable. It defends a database-only leak, not a full server compromise.
  • Upgrading the cost is easy: on the next successful login you have the plaintext momentarily, so you rehash at the new cost and replace the stored value. You never need the old password to raise the bar.
  • Store the algorithm, cost, and salt alongside the hash (most KDF outputs encode all of them in one string), so you can verify old hashes and migrate to stronger settings without a flag day.
The functions: bcrypt, scrypt, Argon2, PBKDF2

Four functions dominate, and they differ in what resource they make expensive. The trend over time is from making the attacker spend CPU to making them spend memory, because memory is far harder to parallelize cheaply on a GPU.

FunctionHard onNotes
PBKDF2CPU (iterations)Simple iterated hash, widely available and FIPS-approved. Cheap for GPUs to attack, so it needs very high iteration counts (OWASP suggests around 600,000 for PBKDF2-SHA256).
bcryptCPU + small memoryBased on the Blowfish key schedule, cost expressed as a base-2 exponent. Battle-tested since 1999; the practical cap is a 72-byte input, so long passwords are truncated.
scryptMemory + CPUDeliberately memory-hard, so custom hardware gains less. Tunable on memory, block size, and parallelism.
Argon2Memory + CPUWinner of the Password Hashing Competition and the current default recommendation (Argon2id). Separate knobs for memory, iterations, and parallelism.

The default modern choice is Argon2id, with bcrypt a completely acceptable and very well understood alternative. PBKDF2 is the right call when a compliance regime requires a FIPS-approved primitive. The one universal rule is to use a vetted library at recommended parameters and never to invent your own scheme.

Fast hash versus password hash

The most common mistake is reaching for a general-purpose hash such as MD5, SHA-1, or SHA-256 because it is labeled cryptographic. Those functions are built to be fast, which is exactly wrong for passwords. A password hash is a different tool that happens to share the word hash.

Fast hash (SHA-256)Password hash (Argon2, bcrypt)
Design goalFast, for integrity and signaturesSlow and tunable, to throttle guessing
SaltNone built inPer-user salt is part of the scheme
SpeedBillions/sec on a GPUDeliberately a handful per second at a good cost
Right useChecksums, HMAC, digital signaturesStoring passwords and deriving keys from them

A useful way to remember it: SHA-256 answers "is this file unchanged?" and Argon2 answers "is this the right password, and can we make guessing it painfully slow?" Using the first for the second is how a leak becomes a mass account takeover.

In the wild
  • The 2012 LinkedIn breach stored roughly 6.5 million passwords as unsalted SHA-1. Because they were unsalted, attackers cracked the vast majority within days using existing tables. Salting alone would have forced a fresh attack per account.
  • The 2013 Adobe breach encrypted passwords with a reversible cipher in ECB mode instead of hashing them, and stored password hints in plaintext. Identical passwords produced identical ciphertext, so the hints effectively cracked the set. It is a case study in every anti-pattern at once.
  • Modern frameworks default to the right thing: Django uses PBKDF2 with a high iteration count, Rails' has_secure_password uses bcrypt, and most languages ship an Argon2 or bcrypt library. The correct move is almost always to use the framework default rather than roll your own.
  • Password managers derive their vault key from your master password with a slow KDF for the same reason, which is why a leaked vault is not instantly openable.
Common pitfalls
  • Using a fast hash (MD5, SHA-1, SHA-256) directly for passwords. Fast is the wrong property; attackers guess billions per second.
  • No salt, or one shared salt for everyone. A shared salt still lets one precomputed table crack the whole database and lets identical passwords be spotted.
  • Rolling your own scheme, such as hashing twice or concatenating in a clever order. Homebrew constructions are how subtle, fatal mistakes get shipped. Use a vetted KDF.
  • Leaving the work factor at a library's old default for years. Costs need to rise as hardware improves; a cost set in 2015 is weak today.
  • Comparing hashes with an ordinary equality check that returns early on the first mismatch, leaking timing. Use the library's constant-time compare.
  • Capping password length aggressively or stripping characters, which shrinks the keyspace. Allow long passphrases; the KDF handles any length (mind bcrypt's 72-byte input limit).
If this comes up in an interview
Why not just hash the password with SHA-256?

SHA-256 is fast and unsalted, which are the two properties you do not want. Fast means an attacker guesses billions per second; unsalted means one precomputed table cracks everyone. You want a slow, salted key-derivation function such as Argon2, bcrypt, or scrypt.

What does the salt do, and can it be public?

The salt is a unique per-user value mixed in before hashing. It makes precomputed tables useless and stops identical passwords from sharing a digest. It is not secret and is stored right next to the hash; its job is uniqueness, not concealment.

What is the work factor and how do you choose it?

It is how many times the hash is folded, which sets how expensive one guess is. Tune it so a single hash takes roughly 100 to 250 milliseconds on your login hardware, and raise it over time as hardware speeds up.

Why compare in constant time?

A naive comparison stops at the first differing character, so its timing reveals how many leading characters matched. A constant-time compare checks every character regardless, so the reject latency leaks nothing an attacker could use.

Does a slow hash make any password safe?

No. A common password on a wordlist is found within the length of that list no matter how slow each guess is. The slow hash buys time only against strong, unique passwords that force the full keyspace. It is a partnership between the KDF and the password.

QuizA leaked database uses a slow, salted KDF. Which accounts are most at risk?

  1. All of them equally, because the salts are in the same leak
  2. The accounts whose passwords are common or on a wordlist
  3. None, because the hash is slow
  4. Only accounts with short salts
Show answer

The accounts whose passwords are common or on a wordlistThe salt and the work factor slow guessing and stop table reuse, but a password already on a common-passwords list is found within the length of that list regardless of cost. Weak passwords are the exposure; strong unique ones are protected by the throttled keyspace search.

References
References

Feedback on this topic →