# How Passwords Should Be Stored (Hashing)

> Never store passwords in plain text. Store a one-way hash made with a slow, salted, password-specific algorithm like bcrypt, scrypt, or Argon2 - so a stolen database doesn't hand attackers everyone's password.


---

# How Passwords Should Be Stored (Hashing)

There's a moment, the first time you build a login system, when you create the `users` table and reach
for a `password` column. It feels natural to drop the password straight in - you'll need it to check
logins, right? That instinct is the single most expensive mistake in account security, and almost
everyone has it before they're shown the alternative.

Here's the relief: the alternative isn't harder, just *different*, and once you see the idea it never
leaves you. You're going to learn why your database should never contain a single readable password -
yours, your users', anyone's - and exactly what to store instead, so that even an attacker who steals the
entire table walks away with nothing useful.

## How to read this
- **Just need the safe answer right now?** Jump to [Phase 3: Use a Slow Hash Built for Passwords](03-use-a-slow-hash.md) - it has the do-this pseudo-code for signup and login.
- **Want it to finally make sense?** Read in order. Each phase fixes a flaw the previous one left open, which is exactly how real-world password storage evolved.

## The phases
1. **[Hashing, Not Encrypting](01-hashing-not-encrypting.md)** - the core idea: store a one-way *hash* of the password, never the password itself, and never reversible encryption.
2. **[Salt (and Why Plain SHA-256 Isn't Enough)](02-salt-and-fast-hashes.md)** - why identical passwords need a per-user *salt*, and why fast hashes like SHA-256 are the wrong tool.
3. **[Use a Slow Hash Built for Passwords](03-use-a-slow-hash.md)** - bcrypt, scrypt, and Argon2: deliberately slow, salted, tunable, and battle-tested. Use a library; never roll your own.

> Deeper material - multi-factor auth, session tokens, OAuth, and rotating leaked credentials at scale - belongs to its own guides. This one does one thing well: get the password out of your database safely. For *who* a logged-in user is allowed to be, see [Authentication vs Authorization](/guides/auth-vs-authz).


---

# Hashing, Not Encrypting

Picture the worst day: someone copies your entire `users` table - a leaked backup, a SQL injection bug, a
misconfigured cloud bucket. It happens to careful teams. The question that decides whether this is an
embarrassing incident or a catastrophe is simple: **when the attacker opens that table, can they read
people's passwords?**

If the answer is yes, it's a catastrophe - not just for your site, but for every other site where your
users reused that password (and most people reuse passwords). If the answer is *no, the table is full of
useless gibberish*, you've turned a disaster into a manageable cleanup. This phase is about how to be in
that second world. The whole trick is one idea: **never store the password - store a one-way hash of it.**

## What a hash actually is

**What it actually is.** A **hash function** is a one-way blender. You feed it any input - a word, a
password, a whole book - and it produces a fixed-size scrambled string called a **hash** (or *digest*).
The defining property: it's a one-way street. Going from password to hash is fast and easy. Going *back*,
from hash to the original password, is designed to be effectively impossible.

📝 **Terminology.** A **hash function** takes input and produces a fixed-length fingerprint of it. The
output is the **hash** or **digest**. "Hash the password" means "run the password through this function and
keep the output."

**What it does in real life.** The same input always produces the same output, and a tiny change to the
input produces a wildly different output. Here's the shape of it, using a common hash function so you can
see what a digest looks like:

```console
$ echo -n "hunter2" | sha256sum
f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7  -

$ echo -n "hunter3" | sha256sum
fb8c2e2b85ca81eb4350199faddd983cb26af3064614e737ea9f479621cfa57a  -
```
*What just happened:* `sha256sum` ran each password through the SHA-256 hash function (`echo -n` just
feeds it the text without a trailing newline). Two things to notice. First, the output is a fixed length
no matter the input. Second, changing one character - `hunter2` to `hunter3` - produced a completely
different hash, with no resemblance to the first. There's no "close"; there's no pattern an attacker can
follow back to the original.

Run this yourself - it's the same hash function, in Python's standard library. Change the password and
watch the entire digest change:

```python runnable
import hashlib

for password in ["hunter2", "hunter3"]:
    digest = hashlib.sha256(password.encode()).hexdigest()
    print(f"{password} -> {digest}")
```

## Why this protects a stolen database

**The mental model.** You never write the password to disk. Instead:

```mermaid
flowchart LR
  subgraph signup["SIGN UP"]
    s1["user types 'hunter2'"] --> s2[hash it] --> s3[("store the HASH<br/>(not the password)")]
  end
  subgraph login["LOG IN"]
    l1["user types 'hunter2'"] --> l2[hash it] --> l3{compare to stored hash}
    l3 -->|match| ok([let them in])
    l3 -->|no match| no([reject])
  end
  s3 -.-> l3
```

On signup you hash the password and store *only the hash*. On login you hash whatever the user just typed
and compare that new hash to the stored one. If they match, the passwords matched - without your server
ever keeping the password around. You verify the password without knowing it.

**Why this saves you later.** When that table leaks, the attacker gets a column of hashes. They can't run
the hash backward to recover `hunter2`, because the function only goes one way. The password the user
typed exists for a fraction of a second in memory during login and is then gone. There is nothing on disk
to steal. *That* is why hashing is the foundation of every responsible login system.

## Hashing is not encryption

This is the distinction that trips up nearly everyone new to this, so let's be precise about it.

📝 **Terminology.** **Encryption** is *reversible by design*: you scramble data with a key, and anyone
holding the key can unscramble it back to the original. It's for data you need to read again later - a
credit card you'll charge next month, a message the recipient must decrypt. **Hashing** is *one-way by
design*: there is no key, and nothing turns the hash back into the original.

⚠️ **Gotcha - do not "encrypt" passwords.** It sounds responsible, but it's the wrong tool, and it's
dangerous. Encryption means a key exists that turns every password back into plain text. Where does that
key live? On your servers, near the database. Steal the database *and* the key - and attackers who reach
one usually reach the other - and every password is instantly readable. You've added a lock and taped the
key to the door. Passwords are exactly the kind of data you *never* need to read back, so you should use
the tool that makes reading them back impossible: a hash.

⚠️ **Gotcha - never, ever store plain text.** No `password` column with the real password in it. Not "just
for now," not "only in the dev database," not "we'll fix it before launch." Dev databases get copied to
laptops; "before launch" becomes "after the breach." If you remember one sentence from this entire guide,
make it this one: **the readable password must never touch your disk.**

💡 **Key point.** Store a one-way *hash*, never the password and never a reversible encryption of it. To
check a login, hash the attempt and compare hashes. A stolen database should reveal exactly zero
passwords.

## Recap

1. A **hash function** is a one-way blender: easy to go password → hash, effectively impossible to go back.
2. On **signup**, store only the hash. On **login**, hash the attempt and compare to the stored hash.
3. A leaked table of hashes reveals no passwords - that's the whole point.
4. **Hashing is not encryption.** Encryption is reversible (a key exists); hashing is not. Passwords need the one-way tool.
5. **Never store plain text, and never encrypt passwords.** The readable password must never be on disk.

You now have the core idea. But there's a crack in it: hashing the same password always gives the same
hash - which an attacker can exploit. Next, we close that crack with a *salt*, and find out why the
fast hash we just used is the wrong one for passwords.

Watch it animated: [password hashing](/explainers/Hashing.dc.html)

## Try it yourself

Type a password and watch its real SHA-256 hash. Change one character - the whole thing changes:

```playground-hash
hunter2
```


---

# Salt (and Why Plain SHA-256 Isn't Enough)

So you're hashing now - already ahead of a frightening number of real systems. But the simple version from
Phase 1 has two weaknesses, and attackers have built an entire economy around both. Each has a clean,
well-understood fix, and understanding *why* they exist is what lets you recognize a safe setup when you
see one. We'll take them in order: identical passwords hashing identically, then the deeper problem that
the hash we used is far too fast.

## Problem 1: identical passwords produce identical hashes

**Why people get this wrong.** The thing that *feels* like a feature of hashing - "the same input always
gives the same output" - is also a leak. Look at what happens when two users happen to pick the same
password:

```text
   user  alice   password "summer2024"  ──►  hash  9c1f...e3
   user  bob     password "summer2024"  ──►  hash  9c1f...e3   ← identical!
   user  carol   password "p@ssw0rd"    ──►  hash  4b77...a9
```

**What it does in real life.** An attacker who steals this table doesn't even need to crack anything to
learn a lot. Identical hashes mean identical passwords, so they instantly know Alice and Bob share one.
Worse, attackers precompute giant lookup tables - every common password run through the hash function in
advance - and just *look up* your stored hash to find the password that made it.

📝 **Terminology.** A **rainbow table** is a precomputed map from hashes back to the passwords that
produce them. If your hash is in their table, the "one-way" function might as well be a dictionary lookup.
Building these tables is worth it precisely *because* the same password always hashes the same way for
everybody.

## The fix: a per-user salt

**What it actually is.** A **salt** is a chunk of random data, unique to each user, that you mix into the
password *before* hashing. You generate it once at signup and store it right alongside the hash (it's not
a secret - it just needs to be unique and random).

📝 **Terminology.** A **salt** is per-user random data combined with the password before hashing, so that
the same password produces a different hash for each user.

**What it does in real life.** Now Alice and Bob, with the same password, get different hashes - because
each was hashed with a different salt:

```text
   alice   "summer2024" + salt "x7Qe.."  ──►  hash  2a8c...11
   bob     "summer2024" + salt "Lp93.."  ──►  hash  d40f...e7   ← now different!
```

On login you fetch that user's salt, mix it into the password they typed, hash it, and compare - exactly
the same flow as before, with one extra ingredient pulled from the database.

**Why this saves you later.** A precomputed rainbow table is now worthless: the attacker would have to
rebuild the entire table separately *for every single user's salt*, which defeats the whole point of
precomputing. Identical passwords no longer reveal themselves. One small random value per user collapses
an entire category of attack.

💡 **Key point.** A unique, random **salt per user** makes identical passwords hash differently and kills
rainbow tables. The salt is stored next to the hash and isn't secret.

## Problem 2: SHA-256 is too *fast*

Here's the one that surprises people. We fixed the rainbow-table problem, but we used SHA-256 to do it,
and SHA-256 is the wrong hash for passwords - not because it's weak, but because it's **fast**. And for
password storage, fast is exactly what you don't want.

**Why people get this wrong.** Everywhere else in computing, a fast hash is good. SHA-256 and MD5 were
*designed* to be fast - they're built to fingerprint files and verify downloads at high speed. That
speed is a virtue there. For passwords, it's a gift to the attacker.

**What it does in real life.** Imagine the attacker has your salted hashes. They can't use a precomputed
table anymore (the salt saw to that), so instead they guess: take a candidate password, add the user's
salt, hash it, check for a match. Repeat. The only thing limiting them is how many guesses per second
they can compute - and with a fast hash on modern hardware, especially GPUs built for exactly this kind
of parallel work, that number is staggering. A password short or common enough will fall quickly when the
attacker can try guess after guess after guess at high speed.

> 📝 The exact guess rate depends entirely on the attacker's hardware and the specific hash, so any
> single number would be made up - but the direction is not in dispute: general-purpose hashes are *orders
> of magnitude* too fast to safely protect passwords. (See OWASP's Password Storage Cheat Sheet:
> <https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html>.)

**The mental model.** Think of it as a lock. SHA-256 is a lock that opens the instant the right key
touches it - and the attacker can try millions of keys without tiring. What you actually want is a lock
that takes a noticeable beat to turn *even with the right key*. For one legitimate login, a fraction of a
second is invisible. For an attacker trying to turn the lock billions of times, that same delay is a
wall. You don't want a *fast* hash. You want a *deliberately slow* one.

⚠️ **Gotcha.** "We salt our SHA-256 hashes" sounds secure, and salting is genuinely necessary - but it is
*not sufficient*. Salt defeats precomputed tables; it does nothing about raw guessing speed. A salted fast
hash is still a fast hash. You need both: a salt *and* a hash that's slow on purpose.

## Recap

1. **Identical passwords hash identically** with a plain hash - leaking that users share passwords and enabling **rainbow tables** (precomputed hash → password lookups).
2. A **per-user random salt**, mixed in before hashing and stored beside the hash, makes identical passwords hash differently and renders rainbow tables useless.
3. **Salt alone isn't enough.** Fast hashes like MD5 and SHA-256 let attackers guess enormous numbers of candidates per second, especially on GPUs.
4. You want a hash that is **deliberately slow** - invisible for one login, a wall for billions of guesses.

We now know the two properties a good password hash needs: it must be salted, and it must be slow. The
final phase introduces the algorithms built to do exactly that - and the safe way to use them.


---

# Use a Slow Hash Built for Passwords

We've arrived at the practical answer. You know *why* a password hash must be salted and slow; now you
just need the tools that do both correctly, and the handful of rules for using them safely. The reassuring
part: you don't build any of this yourself - the hard work has been done, vetted by cryptographers, and
packaged into a library for your language. Your job is to call it correctly.

If you skipped straight here, the cheat-card is right below. The rest of the phase explains each line so
you can trust it rather than just copy it.

## The cheat-card

| You want to… | Do this |
|---|---|
| Pick an algorithm | **Argon2** (Argon2id) if available; **bcrypt** is a fine, ubiquitous default; **scrypt** also good. |
| Hash on signup | `hash = passwordHasher.hash(plainPassword)` - the library generates the salt and embeds it. |
| Verify on login | `passwordHasher.verify(plainPassword, storedHash)` - returns true/false; do **not** compare strings yourself. |
| Choose the slowness | Tune the **work factor** so one hash takes a noticeable fraction of a second on your hardware. |
| Write your own crypto | **Don't.** Use the vetted library. |
| Store | Just the single hash string the library returns (salt + work factor are baked into it). |

## The three good algorithms

**What they actually are.** **bcrypt**, **scrypt**, and **Argon2** are *password hashing functions* -
hash functions designed specifically for storing passwords, not for general use. Each one builds in
exactly the two properties from the last two phases: it generates and embeds a salt for you, and it's
deliberately slow.

📝 **Terminology.** A **password hashing function** (also called a *key derivation function* in this
context) is built to be slow and salted on purpose, unlike a general-purpose hash like SHA-256.

**What they do in real life.** The defining feature is a **work factor** (sometimes called *cost* or
*rounds*): a dial controlling how much computation each hash takes. Turn it up and every hash gets slower.
Set it high enough that a single login costs a barely-noticeable fraction of a second - and that same cost,
multiplied across an attacker's billions of guesses, becomes prohibitive. As hardware gets faster, you
raise the work factor to keep pace.

📝 **Terminology.** The **work factor** (cost / rounds) is a configurable number that sets how slow the
hash is. Higher = slower = harder to brute-force. It's stored inside the resulting hash so verification
knows how much work to redo.

A quick, no-nonsense comparison so you can choose without agonizing:

| Algorithm | In one line | When to reach for it |
|---|---|---|
| **Argon2** (Argon2id) | Newest; winner of the Password Hashing Competition; resists GPU *and* memory-based attacks. | The current first choice when your library supports it. |
| **bcrypt** | Old, boring, everywhere, well-understood. | A safe, ubiquitous default - especially if it's already in your stack. |
| **scrypt** | Deliberately memory-hungry, which frustrates specialized cracking hardware. | A solid choice; common where it's already available. |

There's no wrong pick among these three - the wrong pick is anything *not* on this list. (OWASP's Password
Storage Cheat Sheet tracks current recommended parameters: <https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html>.)

## Hash on signup, verify on login

Here's the entire flow in annotated pseudo-code. It's intentionally generic - your library's function
names will differ, but the shape is universal.

```text
# ── SIGN UP ────────────────────────────────────────────────
function register(username, plainPassword):
    # the library generates a random salt AND applies the work
    # factor, then bundles salt + work factor + digest into ONE string
    storedHash = passwordHasher.hash(plainPassword)

    db.save(username, storedHash)     # store ONLY this string
    # never store plainPassword anywhere - not in a log, not in a variable you keep
```
*What just happened:* You called the library's `hash` function with the plain password. It minted a fresh
random salt, ran the slow hashing with your configured work factor, and returned a single self-contained
string that already includes both. You save that one string - no separate salt column to manage.

```text
# ── LOG IN ─────────────────────────────────────────────────
function login(username, plainPassword):
    storedHash = db.lookupHash(username)
    if storedHash is null:
        reject("invalid username or password")   # same vague message either way

    # verify() re-reads the salt + work factor from storedHash,
    # hashes the attempt the same way, and compares - in CONSTANT TIME
    if passwordHasher.verify(plainPassword, storedHash):
        accept()
    else:
        reject("invalid username or password")
```
*What just happened:* `verify` pulled the salt and work factor back out of the stored hash, applied the
identical slow hashing to the password just typed, and compared the result against the stored digest using
a constant-time comparison (more on that next). One call in, true or false out.

📝 **Terminology.** A **constant-time comparison** checks two values in a way that takes the same amount of
time whether they match at the first character or the last. A naive `==` can bail out early on the first
mismatched byte, and an attacker measuring tiny timing differences could slowly learn the correct value.
Good libraries' `verify` functions compare in constant time for you.

## The rules that keep this safe

⚠️ **Gotcha - never roll your own scheme.** The most dangerous instinct here is "I'll combine a few SHA-256
calls and a salt and call it good." Cryptography fails in subtle, invisible ways - it'll look like it works
perfectly while being trivially breakable. bcrypt, scrypt, and Argon2 exist because experts spent years
getting the details right. Use the vetted library for your language; this is the one area of programming
where *not* being clever is the senior move.

⚠️ **Gotcha - use the library's `verify`, not `==`.** Don't hash the attempt and compare the strings
yourself with `==`; that risks both the timing leak above and subtle format mismatches. The library's
verify function is built to re-derive and compare correctly. Let it.

⚠️ **Gotcha - a slow hash doesn't fix a weak password.** Argon2 protects `hunter2` exactly as well as it
protects a random 20-character passphrase - a common password is still guessed early no matter how slow
the hash. So at signup, also check the password against a list of known-breached and common passwords and
reject the worst ones - the "Have I Been Pwned" Pwned Passwords range API is the standard tool for this
(<https://haveibeenpwned.com/Passwords>). Strong storage and strong passwords are two different jobs.

💡 **Key point.** Reach for **Argon2, bcrypt, or scrypt** through a vetted library. Let it generate the
salt, set the **work factor** so a login takes a noticeable fraction of a second, store the single string
it returns, and verify logins with its **constant-time** verify function. Never invent your own scheme.

## Where this fits in the bigger picture

You've now got the password *stored* correctly: hashed, salted, slow, breach-checked, verified safely. But
storing the password is only half of a login system. Once you've confirmed *who* someone is - that's
**authentication** - there's a separate question of *what they're allowed to do*: **authorization**. Those
two are constantly confused, and confusing them causes its own breaches.

> ⏭️ Next, read [Authentication vs Authorization](/guides/auth-vs-authz) to see how proving identity
> (which a stored password is one way to do) differs from granting permissions.

## Recap

1. Use a **password hashing function** - **Argon2**, **bcrypt**, or **scrypt** - never a general-purpose hash.
2. The **work factor** dials in slowness; set it so a single login takes a noticeable fraction of a second, and raise it over the years.
3. **On signup**, call the library's `hash`; it makes the salt and returns one self-contained string. Store only that.
4. **On login**, call the library's `verify`; it re-derives and compares in **constant time**. Don't compare strings yourself.
5. **Never roll your own** crypto, and **add a breached/weak-password check** at signup - strong storage and strong passwords are separate jobs.

That's the complete, responsible way to store a password. Stored like this, even a full database leak
hands an attacker nothing they can practically use.
