Có một nghịch lý ở tim của việc lưu mật khẩu: mọi thứ khác trong lập trình, ta muốn nhanh; riêng băm mật khẩu, ta muốn chậm — và chậm có chủ đích. Dùng SHA-256 hay MD5 để băm mật khẩu (một sai lầm vẫn còn phổ biến) chính là dùng nhầm công cụ, vì chúng được thiết kế để nhanh, và "nhanh" ở đây nghĩa là kẻ tấn công dò được hàng tỉ mật khẩu mỗi giây. Bài này (phần 18, khép lại loạt Mật mã) đo thật bằng Go để thấy vì sao Argon2 — hàm băm mật khẩu hiện đại, chậm và tốn bộ nhớ có chủ đích — là câu trả lời đúng.

Vì sao hàm băm nhanh là thảm hoạ cho mật khẩu

Khi CSDL người dùng bị lộ (chuyện xảy ra thường xuyên), kẻ tấn công cầm trong tay các hash mật khẩu và bắt đầu dò ngược: thử hàng loạt mật khẩu phổ biến, băm chúng, so với hash lộ ra. Nếu bạn băm bằng SHA-256, cuộc dò này nhanh khủng khiếp — SHA-256 sinh ra để băm dữ liệu tốc độ cao, và GPU hiện đại băm được hàng tỉ lần mỗi giây. Mọi mật khẩu trong danh sách phổ biến bị bẻ trong tích tắc.

Băm mật khẩu cần điều ngược lại: chậm (mỗi lần thử tốn thời gian đáng kể) và khó song song hoá (GPU/ASIC không thể nhân tốc độ). Đó chính xác là những gì Argon2 được thiết kế để làm.

Argon2id: chậm cộng "bộ nhớ cứng"

Argon2 (bản khuyến nghị là Argon2id) có ba tham số điều chỉnh chi phí:

  • t (time cost): số vòng lặp — càng nhiều càng chậm.
  • mem (memory cost): lượng RAM mỗi lần băm chiếm, tính KiB — đây là điểm mấu chốt.
  • par (parallelism): số luồng song song.
argon2.IDKey(pw, salt, t, mem, par, 32)  // Argon2id, ra 32 byte

Tính bộ nhớ cứng (memory-hard) là vũ khí chính. Mỗi lần băm chiếm hàng chục MiB RAM. GPU có hàng nghìn lõi tính toán, nhưng thiếu RAM để cấp cho mỗi lõi vài chục MiB cùng lúc — nên chúng không thể chạy nghìn phép băm song song như với SHA-256. Chi phí bộ nhớ vô hiệu hoá đúng lợi thế của phần cứng dò mật khẩu.

Ảnh chụp đoạn mã nền tối minh hoạ Argon2 vì sao băm mật khẩu phải chậm và tốn bộ nhớ có chủ đích, sai lầm dùng hàm băm nhanh SHA-256 MD5 cho mật khẩu SHA-256 thiết kế để nhanh GPU băm hàng tỉ lần mỗi giây lộ CSDL kẻ tấn công dò mọi mật khẩu phổ biến trong tích tắc băm mật khẩu cần điều ngược lại chậm và khó song song hoá, Argon2id chậm cộng bộ nhớ cứng memory-hard argon2 IDKey pw salt t mem par 32 t số vòng lặp time cost mem bộ nhớ dùng tính KiB memory cost điểm mấu chốt par số luồng song song mỗi lần băm chiếm hàng chục MiB RAM GPU ASIC có nghìn lõi nhưng thiếu RAM cho mỗi lõi không nhân song song được nữa, luôn kèm salt cộng so hằng thời gian salt random 16 byte mỗi mật khẩu một salt chặn rainbow table subtle ConstantTimeCompare h h2 so kết quả băm tham số khuyến nghị OWASP 2024 Argon2id t2 mem 19 MiB par 1 chỉnh mem t sao cho mỗi lần băm vài chục ms trên server bạn

Hình 1: Băm mật khẩu cần chậm và khó song song, ngược với SHA-256. Argon2id điều chỉnh bằng t (vòng), mem (bộ nhớ — vũ khí chống GPU), par (luồng); luôn kèm salt ngẫu nhiên và so kết quả bằng hàm hằng thời gian.

Đo thật: Argon2 chậm hơn SHA-256 gần 559.000 lần

Mình đo bằng Go với tham số khuyến nghị OWASP (Argon2id, t=2, mem=19 MiB, par=1):

Ảnh chụp bảng kết quả chạy thật Go x crypto argon2 output thật, một băm Argon2id OWASP t2 mem 19 MiB par 1 salt IVX4 hash YBI6EXY thời gian băm 1 lần 18.3 ms chậm có chủ đích, hai xác minh so hằng thời gian đúng mật khẩu match true sai mật khẩu match false, ba tăng bộ nhớ vòng thời gian tăng chỉnh chi phí kẻ tấn công t1 mem 8 MiB 3.7 ms t2 mem 19 MiB 22.1 ms t3 mem 64 MiB 120.9 ms t4 mem 128 MiB 323.9 ms, bốn SHA-256 trần vì sao không dùng cho mật khẩu 1 lần SHA-256 32.8 ns khoảng 31 triệu lần mỗi giây mỗi lõi Argon2id t2 19MiB chậm hơn SHA-256 khoảng 559 nghìn lần SHA nhanh mồi ngon cho GPU dò mật khẩu Argon2 thì không

Hình 2: Chạy thật — Argon2id băm 1 lần mất 18,3 ms; xác minh đúng/sai cho true/false; tăng bộ nhớ 8→128 MiB kéo thời gian 3,7 → 323,9 ms; SHA-256 chỉ 32,8 ns/lần (~31 triệu lần/giây/lõi), tức Argon2id chậm hơn ~559.000 lần.

Ba điều rút ra, tất cả đo thật:

  • Một lần băm SHA-256 mất 32,8 ns — khoảng 31 triệu lần/giây trên một lõi CPU (GPU còn nhanh hơn nhiều bậc). Đây là vì sao SHA-256 là "mồi ngon" cho dò mật khẩu.
  • Argon2id với tham số OWASP mất 18,3 ms — chậm hơn SHA-256 khoảng 559.000 lần. Với người dùng thật đăng nhập, 18 ms là không đáng kể; với kẻ dò hàng triệu mật khẩu, nó là bức tường.
  • Tăng mem/t kéo dài thời gian tuyến tính: 3,7 ms (8 MiB) → 22,1 ms (19 MiB) → 120,9 ms (64 MiB) → 323,9 ms (128 MiB). Bạn tự chỉnh chi phí này để cân giữa trải nghiệm người dùng và độ khó cho kẻ tấn công.

Hai thứ không bao giờ được quên: salt và so hằng thời gian

Argon2 mạnh nhưng chỉ đúng khi dùng kèm hai thứ:

  • Salt ngẫu nhiên mỗi mật khẩu (16 byte trong demo): khiến hai người dùng cùng mật khẩu có hash khác nhau, và vô hiệu hoá "rainbow table" (bảng hash tính sẵn). Không salt là hỏng dù thuật toán tốt.
  • So kết quả bằng hàm hằng thời gian (subtle.ConstantTimeCompare) — đúng bài học phần trước: so hash bằng == mở ra kênh phụ thời gian.

Đánh đổi cần cân nhắc

Tham số phải chỉnh theo phần cứng server của bạn, không copy mù. Con số OWASP (19 MiB, t=2) là sàn khuyến nghị. Hãy đo trên chính server production và tăng mem/t tới mức mỗi lần băm khoảng vài chục ms — đủ chậm cho kẻ tấn công nhưng người dùng không thấy trễ. Đặt quá cao (như 324 ms/lần ở demo) có thể khiến server sập dưới tải đăng nhập lớn; đặt quá thấp thì không bảo vệ đủ. Đây là bài toán cân bằng, cần đo lại định kỳ khi phần cứng mạnh lên.

Argon2id là lựa chọn mặc định, nhưng bcrypt/scrypt vẫn ổn. Nếu ngôn ngữ/thư viện của bạn chưa có Argon2 chín muồi, bcrypt (giới hạn 72 byte đầu vào) hoặc scrypt vẫn an toàn hơn xa hàm băm thường. Điều cấm kỵ là dùng SHA/MD5 trần cho mật khẩu — bất kể "có thêm salt" cũng không cứu được vì chúng vẫn quá nhanh.

Băm mật khẩu chỉ là một lớp. Nó bảo vệ khi CSDL đã lộ, nhưng không thay cho: giới hạn số lần đăng nhập sai, phát hiện dò mật khẩu, xác thực hai yếu tố, và không lưu mật khẩu ở nơi không cần. Phòng thủ theo lớp — Argon2 là lớp cuối khi mọi thứ khác đã thủng.

Ba ý mang về

  1. Băm mật khẩu phải chậm có chủ đích: đo thật, SHA-256 chạy ~31 triệu lần/giây/lõi (mồi cho GPU dò mật khẩu), còn Argon2id chậm hơn ~559.000 lần (18,3 ms/lần) — không đáng kể cho người dùng nhưng là bức tường cho kẻ tấn công.
  2. Bộ nhớ cứng là vũ khí chống GPU/ASIC: đo thật, tăng mem 8→128 MiB kéo thời gian 3,7 → 323,9 ms; vì mỗi lần băm chiếm hàng chục MiB RAM, phần cứng nghìn lõi không nhân song song được — hãy chỉnh tham số theo server của bạn.
  3. Luôn kèm salt ngẫu nhiên và so hằng thời gian: salt mỗi mật khẩu chặn rainbow table; so hash bằng ConstantTimeCompare tránh kênh phụ thời gian — và tuyệt đối không dùng SHA/MD5 trần cho mật khẩu.

Nguồn

Đến đây khép lại loạt Mật mã — từ băm và HMAC, mã hoá đối xứng AES-GCM và ChaCha20-Poly1305, PBKDF2, RSA và đường cong elliptic, chữ ký số, trao khoá X25519, CSPRNG, TOTP, JWT, mTLS, xoay khoá, so sánh hằng thời gian, cho tới băm mật khẩu Argon2 ở bài này. Điểm chung xuyên suốt: mật mã không phải phép thuật mà là những công cụ có tính chất đo được — và mỗi bài đều chạy công nghệ thật, đo output thật, để bạn thấy chính xác chúng bảo vệ được gì và không bảo vệ được gì. Nguyên tắc cuối cùng, cũng là quan trọng nhất: đừng tự chế mật mã — hãy dùng thư viện chuẩn, tham số khuyến nghị, và hiểu rõ mô hình đe doạ của mình.