Building an App with WebCrypto - Dos and Don’ts from the Front
Christian Olbrich, Developer & Software Security Advisor at Boxcryptor
Christian Olbrich | Developer | Software Security Advisor
@olbri_ch
Thursday, September 1, 2016

Building an App with WebCrypto in 2016 – Dos and Don’ts from the Front

While working on our secure file transfer service Whisply, WebCrypto became our new best friend – even when it was sometimes hard to deal with, depending on what we wanted to do. If you want to create a browser-based app which involves crypto operations, you should definitely take advantage of what WebCrypto has to offer, for reasons of speed and security.

We used the WebCrypto API to create keys, and its encryption primitives for all possible encryption tasks Whisply has to handle. By allowing us to outsource all cryptographic operations to the client, WebCrypto allowed us to make our service zero knowledge to begin with.

Creating an end-to-end encrypted file transfer service that works in all browsers (and with “all” we mean no browsers from the dark ages like IE10 and below) gave us important insights into the specifics and pitfalls of WebCrypto. Now we want to share our experiences and our knowledge. We won’t just tell you how to successfully work with WebCrypto and how the state of the art is; but also what you have to do to create the worst, most unsecure browser-based app ever (which of course shouldn’t be your objective). Have fun with our story, and our dos and (definitely) don’ts.

What is WebCrypto?

WebCrypto is an API available in all modern browsers. It offers cryptographic primitives and allows the secure storage of keys. “WebCrypto, which provides asynchronous, multithreaded, hardware accelerated, near native performance, has made working with cryptography in web browsers a breeze, at least from a performance perspective” (Source: Medium). This graphic shows an overview of the possibilities of WebCrypto:

WebCrypto_Img1

And this is what it looks like:

window.crypto.subtle.generateKey(
    {
        name: "AES-CBC",
        length: 256,
    },
    false,
    ["encrypt", "decrypt"]
)
.then(function(key){
    console.log(key);
})
.catch(function(err){
    console.error(err);
});

The Main Arguments for WebCrypto: Speed and Security

Before the development of WebCrypto, one would work with JavaScript libraries to provide web-based encryption. However, the shift to this new API is beneficial for two important reasons: speed and security.

Compared to JavaScript cryptography libraries, WebCrypto is fast, really fast. In this comparison of JavaScript Cryptography Libraries the author comes to the following conclusion:

Tests show that WebCrypto is between two and fifteen times faster than any other JavaScript cryptography library. In real world application, the difference is even more significant since pure JavaScript libraries need to run inside Web Workers or restrict usage to very small data sets to reduce UI thread blocking.

Speed is an important factor in building apps, because users do not like to waste time waiting for apps to do what they are supposed to do. Still, the point of security is even more important. Having to wait for an app to perform can hurt the provider of the app, because the users will not enjoy using it when it is slow. Hence he will use a faster, but more insecure solution. Implementing bad security, however, is a nightmare, because the user’s privacy and the provider’s credibility are at stake.

JavaScript libraries are often insecure for a number of reasons:

  • Many JS-libraries are built by amateurs and amateurs almost always make bad mistakes, since crypto is just very difficult to get right.
  • Many tasks, such as creating secure, long RSA keys, take way too long if done correctly. As a “solution”, many JS-libraries just create insecure ones.
  • Some things are impossible to do without WebCrypto. Only with WebCrypto you get properly secure random numbers.

WebCrypto offers some built-in security measures, which is great for your user, but which again can make your life harder.

  • WebCrypto offers protection against overwriting (in Chrome), since window.crypto.subtle is read-only. This is good news for security but bad news for usability, because it makes it harder to work with polyfills.
  • Non-extractable keys make it very hard for attackers to steal your keys. Therefore, the “JavaScript is always insecure” issue is mostly resolved. You can create a key, store it at the client, and use it. But if you don’t specify it, you can never read the key, which means that attackers cannot do so. Tip: Use IndexedDB to store the keys.
  • WebCrypto uses SSL only (with the exception of localhost and extensions) which ensures that the data sent between the web server and the browser is encrypted and secure.

Another good reason for WebCrypto is that some things are not even possible without it, for example generating secure random numbers.

The Purpose of WebCrypto – What is the Threat Model?

Very often, WebCrypto is misunderstood regarding its purpose. It is not meant to protect the user from the server. If your goal is to protect yourself from the server, WebCrypto cannot help you, because the server itself decides what you get. Instead, valid use cases are to protect the server from the user, to protect users from passive attackers, and in our case, to enable us to implement our service as zero knowledge service.

A hypothetical example of the server being protected from the user is a cloud storage provider, such as Mega Upload, protecting themselves from legal issues. In the past, Mega was facing legal consequences, because its users uploaded copyright protected content, and Mega was accused of supporting this. Now, if Mega lets the user encrypt his content before uploading, Mega could just say “How should we know what the user uploads? We can’t read it!”, thus protecting themselves from that scenario.

WebCrypto also protects users from passive attackers, for example by pre-hashing passwords. And in our case with Whisply, thanks to WebCrypto we were able to outsource the crypto to the client and, therefore, to give the user maximum control over the data.

Polyfills – Why it is Likely You Need Them

When you work with the WebCrypto API you are going to need polyfills to have everything run smoothly on all platforms. This following graph suggests that WebCrypto runs perfectly fine on Chrome, Firefox, Edge and Safari.

WebCrypto2

(Source: caniuse)

However, reality – as often in life and coding – paints a very different picture. The next graph shows why polyfills are a must have to deliver a neat cross browser experience to the users.

WebCrypto3

As you can see, the results with Chrome are not that bad, and with Firefox there is still parameter support of 72%. However, functionality with Edge is much lower, and with Safari it is actually very limited. For more information how the functionality of the browsers has been tested, go here (Disclaimer: Tests that have not completed or did not show any result in the test page are counted as “not working”).

As Safari does not implement the newest specification (see below) and Edge keeps constantly improving, the real number can be far higher – at least for our use cases. After some struggles it turned out that we could use everything we needed with WebCrypto.

Working with Polyfills – Or How to Guess the Meaning of Browser Exceptions

In the very likely case that something does not work, you have to work with polyfills. Internet Explorer, for example, implements an old Spec. However, with the following code you can polyfill Internet Explorer and make it work seamlessly:

"use strict";

function promisifyMsCryptoOperation(msSubtle, func) {
    return function () {
        var _arguments = arguments;

        return new Promise(function (resolve, reject) {
            var operation = func.apply(msSubtle, _arguments);
            operation.onerror = function (event) {
                return reject(event);
            };
            operation.oncomplete = function (event) {
                return resolve(event.target.result);
            };
        });
    };
}

function createSubtle(msSubtle) {
    var result = {};
    for (var key in msSubtle) {
        result[key] = promisifyMsCryptoOperation(msSubtle, msSubtle[key]);
    }
    return result;
}

var subtle = createSubtle(window.msCrypto.subtle);

But before you get started with looking for a solution, you have to figure out what exactly does not work. For that one must rely on the exceptions the browsers throw. Those can be a real pain. Good browsers give you exceptions that point you in the right direction. In the case of Firefox we stumbled across the exception „SyntaxError: An invalid or illegal string was specified” – wow, Mozilla, thanks for this “helpful” error message. Meaningful exceptions at least let us know that there is something wrong with the import key, or with the code trying to use AES.

WebCrypto is a relatively new field and the exceptions of the different browsers already improved significantly compared to 2015. Back then, we encountered exceptions saying something like “something went wrong because something went wrong”. Very helpful. Thanks for nothing. However, there is hope that the exception will evolve quickly and become more helpful over the next years.

Polyfills are not avoidable when your App is supposed to work across different platforms and different browsers. The problem is that we did not find a polyfill that is complete and secure. Actually, there cannot be one, since RSA Ops are too expensive. The best polyfills are still missing basic operations, such as AES-CBC. The only solution is to use many polyfills or to create a solution yourself.

Problems in WebCrypto – Tales from the Front

Creating Keys with WebCrypto and Polyfills

An example of possible thresholds when working with polyfills is the creation of keys. The problem is that just because the browser lets you create a key, it does not mean that you can use it for other WebCrypto operations, or for JS operations, because the keys are secured. What if WebCrypto creates a key and the polyfill has to use it? For that reason, the polyfill always has to calculate two keys. It has to create a WebCrypto key and append a JS-key equivalent, so that the WebCrypto key is still usable as a parameter. If a polyfill is required, the polyfill checks for an appended JS-key.

Importing RSA Keys and “Bending Bricks”

There are many ways to encode RSA keys. However, we found JWK to be the currently most supported way to import general keys. In order to use it in Whisply, we have to unpack our DER-encoded keys manually, and create a valid JWK structure ourselves. This is kind of general RSA Key format hell. But with WebCrypto we even have more problems due to the fact that different browsers implement different version of the spec. Surprise, surprise.

Let’s assume, for example, that we have RSA keys in JWK format. Importing a key for use would seem easy, at the first glance:

var jwk = {
    alg: 'RSA-OAEP',
    key_ops: ['encrypt'],
    ext: true,
    e: 'AQAB',
    kty: 'RSA',
    n: 'voFDcAgBSoGnffppOk_ESoQnPg6JOYXJZRv4SD6USJgsSzmFuyqw-En7LS-WvW6twN1wwJjJAyXbzohEZQEvsaR7uTXdcFqNERXef_b-MZv7NdTm9BG2euG9zaHe5NZad89U5b9hGOS9RB_rBgIsg5VZXAuDF6HnltepsQFPWFc'
};

window.crypto.subtle
    .importKey('jwk', jwk, {name: 'RSA-OAEP', hash: {name: 'SHA-1'}}, false, ['encrypt'])
    .then(function(publicKey) {console.log(publicKey)})
    .catch(function(err) {console.error(err)})

This works in Chrome, Edge and Firefox. But what about Safari and Internet Explorer 11? If you try this in Safari you'll get:

TypeError: Only ArrayBuffer and ArrayBufferView objects can be passed as CryptoOperationData.

So what can be passed instead and what exactly is wrong here? Both question share the same answer: WebCrypto is not stable yet, since Safari does not implement the latest spec. If we take a look at the W3C Working Draft 25 from March 2014 we find the answer: “As currently specified, it is a JSON-encoded Javascript object, converted to a UTF-8 byte sequence”.

So now we have an answer, but the question remains, how to build a solution. While converting to JSON is pretty easy, converting to a UTF-8 byte sequence is hard. Therefore, we changed the specification. Once all browsers implement the new spec, developers do not have to deal with this anymore.

And this is how we solved the problem in the end:

function str2ab(str) {
  var buf = new ArrayBuffer(str.length);
  var bufView = new Uint8Array(buf);
  for (var i=0, strLen=str.length; i<strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

var data = str2ab(JSON.stringify(jwk));

window.crypto.webkitSubtle
    .importKey('jwk', data, {name: 'RSA-OAEP', hash: {name: 'SHA-1'}}, false, ['encrypt'])
    .then(function(publicKey) {console.log(publicKey)})
    .catch(function(err) {console.error(err)})

Still, it felt like we were trying to bend bricks, since there were so many additional browser quirks we had to deal with, such as „0100 is not a number because it starts with a zero“. While every calculator understands that “0100” = “100”, browsers sometimes apparently do not.

Encrypting Data with WebCrypto – an API Still in its Infancy

Another indicator that WebCrypto is a very young API, is the way that the encryption of data works. First, if you want to encrypt files, you would normally expect an application to load your files into memory in blocks and encrypt them. However, this is not how it works with WebCrypto. Instead, you load your complete file into memory – no matter how large it is – and encrypt it in one run. Second, there is no indication of “progress”. Users are very patient when the app freezes, right? When they do not have an indication that something is happening, they might turn their back on an app. Because of this, we even had to write our own polyfill to replace a fully functional WebCrypto-PBKDF2-function, in cases where we needed reporting of progress.

Implementation Keeps Improving

We run into the problem that PBKDF2 in Microsoft’s Edge was very slow, and our fallback was even slower. We tracked down that Hmac-SHA512 was the main show stopper. It took a ridiculous amount of time for one calculation. While we were spinning our head around and did a fallback implementation with the awesome asmcrypto library, the anniversary update arrived, and Edge was fast, all of a sudden.

This incident taught us to always keep in mind that we are in a work in progress terrain. If you have a problem, it might be worth to wait for a browser update.

Don’ts in WebCrypto – Or How to Build the Most Unsecure App Ever with WebCrypto

If you are working with WebCrypto and you are doing one of the following points, you should definitely think again and revisit your security strategy.

  • Be lazy! Everything is so much easier if every key is extractable by default, right?
  • Ignore best practices: For encryption, we need a very specific data set as a key. For AES, that’s either 16 or 32 byte of pseudorandom bytes. What do you do with an 8 character long password? Insecure answer: Just add “0” until it is 16 byte long, then use this as a key. Then the “key” does not at all fit the security requirements for an AES key, yet, it fits the format and can therefore be used with the API. The secure way: Use something standardized such as PBKDF2 to create a key from a password.
  • Trust all WebCrypto functions: If they are offered, they must be awesome and secure, right? Well, no. Old algorithms are required for legacy-reasons. However, a proposal to mark those old algorithms with console warnings has been turned down.
  • Use WebCrypto without a basic background in cryptography. Actually, we would argue that a basic course, such as Stanford’s free “Cryptorgraphy I” online course, is sufficient for most use-cases. However, if you do not understand what is going on in this course, you should probably keep your hands off WebCrypto.

What Will the Future Bring?

We could see clear improvement in WebCrypto in 2016 compared to 2015. There is more API coverage and it is becoming more and more stable. Documentation and tutorials are on the rise and they are becoming more consistent (you’ll find less tutorials with old API-calls that no longer work). We expect WebCrypto to behave like a common, stable API by the end of 2016 if improvements keep progressing as rapidly as they do now. However, other things are likely to stay suboptimal. For example, there are some things missing in the spec, such as indication of progress and byte by byte input feeding.

Nevertheless, the WebCrypto API is a valuable innovation that – if implemented correctly – can help enhance web security and privacy a lot. We are looking forward to seeing many examples of well implemented WebCrypto in the future.

Are you working on a project that profits from WebCrypto? We would like to hear your thoughts on it and how it is going for you. Feel free to reach out on Twitter.

About the Author: Christian is a software developer at Boxcryptor since 2012. During his master’s degree in Passau he specialized in cryptography and IT-security. When he is not working on making Boxcryptor and Whisply even more awesome, he loves to hone his skills in fencing or – less combative – in dancing.

Did you like what you read?

Then join over 80.000 subscribers and sign up for our free newsletter. Get info on data privacy, cloud stories, security tips, and insights from our crypto experts.

By entering your email address you accept our privacy policy.
Share this post