Stop storing passwords with SHA-256. Use Argon2id.
If you're storing user passwords with SHA-256, even with a per-user salt, even with HMAC, you have a problem. Not a theoretical problem — a real one that the next leaked database will demonstrate the hard way.
The reason isn't that SHA-256 is broken. It isn't broken. No practical attacks exist against it. The reason is that SHA-256 is too fast. And speed, in the password-hashing context, is not a feature.
"But SHA-256 is cryptographic, isn't it?"
Yes — and that's exactly the trap. The word "cryptographic" attached to a hash function means it has three properties: pre-image resistance (you can't reverse it), second pre-image resistance (you can't find a different input with the same output), and collision resistance (you can't find any two inputs with the same output). SHA-256 has all three. So does SHA-512. So does SHA-3.
None of those three properties say anything about how fast the function should be. And for the use cases SHA-256 was designed for — file integrity, content addressing, signing — fast is good. You want to hash a 4 GB ISO and check the checksum in under a minute. You want Git to compute SHA-256 of every blob without slowing your commit. You want TLS handshakes to complete quickly.
For password storage, fast is actively dangerous. If you can hash a million candidates per second, an attacker who steals your database can hash a million candidates per second against every user. That asymmetry — defender's speed equals attacker's speed — is what password-storage hashes are specifically designed to defeat.
The math, with current hardware
On a single modern GPU (an RTX 4090, say, or a cloud GPU instance), the throughput numbers for password-cracking against various algorithms are roughly:
- SHA-256: 22 billion guesses per second.
- MD5: 130 billion guesses per second (yes, still in use places it shouldn't be).
- bcrypt at cost factor 12: 8,000 guesses per second.
- Argon2id at OWASP-recommended parameters: roughly 100–500 guesses per second.
That's not a 2× gap. It's not a 10× gap. It's a six-orders-of-magnitude gap. An attacker who would take 100,000 years to brute-force an Argon2id-protected database can rip through a SHA-256-protected one in a weekend.
The rockyou.txt math, made concrete
RockYou was a social-network company that suffered a database breach in 2009. The breach leaked 32 million passwords, in plaintext, with no hashing at all. The resulting list became the canonical password-cracking dictionary — every offline cracking tool ships with it.
RockYou contains about 14 million unique passwords. To check every one against a single user's salted SHA-256 hash takes about 0.6 milliseconds on a modern GPU. Across 10 million users in a hypothetical leak, that's 6,000 seconds — under two hours to test every RockYou candidate against every user.
The same exercise against Argon2id-hashed passwords, at OWASP parameters, would take about 4,400 years for the same 10 million users. Not because Argon2id is "stronger" in a mathematical sense — both algorithms are mathematically unbreakable. But because Argon2id is intentionally slow and memory-hard, the attack throughput collapses by six orders of magnitude.
"What does Argon2id actually do differently?"
Argon2id is the winner of the 2015 Password Hashing Competition and the OWASP-recommended default as of 2024. It does three things that SHA-256 does not:
- It iterates. Each call to the function internally runs many rounds of hashing — you pick the count. Tuned correctly, a single Argon2id evaluation takes ~100 milliseconds on the server. SHA-256 takes nanoseconds.
- It uses memory. Argon2id is "memory-hard": each evaluation requires a configurable amount of RAM (the OWASP default is 19 MiB). A GPU has lots of compute but limited memory per core; the memory-hardness intentionally turns the GPU's advantage into a disadvantage.
- It is tunable. As hardware gets faster, you can increase the iteration count or memory cost to maintain the ~100ms-per-evaluation target. SHA-256 has a fixed cost that gets cheaper for attackers every year.
bcrypt does (1) and partially (2), with a simpler interface. scrypt does (1) and (2) more aggressively than bcrypt but is harder to tune. Argon2id supersedes both as the modern default.
"What if I can't migrate? I have a SHA-256 user database."
You can migrate without forcing every user to reset their password. The pattern is:
- At login, take the user's submitted plaintext password and compute the existing SHA-256 hash. Compare it against the stored SHA-256 hash. If it matches, the user is authenticated.
- If authenticated, take the same plaintext password (which you have only for this request) and re-hash it with Argon2id. Replace the stored SHA-256 hash with the new Argon2id hash. Add a column or use a hash-prefix to indicate which algorithm was used.
- Over time, as users log in, their hashes migrate to Argon2id. Inactive users remain on SHA-256, but inactive users aren't getting cracked because they aren't being targeted.
- After a year, force a password reset on any account still on SHA-256.
This is the standard migration pattern. It works. Almost every framework's auth library supports it natively (Devise, Rails 8's authentication generator, Django's auth.hashers, etc.). The hardest part is the decision to start, not the implementation.
"OK but isn't bcrypt fine?"
bcrypt is fine. If you have a working bcrypt implementation with a cost factor of at least 12, you do not have a password-storage problem. Switching to Argon2id is an upgrade, not a fix. Don't refactor working bcrypt code to chase the latest OWASP guidance; spend that time on the SHA-256 systems where the actual risk lives.
The hierarchy:
- Plain SHA-256, MD5, SHA-1: wrong. Migrate.
- Salted SHA-256 with HMAC or PBKDF2 at low iteration count: not great. Migrate when convenient.
- PBKDF2 with ≥600,000 iterations: acceptable. Argon2id is better, but not urgently.
- bcrypt cost ≥12: fine. Argon2id is a marginal improvement.
- scrypt or Argon2id with current OWASP parameters: modern best practice.
What this calc-hammer hash tool is and is not
Our hash generator computes SHA-256, SHA-512, SHA-384, SHA-1, and MD5 of arbitrary input. It is useful for file integrity, fingerprints, and cache keys. It is not a password-hashing tool, and the page itself says so explicitly now. If you find yourself reaching for it to hash a password, stop, install the bcrypt or argon2 library for your language, and use that instead. The two-line difference in code is the difference between "secure for a decade" and "trivially crackable next week."