How to Store Passwords Securely: bcrypt, Argon2, and scrypt Explained
Meta Description: Learn how to hash passwords securely with bcrypt, Argon2, and scrypt. Understand why MD5 and SHA are wrong for passwords and implement secure storage.
Target Keywords: bcrypt generator, password hashing, bcrypt vs argon2, secure password storage, scrypt
In 2024, a major company lost 150 million password hashes in a breach. Because they used SHA-1, attackers cracked 90% of them within 72 hours.
That same breach, with bcrypt, would have yielded maybe 0.1% of passwords—and taken years.
The algorithm matters. Here's how to choose the right one.
Why MD5 and SHA Are Wrong for Passwords
"But SHA-256 is secure!" Yes, for file integrity. Not for passwords. Here's why:
The Speed Problem
| Algorithm | Hashes/Second (GPU) | Time to Crack 8-char Password |
|---|---|---|
| MD5 | ~180 billion | < 1 minute |
| SHA-256 | ~10 billion | ~30 minutes |
| bcrypt (cost 12) | ~30,000 | ~300 years |
SHA-256 is too fast. Attackers can try billions of guesses per second. Password hashing algorithms are intentionally slow.
The Rainbow Table Problem
Precomputed tables map common passwords to hashes:
password123 → 482c811da5d5b4bc6d497ffa98491e38 (MD5)
Without salting, one table cracks millions of accounts. Password hashes require unique salts per password.
The Right Way: Purpose-Built Algorithms
Password hashing algorithms are designed with:
- Slowness: Configurable work factor makes hashing slow
- Built-in salt: Each password gets unique random salt
- Memory hardness: Some require significant RAM, defeating GPU attacks
- Resistance: Can't be sped up with custom hardware
bcrypt: The Battle-Tested Standard
Released: 1999 Based on: Blowfish cipher Status: ✅ Secure, widely deployed
How bcrypt Works
- Generate random 16-byte salt
- Use password + salt as Blowfish key
- Encrypt magic string "OrpheanBeholderScryDoubt" 64 times
- Output: algorithm + cost + salt + hash
bcrypt Output Format
$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/..0J4mMLdHn.J3Hqy
│ │ │ │
│ │ │ └── Hash (31 chars)
│ │ └── Salt (22 chars)
│ └── Cost factor (4-31, higher = slower)
└── Algorithm version (2b = current)
bcrypt in Practice
Node.js:
import bcrypt from 'bcrypt';
// Hash password
const saltRounds = 12;
const hash = await bcrypt.hash('user-password', saltRounds);
// Verify password
const match = await bcrypt.compare('user-password', hash);
Python:
import bcrypt
# Hash password
password = b'user-password'
salt = bcrypt.gensalt(rounds=12)
hash = bcrypt.hashpw(password, salt)
# Verify password
if bcrypt.checkpw(password, hash):
print("Password correct!")
Choosing bcrypt Cost Factor
| Cost | Hashes/sec (single core) | Recommended For |
|---|---|---|
| 10 | ~10 | Development/testing |
| 12 | ~2-3 | Web applications |
| 14 | ~0.5 | High security |
| 16+ | <0.1 | Ultra-sensitive |
Rule of thumb: Hash should take 250ms-500ms on your server.
bcrypt Limitations
- 72-byte password limit: Passwords longer than 72 bytes are truncated
- CPU-bound only: Doesn't use memory, vulnerable to GPU attacks (though still slow)
- Older design: Newer algorithms have additional protections
scrypt: Memory-Hard Protection
Released: 2009 Status: ✅ Secure, used in cryptocurrency
How scrypt Works
scrypt requires significant memory to compute, defeating GPU and ASIC attacks:
- Generate large memory buffer from password + salt
- Mix buffer contents repeatedly
- Output hash from final buffer state
GPUs have many cores but limited memory per core. scrypt turns this weakness against attackers.
scrypt Parameters
scrypt(password, salt, N, r, p, keyLength)
| Parameter | Meaning | Typical Value |
|---|---|---|
| N | CPU/memory cost (power of 2) | 16384 (2^14) |
| r | Block size | 8 |
| p | Parallelization | 1 |
| keyLength | Output length | 32 |
scrypt in Practice
Node.js:
import { scrypt, randomBytes } from 'crypto';
import { promisify } from 'util';
const scryptAsync = promisify(scrypt);
// Hash password
const salt = randomBytes(16);
const hash = await scryptAsync('user-password', salt, 64, {
N: 16384,
r: 8,
p: 1
});
Python:
import hashlib
# Hash password
hash = hashlib.scrypt(
b'user-password',
salt=os.urandom(16),
n=16384,
r=8,
p=1,
dklen=64
)
scrypt Use Cases
- Cryptocurrency wallets (Litecoin, Dogecoin)
- Disk encryption
- High-security password storage
Argon2: The Modern Standard
Released: 2015 Won: Password Hashing Competition Status: ✅ Recommended for new projects
Argon2 Variants
| Variant | Best For | Protection Against |
|---|---|---|
| Argon2d | Cryptocurrency | GPU/ASIC attacks |
| Argon2i | Password hashing | Side-channel attacks |
| Argon2id | General use (recommended) | Both attack types |
Use Argon2id for password hashing. It combines the strengths of both variants.
Argon2 Parameters
argon2id(password, salt, memory, iterations, parallelism, hashLength)
| Parameter | Meaning | OWASP Recommendation |
|---|---|---|
| memory | Memory cost (KB) | 46 MB (46080 KB) |
| iterations | Time cost | 1 |
| parallelism | Threads | 1 |
| hashLength | Output length | 32 bytes |
Argon2 in Practice
Node.js:
import argon2 from 'argon2';
// Hash password
const hash = await argon2.hash('user-password', {
type: argon2.argon2id,
memoryCost: 46080, // 46 MB
timeCost: 1,
parallelism: 1
});
// Verify password
const valid = await argon2.verify(hash, 'user-password');
Python:
from argon2 import PasswordHasher
ph = PasswordHasher(
memory_cost=46080, # 46 MB
time_cost=1,
parallelism=1
)
# Hash password
hash = ph.hash('user-password')
# Verify password
try:
ph.verify(hash, 'user-password')
print("Password correct!")
except:
print("Invalid password")
Argon2 Output Format
$argon2id$v=19$m=46080,t=1,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
│ │ │ │ │
│ │ │ └── Salt └── Hash
│ │ └── Parameters (memory, time, parallelism)
│ └── Version
└── Algorithm
Comparison Table
| Feature | bcrypt | scrypt | Argon2id |
|---|---|---|---|
| Released | 1999 | 2009 | 2015 |
| Memory-hard | ❌ | ✅ | ✅ |
| GPU resistant | ⚠️ Moderate | ✅ Strong | ✅ Strong |
| ASIC resistant | ❌ | ✅ | ✅ |
| Password limit | 72 bytes | None | None |
| Side-channel resistant | ✅ | ❌ | ✅ (Argon2id) |
| Ecosystem | Excellent | Good | Growing |
| OWASP recommended | ✅ Yes | ✅ Yes | ✅ Primary |
Which Should You Use?
Use Argon2id When:
- ✅ Starting a new project
- ✅ Can use recent libraries
- ✅ Want best available security
- ✅ OWASP/NIST compliance matters
Use bcrypt When:
- ✅ Maintaining existing systems using bcrypt
- ✅ Language/platform lacks good Argon2 support
- ✅ Need battle-tested, widely audited algorithm
- ✅ Password limit of 72 bytes is acceptable
Use scrypt When:
- ✅ Already using scrypt in your stack
- ✅ Need memory-hard without Argon2 support
- ✅ Cryptocurrency-related applications
Implementation Checklist
✅ Never store plain-text passwords
✅ Use bcrypt, scrypt, or Argon2id—never MD5/SHA for passwords
✅ Don't implement crypto yourself—use established libraries
✅ Use sufficient work factors (bcrypt cost 12+, Argon2 46MB+ memory)
✅ Upgrade work factors over time as hardware gets faster
✅ Hash on server side, never client side (prevents hash-the-hash attacks)
✅ Rate limit login attempts (prevent online brute force)
Generate Password Hashes
Test password hashing with our tools:
- bcrypt Generator — Generate bcrypt hashes
- Argon2 Generator — Generate Argon2id hashes
- Password Hash Checker — Verify passwords against hashes
FAQ
Can I migrate from MD5 to bcrypt without resetting all passwords?
Yes, using lazy migration:
- Keep old MD5 hashes
- On login, verify with MD5
- If valid, hash with bcrypt and update database
- Over time, most active users migrate automatically
What cost factor should I use for bcrypt?
Start with 12 and measure. Aim for 250-500ms hash time on your production server. Increase as hardware improves.
Is Argon2 supported in my language?
Yes, for most languages: JavaScript (argon2 npm), Python (argon2-cffi), Go (golang.org/x/crypto/argon2), PHP (password_hash), Java (Bouncy Castle), Ruby (argon2 gem).
Should I pepper passwords too?
A pepper is a secret added to all passwords before hashing. It adds defense-in-depth but complicates key rotation. Use if your threat model requires it.
How do I increase bcrypt cost for existing users?
On successful login, check if hash uses old cost, re-hash with new cost, and update database. Same lazy migration as algorithm changes.
Related Tools:
- bcrypt Generator — Secure password hashing
- Hash Generator — Generate various hashes
- Password Strength Checker — Test password strength