"Mã hoá bằng mật khẩu" nghe như một bước. Trong một công cụ làm đúng, đó là ba bước: biến mật khẩu thành khoá, mã hoá dữ liệu bằng khoá đó, rồi đóng gói mọi thứ bên kia cần để giải mã. Mỗi bước có lý do riêng và có cách sai riêng. Bài này theo chân một tin nhắn năm chữ cái đi qua công cụ mã hoá và giải mã văn bản từng byte một, rồi chỉ ra kết quả bảo vệ bạn khỏi điều gì và không bảo vệ khỏi điều gì.
Công cụ chạy trên Web Crypto có sẵn trong trình duyệt. Mọi con số dưới đây đều do chính mã của công cụ tạo ra.
Bước 1: mật khẩu không phải là khoá
AES-256 cần một khoá dài đúng 32 byte trông như ngẫu nhiên. Mật khẩu như correct horse là 13 ký tự do con người chọn, không đúng độ dài và cũng không ngẫu nhiên. Cầu nối là một hàm dẫn xuất khoá. Công cụ dùng PBKDF2 với HMAC-SHA-256 và 600.000 vòng lặp, đúng con số mà hướng dẫn lưu trữ mật khẩu của OWASP đưa ra cho hàm này.
Hai chi tiết giúp việc dẫn xuất an toàn:
- Một salt ngẫu nhiên 16 byte được trộn vào. Cùng một mật khẩu cho ra khoá khác nhau mỗi lần mã hoá, nên kẻ tấn công không thể tính sẵn bảng khoá cho các mật khẩu phổ biến rồi dùng lại cho tệp của mọi người.
- 600.000 vòng lặp khiến mỗi lần đoán trở nên đắt đỏ. Trên chiếc laptop dùng để viết bài này, một lần dẫn xuất mất khoảng 49 mili giây trong Node.js, so với khoảng 0,1 mili giây nếu chỉ 1.000 vòng. Sự chậm đó không ảnh hưởng gì khi bạn gõ mật khẩu một lần, nhưng kẻ tấn công phải trả đúng 49 ms cho mỗi lần đoán.
Trước tất cả những việc trên, mật khẩu được chuẩn hoá (Unicode NFKC). Chữ chào gõ bằng ký tự à dựng sẵn dài 4 ký tự, gõ bằng a cộng dấu huyền tổ hợp thì dài 5, trong khi nhìn y hệt nhau. Không chuẩn hoá thì cùng một mật khẩu gõ trên điện thoại và trên laptop có thể sinh ra hai khoá khác nhau, và tệp "không mở được".
Bước 2: AES-GCM vừa mã hoá vừa niêm phong
AES-GCM làm hai việc cùng lúc. Nó mã hoá dữ liệu và tính một thẻ xác thực 16 byte trên kết quả. Khi giải mã, thẻ được kiểm tra trước. Chỉ cần một bit của bản mã hoặc của phần đầu bị thay đổi, việc giải mã sẽ từ chối trả về bất cứ thứ gì.
GCM còn cần một nonce (IV) 12 byte không bao giờ được lặp lại với cùng một khoá. Công cụ lấy một nonce ngẫu nhiên mới mỗi lần, và vì salt cũng làm khoá thay đổi, hai lần mã hoá cùng một văn bản không bao giờ giống nhau. Mã hoá hello hai lần với cùng mật khẩu cho ra hai kết quả 58 byte hoàn toàn khác nhau.
Bước 3: phong bì
Kết quả phải mang theo mọi thứ trừ mật khẩu. Với hello, 58 byte được chia như sau:
| Số byte | Nội dung |
|---|---|
| 3 | dấu hiệu ENC |
| 1 | phiên bản định dạng (1) |
| 4 | số vòng lặp (600.000) |
| 16 | salt ngẫu nhiên |
| 12 | nonce ngẫu nhiên |
| 1 | loại nội dung (0 = văn bản, 1 = tệp) |
| 5 | chữ hello đã mã hoá |
| 16 | thẻ GCM |
Như vậy phần thêm vào cố định là 53 byte: Xin chào dài 9 byte UTF-8 thành 62 byte, một câu tiếng Việt 32 byte thành 85 byte. Ở dạng văn bản, phong bì được viết bằng Base64URL, bốn ký tự cho mỗi ba byte, nên hello thành 78 ký tự. Vì các byte đầu luôn giống nhau, mọi phong bì tạo bằng thiết lập mặc định đều bắt đầu bằng RU5DAQAJJ8: riêng dấu hiệu ENC đã thành RU5D, và công cụ kiểm tra dấu hiệu này khi bạn dán nội dung vào.
Toàn bộ 36 byte phần đầu cũng được đưa vào thẻ GCM dưới dạng "dữ liệu xác thực bổ sung". Điều này chặn kiểu hạ cấp âm thầm: chúng tôi sửa số vòng lặp trong một phong bì hợp lệ từ 600.000 xuống 1.000, và việc giải mã thất bại y như khi sai mật khẩu. Kẻ tấn công không thể sửa tệp để nó dễ bẻ hơn.
Sai mật khẩu trông như thế nào
Ba thay đổi khác nhau đều cho ra cùng một lỗi trong thử nghiệm của chúng tôi: mật khẩu sai chữ hoa chữ thường ở một ký tự, một bit bị lật trong bản mã, và số vòng lặp bị sửa. AES-GCM không phân biệt được "sai mật khẩu" với "dữ liệu hỏng hoặc bị can thiệp"; cả hai đều không qua được bước kiểm tra thẻ. Đó là chủ ý. Một công cụ báo "mật khẩu gần đúng" hay trả về chữ lộn xộn chính là đang rò rỉ thông tin.
Rủi ro thật nằm ở mật khẩu
Mã hoá AES-256 không phải điểm yếu. Mật khẩu mới là điểm yếu, vì ai có phong bì của bạn đều có thể đoán ngoại tuyến nhanh hết mức phần cứng cho phép, và bạn không hề biết.
Ví dụ tính bằng con số 49 ms ở trên, trên một lõi laptop:
- Mã PIN 6 chữ số có 1.000.000 khả năng: khoảng 13,6 giờ để thử hết. Vài máy tính làm xong trong vài phút.
- Tám chữ cái thường ngẫu nhiên cho 26⁸, khoảng 2,1 × 10¹¹: chừng 320 năm trên một lõi đó, nhưng kẻ tấn công dùng nhiều lõi và card đồ hoạ, nên hãy coi đó là mức sàn chứ không phải lời hứa.
- Bốn từ ngẫu nhiên từ danh sách 7.776 từ cho khoảng 3,7 × 10¹⁵, gấp hơn 17.000 lần tám chữ cái.
Số vòng lặp nhân công sức của kẻ tấn công với một hằng số; độ ngẫu nhiên của mật khẩu nhân nó theo cấp số mũ. Hãy dùng mật khẩu hoặc cụm mật khẩu dài, ngẫu nhiên từ công cụ tạo mật khẩu, và đọc thế nào là mật khẩu mạnh để xem cách tính.
Mã hoá không bảo vệ được gì
- Kích thước. Phong bì dài hơn nội dung 53 byte, nên ai cũng phân biệt được ghi chú ngắn với tệp dài.
- Kênh gửi mật khẩu. Gửi phong bì và mật khẩu trong cùng một cuộc trò chuyện hay e-mail thì chẳng bảo vệ được gì. Hãy gửi mật khẩu qua đường khác, hoặc thống nhất trực tiếp.
- Thiết bị. Nếu máy tính nơi bạn gõ văn bản có mã độc hay phần mềm ghi màn hình, nó thấy văn bản trước khi được mã hoá.
- Quên mật khẩu. Không có cách đặt lại, không có cửa sau. Mất mật khẩu là mất dữ liệu.
- Độ tin cậy của công cụ. Mã hoá trong trình duyệt chỉ đáng tin bằng mã của trang. Công cụ này không gửi gì qua mạng, điều bạn có thể tự kiểm tra trong bảng Network của trình duyệt.
Những lỗi thường gặp
- Dùng lại mật khẩu của một trang web để mã hoá tệp. Trang đó bị lộ dữ liệu là tệp cũng mở được.
- Dán kết quả vào nơi tự thay đổi ký tự, như trình soạn thảo đổi dấu ngoặc kép hay tự xuống dòng. Base64URL đi qua được hầu hết các kênh, nhưng hãy kiểm tra thứ nhận được có giống hệt không.
- Nghĩ rằng mã hoá giấu được người gửi. Phong bì không chứa tên, nhưng tin nhắn đi kèm thì có.
Để thử, hãy dán một câu vào công cụ mã hoá và giải mã văn bản, mã hoá, rồi giải mã bằng một mật khẩu khác đúng một ký tự và xem nó từ chối.