Password hashing simulator
A server should never store passwords, only slow, salted hashes of them. This simulator hashes for real in your browser with SHA-256, PBKDF2, bcrypt, scrypt and Argon2id, times each one on your device, and then lets you attack a leaked table of six users with a list of 100 weak passwords. Below it: why speed is the enemy, what salts do, and which settings to use.
Argon2id with 19 MiB and 2 passes, one of the OWASP settings.
- Salt (random, stored with the hash)
- 00000000000000000000000000000000
- What the database stores
- …
- Time on this device
- –
| User | Stored hash | Cracked |
|---|---|---|
- One hash
- –
- Guesses per second, one thread
- –
- Hashes for the whole list, at most
- 600
With a salt, every user needs their own hash of every word: 6 times the work.
- Guesses per second
- no figure
- All 8-letter lowercase passwords
- –
- The same on this device
- –
The benchmark below has no figure for Argon2id at these settings: memory-hard functions are limited by memory, not arithmetic, so they do not scale the same way.
How to use the simulator
Choose an algorithm and a cost setting. The three steps follow a password through its life. Sign up hashes the password with a fresh random salt and shows the string a database would store, and how long that took on your device. Log in checks an attempt the way a server does, by hashing it again with the stored salt and settings. The database leaks builds a table of six users, hashed with the current settings, and runs a dictionary attack on it: every word of a 100-word list is tried against every user.
All the hashing is real. SHA-256 and PBKDF2 use your browser’s Web Crypto API; bcrypt, scrypt and Argon2id use the hash-wasm library. The outputs match Python’s hashlib, bcrypt and argon2-cffi for the same inputs. Things to try:
- Attack the table with SHA-256. It finishes almost instantly, and alice and dave have the same hash, so cracking one cracks both.
- Switch to SHA-256 + salt. The hashes differ, and the attack does 6 times the work, but it is still fast.
- Switch to Argon2id or bcrypt cost 12 and run the attack again. Watch the rate. erin’s password is not on the list, so no setting reveals it.
What is password hashing?
Password hashing is how a system checks passwords without keeping them. When a user signs up, the server runs the password through a one-way function and stores the result. When they log in, it runs what they typed through the same function and compares. The server never needs the password itself, so it never stores it.
The threat it defends against is a leak of the database, through a bug, a stolen backup or an insider. Hashes cannot be reversed, but they can be guessed: an attacker hashes candidate passwords and looks for matches. Since people reuse passwords, every cracked one is a risk to the user’s other accounts too. Good password hashing does not make guessing impossible. It makes each guess expensive, so that only weak passwords fall, and slowly.
That is why password hashing functions are different from ordinary hash functions. They take a salt, so equal passwords give different results, and a cost setting, so they can be made as slow as the login server can afford.
Why fast hashes are unsafe for passwords
MD5, SHA-1 and SHA-256 are designed to be fast, which is what you want for checking file downloads and what an attacker wants for guessing passwords. Graphics cards compute them in parallel at enormous rates. A published hashcat v6.2.6 benchmark on a single NVIDIA RTX 4090 gives these speeds:
| Hash (hashcat mode) | Settings in the benchmark | Guesses per second | All 8-letter lowercase passwords |
|---|---|---|---|
| MD5 (0) | one pass | 164 billion per second | 1 s |
| SHA-256 (1400) | one pass | 22.0 billion per second | 10 s |
| PBKDF2-HMAC-SHA256 (10900) | 999 iterations | 8.87 million per second | 7 hours |
| bcrypt (3200) | cost 5 | 184,000 per second | 13 days |
| scrypt (8900) | N = 16,384 | 7,126 per second | 339 days |
Every 8-letter lowercase password, all 208,827,064,576 of them, takes about 10 s on one card with SHA-256. Attackers do not even try them in order: they start with lists of leaked passwords and rules that mutate them (capital first letter, a digit at the end), which find most human-chosen passwords far sooner.
The settings in that benchmark are much lower than anyone should use. Work grows in proportion to PBKDF2’s iteration count and to 2cost for bcrypt, so the same card manages about 14,761 guesses per second against PBKDF2 at 600,000 iterations and about 5,750 against bcrypt at cost 10. For every 8-letter lowercase password, that is 164 days and 420 days respectively, instead of 10 s. Real passwords are rarely that uniform, but the ratio holds for any list of guesses.
Salts and rainbow tables
Without a salt, the same password always gives the same hash. That has two consequences, both visible in the simulator. Users with the same password have the same hash, so cracking one cracks all of them, and the attacker can see who shares a password before cracking anything. And an attacker can compute the hashes of common passwords once and reuse them against every leaked database. Lookup tables and their compressed form, rainbow tables (Philippe Oechslin, 2003), store exactly that.
A salt is a random value generated for each password when it is set, typically 16 bytes, hashed together with the password and stored next to the hash. It does not need to be secret. Because each user’s salt is different, precomputed tables are useless and every guess has to be hashed separately for every user. In the simulator, the salted attack computes up to 6 times as many hashes as the unsalted one.
A salt alone does not make a fast hash safe. Salted SHA-256 still runs at billions of guesses per second per user; the salt only stops the attacker from sharing work between users. It has to be combined with a slow function.
Slow hashes: PBKDF2 and bcrypt
The answer to fast hardware is key stretching: repeat the work so that one hash takes a noticeable time. A user logging in waits a few hundred milliseconds once; an attacker trying billions of guesses waits a few hundred milliseconds billions of times, divided only by how much hardware they have.
PBKDF2 (password-based key derivation function 2, from PKCS #5, now RFC 8018) applies HMAC with a hash such as SHA-256 thousands of times. It is simple, standardised and available almost everywhere, including in FIPS-140 validated libraries, which is why it is still common. Its weakness is that it needs almost no memory, so GPUs run it efficiently.
bcrypt, by Niels Provos and David Mazières (USENIX 1999), is built on the Blowfish cipher’s expensive key setup, repeated 2cost times, so each increase in the cost doubles the time. It uses 4 KiB of fast-changing memory, which made it awkward for GPUs, and it has held up well for over two decades. Two limits matter in practice: bcrypt only uses the first 72 bytes of the password, and some implementations stop at a zero byte, which is why pre-hashing the password with a fast hash before bcrypt needs care.
Memory-hard hashes: scrypt and Argon2
GPUs and custom chips are good at arithmetic and comparatively poor at memory: each core has little of it. A memory-hard function forces each guess to fill and read a large block of memory, so an attacker running many guesses in parallel needs that much memory for each one. Time is no longer the only cost; memory is too.
scrypt, designed by Colin Percival in 2009 for the Tarsnap backup service and standardised as RFC 7914, has three parameters: N, the memory and time cost (a power of two), r, the block size, and p, the parallelism. It uses about 128 × N × r bytes, so N = 217 with r = 8 needs 128 MiB.
Argon2, by Alex Biryukov, Daniel Dinu and Dmitry Khovratovich, won the Password Hashing Competition in 2015 and is standardised as RFC 9106. Its memory cost (m, in KiB), time cost (t, the number of passes over the memory) and parallelism (p) are set separately. There are three variants: Argon2d resists GPU attacks best but its memory access depends on the password, which can leak through side channels; Argon2i avoids that; Argon2id combines the two and is the one to use for passwords.
In the simulator, Argon2id and scrypt run in WebAssembly in a single thread, which is slower than native code on a server. The comparison between them and the fast hashes is still fair in proportion.
Which algorithm and settings to use
The OWASP Password Storage Cheat Sheet recommends, in order: Argon2id; scrypt if Argon2id is not available; bcrypt for legacy systems; and PBKDF2 where FIPS-140 compliance is required. The simulator includes its minimum settings alongside some older ones:
| Algorithm | Settings in the simulator |
|---|---|
| PBKDF2 | PBKDF2-HMAC-SHA256, 1,000 iterations, a common setting in older systems. PBKDF2-HMAC-SHA256, 100,000 iterations. PBKDF2-HMAC-SHA256, 600,000 iterations, the OWASP recommendation. |
| bcrypt | bcrypt with cost 8: 2⁸ rounds of key setup. bcrypt with cost 10, the OWASP minimum. bcrypt with cost 12: four times the work of cost 10. |
| scrypt | scrypt with N = 2¹³, r = 8, p = 10 (8 MiB), one of the OWASP settings. scrypt with N = 2¹⁵, r = 8, p = 3 (32 MiB), one of the OWASP settings. scrypt with N = 2¹⁷, r = 8, p = 1 (128 MiB), the first OWASP setting. |
| Argon2id | Argon2id with 7 MiB of memory and 5 passes, one of the OWASP settings. Argon2id with 19 MiB and 2 passes, one of the OWASP settings. Argon2id with 46 MiB and 1 pass, one of the OWASP settings. |
The 1,000-iteration PBKDF2 setting and bcrypt cost 8 are there to show what older systems used. OWASP describes the settings within its Argon2id and scrypt lists as equally strong, trading memory for passes. The right value is the highest your login servers can sustain at peak load, so measure it on production hardware rather than copying a number. Remember that login is also a way to make your servers do expensive work, so rate-limit it.
What the stored string means
Modern libraries store everything needed to check a password in one string: the algorithm, its version, the parameters, the salt and the hash. An Argon2id hash in the PHC string format looks like this:
$argon2id$v=19$m=19456,t=2,p=1$<salt in base64>$<hash in base64> argon2id is the algorithm, v=19 the version (0x13), m the memory in KiB, t the passes and p the parallelism. bcrypt uses the older modular crypt format: $2b$12$ (or $2a$, as in the simulator) followed by 22 characters of salt and 31 of hash, in bcrypt’s own base64 alphabet. Linux stores hashes in /etc/shadow in the same style, with prefixes such as $6$ for SHA-512 crypt and $y$ for yescrypt.
Because the parameters travel with each hash, a system can raise its settings at any time: new hashes use the new values, and old ones are still checked with the values they were made with.
Peppers, upgrades and common mistakes
- Pepper. A secret value shared by all passwords and stored outside the database, for example in a secrets manager or a hardware security module. Mixing it in (or encrypting the hashes with it) means a leaked database alone is not enough to start guessing. It adds defence in depth on top of a salted slow hash.
- Upgrading. On each successful login, check whether the stored hash uses old settings and, if so, rehash the password the user just typed. For accounts that never log in, wrap the old hash in the new function, for example Argon2id(SHA-1(password)), and mark the record.
- Comparing. Compare hashes in constant time, so that response times do not reveal how many leading bytes matched. Library verify functions do this.
- Do not roll your own. Use the password hashing functions of a maintained library (libsodium, the language’s standard bcrypt or Argon2 binding) rather than combining primitives yourself.
- Do not encrypt. Encrypted passwords can all be recovered by anyone who gets the key.
- Length limits. Allow long passwords, but cap the length (for example at a few hundred characters) so a huge input cannot be used to tie up the server; with bcrypt, account for its 72-byte limit.
Lessons from breaches
Real breaches show each mistake. LinkedIn’s 2012 breach exposed passwords stored as unsalted SHA-1 hashes, which were then cracked in large numbers. Adobe’s 2013 breach exposed passwords that were encrypted with 3DES in ECB mode rather than hashed, so equal passwords had equal ciphertexts and were stored next to users’ password hints. Ashley Madison (2015) hashed passwords with bcrypt at cost 12, but researchers found millions of accounts that also stored an MD5-based token derived from the username and lowercased password, and cracked those instead. A strong hash does not help if a weak copy of the same secret sits next to it.
Password hashing in interviews
“How would you store passwords?” is a common question in backend and security interviews. A complete answer covers: never store or log plain passwords; use a slow, salted, memory-hard function (Argon2id, or bcrypt with a cost of at least 10); a unique random salt per password, stored with the hash; parameters stored with the hash so they can be raised later; constant-time comparison; rate limiting and lockouts on login; optionally a pepper kept outside the database; and a plan to rehash on login when settings change.
Follow-ups often ask why not SHA-256 (speed, as the table above shows), what a rainbow table is and why salts defeat it, and the difference between hashing and encryption.
Password hashing questions and answers
What is password hashing?
Password hashing means storing the output of a one-way function of each password instead of the password itself. To log a user in, the server hashes the password they type with the same salt and parameters and compares the result with the stored value. If the database leaks, the attacker gets hashes, and has to guess passwords and hash each guess to find out which ones match.
Why should I not use SHA-256 or MD5 to hash passwords?
Because they are fast. A general-purpose hash is designed to process data quickly, so an attacker can test guesses quickly too. A published hashcat benchmark on one RTX 4090 graphics card computes about 22 billion SHA-256 hashes and 164 billion MD5 hashes per second. Password hashing functions such as bcrypt, scrypt and Argon2 are deliberately slow and tunable, so each guess costs thousands to millions of times more.
What is a salt and why is it needed?
A salt is a random value, unique to each password, that is hashed together with the password and stored next to the hash. It makes two users with the same password get different hashes, and it makes precomputed tables such as rainbow tables useless, because the attacker would need a separate table for every salt. The salt does not need to be secret.
Which is better, bcrypt, scrypt or Argon2?
For new systems, OWASP recommends Argon2id first, then scrypt if Argon2id is not available, and bcrypt for legacy systems. Argon2id and scrypt are memory-hard: each guess needs a configurable amount of memory, which makes large-scale attacks on GPUs and custom hardware more expensive. bcrypt is still reasonable with a cost of 10 or more, but it only uses 4 KiB of memory and ignores password bytes after the 72nd.
What parameters should I use for Argon2id?
OWASP lists several equivalent minimum settings for Argon2id, among them 19 MiB of memory with 2 iterations and parallelism 1, or 46 MiB with 1 iteration. RFC 9106 recommends 2 GiB with 1 iteration where that much memory is available, or 64 MiB with 3 iterations otherwise. Pick the most your login servers can afford, measured on your own hardware.
How many PBKDF2 iterations should I use?
OWASP recommends 600,000 iterations for PBKDF2-HMAC-SHA256 and 220,000 for PBKDF2-HMAC-SHA512. PBKDF2 is the choice when a FIPS-140 validated implementation is required; otherwise a memory-hard function is stronger for the same login time.
What is a pepper?
A pepper is a secret value shared by all passwords and kept outside the database, for example in a hardware security module or a secrets manager. It is mixed into the hash or used to encrypt the stored hashes, so a leak of the database alone is not enough to start guessing. It is an extra layer on top of a salted slow hash, not a replacement for one.
Is hashing the same as encryption?
No. Encryption is reversible with the key; hashing is one-way. Passwords should be hashed, because the server never needs to recover them, only to check them. If encrypted passwords leak together with the key, every password is exposed at once.
How do I upgrade old password hashes?
Store the algorithm and parameters with each hash, which formats like $argon2id$v=19$m=19456,t=2,p=1$... already do. When a user logs in successfully, the server has the plain password for a moment and can rehash it with the new algorithm. For accounts that do not log in, the old hash can be wrapped: hash the old hash with the new algorithm, and remember that the record needs both steps.
Further reading
- OWASPPassword Storage Cheat SheetWhich algorithm, which parameters, and peppers.
- RFC 9106, 2021Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work ApplicationsThe Argon2 specification and its recommended parameters.
- RFC 7914, 2016The scrypt Password-Based Key Derivation Functionscrypt’s algorithm and parameters.
- Provos and Mazières, USENIX 1999A Future-Adaptable Password SchemeThe bcrypt paper.
- hashcat v6.2.6 benchmarkRTX 4090 hashcat benchmarksThe attacker speeds used on this page.
- SemicolonyJWT lifecycle simulatorWhat happens after the password check: issuing and verifying tokens.