Bỏ qua đến nội dung chính
passwordhashingbcryptArgon2security

Password Hashing: bcrypt vs Argon2, Cost, Salt

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

Xuất bản 7 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.

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

npm install bcrypt
import 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

Argon2 — recommended cho mới

npm install argon2
import 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

// 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.

// ❌ Sai
if (storedHash === computedHash) // timing leak

// ✓ Đúng
import { timingSafeEqual } from 'node:crypto'
if (timingSafeEqual(Buffer.from(stored), Buffer.from(computed)))

Migration từ MD5/SHA1

async 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.

Zalo