In 2024, security researchers are still finding production databases with passwords stored in plain text — and it's not always small applications
The LinkedIn breach of 2012 exposed 117 million passwords stored as unsalted SHA-1 hashes. RockYou2024, published in 2024, compiled nearly 10 billion plaintext passwords from multiple breaches accumulated over years. Adobe (2013) stored passwords with 3DES encryption using the same key for all passwords — not even a hash. These aren't ancient history; they're the inevitable result of specific, well-understood, still-repeated mistakes.
Mistake 1: Plain-text storage
The most damaging mistake: storing passwords exactly as users type them.
# WRONG — never do this
def create_user(username, password):
db.execute("INSERT INTO users (username, password) VALUES (?, ?)",
username, password) # 'password' is the literal string
Why it still happens: tutorials that prioritise getting something working quickly; copy-pasted code from answers that omit security; developers who assume the database will never be breached; legacy codebases written before the developer knew better.
What attackers do with it: immediately use every credential. They don't need to crack anything. The breach-to-exploitation pipeline is instantaneous.
Mistake 2: Unsalted fast hashes (MD5, SHA-1, SHA-256)
# WRONG — fast hash without salt
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
Why MD5 and SHA-1 are wrong for passwords:
- They're designed to be fast — a GPU can compute billions per second
- Without salt, identical passwords produce identical hashes
- Pre-computed rainbow tables exist for billions of common passwords
The LinkedIn 2012 breach in numbers:
- 6.5 million (later revealed to be 117 million) unsalted SHA-1 hashes exposed
- Within days of the breach, 90%+ of the hashes were cracked
- Single GPU can compute approximately 10 billion SHA-1 hashes per second
- A dictionary of 1 billion common passwords takes approximately 1.7 minutes to exhaust
Mistake 3: Salted fast hashes
# STILL WRONG — salt helps but fast hash is still wrong
import hashlib, os
salt = os.urandom(16).hex()
password_hash = hashlib.sha256((salt + password).encode()).hexdigest()
# Stores salt + hash together
Why this is still wrong: the salt prevents rainbow tables and ensures different hashes for identical passwords — correct so far. But SHA-256 is still fast. At 10+ billion hashes per second on a GPU, a unique salt per user means the attacker must crack each hash independently, but each individual hash is still crackable quickly for short/common passwords.
An 8-character password from lowercase letters and digits has 36⁸ ≈ 2.8 trillion possibilities. At 10 billion SHA-256 attempts per second: 2.8 trillion ÷ 10 billion = 280 seconds to exhaust the entire space. Most passwords are shorter, more common, and fall much faster.
The correct approach: purpose-built slow password hashing
Password hashing functions (PHFs) are deliberately slow — designed to be computationally expensive regardless of the attacker's hardware.
bcrypt:
import bcrypt
# Hashing (store the result)
password = b"user_password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# Verification
bcrypt.checkpw(password, hashed) # True
At cost factor 12: approximately 0.25 seconds per hash on modern server hardware. An attacker with a GPU gets approximately 25,000 attempts per second (not billions). An 8-character password space now takes: 2.8 trillion ÷ 25,000 = 112 million seconds ≈ 3.5 years per user, per GPU.
Argon2 (the current recommended default):
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # number of iterations
memory_cost=65536, # 64 MB of memory
parallelism=4 # parallel threads
)
hashed = ph.hash("user_password")
ph.verify(hashed, "user_password") # True
Argon2 won the Password Hashing Competition (2015) and is the algorithm recommended by OWASP. Its memory cost parameter is particularly important: it forces attackers to use large amounts of memory per attempt, preventing the massively parallel GPU attacks that cost-factor-only algorithms face.
NIST SP 800-63B guidance (2017, updated 2024):
- Use a salt of at least 32 bits (generated by a CSPRNG)
- Use an approved password-based key derivation function: bcrypt, scrypt, Argon2, or PBKDF2 with SHA-256 at 600,000+ iterations (for FIPS compliance contexts)
- Do NOT use MD5, SHA-1, SHA-256/512 without a proper KDF structure
Mistake 4: Symmetric encryption instead of hashing
Adobe (2013) encrypted passwords with 3DES using a single key, rather than hashing them. This is wrong for two reasons:
- Encryption is reversible — if an attacker gets the key (along with the database), they decrypt every password instantly. Bcrypt is not reversible.
- Same password = same ciphertext when using ECB mode (which Adobe used) — the frequency analysis that defeated Caesar ciphers applies here too. Password hints exposed which accounts shared passwords.
The rule: password verification does not require storing a reversible value. You only need to check whether the candidate matches the stored hash — you never need to recover the original. Therefore: hash, not encrypt.
Mistake 5: Truncating passwords before hashing
Some implementations truncate long passwords to 72 characters (a bcrypt implementation detail) or even shorter lengths:
# WRONG — some implementations silently truncate
hashed = bcrypt.hashpw(password[:16].encode(), bcrypt.gensalt())
bcrypt itself truncates at 72 bytes (a property of the underlying Blowfish cipher). If you need to support arbitrarily long passwords: pre-hash with SHA-256, base64-encode the result, then feed that to bcrypt:
import bcrypt, hashlib, base64
def hash_password(password: str) -> bytes:
# Pre-hash to handle arbitrary length; base64 ensures no null bytes
pre_hashed = base64.b64encode(hashlib.sha256(password.encode()).digest())
return bcrypt.hashpw(pre_hashed, bcrypt.gensalt(rounds=12))
How to use the Hash Generator on sadiqbd.com
The Hash Generator produces MD5, SHA-1, SHA-256, and SHA-512 hashes. These are appropriate for:
- File integrity verification — hash a file before and after transfer; mismatched hashes indicate corruption or tampering
- Data deduplication — hash file contents to detect duplicates without byte-by-byte comparison
- Checksums for build artifacts — verify downloaded software against published checksums
- Non-security MACs and fingerprints — generating identifiers for content
They are not appropriate for:
- Storing passwords (use bcrypt/Argon2 via the Bcrypt Generator)
- Security-sensitive MACs without a key (use HMAC-SHA256 instead)
Frequently Asked Questions
Is PBKDF2 acceptable for password hashing? Yes, with sufficient iterations. NIST SP 800-63B recommends PBKDF2 with SHA-256 and a minimum of 600,000 iterations. PBKDF2 is FIPS 140-2 approved, making it required in US government contexts. For most applications, Argon2id is preferred because its memory cost provides better resistance to GPU attacks.
How do I migrate an existing MD5/SHA-1 password database to bcrypt? On next login: take the user's plaintext password (they just submitted it), bcrypt-hash it, store the new hash, and flag the account as migrated. Users who don't log in: require password reset. Never try to decrypt or re-hash old hashes in bulk without the original plaintext — the whole point is that you can't.
Is the Hash Generator free? Yes — completely free, no sign-up required.
Try the Hash Generator free at sadiqbd.com — generate MD5, SHA-1, SHA-256, and SHA-512 hashes for file integrity and data fingerprinting.