Bỏ qua menu, vào nội dung chính

Băm mật khẩu: bcrypt hay Argon2, cost và salt

Password hashing đúng cách: bcrypt vs Argon2, cost factor, salt, pepper. Code Node.js, migration password cũ, timing-safe comparison.

3 phút đọc

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.

OWASP Password Storage Cheat Sheet
OWASP Password Storage Cheat Sheet (cheatsheetseries.owasp.org), ảnh chụp ngày 27/09/2026.

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 bcrypt
javascriptimport 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:

CostTimeKhi dùng
10~75msMobile, low-end server
12~300msDefault 2026
14~1.2sHigh-security app
bashnpm install argon2
javascriptimport 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.

Nhận bài mới

Để lại email để nhận bài mới của series Kiến thức lập trình, hoặc theo dõi bằng RSS.

RSS Kiến thức lập trình

Email chỉ dùng để gửi bài mới, xem Chính sách bảo mật.

Trao đổi về dự án

Cần tư vấn?