Cryptography in the Browser

One of the challenges we encountered in creating Opal was finding a fast and secure way to do encryption and decryption in the browser.

This post describes why browser-side cryptography has been a difficult problem, and how recent technological advances provide a solution.

Three options for doing cryptography in a web app

The only language with universal browser support is JavaScript. Web apps like Opal are written in JavaScript so that they can run in any modern browser. For cryptographic functions to be useful to a web app like Opal, they must be accessable in JavaScript.

There are really only three options for exposing cryptographic functions to browser JavaScript:

1. Do the cryptography in a plugin.

Plugins are compiled code that run inside a browser and can be called by JavaScript.

For example, there are cryptography libraries that exist in both Java and Flash. These implementations are often very fast, but unfortunately they require users to install browser plugins, which many are unwilling or unable to do (for example, if they are on a public computer).

Another option is to use Chrome's Native Client, which allows browsers to run machine code compiled from C or C++. Again, such implementations can be very fast, but right now Native Client is only available on Chrome.

So while plugins and Native Client have the benefit of speed, they lack portability because they require users to install specific plugins or use a specific browser.

2. Use a Web API.

The upcoming Web Crypto API will expose native cryptographic primitives to JavaScript, allowing web apps to use fast encryption and decryption routines. However, this API is still in the draft stage, and it may be a while before it sees wide adoption among the major browser vendors. As it stands now, only crypto.getRandomValues() has been implemented in most browsers.

Until the Web Crypto API becomes widely implemented, it's not a viable option for browser-side cryptography.

3. Do the cryptography directly in JavaScript.

This approach has the benefit of being extremely portable. All browsers can run JavaScript, so all browsers can run a JavaScript cryptography library.

There are two main criticisms of doing cryptography in JavaScript: security and speed. We discuss each of these criticisms in turn.

JavaScript cryptography can be safe

This post claims that "JavaScript Cryptography Considered Harmful", and lists a lot of evidence to support that claim.

Some of the statements on that post are no longer accurate. For example, the post makes the point that Math.random() is not a good source of randomness, and thus it is impossible to get a suitable amount of random numbers for cryptography. While it is true that Math.random() is not a good source of randomness, modern browsers have a crypto.getRandomValues() function which does provide enough randomness.

There are certainly use cases where JavaScript cryptography is a bad idea, but there are cases where it makes sense.

This response does a great job of refuting many of the claims in the first post, and also points out two valid use cases for JavaScript cryptography: end-to-end message security (a.k.a. host-proof applications), and secure remote password authentication. These are exactly Opal's cryptographic use cases, so from a security standpoint we are comfortable doing cryptography in JavaScript.

JavaScript cryptography can be fast

Until recently, JavaScript has been too slow to perform the many complex computations required for secure cryptography. This has caused many applications to rely on plugins for cryptography, which are less portable and often annoying to users.

Fortunately, JavaScript performance has been increasing at an amazing rate in recent years, so that it is now practical to perform cryptographic operations entirely in JavaScript. Today, there are many javascript cryptography libraries to choose from.

The problem then becomes which one to choose.

NaCl: a trusted C cryptography library

NaCl (pronounced "salt") is a C library that exposes high-level functionality for symmetric and public-key encryption, decryption, signing, verification, etc. It was written by cryptographers, and is well-known and trusted in the cryptographic community. The one problem with NaCl is that it's written in C, and C is not JavaScript.

js-NaCl: NaCl compiled to JavaScript

Luckily, we can compile NaCl to LLVM bytecode, and then use emscripten to compile LLVM bytecode to JavaScript. Moreover, the LLVM compiler can perform many optimizations during compilation, so the resulting JavaScript is highly optimized. Thus, we can compile the NaCl C library into JavaScript, ready to be run in a browser!

The js-nacl project is exactly this: the NaCl cryptography library compiled into JavaScript.

asm.js is fast!

What's even better is that emscripten can compile to a very special subset of JavaScript, called asm.js. You can think of asm.js as assembly language dressed up like JavaScript. When a browser encounters a chunk of asm.js code, it can compile it to very efficient machine code which runs at near-native speeds.

Currently, Firefox is the only major browser to support asm.js optimizations. This makes the encryption and decryption with js-nacl very fast in Firefox, between 2 and 8 times faster than Chrome, depending on the operation. But even in Chrome, js-nacl is blazingly fast, outperforming all the other cryptography libraries we tested. crazy fast in FF

The combination of a trusted security library like NaCl with fast performance in modern browsers makes js-nacl the obvious cryptography library for web apps like Opal.

For similar reasons, Opal uses an emscripten-compiled asm.js version of the popular scrypt library for passphrase stretching (working on the post now). You can see exact benchmarks of js-nacl and js-scrypt from the project maintainer here. We also set up some jsperf tests for js-nacl to get a better idea of performance across browser versions, feel free to run them.

Update: asm.js is getting even faster!

Since writing this post, the V8 team has implemented many optimizations that increase asm.js performance. asm.js benchmarks However, V8 still ignores the "use asm" pragma, and applies no special optimizations to the asm.js code. The issue for implementing asm.js-specific optimizations in V8 can be found here.

In our most recent testing, FireFox is still 2-4 times faster than Chrome when encrypting and decrypting with the NaCl library (both for public key and symmetric encryption), although Chrome is twice as fast at generating a key pair. A more in-depth post about these benchmarks will be published soon.

Tweet