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.
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.
Fig. 1
And bingo! We notice a function named _encryptBeforeSending
!
It's time to open the file on line 36 as shown.
Fig. 2
Let's get this line and display it in a more readable way.
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:
Then call the module as follow:
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.
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
.
Fig. 3
Nice, it contains the public key and the identifier of this key.
Note that the public key and the identifier change every day.
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.
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:
#PWD_BROWSER
,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.
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:
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:
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:
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:
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:
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:
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:
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,
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:
The cipher is split from the 16 end bytes.
The two parts of the cipher are inverted and added to the key.
The key is finally complete and valid. Here is a short summary of the contents of the key:
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:
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.