--- tags: Facebook, \#PWD_BROWSER, password, crypto, encryption title: Demistify the Facebook client-side password encryption description: "Demistify the Facebook client-side password encryption ! Are you wondering where this famous #PWD_BROWSER comes from, and how the encryption is generated? All is revealed here" image: https://wi-images.condecdn.net/image/oYAW0MbXYMg/crop/1620/f/encryption_1.jpg --- # Demistify the Facebook client-side password encryption ## Introduction Some time ago, I developed a private API of instagram to be able to perform some actions I needed. The API was working very well until one day in 2020, when the parameters sent to the server to authenticate changed. Indeed, the password was no longer sent in clear text, but encrypted in this way: `#PWD_BROWSER:5:1617376020:AdhQAKb3zEewux6J98xFvie1HjaFRlSTWesGmeAuwW03KpZ1ia4jCMf4jv6ekezoGltbU5QPqbC2alzFutmA7xOQ2M1S1Lkge9qGB94F6rWeWMDqHchFb8uD8MRY9oid0QTZm5nOumSR24lfTaVO29xh2Q==`. It was still possible to authenticate by sending your credentials in clear text, but I asked how the encrypted password was generated. I didn't find anything concrete, so I wanted to search by myself, in order to understand and be able to generate this encrypted password and authenticate myself with it. ## Trace analysis Let's make a connection request with the following identifiers: email: `test@test.com` password: `mysuperpassword` In the fields of our POST request, we notice the following field: `encpass="#PWD_BROWSER:5:1617376020:AdhQAKb3zEewux6J98xFvie1HjaFRlSTWesGmeAuwW03KpZ1ia4jCMf4jv6ekezoGltbU5QPqbC2alzFutmA7xOQ2M1S1Lkge9qGB94F6rWeWMDqHchFb8uD8MRY9oid0QTZm5nOumSR24lfTaVO29xh2Q=="` The name of the field is quite explicit, we strongly assume that it is our encrypted password. At first glance, it looks like the /etc/shadow file format used under Linux or UNIX-like system. Field 5 could be SHA-256. Let's have a look at it. Since it is necessarily encrypted on the client side, we analyze the trace of our POST request to find the function that encrypts the password. ![traces](https://i.imgur.com/YqxqFQa.png) Fig. 1 And bingo! We notice a function named `_encryptBeforeSending` ! It's time to open the file on line 36 as shown. ![functions](https://i.imgur.com/ijf5sHV.png) Fig. 2 Let's get this line and display it in a more readable way. ## _encryptBeforeSending: High-layer encryption function ```javascript= __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() }, ... ``` Even without necessarily understanding all the variables displayed, it is generally understood that the `init` function is the entry point (see also Fig. 1) and calls the `_encryptBeforeSending(a)` function. This parameter passed as an argument is a function that allows to submit the connection form. Note the function `__d`. The function `__d` is API for RequireJS used to define a Module. Refered to the RequireJS doc: ```javascript= //Explicitly defines the "foo/title" module: define("foo/title", ["my/cart", "my/inventory"], function(cart, inventory) { //Define foo/title object in here. } ); ``` Then call the module as follow: ```javascript= require("foo/title") ``` By the way, we strongly assume that the `b` function refers to the `require` function. We then notice in the `_encryptBeforeSending` function that an input with `name: "encpass"` is created with the value `c`, returned by the call to the function `e.encryptPassword(c.keyId, c.publicKey, d.value, f)`. This is the POST field we observed earlier, which contained the encrypted password. This input is well created before the call to `a()`, which submits the form. Before going into the code of this function in more detail, we will try to understand where the arguments given to this function come from. ### Middle-layer encryption function, call parameters #### c.keyId & c.publicKey parameters Going back to `c`, we see that this object is created higher as follows: `var c = h.loginFormParams && h.loginFormParams.pubKey;` So now we have to go back to `h`. We notice quite quickly that `h` corresponds to the current module, so we will use the firefox console to display it, and see if we can find these `keyId` and `pubKey`. ![](https://i.imgur.com/iAFlGOo.png) Fig. 3 Nice, it contains the public key and the identifier of this key. ``` Object { publicKey: "53d38c45d2b6ff5bb0b843dfef4e060446596a93f970510b5fe615671ef3c457", keyId: 216 } ``` :::info Note that the public key and the identifier change every day. ::: #### d.value & f parameters For `d.value`, this is the user's password; and `f` the timestamp. We can now look at the contents of the `encryptPassword` function, called by the `FBBrowserPasswordEncryption` module. We find this module in the same file as Fig. 2, and we proceed to the same manipulation. ## encryptPassword: Middle-layer encryption function ```javascript= __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); ``` We find our `encryptPassword` function in which all the variable names are also letters, but I have manually replaced some variable names in order to see more clearly. We notice on line 18 that a `join` function is called on an array containing all the elements of the encrypted password pattern. We find there in the order: * The id `#PWD_BROWSER`, * The number 5, * The timestamp, * `j` returned by the `case 0`, encoded in base64. The first 3 values are trivial to obtain, what really interests us is this value `j`, obtained by calling `b("EnvelopeEncryption").encrypt(keyID, publicKey, passUTF8_decoded, timestampUTF8_decoded)`. This is the 3rd nested "encrypt" function, maybe the last one haha. Let's see what it contains. ## encrypt: Low-layer encryption function ```javascript= __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); ``` This function seems to be the right one, the one that finally generates the encrypted key. We notice towards the end, the call to the `generateKey` function, so we'll first focus directly on this part of the code: ### sublte.generateKey ```javascript= 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); ``` By inquiring about the `generateKey` function, we learn that is a method of the SubtleCrypto interface to generate a new key (for symmetric algorithms) or key pair (for public-key algorithms). In our case, the variable `g` corresponds to `window.crypto`. This object allows web pages to run various cryptographic operations on the browser side. Here is the syntax of the `generateKey` function: `const result = crypto.subtle.generateKey(algorithm, extractable, keyUsages);` with: * `algorithm` is a dictionary object defining the type of key to generate and providing extra algorithm-specific parameters. * `extractable` is a Boolean indicating whether it will be possible to export the key using SubtleCrypto.exportKey() or SubtleCrypto.wrapKey(). * `keyUsages` is an Array indicating what can be done with the newly generated key. In our case, `algorithm` is `v`, corresponding to the following object: ```javascript= v = { name: "AES-GCM", length: n * 8 }; ``` `length` is the length in bits of the key to generate. Here, `n` is a variable defined above, equal to 32. So we learn that the generated key will be a symmetrical key, AES-GCM, of 256 bits. But wait, the format `#PWD_BROWSER:5:1617376020:...` was leading us into error after all? It's not SHA-256? It looks like it, let's continue `extractable` is set to true, and `keyUsages` indicates that it is possible to encrypt and decrypt with the generated key. Finally, `result` is a Promise that fulfills with a CryptoKey (for symmetric algorithms) or a CryptoKeyPair (for public-key algorithms). For our case, it's a CryptoKey. Now let's look at the first part of the code, the result of the first Promise: ```javascript= 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]) }) ``` The freshly generated AES-GCM key is exported in the `c` variable. It is stored in `raw`, in an `ArrayBuffer`. Then the `encrypt` function is called. This function has the following signature: `const result = crypto.subtle.encrypt(algorithm, key, data); ` In our case, `algorithm` corresponds to `w`, which is the next object: ```javascript= w = { name: "AES-GCM", iv: new Uint8Array(12), additionalData: e, tagLen: o }; ``` :::info * iv: A BufferSource — the initialization vector. This must be unique for every encryption operation carried out with a given key. * additionalData: A BufferSource. This contains additional data that will not be encrypted but will be authenticated along with the encrypted data. If additionalData is given here then the same data must be given in the corresponding call to decrypt(): if the data given to the decrypt() call does not match the original data, the decryption will throw an exception. This gives you a way to authenticate associated data without having to encrypt it. ::: `key` is the `a` CryptoKey generated `d.buffer`, corresponds to the buffer of the `Uint8Array` which contains our password in clear text. The function returns in the variable `a` a Promise that fulfills with an ArrayBuffer containing the "ciphertext". The size of the cipher is equal to the size of the password plus 16 bytes. Now that we have seen the result of this first Promise, let us look at the result of the second: ```javascript= .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 }) ``` As we saw before, we know that the argument `a` corresponds to an array `[AES_Key_buffer, CipherText_buffer]`. For the rest of the variables, you just have to look above to make the correspondences. I put the code back after making the variable names more understandable: ```javascript= then(function(a) { var aes_key = new Uint8Array(a[0]); aes_key_encrypted = b("tweetnacl-sealedbox-js").seal(aes_key, publicKey_uint8array); key[u] = aes_key_encrypted.length & 255; key[u + 1] = aes_key_encrypted.length >> 8 & 255; u += m; // u = 4 key.set(aes_key_encrypted, u); u += n ; // u = 36 (n=32) u += l; // u = 84 (l=48) if (aes_key_encrypted.length !== n + l) throw new Error("encrypted key is the wrong length"); cipher = new Uint8Array(a[1]); cipher_rigth_part = cipher.slice(-o); cipher_left_part = cipher.slice(0, -o); key.set(cipher_rigth_part, u); u += o; // u = 100 key.set(cipher_left_part, u); return key }) ``` `b("tweetnacl-sealedbox-js").seal(buffer, publicKey);` is used to encrypt the `buffer` using the public key provided. Here, the encryption key `aes_key` is encrypted using the public key given in parameter. The size of the encrypted buffer is equal to the size of the buffer + 48 bytes of overhead. Here the AES key is 256 bits, or 32 bytes; the size of the encrypted aes key will therefore be 48+32=100 bytes. `key` is a Uint8Array defined above as follows: ```javascript= var h = 64, i = 1, j = 1, k = 1, l = b("tweetnacl-sealedbox-js").overheadLength, // always 48 m = 2, n = 32, o = 16, p = j + k + m + n + l + o; // = 100 ... case 0: f = p + pass.length; // = 100 + pass.length ... case 6: key = new Uint8Array(f); // key.length = 100 + pass.length u = 0; key[u] = i; u += j; key[u] = keyID; u += k; ``` Note that the first values of the key are always fixed by constants. `key[0]=1` `key[1]=keyID` `key[2]=aes_key_encrypted.length & 255 = 80` `key[3]=aes_key_encrypted.length>>8 & 255 = 0` Then, ```javascript= key.set(aes_key_encrypted, u); // u == 4 ``` We store in the key at index `u`, the value of the encrypted aes key. We suspect that Facebook has the private key to decrypt this key. Finally, the last interesting part: ```javascript= // o = 16 cipher = new Uint8Array(a[1]); cipher_rigth_part = cipher.slice(-o); cipher_left_part = cipher.slice(0, -o); key.set(cipher_rigth_part, u); u += o; // u = 100 key.set(cipher_left_part, u); return key ``` The cipher is split from the 16 end bytes. The two parts of the cipher are inverted and added to the key. ## Resume The key is finally complete and valid. Here is a short summary of the contents of the key: ![](https://i.imgur.com/z1groBM.png) Fig. 4 The key is generated, but a small detail is missing. Indeed, remember, this key is then encoded in base 64. Let's go back to the previous function: ```javascript= 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(":")); ``` A simple call to `b("tweetnacl-util").encodeBase64(j)` on our freshly generated key, and we have our base64 key. We `join` the whole thing and we have a complete and valid key, ready to send to the Facebook server.