Why General-Purpose Hashes Are Wrong for Passwords

SHA-256 and MD5 are designed to be fast — that's their purpose. For passwords, fast is bad. A modern GPU can compute 10 billion SHA-256 hashes per second. If your database is breached and passwords are SHA-256 hashed, an attacker can crack all common passwords in seconds using precomputed rainbow tables or brute-force.

Real numbers: An RTX 4090 can crack SHA-256 hashed passwords at ~22 billion hashes per second. The entire rockyou.txt wordlist (14 million passwords) can be tested in under 1 millisecond. bcrypt at cost=12 reduces this to ~200 hashes per second — 10 million times slower.

Password hashing algorithms are deliberately slow and memory-intensive. They include a configurable cost factor you increase over time as hardware gets faster, ensuring cracking remains expensive even years after your database is compromised.

Salting: Why It Matters

A salt is a random value added to the password before hashing. It ensures two users with the same password get different hashes, defeating precomputed rainbow tables and preventing an attacker from cracking all identical passwords at once.

All modern password hashing algorithms generate a unique salt automatically and include it in the output hash string. You do not need to manage salts separately — they are stored in the hash output. Never use a shared, static salt.

bcrypt

bcrypt has been the recommended password hashing algorithm for over 20 years. It uses the Blowfish cipher, has a configurable cost factor (typically 10-14), and has extensive library support in every major language.

  • Cost factor: Each increment doubles the computation time. Cost 10 ≈ 100ms, cost 12 ≈ 400ms, cost 14 ≈ 1.5s on modern hardware.
  • Limitation: bcrypt truncates passwords at 72 bytes. Passwords longer than 72 characters are all hashed identically past that point.
  • When to use: Legacy systems, or when Argon2 isn't available in your stack.
auth.py Python
from passlib.hash import bcrypt

# Hash a password (cost=12)
hashed = bcrypt.using(rounds=12).hash("user_password")
# → "$2b$12$..." — salt is embedded in the output

# Verify — automatically extracts and uses the stored salt
is_valid = bcrypt.verify("user_password", hashed)  # → True

Argon2

Argon2 won the Password Hashing Competition in 2015 and is the current OWASP recommendation. It has three variants: Argon2d (GPU resistance), Argon2i (side-channel resistance), and Argon2id (recommended — combines both).

Argon2 is tunable across three dimensions: time cost (iterations), memory cost (RAM usage), and parallelism. Memory-hardness is its key advantage over bcrypt — it forces attackers to use substantial RAM per crack attempt, making GPU/ASIC attacks significantly more expensive.

auth.py Python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

# OWASP-recommended parameters
ph = PasswordHasher(
    time_cost=2,       # iterations
    memory_cost=65536,  # 64 MB RAM per hash
    parallelism=1,
    hash_len=32,
    salt_len=16
)

hashed = ph.hash("user_password")

try:
    ph.verify(hashed, "user_password")  # True
except VerifyMismatchError:
    pass  # wrong password

Which to Choose

  • New application: Use Argon2id — it's the OWASP recommendation, memory-hard, and future-proof.
  • Existing bcrypt application: bcrypt is fine. No need to migrate unless you need the 72-byte limit fix or memory-hardness.
  • Resource-constrained environment: bcrypt uses minimal memory. Argon2id with reduced memory cost is still better than bcrypt.
  • Never use: MD5, SHA-1, SHA-256, SHA-512 for passwords. Also avoid PBKDF2 unless compliance requirements mandate it (FIPS).

Migrating Legacy Hashes

You cannot bulk-rehash passwords — you don't have the plaintext. The correct migration approach: on next login, verify the old hash, then rehash with the new algorithm and update the stored hash. After some time, any remaining old-format hashes belong to inactive accounts and can be invalidated.

Upgrade cost factor too: If you're already using bcrypt but with cost=10 from 2010, incrementally increase the cost on each login. Check if the stored hash was made with the old cost factor and rehash if so.

Find Weak Password Hashing in Your Codebase

AquilaX SAST detects MD5/SHA-1 usage in password contexts, missing salt generation, and insecure password storage patterns across your entire codebase.

Start Free Scan