TL;DR
Sử dụng bcrypt.
Tại sao không phải là {MD5, SHA1, SHA2, SHA3,...}?
Các function trên đều là các hàm băm (hash) được sử dụng với mục đích chung, vốn được thiết kế để tính toán băm một lượng data rất lớn trong thời gian ngắn nhất có thể. Điều này có nghĩa là những hàm này rất tốt trong việc kiểm tra/đảm bảo tính toán vẹn của data nhưng lại hoàn toàn dở trong việc lưu trữ password.
Một server hiện đại có thể tính toán khoảng 330MB MD5 hash mỗi giây. Nếu user của bạn lưu mật khẩu dạng chữ thường, kèm số và chỉ 6 kí tự, thì cứ mỗi 40 giây bạn có thể tìm ra được một mật khẩu trùng khớp.
Nếu bạn sẵn sàng bỏ ra 2000$ và một tuần hoặc đầu tư hẳn CUDA, thì bạn có thể tạo ra một siêu máy tính nho nhỏ giúp bạn hash với 700 triệu password một giây. Với những password được hash bằng những hàm trên bạn có thể đạt tỉ lệ lấy được password chỉ trong hơn 1 giây...
Thêm muối (salt) chẳng giúp gì cả
Một điều quan trọng cần ghi nhớ là muối (salt) là vô dụng trong việc ngăn chặn tấn công từ điển (dictionary attacks) hoặc tấn công liên tục (brute force attacks). Bạn có thể sử dụng muối to, nhiều muối hoặc các kiểu muối khác nhau (hand-harvested, shade-grown, organic Himalayan pink salt). Nhưng rốt cục nó chẳng ảnh hưởng gì tới tốc độ tấn công bằng cách thử các password cả.
Có muối hay không thì với các general-purpose hash function được thiết kế để đẩy nhanh tốc độ thì nó không có ý nghĩa gì cả.
bcrypt sẽ giúp bạn giải quyết vấn đề!
Giải quyết như thế nào? Về cơ bản, bcrypt chậm. Nó sử dụng một biến thể của thuật toán mã hóa Blowfish, dùng work factor cho phép bạn xác định mức độ "tốn kém" của một hàm băm. Chính vì điều này, bcrypt tuân theo định luật Moore. Có nghĩa là tốc độ máy càng nhanh, bạn có thể thêm work factor và việc hash sẽ càng chậm.
Ví dụ so với MD5, tuỳ theo work factor thì với 12 work factor, bcrypt sẽ hash mất 0.3 giây nhưng MD5 chỉ mất chưa tới 1 micro giây.
Với những điều trên, thay vì chỉ mất mỗi 40 giây để tìm ra một mật khẩu có khả năng khớp thì chúng ta sẽ với mất mỗi 12 năm với bcrypt. Có thể bạn không cần bảo mật chặt chẽ đến vậy, hoặc cần một thuật toán cho tốc độ so sánh nhanh hơn, nhưng bcrypt cho phép bạn cân bằng giữa tốc độ và độ bảo mật.
Tìm hiểu về thuật toán của bcrypt
Thuật toán bcrypt sẽ trả về kết quả của việc mã hoá (ví dụ) đoạn text "OrpheanBeholderScryDoubt" 64 lần sử dụng Blowfish. Sử dụng Blowfish cũng sẽ sử dụng một function "EksBlowfishSetup" để tính expensive.
Function bcrypt
Input:
cost: Number (4..31) log2(Iterations). e.g. 12 ==> 212 = 4,096 iterations
salt: array of Bytes (16 bytes) random salt
password: array of Bytes (1..72 bytes) UTF-8 encoded password
Output:
hash: array of Bytes (24 bytes)
//Initialize Blowfish state with expensive key setup algorithm
state ← EksBlowfishSetup(cost, salt, password)
//Repeatedly encrypt the text "OrpheanBeholderScryDoubt" 64 times
ctext ← "OrpheanBeholderScryDoubt" //24 bytes ==> three 64-bit blocks
repeat (64)
ctext ← EncryptECB(state, ctext) //encrypt using standard Blowfish in ECB mode
//24-byte ctext is resulting password hash
return Concatenate(cost, salt, ctext)
Có thể thấy cost sẽ được setup theo mức độ bảo mật, để tìm hiểu tại sao cost lại gây khó chịu cho crackers, thì ta sẽ tìm hiểu sâu hơn về cách tính expensive.
Function EksBlowfishSetup
Input:
cost: Number (4..31) log2(Iterations). e.g. 12 ==> 212 = 4,096 iterations
salt: array of Bytes (16 bytes) random salt
password: array of Bytes (1..72 bytes) UTF-8 encoded password
Output:
state: opaque BlowFish state structure
state ← InitialState()
state ← ExpandKey(state, salt, password)
repeat (2^cost)
state ← ExpandKey(state, 0, password)
state ← ExpandKey(state, 0, salt)
return state
Ta có thể thấy func sẽ lặp lại 2^cost nên nên set cost càng cao đồng nghĩa với việc sẽ thêm nhiều rotate salt, giảm tốc độ hash ra một password, việc tấn công thử/lặp thử sẽ bị giảm thiểu rất rõ ràng.
Hãy sử dụng bcrypt!
Tham khảo: