Password Hashing Explained: Salts, Slow Hashes, and Why Argon2 Beats SHA-256
Why storing passwords is harder than it looks, what a salt actually does, and why a fast hash like SHA-256 is the wrong tool for the job. A plain English primer for people learning security.
Picture the reception desk at a busy office. Someone has to check that visitors are who they say they are, but keeping a drawer full of everyone's house keys would be madness. If a thief ever got into that drawer, they would own every home in the building. A far safer plan is to keep something that lets you verify a key without ever storing the key itself. That is the exact problem every website faces when it handles your password, and the way it is solved is one of the most quietly important ideas in security.
Storing passwords sounds trivial at first. Save what the user typed, compare it next time, done. That approach is also precisely how organisations end up in a breach headline with millions of accounts exposed. This is a beginner friendly tour of how passwords should really be stored, and, just as importantly, why each step exists. By the end you will understand not just what a salt is but why leaving it out is dangerous, and why the fast hash that is perfect for one job is a serious mistake for this one.
Rule zero: never store the password itself
If your database holds passwords in plain readable text, then anyone who reads that database instantly has every user's password. And plenty of people can end up reading a database: an attacker who breaks in, a disgruntled employee with too much access, or simply someone who finds an old backup left on an unsecured server. The moment those plain passwords leak, the damage is total.
Worse still, people reuse passwords across sites. Studies and breach after breach have shown this happens constantly. So a leaked password from a small forum can quietly unlock the same person's email, their online shopping, even their bank. You have not just exposed one account on your own service, you may have handed over their entire digital life.
So the golden rule is simple: we do not store the password. We store a hash of it instead.
What a hash is
A cryptographic hash is a one way function. Feed it any input and it produces a fixed size output that looks like random gibberish. The same input always gives the same output, which is what makes it useful for checking, but you cannot run the function backwards to recover the input. It is a bit like a paper shredder. Turning a document into confetti is easy and reliable, but reassembling the confetti into the original document is effectively impossible.
"correcthorse" -> hash -> 9b74c9897bac770ffc029102a200c5de...To check a login, the server hashes whatever the user just typed and compares it to the hash it stored when they signed up. If the two hashes match, the password was correct, and yet the server never kept the actual password around. If the database leaks, an attacker gets a pile of hashes rather than a pile of passwords. That is a much better starting position, though as we will see it is not the end of the story. If the idea of one way functions is new to you, the broader picture lives in what is cryptography.
One way, in theory
You cannot reverse a hash mathematically. But you can guess. An attacker takes a huge list of likely passwords, hashes each one, and looks for a matching output in the stolen data. When a guess hashes to a stored value, that password is cracked. This guessing game is what every technique below is designed to slow down. Hashing does not make guessing impossible, it makes it expensive.
Problem one: identical passwords look identical
Here is the first crack in the plan. If you hash passwords plainly, two users who happened to choose the same password will get the exact same hash. An attacker who steals the database can see that at a glance, which already leaks information. Worse, attackers do not even need to guess from scratch. They can prepare, well in advance, a gigantic lookup table that pairs millions of common passwords with their hashes. This is called a rainbow table. Steal a database of plain hashes and you simply look each one up in the table, cracking huge numbers of accounts in a single pass with almost no effort.
The fix is a salt. A salt is a unique random value added to each password before it is hashed. Because the salt is different for every user, the same password produces a completely different hash for each person.
hash( "correcthorse" + "a9f3..." ) -> one result
hash( "correcthorse" + "7c1b..." ) -> a totally different resultThe salt is stored right alongside the hash, and, perhaps surprisingly, it does not need to be secret. Its job is not to be hidden but to be unique. Because each user has their own random salt, a precomputed rainbow table is useless: the attacker would need a separate table for every single salt, which is wildly impractical. Salting also hides the fact that two users picked the same password, since their stored hashes now look nothing alike. Every password gets its own fresh salt, generated randomly at sign up.
Salt is about uniqueness, not secrecy
A common beginner worry is that storing the salt next to the hash defeats the point. It does not. The salt is not trying to keep anything hidden. It is there to make every stored hash one of a kind, so that attackers cannot reuse precomputed work across users or across different sites. Even if the attacker sees every salt, they still have to attack each password separately.
Problem two: fast hashes are too fast
Now for the mistake that catches almost everyone out, because it feels so counterintuitive. Hashes like SHA-256 and MD5 are excellent at their real jobs, such as verifying that a downloaded file has not been corrupted or tampered with. They are engineered to be blisteringly fast, so a computer can fingerprint an enormous file in an instant. That speed is a virtue for file checks. For passwords, it is a disaster.
Think about who benefits from speed. When a user logs in, your server hashes their password exactly once. That is a single operation. An attacker who has stolen your database, on the other hand, wants to hash billions of candidate passwords to find matches. A modern graphics card, the same kind used for gaming and video, can compute billions of SHA-256 hashes every second. So even with a unique salt on every account, a fast hash lets an attacker chew through common and weak passwords at a terrifying rate. The salt stopped them reusing precomputed tables, but it did nothing to slow down each individual guess.
Speed helps the attacker, not you
You hash a password once when a user logs in. The attacker hashes it billions of times while guessing. Any speed you gain from a fast hash, the attacker gains far more of, because they are doing the operation on an industrial scale. For passwords, slowness is not a bug, it is the whole point. You want each hash to be just slow enough that a single login feels instant to a human but attacking millions of guesses becomes painfully expensive.
The answer: slow, deliberate hashing
The solution is to use hashing algorithms designed specifically for passwords, which are deliberately, tunably expensive. They let you dial up how much work each hash costs, so as attackers buy faster hardware you simply turn the dial higher. The names worth knowing are these.
- bcrypt is battle tested and has been trusted for decades. It carries a cost factor you can increase over time as computers get quicker. It remains a solid, sensible default.
- scrypt is also memory hard, which means it deliberately demands a large amount of memory as well as time. This matters because specialised cracking hardware is good at doing many cheap calculations at once but struggles when each calculation also needs lots of memory.
- Argon2 is the modern winner of the Password Hashing Competition, a public contest run to find the best password hashing design. It is memory hard and highly tunable, and its Argon2id variant is the recommended choice for new systems today.
- PBKDF2 is older and only compute hard rather than memory hard, so it is easier for specialised hardware to attack, but it is still acceptable where a library, standard, or compliance requirement calls for it.
The common thread is the work factor, sometimes called the cost parameter. It is a setting that controls how much time and, for the memory hard options, how much memory each hash consumes. You tune it so that a single login takes a small fraction of a second on your server, which no human will ever notice, yet becomes ruinously slow when multiplied across billions of attacker guesses.
Never invent your own scheme
A tempting beginner shortcut is to run a fast hash several times in a loop, or to mix in a clever secret ingredient of your own design, and call it good enough. Resist this. Real password hashing algorithms have been scrutinised by experts for years and handle subtle issues like timing and memory correctly. A homemade scheme almost always has a weakness the author cannot see. Reach for a well studied library that implements Argon2id, bcrypt, or scrypt, and let it do the work.
Putting it together
Good password storage fits in a single sentence: hash each password with a unique random salt using a slow, memory hard algorithm like Argon2id, with a work factor tuned as high as your hardware comfortably tolerates.
store: argon2id( password + unique_salt, cost ) + the saltThat combination is the difference between a stolen database being a minor, contained incident and being an outright catastrophe. With plain fast hashes and no salt, an attacker cracks most accounts within hours. With salted, slow, memory hard hashing tuned sensibly, even a determined attacker may only ever recover the handful of users who chose genuinely terrible passwords, and everyone with a decent one stays safe.
It is worth adding that hashing is the last line of defence, not the only one. Strong unique passwords, a password manager to remember them, and multi factor authentication all reduce the damage further. But when a breach does happen, and eventually one always does, proper hashing is what stands between the attacker and your users' real passwords.
See it from the other side
This is exactly why long random passwords defeat offline attacks like Kerberoasting, where a stolen ticket is cracked at full graphics card speed with no server to slow the attacker down. Read the Kerberoasting writeup to see the same maths working in the attacker's favour, and you will understand from both directions why slow hashing and strong passwords matter so much.
The takeaway
Never store the password itself, store a hash of it. Add a unique random salt to every password so identical passwords do not produce identical hashes and rainbow tables become useless. Then, and this is the step people miss, use a hash built for passwords rather than a fast general purpose one, because speed helps the attacker far more than it helps you. Argon2id is the modern recommendation, with bcrypt and scrypt as trusted alternatives, all tuned with a work factor high enough to hurt a mass guessing attack while staying invisible to a real user. Get those three pieces right, unique salt, slow algorithm, and sensible work factor, and a leaked database goes from a five alarm fire to a manageable inconvenience.