Password Hashing Simulator: done right.

Password "hashing" is not general-purpose hashing. It is deliberately slow, salted, and — done correctly — memory-hard. The attacker has your database and a GPU rig; the only thing standing between them and every account is the cost of one guess. Type a password. Pick an algorithm. Watch what they pay.

algorithm
Argon2id
hash time
GPU rig
20.8 K/s

algo: salt:
presets:
m = 65,536 KiB
salt (16 bytes) 738647170f004e195e10fa50cee5df58
database row
(click "Hash it")
raw digest 0 bytes
attacker's view — small consumer GPU rig at 20.8 K/s
password classguessesexpected crack time
8-char common words ("password123") 1010 2.8 days
10-char mixed (Aa1!Bb2@Cc) 1014 76.2 yr
16-char passphrase (4 random words) 1020 76.2M yr
same password, hashed twice (with salt → rainbow tables useless)

What you're looking at

The control row picks an algorithm and toggles the salt. Below it, the database row shows the PHC string a server would actually store, with the salt and cost parameters sitting in plaintext right next to the digest. The red attacker table converts one number, guesses per second on a small GPU rig, into expected crack times for three password strengths. The twin panel at the bottom hashes the same password twice so you can see exactly what the salt buys.

Start with MD5 and the preset password123: the hash lands in a microsecond and the 8-char row in the attacker table cracks in a blink. Then switch to Argon2id at 64 MiB and hash again. Your server pays roughly 100 ms once per login; the attacker pays it per guess, and the same password class jumps to years. What should surprise you is the lever: nothing about the password changed, only the price of one guess. Then toggle salt off and hash twice. Identical digests, which is the rainbow-table giveaway.


Hashing for storage vs. hashing for speed

MD5 and SHA-256 were optimised for the opposite of what passwords need.

MD5 and the SHA family exist to fingerprint files quickly and with collision resistance. They're engineered for throughput — gigabytes per second per core, instructions small enough that the chip vendors fused them into silicon (Intel's SHA-NI, ARMv8's cryptographic extension). Pipe a 4 GB ISO through SHA-256 and it returns in a second. That is exactly the property you want for content addressing, integrity checks, and TLS. It is exactly the property you do not want for password storage.

Password hashing wants the opposite. Per-password slow. GPU-resistant. Ideally memory-hard, because attackers don't have one CPU, they have warehouses of them. Putting a user's password through unsalted SHA-256 is the single most common authentication bug of the last twenty years, and the breach pages prove it: LinkedIn 2012 (unsalted SHA-1, 6.5 million hashes, 90% cracked inside a week), Adobe 2013 (custom 3DES nonsense, 150 million accounts, hint field included), Yahoo 2013–14 (MD5, 3 billion accounts). The fix in every case was not "use a bigger hash." It is to use a fundamentally different family of functions: bcrypt (Provos & Mazières, 1999), PBKDF2 (NIST-standardised, parameterised but not memory-hard), scrypt (Percival, 2009), and Argon2id (Biryukov & Khovratovich, PHC winner 2015).


Salt is not pepper

One sits in the database next to the hash. The other lives in a vault.

A salt is a per-password random value — the convention is 16 random bytes — stored in plaintext alongside the hash. Its only job is to defeat precomputed lookup tables. Without salt, "password" always hashes to the same digest, so an attacker pre-computes one rainbow table (a compressed map from hash(common_password) → password) once and cracks every stolen database forever. With salt, the attacker has to brute-force per row. Ten million users means ten million separate attacks, not one shared one. That is the entire point.

Salt does not need to be secret. It is stored in the clear, encoded right into the PHC string the simulator shows above: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>. An attacker who has the database has the salts. That's fine — the salt's value is in being per-row, not in being hidden. Pepper is a different beast: a single server-wide secret that gets mixed into the input before hashing, kept in a config file or HSM, never in the database. Pepper helps in exactly one specific scenario — the attacker dumps the database but does not own the app server. That happens often enough (misconfigured backups, SQL injection without RCE) that the belt-and-suspenders combo is recommended for high-value systems.


Tune the cost parameter, then keep tuning

The right value is the highest your login latency budget can absorb.

Every password hash has a cost knob. bcrypt has its cost factor (each +1 doubles work); scrypt has N, r, p; Argon2 has m, t, p. There is no universally correct value — only one that's correct for your hardware in this calendar year. The rule is: pick the highest cost that keeps a server-side login under your latency budget, typically 100–500 ms. The OWASP Password Storage Cheat Sheet tracks the floor — roughly Argon2id at m ≥ 19 MiB, t = 2, p = 1, or bcrypt cost ≥ 12, or scrypt N ≥ 2¹⁷, r = 8, p = 1 — and revises it as hardware moves, so check the cheat sheet itself, not a copy of its numbers.

Hardware does not stop. A bcrypt cost that hurt attackers when it was chosen is comfortable a decade later and cheap the decade after. The PHC string is engineered for exactly this migration: the parameters live in the string, so on every successful login the server can check whether the stored hash uses today's recommended parameters, and if not, transparently re-hash the submitted plaintext with stronger parameters and write the new digest back. Every production auth stack should ship with this logic — without it, a password set a decade ago is still defended only as well as a decade-old cost factor allows.


Don't roll your own; don't trust your own

Auth is the worst place in the codebase to be creative.

Use a library that has a hash-and-verify interface with the parameters embedded in the output, and let it pick: libsodium's crypto_pwhash (Argon2id under the hood), Python's passlib, Go's golang.org/x/crypto/bcrypt and argon2, Spring Security's BCryptPasswordEncoder, Node's argon2 package. These verify the PHC string for you and re-hash transparently on upgrade. If you can hand the problem to a managed identity provider (Auth0, Clerk, Cognito, Firebase Auth, an OIDC SaaS), do it — they pay full-time engineers to do this one thing.

The recurring mistakes are predictable enough to list. Writing a custom salting scheme (invariably done wrong, salt prepended to MD5 of password, no per-row randomness). Using HMAC-SHA256 as a "password hash" because someone heard HMAC is good — it's fast and not memory-hard, so it's no better than SHA-256. Using only PBKDF2-SHA256 because it's FIPS-approved; PBKDF2 is GPU-friendly and the iteration counts most teams pick are an order of magnitude too low. Storing the pepper in the same database as the hashes, which defeats its only purpose. Truncating bcrypt inputs by accident (bcrypt silently caps at 72 bytes; hashing a long passphrase concatenated with a username can throw away the passphrase entirely). The project that says "I know what I'm doing" is the one that ends up in the next breach disclosure.

Found this useful?