An Introduction to Footguns

or: Implicit conversions have never hurt anyone

June 12, 2020

Cryptography is a fascinating field, if for no other reason than it is incredibly easy to fail in exciting and spectacular ways while using it. It’s like watching an American Ninja blooper reel but with more math.

Footguns, a wonderful term for a tool that is perfectly crafted to shoot yourself in the foot with, are plentiful in cryptography. Algorithm choice, key management, and random numbers are all great places to make a small mistake that can render cryptographically strong tools nearly worthless.

In future blogs I'll go after some of the more intricate footguns that get much deeper into the underlying algorithms, but today's example is a very high-level mistake involving a type-error that has tragic consequences for the security of this example system.

Below we've got the bones of a simple system that takes in a password, converts it to a strengthened key, encrypts some text with that key, then attempts to decrypt it with the decryption password. We’re using the SubtleCrypto API with PBKDF2, SHA-512, PRNG (pseudo-random number generator), and AES-GCM. Don’t worry about the intricacies of all that if they're not familiar. The mistake here is much higher level than that.

                    
    //Pull the values from the page fields
    const clearMessage = document.getElementById('clearText').value;
    const encPwd = document.getElementById('encPassword').value;
    const decPwd = document.getElementById('decPassword').value;

    //Create a key to encrypt the text
    const encKey = await createKey(encPwd);

    //Encrypt the text
    let cipherText = await aesEncrypt(encoder.encode(clearMessage), encKey);

    //Create a key to decrypt the text from the second password
    const decKey = await createKey(decPwd);

    try {
        //Decrypt the text
        document.getElementById('decryptedText').value =
            await aesDecrypt(cipherText.cipherText, decKey, cipherText.iv);
    } catch (e) {
        //If this failed show the error
        document.getElementById('decryptedText').value = "Decryption Failed";
    }
                    
                

Try it out here:

If you tried out the live system above you can see it correctly en/decrypts your text. For those of you with a more pen-testing mindset, you might have already found the problem, but if not, go up and change the decryption password to something different from the encryption password.

As it turns out, any password will work to decrypt the message, which is a little less secure than we would normally want. It’s one of those scenarios you run into in programming where not only is the outcome wrong, it’s so incredibly wrong that it’s actually impressive. How could can literally any key decrypt an AES encrypted message?

Lets take a closer look at our key generation code. We're simulating a key rotation scheme that will use our entered password as a base then append the month and hash that result to give a different key for every month. That hash is used as a basis for the password strengthening PBKDF2 process that will give us the actual encryption key.

                    
                        //Simulate a key-rotation scheme by adding the month to the base password
                        //Then use the resulting hash as a base for the key generation
                        const pwdHash = await window.crypto.subtle.digest('SHA-512',
                            encoder.encode(pwd + new Date().getUTCMonth()));

                        //Get the key material
                        let keyMaterial = await window.crypto.subtle.importKey(
                            "raw",
                            encoder.encode(pwdHash),
                            {name: "PBKDF2"},
                            false,
                            ["deriveKey"]
                        );
                    
                

Lets dive into the red line above.

The TextEncoder.encode method takes a string and returns it as an array of Uint8s. But we're not giving it a string. The SubtleCrypto digest method returns, eventually, an ArrayBuffer. So what is coming out of the encode method?

Javascript Console

Console.log, is there anything you can't fix?

Some experimentation above shows us that no matter what string we hash, we always get the same array of Uint8s when we encode it. Either SHA512 has a serious collision problem or the encode method is doing something goofy.

Javascript Console

It can even do CSS

Since encode is supposed to take in a string, what if we've introduced a run-time type-error in Javascript? I know, I know, no one has ever had a problem with types in Javascript before.

Per the above tests, encode is taking a "making lemons from lemonade" view towards its input and finding a way to treat our ArrayBuffer as a string instead of just failing. The string being encoded has nothing to do with the contents of the ArrayBuffer and is, instead, just the fixed "[object ArrayBuffer]". We always end up with the same set of key data being fed into the key generation scheme.

So, we've coded up a crypto scheme that has a rotating key system, 100,000 iterations of PBKDF2, giant 512 bit hashes, but with the tiny problem that it uses the same hardcoded password for every operation.

                    
    const pwdHash = await window.crypto.subtle.digest('SHA-512', encoder.encode(pwd + new Date().getUTCDay()));

    //Get the key material
    let keyMaterial = await window.crypto.subtle.importKey(
        "raw",
        pwdHash,
        {name: "PBKDF2"},
        false,
        ["deriveKey"]
    );
                    
                

The fix here is simple. Just send through the ArrayBuffer to the importKey method. It can already work with an ArrayBuffer or a TypedArray (you know this because you, of course, read the documentation) so converting it was unnecessary in the first place and led to the whole mess.

Hopefully this has demonstrated the care and diligence that's needed to work with cryptography. Small mistakes that would have been just annoyances in another section of your app can become critical problems, and because of the complexity of cryptography, the symptoms can be easy to miss.