Should you hash a password with SHA-256?
You are looking at a users table with a column of 64-character digests in it, and you want to know how much trouble you are in. SHA-256 is an outstanding fingerprint for files — Rocket Hash streams it at roughly 2 GB/s on Apple Silicon — and that speed is precisely why it must not be the only thing standing between a stolen database and everybody's password.
The sentence is almost always written in good faith. It shows up in a security questionnaire, in a README, in a pull request nobody pushes back on: “passwords are hashed with SHA-256,” as though naming a respectable algorithm settled the matter. Somewhere upstream was a tutorial by an author who knew SHA-256 was the right answer for checksums and assumed it was therefore the right answer for everything.
It is not, and the answer is unambiguous: do not store passwords as SHA-256 digests. Not SHA-512, not SHA3-512, not with a salt, not with a pepper, not run through the function five times. Password storage needs a function designed to be slow and awkward to run in parallel — Argon2id, scrypt, bcrypt, or PBKDF2-HMAC-SHA-256 where a standard insists on it.
The interesting part is why, because nothing about SHA-256 is broken. This is not the story of MD5 or SHA-1, where a property the function promised was eventually taken away from it. SHA-256 keeps every promise it ever made. The promises are simply about the wrong thing.
Everything SHA-256 guarantees is about bytes you already have. A password is a secret somebody has to guess, and guessability is a property of the input, not of the hash.
Nothing about SHA-256 is broken here
Two different guarantees get muddled in these conversations, and the distinction decides the whole argument.
- Preimage resistance. Given a digest, nobody can work backwards to an input that produces it. For SHA-256 this holds completely — there is no known method better than trying inputs one at a time.
- Collision resistance. Nobody can construct two different inputs that share a digest. This is the property MD5 lost in 2004 and SHA-1 lost publicly in 2017, and it is the one that matters when a digest is standing in for a file's identity.
Cracking a password hash breaks neither guarantee. The attacker does not invert anything and does not need two inputs that collide. They run the function forwards, over a list of the passwords people actually choose, and compare the output with the digests they stole. It is a hunt for a preimage, but a brute-force one over a candidate space small enough to exhaust — and no hash function, however strong, ever promised to make a short list of guesses expensive. The function is being used exactly as designed, correctly, at full speed, in the attacker's favor.
So the number that decides whether your users are safe is not the 256 bits of digest. It is the entropy of what they typed, multiplied by what it costs to test one guess. The first number you do not control. The second one you do, and choosing SHA-256 is choosing to make it as close to zero as the hardware allows.
The arithmetic of an offline guessing attack
Picture the situation the hash actually has to survive. Somebody has a copy of the table. The rate limiting on your login endpoint is irrelevant, because nobody is using your login endpoint. The lockout after five bad attempts is irrelevant. The alerting is irrelevant, because there is nothing to alert on — the guessing happens on their hardware, in silence, for as long as they feel like it.
The only brake left is the cost of one guess, and cost per guess is a design parameter of the function you picked.
| Function | Guesses per second, one GPU | Time to exhaust eight lowercase letters |
|---|---|---|
| MD5 | Order of 100 billion | A couple of seconds |
| SHA-256 | Order of 10 billion | About twenty seconds |
| SHA-512 | Order of 3 billion | A minute or two |
| PBKDF2-HMAC-SHA-256, 600,000 iterations | Tens of thousands | Months |
| bcrypt, cost 12 | A few thousand | A couple of years |
| Argon2id, 64 MiB of memory | A few thousand, and the memory caps how many run at once | Years, on much more expensive hardware |
Those figures are rounded hard and they move with every hardware generation; the only thing worth reading in that table is the ratio between the rows. Eight random lowercase letters is about 38 bits of entropy, which is already more than a great many real passwords carry, and it is the difference between twenty seconds and two years.
Notice what the first three rows have in common. Moving to SHA-512 or SHA-3 buys a constant factor in the single digits against an advantage measured in millions. And for most real accounts the honest figure is not twenty seconds but instant, because a large share of passwords sit in a wordlist somebody published years ago.
What a salt fixes, and what it does not
A salt is a unique random value, generated per password and stored in the clear beside the digest. It does two genuinely valuable things. It destroys precomputation, so an attacker cannot arrive with a ready-made table of digests for a million common passwords. And it breaks the link between accounts, so cracking one row tells you nothing about the nine other people who chose the same password.
The second is easy to see for yourself: hash the same string twice with no salt and you get the same 64 characters, so identical passwords show up as identical rows to anybody holding the table.
The free Text tool makes it concrete. Type a password-shaped string, read the SHA-256 row, clear the field with the ⊗ button at its corner and type the same thing again: the identical 64 characters come back, because they always will. That repetition is the clue an unsalted column hands over — two matching rows mean two people chose the same password, learned without computing anything. The byte count under the field is worth a glance too: a trailing space you did not mean to type moves it by one and changes all 64 characters, which is the same sensitivity behind two tools disagreeing about one input.
Here is what a salt does not do: make a single guess cost more. The attacker reads your salt out of the same dump, sticks it on the front of each candidate, and carries on at ten billion a second. Against one targeted account — the CEO, the administrator, the one email address they came for — salted SHA-256 is exactly as fast to break as unsalted SHA-256. A salt is necessary and not sufficient; “but we salt them” does not answer the question.
A pepper is a better adjunct: a secret key held outside the database and mixed in with HMAC, so that a stolen table alone is not enough to start guessing. It helps for exactly as long as the key stays out of the same backup — an operational assumption, not a cryptographic one. Keep it as a second layer, never the first.
What to use instead
Four options, in the order most projects should consider them.
- Argon2id. The default recommendation. It charges the attacker in memory as well as in time, which is what takes the advantage away from graphics cards and custom silicon — a chip can hold thousands of hash cores but not thousands of 64 MiB scratchpads. It won the Password Hashing Competition in 2015 and has had a decade of scrutiny since.
- scrypt. The older memory-hard design: well understood, available almost everywhere, a perfectly respectable answer when Argon2 is not on hand.
- bcrypt. Published in 1999 and still sound. Its cost factor and salt travel inside the output string, which is why a bcrypt record starts with
$2b$and carries everything needed to verify it. One sharp edge: it ignores input past 72 bytes, so long passphrases must be pre-hashed — and that is a place SHA-256 legitimately belongs in a password pipeline. - PBKDF2-HMAC-SHA-256. For when an auditor or a standard requires approved primitives. It is not memory-hard, so it leans entirely on iteration count, and current published guidance puts that in the hundreds of thousands.
Whichever you pick, use the implementation your platform already ships, and choose parameters by measuring rather than by copying a number out of a blog post: tune the cost until one verification takes a few hundred milliseconds on the hardware you actually run, under the load you actually get. Then store the algorithm and its parameters beside every digest, so that raising the cost in two years is a migration rather than an excavation.
Where SHA-256 does belong in a login system
This is not an argument that SHA-256 has no place near authentication. It has several, and they are worth knowing because they look superficially like the thing you must not do.
Inside the key-derivation function. PBKDF2-HMAC-SHA-256 is SHA-256, six hundred thousand times, with a salt and a standardized construction around it. The primitive was never the problem; using one round of it was.
Hashing secrets you generated yourself. API keys, session identifiers, password-reset tokens. Store the SHA-256 of the token rather than the token, and a database dump hands over nothing usable. A fast hash is the right call here: the token is 128 or more bits straight out of a cryptographic random number generator, so there is no wordlist to try and no pattern to exploit, and no hardware will ever work through a 128-bit space. Speed costs you nothing when the attacker has nothing to speed through. Compare in constant time, index the column, move on.
Proving bytes are unchanged. Configuration blobs, signed artifacts, the installer you are about to run. That is the original job, and a local digest is still the cleanest way to do it — Rocket Hash computes every algorithm for a file in one pass, and the SHA-256 generator here does the same arithmetic on a string you paste, which is all a config value or a token needs. For a job of that shape, the decision tree for picking an algorithm is a better guide than instinct.
If you have already shipped SHA-256 passwords
You cannot recover the passwords to re-hash them, which is the one thing the current scheme is doing correctly. There are two routes out, and the good news is that they compose.
- Upgrade on login. The next time somebody signs in you briefly hold their plaintext. Verify it against the old digest, immediately write a fresh Argon2id hash, record the new scheme on the row, and discard the old value. Most active accounts migrate within a few weeks of ordinary traffic.
- Wrap the existing digests now. For every row, store
argon2id(sha256_digest)and note the scheme as a two-layer one. Verification becomes: SHA-256 the submitted password, feed that into Argon2id, compare. Nobody has to log in for this to take effect, and from the moment it ships an attacker pays the Argon2 price per guess instead of nothing.
Do both: wrap everything this week so dormant accounts get the new floor immediately, then let ordinary logins replace the two-layer records with clean single-layer ones. And if there is any chance the old table has already been copied, treat those digests as the passwords themselves — for anybody who chose a human password, that is very nearly what they are — and force a reset where it counts.
Troubleshooting
Our security standard says to use SHA-256
Read the clause again, because it almost certainly says something narrower than it appears to. Requirements of this kind normally demand an approved one-way function with a salt and a high iteration count, which PBKDF2-HMAC-SHA-256 satisfies and a bare digest does not. Naming SHA-256 as the underlying primitive is not the same instruction as storing its raw output.
We hash the password in the browser before sending it
Then the digest is the password, as far as your server is concerned, and anybody holding a stolen row can submit it directly. Client-side hashing can be a reasonable extra — it keeps the plaintext away from your logs and your crash reports — but it changes nothing about what the server must store. Whatever arrives still has to go through a real key-derivation function before it is written down.
Logins got slower after we switched
That is the feature, not a regression, and a few hundred milliseconds on a sign-in is invisible to a human. What is worth planning for is capacity: the cost is paid per login rather than per request, and with a memory-hard function your ceiling is now the memory parameter multiplied by simultaneous logins. Bound that endpoint before a burst of traffic discovers it for you.
Can we just iterate SHA-256 ourselves?
That is PBKDF2 with the specification removed and the review process skipped. The construction is fiddlier than it looks — how the salt is mixed in, how the counter is handled, whether the comparison at the end leaks timing — and a homegrown version of a standard function is strictly worse than the standard one, which your language already ships.
It is only hashes that leaked, not passwords
With a fast digest over human-chosen passwords, that distinction is thinner than the incident report implies. Assume the common ones are recovered within the hour, respond as you would to a credential breach, and remember that a meaningful share of those passwords also unlocks an email account somewhere else.
Frequently asked questions
Is SHA-256 secure enough for storing passwords?
No. SHA-256 is a secure hash function, but password storage does not need a fast secure hash — it needs a deliberately slow one. A stolen table of SHA-256 digests can be attacked offline at roughly ten billion guesses a second on a single graphics card, which is enough to work through every password most people actually choose. Use Argon2id, scrypt, bcrypt, or PBKDF2-HMAC-SHA-256 instead.
Is salted SHA-256 good enough for passwords?
No. A salt stops precomputed tables and stops one cracked password exposing every account that shared it, both of which are worth having. What it cannot do is make an individual guess cost more, because the salt is stored in the clear right next to the digest. Against a single targeted account, salted SHA-256 falls at exactly the same speed as unsalted SHA-256.
What is the best algorithm for hashing passwords?
Argon2id is the current default recommendation, because it charges an attacker in memory as well as in time and that is what blunts GPU and custom-hardware attacks. scrypt and bcrypt are both solid alternatives, and PBKDF2-HMAC-SHA-256 with a high iteration count is the right answer when a standard demands approved primitives. Use your platform's implementation and tune the cost so one verification takes a few hundred milliseconds on your own hardware.
Can a SHA-256 password hash be cracked?
It cannot be reversed — there is no way to run SHA-256 backwards. It can be guessed, which in practice amounts to the same thing: an attacker hashes candidate passwords by the billion and watches for a match. A genuinely random twenty-character password would survive that indefinitely even under SHA-256. The trouble is that almost nobody uses one.
Why is a fast hash bad for passwords?
Because of who pays for the speed. You run the function once per login, so you would not notice it taking a quarter of a second. An attacker runs it once per guess, billions of times over, so every microsecond you shave off is multiplied by their entire candidate list. Slowness is the only lever that costs the defender almost nothing and the attacker everything.
Are SHA-512 or SHA-3 better than SHA-256 for passwords?
Not in any way that matters. They are all fast general-purpose hashes, and swapping between them changes an attacker's throughput by a small constant factor — single digits — against an advantage measured in millions. Picking a longer digest does not address the problem, which is cost per guess rather than digest size.
Is it safe to hash an API key or session token with SHA-256?
Yes, and it is the right tool there. A token you generated from a cryptographic random number generator has 128 bits or more of entropy, so there is no wordlist to run and no shortcut to take; speed buys an attacker nothing. Store the digest rather than the token, compare in constant time, and a database leak hands over nothing anybody can use.