正在加载,请稍候…

Password Hashing Explained: bcrypt, SHA-256, Argon2, and What to Actually Use

Understand why password hashing is different from encryption, how bcrypt, SHA-256, PBKDF2, and Argon2 work, and which algorithm to choose for storing passwords securely in 2026.

Hashing vs Encryption: A Critical Distinction

Before choosing an algorithm, understand what you're actually doing.

Encryption is reversible — you can decrypt ciphertext back to plaintext with the right key. You'd use encryption to protect data you need to read later: credit card numbers, medical records, private messages.

Hashing is one-way — a hash function takes input and produces a fixed-size output (the hash or digest), and there's no algorithm to reverse it. You cannot go from the hash back to the original password.

Why is one-way better for passwords? Because you don't need to know the user's password — you only need to verify it. When a user logs in, you hash what they typed and compare it to the stored hash. If they match, the passwords match. The actual password never needs to be stored or recovered.

If someone steals your database, they get hashes — not passwords. They'd have to crack each hash individually.

Why Not Just Use SHA-256?

SHA-256 is a cryptographic hash function designed for speed. It can compute billions of hashes per second on modern hardware, especially with GPU acceleration. That's great for verifying file integrity. It's catastrophic for password storage.

An attacker with a leaked database of SHA-256 password hashes can run dictionary attacks and rainbow table attacks at staggering speed:

  • A modern GPU can compute ~10 billion SHA-256 hashes per second
  • An 8-character password with lowercase letters and digits has ~2.8 trillion combinations
  • At 10 billion/second, cracking all combinations takes about 4.6 minutes

SHA-256 is explicitly not designed for password hashing. Using it for passwords is a well-known security mistake.

The same applies to MD5 (even faster, also broken for other reasons) and raw SHA-1/SHA-512.

What Makes a Good Password Hashing Algorithm?

Good password hashing algorithms are designed to be slow — deliberately, tunable, and in ways that help defenders more than attackers.

Key properties:

  1. Slow by design — Each hash should take 100–300ms on your hardware. Fast for a legitimate user login; painfully slow when an attacker needs to try millions of guesses.

  2. Work factor adjustable — As hardware gets faster, you need to increase the cost. Good algorithms let you tune the cost parameter.

  3. Salt included — A random value mixed into each hash before computing, unique per password. This prevents rainbow table attacks (precomputed hash tables) and ensures two users with the same password get different hashes.

  4. Memory-hard (ideal) — Some algorithms require significant RAM per computation. GPUs have many cores but limited memory bandwidth per core, making memory-hard algorithms especially resistant to GPU cracking.

The Algorithms

bcrypt

Designed specifically for password hashing in 1999. Still the most widely deployed password hashing algorithm in the world.

How it works: Takes a password and a random 16-byte salt. Uses the Blowfish cipher key schedule (known to be expensive) iteratively. The cost parameter N means the algorithm runs 2^N iterations.

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/lfz3Fkxtu6RNWz9tK
 ^^  ^^                              
 |   cost factor (12 = 2^12 = 4096 rounds)
 version identifier

Cost factor guide:

Cost Approx time on modern server Use case
10 ~100ms Minimum for production
12 ~300ms Recommended default
14 ~1 second High-security accounts
16 ~4 seconds Admin accounts, financial

Limitation: bcrypt truncates passwords at 72 bytes. A password longer than 72 characters gets truncated silently. This rarely matters in practice but is worth knowing.

// Node.js
const bcrypt = require('bcrypt');
const saltRounds = 12;

// Hash
const hash = await bcrypt.hash(plainTextPassword, saltRounds);

// Verify
const match = await bcrypt.compare(plainTextPassword, storedHash);
# Python
import bcrypt

# Hash
password = b'mysecretpassword'
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))

# Verify
bcrypt.checkpw(password, hashed)  # True or False

PBKDF2

Password-Based Key Derivation Function 2. Defined in RFC 8018. Built into many standard libraries and compliance frameworks (NIST-approved, FIPS 140 compatible).

How it works: Applies a pseudorandom function (typically HMAC-SHA256 or HMAC-SHA512) repeatedly, using the iteration count as the work factor.

Iteration count guide (2026):

  • OWASP recommends: 600,000 iterations with HMAC-SHA256
  • NIST SP 800-63B recommends: at least 10,000 (older guidance, aim higher)
import hashlib
import os

# Hash
salt = os.urandom(32)
key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 600000)

# Store: salt + key (both as hex or bytes)

PBKDF2's weakness: it's not memory-hard. GPUs can parallelize it efficiently. At equivalent speed settings, bcrypt and Argon2 are harder to crack with specialized hardware.

Argon2

The winner of the Password Hashing Competition (2015). The current gold standard, recommended by OWASP for new systems.

Three variants:

  • Argon2d — Maximizes resistance to GPU cracking, susceptible to side-channel attacks. For cryptocurrency.
  • Argon2i — Resistant to side-channel attacks. For password hashing in most apps.
  • Argon2id — Hybrid of both. Recommended default.

Configurable parameters:

  • Memory cost (m) — RAM usage in kilobytes. Minimum 64 MB, recommend 128 MB+.
  • Time cost (t) — Number of iterations. Start at 3.
  • Parallelism (p) — Number of parallel threads. Match your server's CPU cores.
# Python (argon2-cffi)
from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=3, memory_cost=131072, parallelism=4)  # 128 MB

# Hash
hash = ph.hash("mysecretpassword")

# Verify
try:
    ph.verify(hash, "mysecretpassword")  # Returns True
except Exception:
    pass  # Wrong password
// Node.js (argon2)
const argon2 = require('argon2');

const hash = await argon2.hash('mysecretpassword', {
  type: argon2.argon2id,
  memoryCost: 131072,  // 128 MB
  timeCost: 3,
  parallelism: 4,
});

const valid = await argon2.verify(hash, 'mysecretpassword');

scrypt

Designed by Colin Percival in 2009. Memory-hard before Argon2 existed. Still widely used and secure.

Parameters: N (CPU/memory cost), r (block size), p (parallelism). OWASP recommends: N=65536, r=8, p=1 as a minimum.

const crypto = require('crypto');
const salt = crypto.randomBytes(32);

crypto.scrypt('password', salt, 64, { N: 65536, r: 8, p: 1 }, (err, derivedKey) => {
  // derivedKey is the hash
});

Which Algorithm Should You Use?

Scenario Recommendation
New application (2026) Argon2id
Platform with FIPS compliance required PBKDF2 with HMAC-SHA512, 600k iterations
Adding password hashing to an existing system bcrypt (widely supported, proven)
Migrating from MD5/SHA1 Any of the above — immediately
Need to hash many passwords per second Tune the cost down (not below safety thresholds)

Never use: MD5, SHA-1, SHA-256, SHA-512, plain or unsalted for password storage.

Migrating from Insecure Hashes

If you have a database of MD5 or SHA-1 password hashes, migrate without requiring a mass password reset:

  1. Add a new column password_hash_v2 (nullable)
  2. On successful login (when you have the plaintext password): compute bcrypt/Argon2 hash, store in v2
  3. Check v2 first on login; fall back to v1 if v2 is null
  4. After 90 days, force a password reset for accounts still on v1
  5. Drop the v1 column

This lets you upgrade silently for active users and handle the rest with a forced reset.

Common Mistakes

Storing plaintext passwords — The most catastrophic mistake. Used by ~30% of breached companies according to public breach reports.

Using fast hash functions — MD5, SHA-1, SHA-256 for password storage. Fast is bad here.

Forgetting the salt — Unsalted bcrypt is far weaker. All good libraries handle salting automatically — don't implement it manually.

Using the same salt for all passwords — The salt must be unique per password, generated randomly each time.

Not re-hashing on login — If you increase the cost factor over time, re-hash the password (using the new cost factor) when the user successfully logs in. Many libraries handle this with a "needs rehash" check.

// bcrypt: re-hash if cost factor changed
const currentHash = getHashFromDB(userId);
if (await bcrypt.compare(password, currentHash)) {
  if (bcrypt.getRounds(currentHash) < TARGET_ROUNDS) {
    const newHash = await bcrypt.hash(password, TARGET_ROUNDS);
    updateHashInDB(userId, newHash);
  }
  // login success
}

→ Use the Bcrypt Tool to hash and verify passwords in the browser — useful for testing cost factors and verifying existing hashes.