Băm và lưu password đúng cách

2018/06/18

Categories: security

Sau khi đọc bài viết băm mật khẩu đúng cách của anh thaidn, mình nhớ lại lúc mình mới ra trường, cũng đã từng nghĩ về vấn đề này (lúc đó mình khá thích môn Bảo Mật Thông Tin ở trường) nhưng chưa bao giờ hiểu tường tận. Chỉ biết là không nên:

Chỉ hiểu rằng phải nên làm thế, nhưng không hiểu tại sao lại như vậy và một số câu hỏi khác cũng chưa trả lời được như:

Hôm nay, mình quyết định đi tìm câu trả lời cho những vấn đề mình thắc mắc, thay vì mặc định nó đúng.

1. Tại sao không nên lưu plain-text, encrypt hoặc dùng MD5, SHA-1

Nếu lưu plain-text, database bị hack, SQL-injection, password user chìa ra theo một cách không thể dễ dàng hơn để đánh cắp.

Nếu mã hóa 2 chiều, sẽ luôn có một cách để giải mã bằng một chìa-khóa nào đó, sẽ phải tìm cách lưu chìa-khóa một cách an toàn.

MD5 và SHA-1 được chứng minh có đụng độ, nghĩa là 2 password khác nhau, khi hash bằng MD5 hoặc SHA-1 có thể ra cùng một chuỗi.

2. Tại sao phải salt

Ta đã biết, hash algorithm là one-way-function, tức là không thể suy ngược trực tiếp ra password nếu có hash_value (khác với mã hóa, có thể giải mã thông qua chìa-khóa).

Tuy nhiên vẫn có cách để từ hash_value có thể suy gián tiếp ra được password ví dụ brute-force attack, dictionary attack -> điểm chung là ta cần thửđoán password nhiều lần cho tới khi đúng cái cần tìm.

Một cách khác đó là ta có thể tính toán trước giá trị hash của tất cả các trường hợp và của tất cả các thuật toán -> cách này khó, tốn thời gian, nhưng bây giờ với tốc độ tính toán của máy tính, ta vẫn có thể làm được. Bảng lưu trữ password + hash_value của password gọi là Rainbow Table, có thể tự tạo hoặc tải một số bản miễn phí hoặc trả tiền để mua. Từ bây giờ nếu ta có hash_value ta có thể mapping để suy ra được password.

Tuy nhiên nếu ta chỉ hash mỗi password, ta gặp vấn đề đó là:

Và nếu chỉ hash password thì nếu mất hash_value, có thể tra trong rainbow table để tìm ra được password của người dùng.

Giờ ta thử thay vì hash(password) ta sẽ hash(salt + password):

Từ md5(123456)

id |          hash_md5                |
---------------------------------------
1  | e10adc3949ba59abbe56e057f20f883e |

Thành md5(7nWZLcCK0vsPzIM + 123456)

id |          hash_md5                |    salt         |
---------------------------------------------------------
1  | 0510210d4b370165658bdc0d0b005244 | 7nWZLcCK0vsPzIM |

Giờ giả sử, ta mất bảng dữ liệu gồm hash_md5, salt, kẻ tấn công sẽ phải tính toán lại rainbow table của tất cả các trường hợp cộng với salt. Nếu salt là random cho từng user, kẻ tấn công sẽ phải tính toán toàn bộ trường hợp cộng với riêng từng salt cho toàn bộ user.

Chi phí cho 2 phép tính trên là vô cùng lớn và tốn rất nhiều thời gian để thực hiện. Vậy tóm lại mục đích của salt và random-salt là:

=> Ta trả lời đc 3 câu hỏi:

3. Hash ở đâu?

Giờ giả sử hash(password) ở client-side thì vấn đề là gì?

Nhìn sơ thì thấy việc dùng duy nhất một salt đã chống lại luận điểm ở mục số 2. Vậy quy trình chứng thực đúng là như thế nào?

4. Tại sao dùng bcrypt thay cho SHA-512

Kết quả của SHA-512 có độ dài 128 kí tự, độ dài của key là 64 bytes. Trông có vẻ cũng khá chắc chắn, vậy tại sao OWASP recommend sử dụng PBKDF2, bcrypt hoặc scrypt hơn là SHA2?

SHA2 là hash algorithm (tất nhiên), nó được thiết kế với mục tiêu là tốc độ, với các CPU hiện đại, có thể generate hàng triệu kết quả trên giây. Nếu dùng một thuật toán có tốc độ như SHA2 tức là bạn đã đem lợi điểm tới cho kẻ tấn công brute-force. Thuật toán nhanh + cấu hình server mạnh, việc brute-force càng trở lên nhanh chóng hơn.

Trong khi đó, bcrypt được gọi là slow-hash algorithm, bcrypt() mất 100ms để tính toán ra chuỗi hash, chậm hơn 10.000 lần so với sha1().

Có nghĩa là vẫn đạt được mục đích hash nhưng giảm thiểu nguy cơ tấn công brute-force.

Tóm lại: SHA-512 không phải là một thuật toán yếu, mà vấn đề là SHA-512 không phù hợp cho việc hash password. Nếu cần hash password thì ta nên dùng các thuật toán slow hash như PBKDF2, bcrypt và scrypt.

5. Tại sao cần Pepper?

Một thực tế là nếu bạn chỉ có “muối” mà không có “tiêu”, ăn thịt gà luộc sẽ không ngon :v. Giả sử, database bạn chạy RAID-1, một ổ cứng hư và cần thay một ổ cứng mới. Nhưng như ta biết, đĩa bị hư là mirror của đĩa còn lại, bạn phải tiêu hủy ổ cứng hư đó nếu không ai đó có thể lục thùng rác và tái tạo lại một phần dữ liệu trong đĩa hư đó.

Xin lưu ý, bạn cần wipe trước khi vứt bỏ một ổ cứng có dữ liệu dù cá nhân hay server, tuy nhiên đĩa bị sốc điện, bad-sector thì wipe cũng chưa đủ an toàn, tốt nhất nên ngiền ra bã.

Dù random-salt đã làm tăng chi phí tạo ra rainbow table nhưng đời không biết đâu mà lần, kẻ tấn công luôn có những động lực không tưởng để đạt được cái mình muốn. Giả sử kẻ tấn công có một siêu siêu máy tính và một mirror ổ cứng hư lục từ một cái thùng rác nào đó. Với siêu máy tính đó, ta có rainbow-table để tra ngược ra password cần tìm.

Vậy làm sao để giảm thiểu nguy cơ trên? Nguyên tắc là không bỏ tất cả trứng trong một giỏ, đó là pepper. Pepper là một chuỗi tương tự như salt, nhưng khác biệt là ta cần giữ bí mật pepper, lưu ở một chỗ khác ngoài database, và không cần pepper-per-user, chỉ cần 1 pepper là đủ.

Từ

hash(salt + password)

Thành

hash(pepper + salt + password)

Ta nên lưu pepper ở application hoặc ở một service khác, nếu database bị compromise, thì kẻ tấn công cũng không có pepper để tạo ra rainbow-table.

6. Bonus

dropbox-store-password

Trong khi tìm câu trả lời để viết bài này, mình tìm được một bài blog của dropbox nói về cách họ lưu password như thế nào. Thấy có 2 điểm khá hay nên muốn nói thêm.

7. Migrate

Sau khi viết bài này, bạn @vanhtuan0409 có một câu hỏi về việc làm thế nào migrate một hệ thống dùng SHA1 –> bcrypt. Mình thấy đây là một điểm cũng khá cần thiết nên xin phép note thêm.

Ở đây có 2 trường hợp:

Về ý tưởng thì ta sẽ dùng SHA1 như cách Dropbox dùng SHA512 để cố định độ dài input.

Với trường hợp đầu ta cần sinh ra thêm salt-per-user và migrate bcrypt(salt, sha1_value) trong đó sha1_value = SHA1(password) -> Tương tự cách của dropbox, may mắn là do sai thiết kế từ đầu (thiếu salt) nên migrate dễ.

Với trường hợp thứ 2 thì hơi lằng nhằng hơn 1 xíu, ta cần migrate kiểu bcrypt(salt, SHA1(salt, password)) nếu thực sự muốn bcrypt với salt hoặc thực ra chỉ cần bcrypt(SHA1(salt, password) cũng được. Tùy tình huống bạn cần tradeoff có thể lựa chọn cách phù hợp.

8. Thông tin của bạn có an toàn?

Như ta thấy, ta đã làm rất nhiều thứ để đảm bảo rằng thông tin của chúng ta trở lên an toàn. Vậy thực sự thông tin của chúng ta đã an toàn hay không? Câu trả lời rất tiếc là KHÔNG? Thông tin của chúng ta có thể an toàn với “bên ngoài” nhưng không an toàn với “bên trong”. Tại sao lại vậy?

Giờ bắt đầu với thông tin cơ bản nhất như địa chỉ, email, sở thích … Khi ta build staing environment, thông tin user được copy sang một môi trường khác, developer có thể overwrite hash_value của user bằng value của hash(salt + 654321) và dễ dàng đăng nhập trên staing environment với password là 654321 và tất cả các thông tin cơ bản của user đã có thể đọc được. Nhưng ít nhất developer vẫn không biết chính xác password của user là gì.

Nhưng, lại là nhưng. Người quản trị hệ thống có mọi quyền trên server mà họ quản trị, password dù đã over HTTPs nhưng tới server vẫn là plain-text. Vẫn có thể capture request ngay ở đầu server và output ra plain-text password của user và cuối cùng thì người quản trị hệ thống vẫn biết được chính xác password của user là gì. Đó là lí do tại sao ta không nên dùng một password cho tất cả các dịch vụ.

Vậy làm thế nào để bảo vệ info của user trong trường hợp này, rất tiếc về mặt kỹ thuật không có cách nào để đảm bảo việc này. Ta chỉ có thể áp dụng policy, NDA về mặt con người để hạn chế vấn đề này thôi.

9. Ref

Lưu ý: