Password hashing protects users by storing a one-way, salted, deliberately slow fingerprint of each password instead of the password itself, so a stolen database does not hand attackers everyone’s credentials. Done correctly, even the service that authenticates you never keeps your actual password, and an attacker who steals the database faces an expensive, per-password uphill battle rather than an instant jackpot. Getting there requires three ideas working together: never storing plaintext, never using a fast hash, and always using a per-user salt with a slow key derivation function. This is a server-side process, and understanding it explains a lot of security advice that otherwise sounds arbitrary.
Why can’t you just store the password?
You cannot store plaintext passwords because a single database breach would then expose every user’s actual password, and because people reuse passwords across sites. If your database stores hunter2 in a column and that database leaks, attackers do not just get into your service; they try the same email and password on banks, email providers, and everything else. Storing plaintext turns your breach into everyone else’s breach.
The natural next thought is to encrypt the passwords. That is better than plaintext but still wrong, because encryption is reversible by design. Reversibility requires a key, and that key has to live somewhere your servers can reach it. If an attacker who steals your database also steals or guesses the key, every password decrypts at once. You have added a lock but kept the key in the same building.
Hashing solves this differently. A cryptographic hash is one-way: it turns the password into a fixed-length value that cannot feasibly be reversed. You never store the password at all. When a user logs in, you hash what they typed and compare it to the stored hash. If they match, the password was correct, and yet the stored value reveals nothing that can be turned back into the original. There is no key to steal because there is no decryption step.
Why are fast hashes like MD5 and SHA-256 the wrong choice?
General-purpose hashes are wrong for passwords precisely because they are fast. MD5, SHA-1, and SHA-256 were built to hash large amounts of data quickly, which is a virtue for checksums and a liability for passwords. If an attacker steals a database of SHA-256 password hashes, the speed of SHA-256 lets them try enormous numbers of guesses per second on ordinary hardware.
Passwords are a soft target for guessing. They are short, drawn from human habits, and heavily reused, so attackers do not brute-force the entire space; they run wordlists of common passwords and known leaks. Against a fast hash, this is cheap. The math is unforgiving: the faster your hash, the more guesses per second an attacker gets, and the more of your users’ passwords fall.
# The problem in one line:
# fast hash -> attacker tries billions of guesses/sec
# slow hash -> attacker tries far fewer guesses/sec
#
# Same wordlist. The only variable you control
# is how expensive each single guess is.
So the goal flips. For a checksum you want speed. For a password you want each individual hash to be slow, so that a legitimate login costs a fraction of a second but an attacker’s billion-guess campaign becomes prohibitively expensive. That is a property you have to choose on purpose, which is what password hashing functions provide.
What does a salt do, and why is it non-negotiable?
A salt is a unique, random value generated for each user and stored next to their hash, and it defeats an entire class of shortcut attacks. Without a salt, identical passwords produce identical hashes. That means an attacker can precompute the hashes of common passwords once, into a giant lookup table (a rainbow table), and instantly match them against your whole database. It also means that if two users share a password, you can see it in the identical stored values.
Salting breaks both. Because each user’s salt is different, the same password hashes to a different value for every user. Precomputed tables become useless, since they would have to be recomputed for every distinct salt. And the attacker can no longer attack all users at once; they must attack each salted hash separately, multiplying their cost by the number of accounts.
| Scenario | Without salt | With unique salt |
|---|---|---|
| Two users, same password | Identical stored hashes | Different stored hashes |
| Precomputed rainbow tables | Work directly | Useless |
| Attacking many accounts | All at once | One at a time |
The salt does not need to be secret; it is fine to store it in plaintext alongside the hash. Its job is uniqueness, not secrecy. What it buys you is that an attacker cannot amortize their effort across users or across a shared table, which is a large part of what makes offline cracking expensive.
What are bcrypt, scrypt, and Argon2?
They are key derivation functions built specifically for passwords, combining a salt with a tunable, deliberate slowness. Rather than hashing once, they apply a costly transformation whose expense you control through a work factor. As hardware gets faster, you raise the work factor, keeping each guess expensive indefinitely without switching algorithms.
- bcrypt is a long-established password hashing function with a cost parameter that sets how much work each hash takes. It handles the salt for you and remains a solid, widely supported choice.
- scrypt adds memory-hardness: it deliberately requires a configurable amount of memory, which blunts attackers who use specialized hardware to parallelize fast hashes. Making each guess need memory as well as time raises the cost of large-scale cracking rigs.
- Argon2 is a modern password hashing function designed to be tuned along time, memory, and parallelism. Its memory-hard variants are a strong default for new systems.
The shared idea is a tunable cost, often called the work factor. You pick a setting where a single legitimate login takes a small but real amount of time, imperceptible to a user, yet that same cost applied to billions of attacker guesses becomes impractical. The salt makes each hash unique; the slowness makes each guess expensive; together they turn a database leak from a disaster into a bounded, costly problem.
Where does the browser fit in, and where does the server?
Password hashing itself is a server-side operation, and it is important not to blur that line. The server is what stores the salted hash and what verifies a login by re-hashing the submitted password and comparing. This work has to happen somewhere the stored hashes live and where the comparison can be trusted, and that is the backend, not the browser. If you take one operational fact from this article, it is that the slow, salted hashing described here runs on the server.
The browser’s role is upstream: helping users choose a strong, unique password in the first place. Hashing protects a password from offline cracking, but a weak or reused password can still be guessed or exposed elsewhere. A long, random password raises the bar so far that even a somewhat under-tuned hash holds up, and it removes the reuse problem entirely. That client-side half is where a tool like our password generator helps: it creates strong random passwords entirely in your browser, so the generated secret is never transmitted anywhere. It pairs naturally with the different-but-related job of hashing, which we contrast with fast checksums in MD5 vs SHA-256.
Put the pieces together and the picture is coherent. Never store plaintext, because a breach would expose real credentials. Never rely on a fast hash, because speed hands attackers guesses. Always salt, so each hash is unique and precomputation fails. And always use a slow, tunable KDF, so every guess costs an attacker real time and memory. A strong password from the client and correct salted, slow hashing on the server are two halves of the same defense, and both matter.