Guide
Security
Password Storage

How Password Hashing Works

Why you must hash passwords, why MD5 and SHA-256 are the wrong tools, and how bcrypt, Argon2, and PBKDF2 actually protect your users.

Last updated: Feb 25, 2026

Part of the complete guide to encryption, hashing, and encoding.

Quick Answer

Password hashing is a one-way transformation that converts a plaintext password into a fixed-length string that cannot be reversed. Unlike encryption, there is no key. Secure password hashing uses algorithms like bcrypt, Argon2, or PBKDF2, which add a random salt and are deliberately slow to resist brute-force attacks at scale.

TL;DR

  • Never encrypt passwords — hash them. Encryption is reversible; hashing is not.
  • MD5 and SHA-256 are general-purpose hash functions, not password hashers. They are too fast — modern hardware can test enormous numbers of guesses per second.
  • Always use a unique random salt per password. This prevents precomputed lookup attacks.
  • Key stretching (work factor) makes each hash computation slow on purpose, making bulk cracking infeasible.
  • Use bcrypt, Argon2id, or PBKDF2-HMAC-SHA256. These are the three algorithms recommended by OWASP and NIST today.
  • Argon2id is the best default for new systems. bcrypt is widely supported and acceptable. PBKDF2 is required in FIPS environments.
  • Never implement password hashing from scratch. Use your framework's built-in library.

What Is Password Hashing?

Password hashing is the process of running a user's password through a one-way function that produces a fixed-length output called a hash or digest. The original password cannot be recovered from the hash — that is the point.

When a user sets a password, you store the hash. When they log in, you hash their input again and compare it to the stored hash. If they match, the password is correct. The plaintext password is never stored.

Registration flow:

password: "hunter2"

↓ generate random salt

salt: "$2b$12$abc123xyz..."

↓ bcrypt(password + salt, cost=12)

stored hash: "$2b$12$abc123xyz...KHGkjh8dkJLs"

Login flow:

input: "hunter2"

↓ extract salt from stored hash

↓ bcrypt(input + salt, cost=12)

compare → match ✓ (or reject ✗)

Why Passwords Must Not Be Encrypted

Encryption is reversible. If you encrypt passwords and store them, you also have to store or manage the decryption key somewhere. An attacker who obtains your database may also find the key — or exploit the system that holds it. The result: all passwords exposed.

With hashing, there is no key to steal. Even if an attacker copies your entire password database, they cannot reverse the hashes directly. They must guess passwords one at a time and check if the hash matches — which is why the hashing algorithm's speed matters enormously.

Never store passwords in any of these forms:

  • Plaintext
  • Encrypted (reversible)
  • Base64 encoded (trivially reversible)
  • MD5 or SHA-256 hash without salt (attackable in bulk)

Why MD5 and SHA-256 Are Not Enough for Passwords

MD5 and SHA-256 are general-purpose hash functions designed to be fast. That is the problem. Modern hardware can compute hundreds of millions or billions of SHA-256 hashes per second. If an attacker obtains your database of SHA-256 password hashes, they can systematically try common passwords at extreme speed.

Dedicated password hashers like bcrypt and Argon2 are intentionally slow. A single bcrypt operation at cost factor 12 takes roughly 250–400ms on a modern server. An attacker testing millions of guesses faces a genuine time barrier. Fast algorithms like SHA-256 offer no such barrier.

For a detailed comparison of MD5 and SHA-256 as general-purpose hashing tools, see MD5 vs SHA-256. Neither is appropriate for passwords.

AlgorithmSpeedSuitable for passwords?
MD5Billions/sec (GPU)No — cryptographically broken
SHA-256Hundreds of millions/secNo — too fast for passwords
bcrypt (cost 12)~4/sec per CPU coreYes
Argon2idConfigurable; memory-hardYes — recommended default

What Is Salting?

A salt is a random string generated uniquely for each password before hashing. The salt is combined with the password before the hash function runs, and then stored alongside the hash.

Salting solves two problems. First, it ensures that two users with the same password produce different stored hashes — an attacker cannot tell they share a password. Second, it defeats precomputed lookup tables (rainbow tables), since any precomputed table would need to include every possible salt value.

Modern password hashing libraries (bcrypt, Argon2, PBKDF2) handle salt generation automatically. The salt is embedded in the output hash string. You do not need to manage it separately.

Without salt — same password produces same hash (bad):

hash("password123") → ef92b778...

hash("password123") → ef92b778... ← attacker sees match

With salt — same password produces different hash (correct):

hash("password123" + salt1) → a3f1c92...

hash("password123" + salt2) → 7b2d41f... ← different

Key Stretching and the Work Factor

Key stretching is the technique of making a hash function intentionally slow by running it many times or requiring large amounts of memory. The parameter that controls this is called the work factor, cost factor, or iteration count depending on the algorithm.

The goal is to find a cost that is acceptable for legitimate logins (a few hundred milliseconds per user) but makes bulk offline attacks infeasible (millions of guesses would take years). Because hardware improves over time, you can increase the cost factor as needed — older hashes can be upgraded transparently when a user logs in.

bcrypt

Work factor: cost (4–31)

Default: cost 12

Mechanism: Exponential — each +1 doubles time

cost 12 ≈ 250–400ms

Argon2id

Work factors: time, memory, parallelism

Default: 3 iterations, 64MB

Mechanism: Memory-hard + time

Resists GPU/ASIC attacks

PBKDF2

Work factor: iteration count

Default: 600,000 (OWASP 2023)

Mechanism: Repeated HMAC

FIPS-compliant

Recommended Algorithms: bcrypt, Argon2, PBKDF2

OWASP and NIST recommend three algorithms for password storage. All three handle salting automatically and support configurable work factors. The choice depends on your environment and requirements.

For a detailed side-by-side comparison including parameter guidance and migration considerations, see bcrypt vs Argon2 vs PBKDF2.

bcrypt

Introduced in 1999, bcrypt is the most widely deployed password hashing algorithm. It has a strong track record and broad library support across every major language and framework. The cost factor doubles computation time with each increment.

Use when: you want a battle-tested default with maximum ecosystem compatibility. Reasonable for most web applications.

Limitation: bcrypt truncates passwords at 72 bytes. If you use bcrypt, either enforce a maximum password length or pre-hash with SHA-256 before passing to bcrypt (using a library that handles this correctly).

Argon2 (Argon2id recommended)

Argon2 won the Password Hashing Competition in 2015 and is the current OWASP first-choice recommendation for new systems. Argon2id (the hybrid variant) is memory-hard and time-hard, making it resistant to GPU-accelerated cracking and side-channel attacks simultaneously.

Use when: starting a new system and not constrained by FIPS requirements. Argon2id is the best technical choice today.

Parameters: OWASP recommends at minimum 19MB memory, 2 iterations, 1 degree of parallelism for interactive logins.

PBKDF2

PBKDF2 (Password-Based Key Derivation Function 2) is FIPS 140-2 compliant and required in US government and healthcare environments. It applies an HMAC function (typically HMAC-SHA256) iteratively. It is less resistant to GPU attacks than Argon2 because it is not memory-hard, so higher iteration counts are necessary.

Use when: FIPS compliance is required. Use PBKDF2-HMAC-SHA256 with at least 600,000 iterations (OWASP 2023).

Common Mistakes

Using SHA-256 or MD5 for password storage

Instead: Use bcrypt, Argon2id, or PBKDF2-HMAC-SHA256. These are designed for password storage; SHA-256 is not.

Using the same salt for all passwords

Instead: Generate a unique random salt for every password. Libraries handle this automatically — do not override it.

Setting the work factor too low for current hardware

Instead: Target ~300ms per hash on your production hardware. Benchmark and increase the cost factor accordingly. Revisit annually.

Comparing hashes with a plain equality check

Instead: Use your library's built-in constant-time comparison (bcrypt.compare, Argon2.verify). String equality short-circuits, leaking timing information.

Ignoring bcrypt's 72-byte password limit

Instead: If using bcrypt, be aware of the 72-byte limit. Either enforce a maximum password length or use a library that handles pre-hashing correctly.

Logging or transmitting plaintext passwords during debugging

Instead: Never log passwords. Hash them at the application layer before any I/O.

Try It: Bcrypt Hash Generator

Generate a bcrypt hash from a password and verify passwords against existing hashes. Runs entirely in your browser — nothing is uploaded. Useful for testing work factors and understanding the output format.

Open Bcrypt Hash Generator →

Frequently Asked Questions

Related Guides and Comparisons

Explore the Encryption Tools Hub for all related tools, guides, and comparisons in one place.