How should you actually hash passwords, and why is SHA-256 alone wrong?
SHA-256 is a fast, general-purpose hash built for integrity checks, not for secrets — which is exactly why attackers can try billions of guesses per second against it.
LEARN BY DOINGPut your intuition to work3 steps
How should you actually hash passwords, and why is SHA-256 alone wrong?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Why "just hash it" isn't the whole answer
Hashing a password before storing it is table stakes — nobody should store plaintext passwords. But "hash it with SHA-256" is a trap, because SHA-256 was designed to be fast. It's a general-purpose cryptographic hash meant for things like verifying file integrity or building Merkle trees, where speed is a feature. For password storage, speed is the vulnerability: modern GPUs and ASICs can compute billions of SHA-256 hashes per second, which means an attacker who steals a database of SHA-256(password) values can brute-force an enormous fraction of them — especially anything not perfectly random — in hours, not centuries.
#Two problems SHA-256 alone doesn't solve
No built-in salt. If two users pick the same password, plain SHA-256 produces the same hash for both, which is exactly what lets precomputed rainbow tables work: an attacker builds one giant lookup table of hash → common password once, then cracks every matching row in every leaked database forever after. A salt — a random value stored alongside the hash and mixed into the input — makes every hash unique even for identical passwords, which kills rainbow tables and forces the attacker back to brute-forcing one password at a time.
No tunable cost. Even with a salt, a raw SHA-256 hash is still fast to compute, so an attacker with a leaked hash and salt can still try tens of billions of guesses per second on commodity GPUs. What password storage actually needs is a hash that is deliberately, adjustably slow — and ideally hard to parallelize on specialized hardware — so that hardware getting faster over time doesn't automatically make cracking easier.
#bcrypt: the mature default
const bcrypt = require('bcrypt');async function registerUser(email, plainPassword) {const SALT_ROUNDS = 12; // cost factor — raise it as hardware gets fasterconst passwordHash = await bcrypt.hash(plainPassword, SALT_ROUNDS);// e.g. "$2b$12$KIXQ2r7z5r1z8N9m1s2H1uJ4h7q..." — algorithm version,// cost, salt, and hash are all packed into this one 60-char string.await db.users.insert({ email, passwordHash });}async function login(email, plainPassword) {const user = await db.users.findByEmail(email);if (!user) return null; // don't reveal whether the email existsconst valid = await bcrypt.compare(plainPassword, user.passwordHash);return valid ? user : null;}
bcrypt.hash generates a fresh random salt internally and embeds it in the output string, so there's no separate salt column to manage — and no way to accidentally forget to salt.
#argon2id: the current best practice
const argon2 = require('argon2');async function registerUser(email, plainPassword) {const passwordHash = await argon2.hash(plainPassword, {type: argon2.argon2id, // hybrid mode: resists GPU cracking AND// side-channel timing attacksmemoryCost: 19456, // ~19 MB of RAM per hash attempttimeCost: 2,parallelism: 1,});await db.users.insert({ email, passwordHash });}async function login(email, plainPassword) {const user = await db.users.findByEmail(email);if (!user) return null;try {return (await argon2.verify(user.passwordHash, plainPassword)) ? user : null;} catch {return null; // malformed hash string — fail closed}}
Argon2's defining feature over bcrypt is memory-hardness: cracking it fast requires not just raw compute, but tens of megabytes of RAM per parallel guess, which is exactly the resource GPUs and ASICs are worst at scaling. That's why OWASP currently recommends argon2id as the first choice for new systems, with bcrypt as a solid, battle-tested fallback where argon2 isn't available.
#Putting it side by side
| SHA-256 (alone) | bcrypt | argon2id | |
|---|---|---|---|
| Designed for | Integrity checks, not secrets | Password storage | Password storage |
| Raw speed | Billions of hashes/sec on a GPU | Deliberately slow, tunable rounds | Deliberately slow and memory-hard |
| Salt | Not built in — easy to forget or reuse | Built into the output | Built into the output |
| Resists GPU/ASIC cracking | No | Somewhat | Yes — large tunable memory cost |
| Tunable as hardware improves | No | Yes (rounds) | Yes (memoryCost, timeCost, parallelism) |
| Recommended for passwords today | No | Yes | Yes — current OWASP first choice |