Password hashing sai = DB leak thành rainbow table attack. Bài này code đúng cách Node.js: bcrypt, Argon2, migration, timing-safe comparison.

Vì sao không SHA-256?
SHA-256 thiết kế để nhanh - GPU crack 1 tỷ hash/giây. Password 8 char common -> crack trong giờ. bcrypt/Argon2 thiết kế CHẬM CHỦ ĐỊNH - và memory-hard (Argon2) chống ASIC.
bcrypt - proven 30 năm
bashnpm install bcryptjavascriptimport bcrypt from 'bcrypt'
const SALT_ROUNDS = 12 // 2^12 iterations, ~250ms
// Hash khi đăng ký
const hash = await bcrypt.hash(password, SALT_ROUNDS)
// hash = "$2b$12$N9qo8uLOickgx2ZMRZoMye..." (60 chars, có salt + cost)
await db.users.create({ email, password_hash: hash })
// Verify khi login
const user = await db.users.findByEmail(email)
const ok = await bcrypt.compare(password, user.password_hash)
if (!ok) return res.status(401).end()Benchmark cost factor:
| Cost | Time | Khi dùng |
|---|---|---|
| 10 | ~75ms | Mobile, low-end server |
| 12 | ~300ms | Default 2026 |
| 14 | ~1.2s | High-security app |
Argon2 - recommended cho mới
bashnpm install argon2javascriptimport argon2 from 'argon2'
const hash = await argon2.hash(password, {
type: argon2.argon2id, // chống GPU + side-channel
memoryCost: 65536, // 64MB
timeCost: 3, // iterations
parallelism: 4,
})
const ok = await argon2.verify(hash, password)Argon2id memory-hard -> ASIC/GPU không có lợi thế lớn. Win password hashing competition 2015. NIST khuyến cáo cho project mới.
Pepper - defense layer
javascript// Secret từ env, KHÔNG lưu DB
const PEPPER = process.env.PASSWORD_PEPPER // 32+ char random
function pepperedPassword(password) {
return crypto.createHmac('sha256', PEPPER).update(password).digest('hex')
}
// Hash
const hash = await argon2.hash(pepperedPassword(password))
// Verify
const ok = await argon2.verify(hash, pepperedPassword(password))DB leak: attacker có hash + salt nhưng không có pepper -> crack cực khó. Trade-off: rotate pepper khó (toàn user phải reset).
Timing-safe comparison
bcrypt.compare và argon2.verify đã timing-safe. Đừng tự code === - leak thông tin qua thời gian.
javascript// ❌ Sai
if (storedHash === computedHash) // timing leak
// ✓ Đúng
import { timingSafeEqual } from 'node:crypto'
if (timingSafeEqual(Buffer.from(stored), Buffer.from(computed)))Migration từ MD5/SHA1
javascriptasync function login(email, password) {
const user = await db.users.findByEmail(email)
if (!user) return null
// Detect hash type
if (user.password_hash.startsWith('$argon2') || user.password_hash.startsWith('$2b$')) {
// bcrypt/Argon2 - verify
return await argon2.verify(user.password_hash, password) ? user : null
}
// Legacy MD5/SHA1
const legacyHash = crypto.createHash('md5').update(password).digest('hex')
if (legacyHash !== user.password_hash) return null
// Login OK with legacy → upgrade hash
const newHash = await argon2.hash(password)
await db.users.update(user.id, { password_hash: newHash })
return user
}User không biết đang được migrate. Sau 6-12 tháng, force password reset user chưa login.
Checklist password storage
- Argon2id (mới) hoặc bcrypt cost ≥12
- KHÔNG SHA-256, MD5, SHA-1
- Pepper từ env cho high-value app
- Min length password 12+ char (NIST 2024)
- Block password trong rainbow list (haveibeenpwned API)
- Rate limit /login để chống brute-force
- MFA cho admin/payment
Kết luận
Password hashing đúng = Argon2id hoặc bcrypt cost 12. Combine pepper + rate limit + MFA cho defense-in-depth. Đừng tự code crypto. Đọc JWT authentication để hiểu phần auth còn lại của hệ thống.





