1. Giới thiệu về Facebook Client-side Encryption (CSE)
Bảo mật thông tin đăng nhập là một trong những ưu tiên hàng đầu của các nền tảng trực tuyến, đặc biệt là các mạng xã hội lớn như Facebook. Để nâng cao mức độ bảo vệ dữ liệu người dùng, Facebook đã triển khai cơ chế mã hóa mật khẩu phía client (Client-side Encryption – CSE) trước khi gửi nó lên máy chủ. Cơ chế này giúp hạn chế các rủi ro như tấn công đánh cắp dữ liệu trên đường truyền (man-in-the-middle) và tấn công thu thập mật khẩu bằng cách ghi log (keylogging, packet sniffing,…).
Trong bài viết này, chúng ta sẽ đi sâu phân tích cách Facebook triển khai CSE, cách thức hoạt động của nó, những điểm mạnh, điểm yếu, cũng như so sánh với các phương pháp bảo mật đăng nhập phổ biến khác.
2. Facebook triển khai mã hóa mật khẩu phía client như thế nào?
2.1. Quá trình đăng nhập thông thường
Khi người dùng đăng nhập vào Facebook, quá trình thông thường diễn ra như sau:
– Người dùng nhập email/số điện thoại và mật khẩu vào form đăng nhập.
– Trình duyệt gửi thông tin này trực tiếp đến máy chủ thông qua giao thức HTTPS.
– Máy chủ kiểm tra mật khẩu, xác thực thông tin và trả về kết quả đăng nhập thành công hoặc thất bại.
Tuy nhiên, dù HTTPS giúp mã hóa dữ liệu trên đường truyền, nhưng nếu có phần mềm độc hại hoặc hacker kiểm soát được thiết bị người dùng, họ vẫn có thể lấy được mật khẩu gốc.
2.2 Cơ chế mã hóa mật khẩu phía Client của Facebook
Để hiểu hơn về cơ chế mã hóa mật khẩu phía Client của Facebook, vào năm 2021, một người đã thử reverse dữ liệu đăng nhập của người dùng được gửi lên facebook. Quá trình dịch ngược diễn ra như sau:
email: test@test.com password: mysuperpassword
Với credential này, trong POST request gửi tới server đăng nhập facebook sẽ có một trường
encpass="#PWD_BROWSER:5:1617376020:AdhQAKb3zEewux6J98xFvie1HjaFRlSTWesGmeAuwW03KpZ1ia4jCMf4jv6ekezoGltbU5QPqbC2alzFutmA7xOQ2M1S1Lkge9qGB94F6rWeWMDqHchFb8uD8MRY9oid0QTZm5nOumSR24lfTaVO29xh2Q=="
Từ tên trường, chúng ta dự đoán đây chính là mật khẩu đã được mã hóa tại client. Từ POST request này tìm ngược về function đóng vai trò mã hóa password, ta tìm thấy hàm _encryptBeforeSending
Chúng ta có thể dễ dàng tìm được định nghĩa hàm như sau:
__d(“LoginFormController”, [“AsyncRequest”, “Button”, “Cookie”, “DOM”, “DeferredCookie”, “Event”, “FBBrowserPasswordEncryption”, “FBLogger”, “Form”, “FormTypeABTester”, “LoginServicePasswordEncryptDecryptEventTypedLogger”, “WebStorage”, “bx”, “ge”, “goURI”, “guid”, “promiseDone”], (function(a, b, c, d, e, f) {
var g, h = { init: function(a, c, d, e, f) { h._initShared(a, c, d, e, f), h.isCredsManagerEnabled = !1, !f || !f.pubKey ? b("Event").listen(a, "submit", h._sendLoginShared.bind(h)) : b("Event").listen(a, "submit", function(b) { b.preventDefault(), h._sendLoginShared.bind(h)(), h._encryptBeforeSending(function() { a.submit() }) }) }, ... _encryptBeforeSending: function(a) { a = a.bind(h); var c = h.loginFormParams && h.loginFormParams.pubKey; if ((window.crypto || window.msCrypto) && c) { var d = b("DOM").scry(h.loginForm, 'input[id="pass"]')[0], e = b("FBBrowserPasswordEncryption"), f = Math.floor(Date.now() / 1e3).toString(); b("promiseDone")(e.encryptPassword(c.keyId, c.publicKey, d.value, f), function(c) { c = b("DOM").create("input", { type: "hidden", name: "encpass", value: c }); h.loginForm.appendChild(c); d.disabled = !0; a() }, function(c) { var d = "#PWD_BROWSER", e = 5, g = b("LoginServicePasswordEncryptDecryptEventTypedLogger"); new g().setError("BrowserEncryptionFailureInLoginFormControllerWWW").setGrowthFlow("Bluebar/main login WWW").setErrorMessage(c.message).setPasswordTag(d).setPasswordEncryptionVersion(e).setPasswordTimestamp(f).logVital(); a() }) } else a() }, ...
Phân tích các tham số được gọi
Chúng ta có thể thấy được hàm _encryptBeforeSending nhận một input encpass được tạo bởi encryptPassword(c.keyId, c.publicKey, d.value, f), trong đó
var c = h.loginFormParams && h.loginFormParams.pubKey
h trỏ tới module hiện tại. Do đó, sử dụng Firefox để xem dữ liệu, ta có thể thấy được giá trị của c.keyId và c.publicKey
Object { publicKey: "53d38c45d2b6ff5bb0b843dfef4e060446596a93f970510b5fe615671ef3c457", keyId: 216 }
d.value là password của người dùng, và f là timestamp
Hàm encrypt trung gian
__d("FBBrowserPasswordEncryption", ["EnvelopeEncryption", "regeneratorRuntime", "tweetnacl-util"], (function(a, b, c, d, e, f) { "use strict"; f.encryptPassword = a; function a(keyID, publicKey, pass, timestamp) { var padding, g, passUTF8, timestampUTF8, encryptedPass; return b("regeneratorRuntime").async(function(k) { while (1) switch (k.prev = k.next) { case 0: padding = "#PWD_BROWSER"; g = 5; passUTF8_decoded = b("tweetnacl-util").decodeUTF8(pass); timestampUTF8_decoded = b("tweetnacl-util").decodeUTF8(timestamp); k.next = 6; return b("regeneratorRuntime").awrap(b("EnvelopeEncryption").encrypt(keyID, publicKey, passUTF8_decoded, timestampUTF8_decoded)); case 6: encryptedPass = k.sent; return k.abrupt("return", [padding, g, timestamp, b("tweetnacl-util").encodeBase64(j)].join(":")); case 8: case "end": return k.stop() } }, null, this) } }), null);
Module FBBrowserPasswordEncryption gọi tới encryptPassword. Tại case 6, chúng ta dễ dàng thấy một hàm join các thành phần của encrypted password, bao gồm
– Id #PWD_BROWSER,
– Số 5,
– Timestamp,
– j từ case 0, base64 encoded
Chúng ta tiếp tục đi tìm hiểu giá trị của j. j có được từ hàm b(“EnvelopeEncryption”).encrypt(keyID, publicKey, passUTF8_decoded, timestampUTF8_decoded)
Hàm encrypt thật sự của quá trình
__d("EnvelopeEncryption", ["Promise", "regeneratorRuntime", "tweetnacl-sealedbox-js"], (function(a, b, c, d, e, f) { "use strict"; f.encrypt = a; var g = window.crypto || window.msCrypto, h = 64, i = 1, j = 1, k = 1, l = b("tweetnacl-sealedbox-js").overheadLength, m = 2, n = 32, o = 16, p = j + k + m + n + l + o; function q(a, c) { return b("tweetnacl-sealedbox-js").seal(a, c) } function r(a) { var b = []; for (var c = 0; c < a.length; c += 2) b.push(parseInt(a.slice(c, c + 2), 16)); return new Uint8Array(b) } function a(a, c, d, e) { var f, s, t, u, v, w, x; return b("regeneratorRuntime").async(function(y) { while (1) switch (y.prev = y.next) { case 0: f = p + d.length; if (!(c.length != h)) { y.next = 3; break } throw new Error("public key is not a valid hex sting"); case 3: s = r(c); if (s) { y.next = 6; break } throw new Error("public key is not a valid hex string"); case 6: t = new Uint8Array(f); u = 0; t[u] = i; u += j; t[u] = a; u += k; v = { name: "AES-GCM", length: n * 8 }; w = { name: "AES-GCM", iv: new Uint8Array(12), additionalData: e, tagLen: o }; x = g.subtle.generateKey(v, !0, ["encrypt", "decrypt"]).then(function(a) { var c = g.subtle.exportKey("raw", a); a = g.subtle.encrypt(w, a, d.buffer); return b("Promise").all([c, a]) }).then(function(a) { var b = new Uint8Array(a[0]); b = q(b, s); t[u] = b.length & 255; t[u + 1] = b.length >> 8 & 255; u += m; t.set(b, u); u += n; u += l; if (b.length !== n + l) throw new Error("encrypted key is the wrong length"); b = new Uint8Array(a[1]); a = b.slice(-o); b = b.slice(0, -o); t.set(a, u); u += o; t.set(b, u); return t })["catch"](function(a) { throw a }); return y.abrupt("return", x); case 16: case "end": return y.stop() } }, null, this) } }), null);
Đây là hàm sinh ra mã hóa thật sự của thuật toán. Tại cuối hàm, ta thấy có một lời gọi tới hàm generateKey. Đây là hàm sinh ra khóa để mã hóa dữ liệu. Ta sẽ tập trung vào hàm này.
x = g.subtle.generateKey(v, !0, ["encrypt", "decrypt"]).then(function(a) { var c = g.subtle.exportKey("raw", a); a = g.subtle.encrypt(w, a, d.buffer); return b("Promise").all([c, a]) }).then(function(a) { var b = new Uint8Array(a[0]); b = q(b, s); t[u] = b.length & 255; t[u + 1] = b.length >> 8 & 255; u += m; t.set(b, u); u += n; u += l; if (b.length !== n + l) throw new Error("encrypted key is the wrong length"); b = new Uint8Array(a[1]); a = b.slice(-o); b = b.slice(0, -o); t.set(a, u); u += o; t.set(b, u); return t })["catch"](function(a) { throw a }); return y.abrupt("return", x);
Đây là một hàm của interface SubtleCrypto để sinh key mới hoặc cặp key mới. Signature của hàm như sau:
crypto.subtle.generateKey(algorithm, extractable, keyUsages)
algorithm: một dictionary object xác định loại key và các tham số bổ trợ
extractable: boolean xác định key có thể được export không
keyUsages: xác định các hoạt động có thể được dùng với key mới được sinh
Trong trường hợp của chúng ta, algorithm được xác định như sau:
v = { name: "AES-GCM", length: n * 8 };
Sau khi đã sinh ra key, đoạn code trên tiếp tục encrypt dữ liệu const result = crypto.subtle.encrypt(algorithm, key, data); trong đó algorithm là
w = { name: "AES-GCM", iv: new Uint8Array(12), additionalData: e, tagLen: o };
a là CryptoKey vừa được sinh ra, d.Buffer là mật khẩu cần được mã hóa
Dữ liệu mã hóa mới sinh ra sẽ được chỉnh sửa một lần nữa để thành một đoạn string theo format của Facebook. Phần này xin được để cho các bạn đọc thử sức decode đoạn mã trên.
3. Ưu điểm và nhược điểm của CSE trên Facebook
3.1. Ưu điểm
Tăng cường bảo mật trên đường truyền: Dữ liệu không bị gửi dưới dạng plaintext ngay cả khi HTTPS bị vô hiệu hóa hoặc bị tấn công.
Giảm nguy cơ bị đánh cắp thông tin qua keylogger hoặc phần mềm gián điệp: Nếu kẻ tấn công có quyền truy cập vào dữ liệu gửi đi từ trình duyệt, họ chỉ có thể thấy phiên bản mật khẩu đã mã hóa.
Hạn chế tấn công Man-in-the-Middle (MITM): Nếu một hacker chặn dữ liệu giữa người dùng và Facebook, họ cũng chỉ nhận được chuỗi mã hóa thay vì mật khẩu thực tế.
3.2. Nhược điểm
Không bảo vệ được nếu thiết bị bị kiểm soát: Nếu hacker có quyền kiểm soát thiết bị của người dùng, họ có thể lấy được mật khẩu trước khi mã hóa.
Phụ thuộc vào JavaScript: Nếu JavaScript bị vô hiệu hóa trên trình duyệt hoặc có lỗ hổng bảo mật trong mã JavaScript của Facebook, cơ chế mã hóa có thể bị vô hiệu hóa hoặc khai thác.
Không bảo vệ được khi mật khẩu yếu: Dù mật khẩu có được mã hóa trước khi gửi đi, nếu người dùng đặt mật khẩu quá yếu (vd: “123456”), hacker vẫn có thể đoán hoặc brute-force dễ dàng.
Nguồn tham khảo: https://hackmd.io/@Ostrym/facebook-client-side-encryption