Lưu trữ mật khẩu là một trong số ít lĩnh vực của phần mềm có một câu trả lời đúng thực sự, đã được công bố, được duy trì và miễn phí. Đây cũng là một trong những lĩnh vực bị làm sai đều đặn nhất, bởi vì các câu trả lời sai từng đúng ở một thời điểm nào đó và không ai quay lại xem xét chúng nữa.

Thất bại hiếm khi kỳ lạ. Đó là một hệ thống dựng năm 2016 theo lời khuyên còn hợp thời năm 2012, vẫn đang chạy, vẫn nhận đăng nhập, và không ai mở lại hàm băm đó kể từ khi người viết nó rời công ty.

Nếu bạn chỉ mang theo một điều: hãy dùng Argon2id, và nếu không thể, hãy dùng bcrypt. Mọi thứ còn lại trong bài viết này chỉ là chi tiết. Một hàm băm đa dụng như SHA-256 không phải là hàm băm mật khẩu, bất kể bạn áp dụng nó bao nhiêu lần, bởi tốc độ của nó là một mục tiêu thiết kế, và tốc độ chính là thứ bạn đang cố từ chối kẻ tấn công đang nắm cơ sở dữ liệu của bạn.


Lưu trữ mật khẩu chỉ có đúng một hình dạng đúng

Hãy lưu một giá trị băm được tạo ra bởi một hàm cố ý chậm và ngốn bộ nhớ, kèm một salt ngẫu nhiên duy nhất cho mỗi mật khẩu, với các tham số được điều chỉnh theo phần cứng của bạn.

Hướng dẫn lưu trữ mật khẩu của OWASP nêu chính xác các khuyến nghị hiện hành, và đáng để trích dẫn dưới dạng con số thay vì dưới dạng nguyên tắc.

Thuật toánTham số khuyến nghịKhi nào dùng
Argon2id19 MiB bộ nhớ, 2 vòng lặp, 1 mức song songLựa chọn đầu tiên
scryptN=2^17 (128 MiB), r=8, p=1Nếu không có Argon2id
bcryptWork factor 10 trở lên, giới hạn mật khẩu 72 byteHệ thống cũ
PBKDF2-HMAC-SHA256600.000 vòng lặpNơi bắt buộc tuân thủ FIPS-140

Hai lưu ý về bảng trên. Con số của Argon2id là mức tối thiểu, và nhiều bộ nhớ hơn với ít vòng lặp hơn là một sự đánh đổi tương đương, nên cấu hình 47 MiB với 1 vòng lặp cho mức an toàn tương đương mức cơ sở. Còn giới hạn 72 byte của bcrypt là cắt cụt thật sự chứ không phải quy tắc kiểm tra đầu vào: mọi thứ vượt quá 72 byte đều bị bỏ qua trong im lặng, điều này quan trọng nếu bạn chấp nhận cụm mật khẩu dài hoặc băm trước dữ liệu đầu vào.

Các tham số này được nâng lên theo đà tiến của phần cứng. Khi triển khai, hãy đọc nguồn gốc thay vì đọc bảng này.

Salt không phải tùy chọn và cũng không đủ

Salt là một giá trị ngẫu nhiên duy nhất được lưu cạnh mỗi giá trị băm. Mục đích của nó hẹp nhưng quan trọng: nó bảo đảm hai người dùng có cùng mật khẩu sẽ tạo ra hai giá trị băm khác nhau, qua đó vô hiệu hóa các bảng cầu vồng tính sẵn và ngăn kẻ tấn công bẻ được một mật khẩu rồi chiếm mọi tài khoản dùng chung mật khẩu đó.

Điều mà salt không làm được là làm chậm bất kỳ ai. Kẻ tấn công có cơ sở dữ liệu của bạn thì cũng có luôn các salt, vì chúng bắt buộc phải đọc được để xác thực một lần đăng nhập. Chỉ thêm salt cho một hàm băm nhanh nghĩa là kẻ tấn công sẽ tấn công từng mật khẩu một với tốc độ khổng lồ, đúng vào tình huống mà bạn đang cố tránh.

Sự chậm chạp phải đến từ chính thuật toán. Đó là toàn bộ ý nghĩa của một hàm ngốn bộ nhớ: nó cố ý tiêu tốn tài nguyên để kẻ tấn công có phần cứng chuyên dụng thu được lợi thế nhỏ hơn nhiều so với khi đối đầu một hàm băm nhanh.

Pepper, một giá trị bí mật áp dụng cho mọi mật khẩu và được cất giữ bên ngoài cơ sở dữ liệu, bổ sung một lớp thật sự, vì chỉ một bản sao cơ sở dữ liệu thôi sẽ không còn đủ. Nó cũng thêm gánh nặng vận hành, bởi xoay vòng nó đòi hỏi băm lại ở lần đăng nhập kế tiếp, còn làm mất nó thì khóa cửa toàn bộ tài khoản. Đáng làm với hệ thống giá trị cao, và có thể bỏ qua với phần lớn trường hợp.

Những sai lầm sống sót nhiều năm

Một hàm băm nhanh kèm salt. SHA-256 kèm salt trông có vẻ trách nhiệm nhưng không phải vậy. Tốc độ chính là lỗ hổng.

Mã hóa thay vì băm. Mã hóa vốn thiết kế để đảo ngược được, nghĩa là tồn tại một khóa, và ai lấy được khóa thì lấy được toàn bộ mật khẩu ở dạng rõ. Mật khẩu không bao giờ nên khôi phục được, chỉ nên kiểm chứng được.

Work factor đặt một lần rồi không bao giờ nâng. Một mức chi phí bcrypt chọn năm 2015 yếu đi đáng kể trước phần cứng năm 2026. Hãy nâng nó ở lần đăng nhập thành công kế tiếp, khi bạn có mật khẩu dạng rõ trong chốc lát và có thể băm lại.

Cắt cụt hoặc hạn chế ký tự. Một độ dài tối đa dưới khoảng 64 ký tự, hoặc lệnh cấm một số ký tự nhất định, thường báo hiệu rằng mật khẩu đang được lưu theo cách bận tâm tới nội dung của nó. Lẽ ra không nên như vậy, vì bạn đang băm nó.

So sánh làm rò rỉ thời gian. Việc xác thực phải dùng phép so sánh có thời gian không đổi. Hàm của thư viện chuẩn làm điều đó, còn phép kiểm tra bằng nhau tự viết thì không.

Ghi mật khẩu vào nhật ký. Nó lọt vào một log lỗi, một bản ghi vết yêu cầu hoặc một hệ thống giám sát, và thế là nó nằm ở ba nơi có kiểm soát truy cập yếu hơn cơ sở dữ liệu.

Chuyển đổi một hệ thống đang chạy

Bạn không thể băm lại thứ bạn không có, vì bạn chỉ giữ các giá trị băm. Cách khả thi là nâng cấp ngay tại thời điểm đăng nhập.

Khi một người dùng xác thực thành công, mật khẩu dạng rõ của họ nằm trong bộ nhớ trong một khoảnh khắc. Hãy đối chiếu với giá trị băm cũ, rồi lập tức tính giá trị mới bằng thuật toán hiện hành và thay thế giá trị đã lưu. Trong vài tháng, phần lớn tài khoản đang hoạt động sẽ chuyển xong mà người dùng không thấy bất kỳ thay đổi nào.

Hãy ghi lại thuật toán theo từng bản ghi để cả hai cùng tồn tại, và định một mốc thời gian sau đó các giá trị băm cũ còn lại bị vô hiệu và những người dùng đó phải đặt lại mật khẩu. Tài khoản ngủ đông với giá trị băm yếu chính là thứ kẻ tấn công muốn.

Đừng bọc giá trị băm cũ trong thuật toán mới như một lối tắt. Cách đó chạy được, và nó để lại hàm băm yếu làm ranh giới an toàn thực tế trong khi mọi thứ trông rất hiện đại.

Điều quan trọng hơn cả thuật toán

Băm đúng cách bảo vệ bạn khi cơ sở dữ liệu bị rò rỉ. Nó không làm được gì trước kiểu tấn công phổ biến hơn, tức là ai đó đăng nhập bằng một mật khẩu thật lấy được từ nơi khác.

Giới hạn tần suất ở bước xác thực, khả năng bật xác thực đa yếu tố, đối chiếu mật khẩu mới với các danh sách đã lộ và cảnh báo theo mẫu hành vi credential stuffing đều nhắm vào chuyện đó, và tất cả đều quan trọng hơn trong đời sống hằng ngày so với việc chọn giữa Argon2id và bcrypt. Các biện pháp bảo mật API thuộc cùng một họ vấn đề.

Cả hai đều đáng làm, và thứ tự thì đáng nói thẳng: hãy làm đúng phần băm một lần vì nó rẻ và bền, rồi dồn công sức thường xuyên vào đường đăng nhập, nơi các cuộc tấn công thực sự đổ tới. Mecanik xem xét cả hai trong khuôn khổ dịch vụ phân tích bảo mật ứng dụng.



Câu hỏi thường gặp

Thuật toán băm mật khẩu tốt nhất năm 2026 là gì? Argon2id, với tối thiểu 19 MiB bộ nhớ, 2 vòng lặp và 1 mức song song. Nếu không có sẵn, scrypt với N=2^17, r=8, p=1 là một lựa chọn thay thế, bcrypt với work factor 10 trở lên phù hợp cho hệ thống cũ, còn PBKDF2-HMAC-SHA256 với 600.000 vòng lặp đáp ứng yêu cầu FIPS-140.

Chỉ thêm salt cho mật khẩu đã đủ chưa? Chưa. Salt bảo đảm các mật khẩu giống nhau tạo ra giá trị băm khác nhau, vô hiệu hóa bảng cầu vồng và ngăn một mật khẩu bị bẻ mở được mọi tài khoản dùng chung nó. Nó không làm chậm kẻ tấn công, vì salt bắt buộc phải đọc được để xác thực đăng nhập. Sự chậm chạp phải đến từ một thuật toán cố ý ngốn bộ nhớ.

Vì sao không nên dùng SHA-256 cho mật khẩu? Vì tốc độ của nó là một mục tiêu thiết kế, và tốc độ chính là thứ bạn đang cố từ chối kẻ tấn công đang giữ cơ sở dữ liệu của bạn. Một hàm băm đa dụng không phải là hàm băm mật khẩu, bất kể nó được áp dụng bao nhiêu lần hay có được thêm salt hay không.

bcrypt có giới hạn độ dài mật khẩu không? Có, 72 byte, và mọi thứ vượt quá đều bị cắt cụt trong im lặng chứ không bị từ chối. Điều này quan trọng nếu bạn chấp nhận cụm mật khẩu dài hoặc băm trước dữ liệu đầu vào rồi mới đưa cho bcrypt, vì việc cắt cụt diễn ra mà không có bất kỳ lỗi nào báo hiệu.

Làm sao nâng cấp giá trị băm mật khẩu khi không có mật khẩu của người dùng? Ngay tại lúc đăng nhập. Khi người dùng xác thực, bạn giữ mật khẩu dạng rõ của họ trong chốc lát, nên hãy đối chiếu với giá trị băm cũ rồi băm lại ngay bằng thuật toán hiện hành. Hãy ghi lại thuật toán theo từng bản ghi để cả hai cùng tồn tại, và định một mốc thời gian sau đó các giá trị băm cũ còn lại bị vô hiệu và những người dùng đó phải đặt lại mật khẩu.