Hashing & Salting Explained

Every time you log in somewhere, your password goes through a pair of transformations designed so that even the people running the server cannot read it. Here's how it works.

Why not just store the password?

The simplest approach — storing passwords exactly as users type them — is called plaintext storage. If an attacker ever gains access to the database (through a breach, a leaked backup, a misconfigured server), they instantly have every user's password. Because people reuse passwords across sites, a single breach can compromise accounts far beyond the original service.

Real-world impact

The 2012 LinkedIn breach exposed ~117 million passwords. Because many were stored without adequate protection, cracking tools recovered a large portion of them within days and the plaintext passwords circulated for years afterward.

The solution is to never store the password itself — only a transformed version of it that can be verified but not reversed.

What is hashing?

A hash function is a one-way mathematical operation. It takes any input — a word, a sentence, a file — and produces a fixed-length string of characters called a digest or hash. Two key properties make it useful for passwords:

Deterministic

The same input always produces the same output. Run SHA-256 on "correct horse battery staple" and you will always get the same 64-character hex string, on any machine, at any time.

One-way (pre-image resistant)

Given the hash, it is computationally infeasible to work backwards to the original input. There is no "unhash" function. The only way to find the input is to guess and check.

Here's what the same password looks like after hashing with SHA-256:

Input:   hunter2
SHA-256: f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7

Input:   hunter2  (identical)
SHA-256: f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7

Input:   hunter3  (one character different)
SHA-256: 89e01536ac207279409d4de1e5253e01ea85473516576ee3d8c40c2b0e19b9a8

Notice the last example: changing a single character produces a completely different hash. This is called the avalanche effect and it is intentional — it prevents attackers from inferring anything about the original input from small hash differences.

How login verification works

  1. Registration. User creates a password. The server runs it through a hash function and stores only the resulting digest. The original password is discarded.
  2. Login attempt. User types their password. The server hashes the input using the same algorithm.
  3. Comparison. The server compares the new hash to the stored hash. If they match, the password was correct — without the server ever seeing the original.
password123 → SHA-256 → ef92b778bafe771... (stored)

The problem with hashing alone

Hashing solves plaintext storage, but introduces a new vulnerability: because the same input always produces the same hash, attackers can pre-compute hashes for enormous lists of common passwords and simply look up the original in a table. These are called rainbow tables.

A rainbow table for SHA-256 covering the top 10 million common passwords can be downloaded for free. If your database is breached and passwords were hashed without any additional protection, an attacker can crack a significant portion in seconds just by matching hashes.

The identical-hash problem

If two users both choose the password sunshine, their stored hashes are identical. An attacker who cracks one has cracked all of them simultaneously — and can identify which users share passwords just by looking at the database.

This is exactly the gap that salting is designed to close.

What is a salt?

A salt is a random string of characters generated uniquely for each user at the time they set their password. Before hashing, the salt is combined with the password. The salt is then stored in the database alongside the hash — it is not secret.

Its only job is to make every hash unique, even when two users have identical passwords.

sunshine + a3f9k2 → SHA-256 → d4e8c1f2... (stored)
sunshine + zq7m4r → SHA-256 → b91a77e3... (stored)

Two users, same password, completely different hashes. An attacker cannot tell they share a password, pre-computed rainbow tables are useless (they would need a unique table per user), and cracking one account gives no information about any other.

Salts are not secret — and that's fine

The salt is stored in plaintext next to the hash. An attacker with full database access can see it. This is intentional and acceptable: the salt's purpose is not confidentiality, it is uniqueness. Knowing the salt only means the attacker must run their dictionary attack against one account at a time, starting from scratch for each one. That is the point.

How the database row looks

A typical record stores three things: the username, the salt, and the hash of (salt + password). Nothing else is needed to verify a login.

username | salt         | hash
─────────┼──────────────┼─────────────────────────────────────
alice    | a3f9k2mxqr   | d4e8c1f29a77b3...
bob      | zq7m4rwp8n   | b91a77e3c20f14...
charlie  | 9kp2lv0xmf   | 51c3a8de77402b...

Not all hash functions are equal

General-purpose hash functions like SHA-256 or MD5 are designed to be fast — useful for checksums and digital signatures, but dangerous for passwords. A modern GPU can compute billions of SHA-256 hashes per second, which means brute-force attacks are fast even with a salt.

Password hashing algorithms are purpose-built to be deliberately slow. They introduce a configurable work factor (or cost factor) that controls how much computation is required per hash. The idea: making login take 100ms is unnoticeable to a real user, but multiplies an attacker's cost by a factor of millions.

Algorithm Built-in salt Speed Recommendation
MD5 No ~10 billion/sec (GPU) Avoid
SHA-256 No ~3 billion/sec (GPU) Avoid for passwords
bcrypt Yes ~20,000/sec (GPU) Widely accepted
Argon2id Yes Tunable (memory-hard) Recommended today
scrypt Yes Tunable (memory-hard) Strong choice

Modern algorithms like Argon2id go further: they are memory-hard, meaning they require large amounts of RAM to compute. This defeats GPU and ASIC-based attacks, since specialised hardware typically has limited per-core memory. Argon2id is the winner of the 2015 Password Hashing Competition and the current recommendation from OWASP.

What a bcrypt hash looks like

bcrypt stores everything in a single string: the algorithm version, cost factor, the 22-character salt, and the 31-character hash — all you need to verify a future login.

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/Lewg9W4K9iBp7iKeq
 ↑   ↑  ↑──────────────────────────────────────────────────────↑
 alg cost        salt + hash (combined, 53 chars)

See it in action

Type a password below to watch a simulated salting and hashing process. A random salt is generated on each keystroke to illustrate uniqueness. This uses a simplified browser-based SHA-256 — real systems use bcrypt or Argon2id.

Random salt (generated per user)
Salt + password (concatenated input)
SHA-256 hash (what is stored)

Putting it all together

  1. User sets a password. The server generates a long, cryptographically random salt unique to this user.
  2. Salt is prepended (or appended) to the password. The combined string is the actual input to the hash function.
  3. A slow password hash function runs. Argon2id, bcrypt, or scrypt transforms the combined string into a digest. This takes ~100ms intentionally.
  4. Salt and hash are stored; password is discarded. The original password never touches persistent storage.
  5. Login: retrieve salt, re-hash, compare. The server fetches the user's salt, hashes the provided password with it, and compares to the stored hash. No plaintext is ever stored or transmitted to the server's database.

The key insight

Hashing makes passwords unreadable. Salting makes each hash unique. A slow algorithm makes brute-force impractical. Together, they mean a stolen database is not a stolen password list.


For further reading: OWASP Password Storage Cheat Sheet · NIST SP 800-63B · RFC 9106 (Argon2)