<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Password Hashing

Password Hashing

If your user database leaks (and you should plan as if it will), the damage depends entirely on how you stored passwords. Store them so that even with the full database in hand, an attacker can't recover the passwords.

That rules out two tempting options:

  • Plaintext: a leak is an instant, total compromise. Never.
  • Encryption: reversible by design. Whoever has the database probably has the key too (it lives on the same servers). Also wrong.

The answer is hashing : a one-way function. You store the hash, and on login you hash the submitted password and compare. You never store, and can never recover, the original.

Hashing is not encryption

These two get conflated constantly, so it's worth being precise. Both transform data into something unreadable, but they answer different questions:

  • Encryption is two-way. It exists so that someone with the key can get the original data back. That's the whole point: protect data in transit or at rest, then decrypt it when you need it. encrypt(data, key) always has a matching decrypt(ciphertext, key).
  • Hashing is one-way. There is no key and no unhash(). A hash function compresses any input down to a fixed-size digest, destroying information along the way. The only way to "reverse" it is to guess inputs and check.

The distinction matters for passwords because of what each one requires you to protect. An encrypted password is only as safe as the key, and the key has to live somewhere your application can reach it, which usually means somewhere the attacker who stole your database can reach it too. A hashed password has no key to steal. Even you, with full access to everything, can't recover it.

The rule of thumb: encrypt data you need to read back (a stored OAuth token, a user's saved payment details), hash data you only ever need to verify (passwords). If you find yourself wanting to decrypt a password, that's a design smell, since verification never requires the original.

Try it · ~5 minutes

SHA-256

Hash a password

Type below and watch each value get hashed. Changing even one character flips roughly half the output — the avalanche effect. Characters that differ from the hash above are highlighted; the last 4 values are kept.

Password → SHA-256 hash

Why salt matters

Alice and Bob both chose the password above. Without salt their hashes are identical, so cracking one cracks both — and a rainbow table cracks them instantly. Turn on salt to break the symmetry.

Alice

no salt — raw password only

Bob

no salt — raw password only

Different hashes: same password, but each user must be attacked individually.

Not just any hash

The obvious choice, a fast hash like MD5 or SHA-256, is wrong here, for two reasons:

  • Fast is bad. Those hashes are built to be fast, so an attacker with a GPU can try billions of guesses per second against a leaked hash.
  • No built-in salt. All hash functions are deterministic. That's what makes them usable for verification at all. But when you hash the raw password with nothing else, everyone who picked password123 ends up with the same hash, so cracking one cracks them all, and precomputed rainbow tables map common hashes straight back to their inputs. A bare MD5 or SHA-256 call gives you nothing to break that symmetry.

Two ingredients fix this:

  • Salt : a random value generated per user and stored alongside the hash. It makes every hash unique even for identical passwords, which defeats rainbow tables and hides the fact that two users chose the same password.
  • A slow, adaptive hash: bcrypt , scrypt, or Argon2id . These are deliberately expensive and have a tunable work factor you raise as hardware gets faster, so brute force stays impractical for years. Argon2id is the current OWASP-recommended choice for new systems (winner of the Password Hashing Competition, resistant to both GPU cracking and side-channel attacks). bcrypt at cost factor 12 or higher remains safe and widely supported; the key is using any of these rather than a fast hash.

How to use it

A good password library generates the salt, applies the work factor, and embeds both in the output string, so you store one value and never manage the salt by hand:

import bcrypt from "bcrypt";

// On sign-up: 12 is the work factor; the salt is generated and stored inside `hash`
const hash = await bcrypt.hash(plainPassword, 12);
// Store `hash`. It looks like: $2b$12$Nf3...<salt+digest>

// On login: bcrypt reads the salt and work factor back out of the stored hash
const ok = await bcrypt.compare(submittedPassword, hash);

When you need to raise the work factor later, do it on next login: the stored hash records the factor it was made with, so you can detect old hashes and re-hash transparently.

Comparing secrets in constant time

There's a subtler attack on the comparison itself. The obvious way to check a token or a hash is ===, but string comparison short-circuits: it returns false at the first byte that differs. That means a near-miss takes microscopically longer than an early mismatch, and an attacker who can measure response times can use that signal to recover a secret one byte at a time. This is a timing attack .

The defense is a constant-time comparison that always examines every byte, so the time taken reveals nothing about how much of the guess was right:

import crypto from "node:crypto";

function safeEqual(expected, supplied) {
  const expectedBuf = Buffer.from(expected);
  const suppliedBuf = Buffer.from(supplied);
  if (expectedBuf.length !== suppliedBuf.length) return false;  // length isn't secret
  return crypto.timingSafeEqual(expectedBuf, suppliedBuf);      // compares all bytes, always
}

Use this anywhere you compare a user-supplied value against a secret: API tokens, password-reset tokens, and webhook HMAC signatures (the APIs guide builds on exactly this). Password libraries like bcrypt already compare in constant time internally, so this matters most for the secrets you compare yourself.

Check your understanding

Question 1 of 2

Why is unsalted SHA-256 nearly as bad as plaintext?

Your database leaks. You stored passwords with SHA-256 and no salt. Why is that nearly as bad as plaintext for common passwords?