Chrome Key - A Cryptography Story
We look into cryptography building blocks, key wrapping techniques, bad entropy (with a bruteforce attack), and finalize with a quick analysis on attack costs.
On the last blog post of this series, we will look at the cryptographic guarantees of this project, some common mistakes developers make and how bad choices can make you vulnerable without being obvious.
Let's remember what we set out to do, to build a Chrome Extension that emulates a Hardware Authentication Device (HAD).
We began this journey by setting the foundation of the Chrome Extension, with special emphasis on the communication between the different components. Because our extension aims at supporting the WebAuthn protocol, on the second part we focused on the protocol itself, and how one can build properly formatted responses using already existing standards like Web Cryptography API, CBOR, JWK among others.
On this post we will begin be looking at why we need to protect the private keys we generate and some of the cryptography building blocks that we are paying extra attention to. We will then look into key wrapping techniques and the consequences of using bad entropy. Because of the latter we will also perform a brute force attack on our own extension. To wrap up we will analyze how key handling may expose you to different attack vectors and we will finalize with a quick analysis of attack costs and how our extension (and WebAuthn specifically) greatly increase said cost to the attacker.
This post assumes you're comfortable with concepts such as hashing, general encryption, initialization vector, and salt.
I cannot emphasize enough that this project by no means replaces a Hardware Authentication Device. The use of this extension is aimed exclusively at development, testing and debugging. If you use it in a production environment, do so at your own risk. We will elaborate on the guarantees (and lack of) that this project provides on this post.
Be it a password or a private key, it is highly recommended that you hide secrets when you save them to a data store.
In our Chrome Extension, because we want to be able to re-use the keys across any device, we need to use Chrome's sync storage. In the previous post we saw that this storage is not protected at all, therefore we're making an attempt at protecting our secrets ourselves.
Remember that for each WebAuthn login we generate a separate private key. This is the private key we are trying to protect. Ultimately we want to wrap it with a symmetric algorithm, so to achieve that we need to ensure we have some important building blocks.
Most modern encryption algorithms rely on some sort of randomness to make it much harder for an attacker to compromise a piece of information. True randomness is really hard to achieve! Arguably the most common type of randomness used is pseudo-random generators.
[Pseudo-random number generator] are algorithms that use mathematical formulae or simply pre-calculated tables to produce sequences of numbers that appear random.
The worse the source of randomness is, the more likely it is that it can be reverse engineered and replicated.
In our quest to encrypt the private key, we will need such a source. The Web Cryptography API provides a standard for random values
(...) implementations are not using a truly random number generator, but they are using a pseudo-random number generator seeded with a value with enough entropy. The PRNG used differs from one implementation to the other but is suitable for cryptographic usages. Implementations are also required to use a seed with enough entropy, like a system-level entropy source.
Our project will rely on AES GCM to perform the encryption of the private key, i.e. wrap it. Combining randomness with initialization vectors and salt, we want to be able to deterministically generate the wrapping key repeatedly by knowing a pin only.
Starting with only a pin, we generate a random salt and apply a key derivation function to it, in this case PBKDF2. With such key we can deterministically derive an
AES GCM wrapping key.
Once more we are deferring to the Web Cryptography API standard and its
The average rate at which information is produced by a stochastic source of data.
Oversimplifying a little, it refers to the range of different values that we can chose from.
Unbeknownst to some developers, entropy is a crucial part of software security. The most common example of tentative entropy increase you may have come across is the password length - the longer the password, the higher the entropy.
What we have done so far in terms of protecting our private key will come across as robust and sufficient. We have used standard algorithms and strong pseudo-random sources. However you likely noticed it above that we used a 4-digit pin as the passphrase, which represents 10000 possibilities. A very low entropy input!
So low in fact that I thought we could have some fun, like brute force ourselves. The scenario is simple: the database has been compromised and our encrypted private keys are now at the hands of the attacker. Only a 4-digit pin stands between them and total domination.
Of note, even though we use a custom storage format, and contrary to what some people may say, obscurity is not security! So for all we care, the attacker also knows the format of the wrapped keys as they were stored on the database.
Here is the function we will use to attack ourselves.
This is a very naive implementation. The key was encrypted using the pin
9999 and the implementation does a sequential test of all possible numbers, so it is the very last combination. Also the function was executed on the Chrome Developer Tools in a single-threaded scenario using my personal laptop which had everything else running on it.
We brute forced the encryption of one entry in just 7 minutes! This is the first major reason why this extension should not be used in a production environment.
Another important topic to cover is the locations that see the key material. The more isolated the better. These can range from disk, memory, wire to an interface.
In a Chrome Extension scenario, we will have to load the key into memory in order to perform the signing operations required by the WebAuthn protocol. This is a place where a true hardware authentication device shines. A device such a Yubikey stores key material in internal memory and it never exports it, under no circumstances. Not only that but it also (usually) requires user presence verification in the form of a touch to access that material and perform whichever cryptographic operation it was asked to perform.
On the first post of the series we defined the need to press the extension icon as the enforcement of this requirement, coupled with the introduction of a pin.
We already mentioned that the keys are loaded into memory. Immediately this puts us at the mercy of the following factors:
- How good is our implementation? If we have a bad bug we may expose sensitive information.
- How good is Chrome's sandboxing? An extension is constrained by several isolation mechanisms, none of which we control and therefore we are as secure as Chrome is.
- If it is in memory a dedicated enough attacker can see it.
This is the second major reason why this extension should not be used in production.
Cost of Attack
To wrap this series up it feels very appropriate to emphasize what brought us here in the first place, what problem are we trying to solve.
In its simplest form, security systems attempt to protect items in such a way that their value is lower than the cost of compromising them.
A bank account holds a certain balance, a social security number enables an attacker to apply for credit cards, your browsing history allows targeted ads. This is to say that "value" is not always straightforward to calculate, but nonetheless the principle stands.
WebAuthn specifically shifts the credential management paradigm in such a way that it greatly increases the cost of an attack. A single database that holds all customers' passwords is (potentially) quite valuable, so targeting a single system may yield great returns.
By moving sensitive information to each customer's machines or their HADs, the cost now shifts from compromising one system to compromising however many customers there are. Combined with properties previously mentioned such as user presence, remote access alone is not enough either since physical access is required to touch the device, further increasing the cost of an attack.
Thank you for participating in this exploratory journey. It is now time to release Chrome Key to the world! There is still plenty of work to do and features to add, but I think we are in a good place to include everyone in its development.
The extension is now available on the Chrome Store and Github. Contribute, report issues and share!
I'd like also like to thank Gavi Galloway for his priceless reviews on both the content side and the code, and his many amazing suggestions that ultimately made this series as fun to write as it was to research.
If you have any comments or suggestions please send me a tweet @joaomppeixoto.