Choosing a Cryptography Library for JavaScript: Noble vs. Libsodium.js
You need to use cryptography in JavaScript? Now comes the question what to use? What to avoid? Let's dive in.
Options
Although there are many options, in this post I want to focus on what I believe are the two most interesting contenders: Noble and Libsodium.js.
Noble
Noble is written in pure Javascript and divided up into 4 packages (ciphers, hashes, curves, post-quantum).
Libsodium.js
Libsodium.js is Libsodium compiled to JavaScript using Emscripten with convinience wrappers. It contains both a WebAssembly version and a JavaScript version for environments that don’t support WebAssembly. Using WebAssembly often still requires specific setup for bundlers or in Node.js. Due the convinience wrappers this is not necessary. Instead you need to await the exported function ready
, before you can use most of it's functions.
Why did I not go with the Web Crypto API?
Let me get elephant out of the room first. If the Web Crypto API supports your use case, it is likely the best option to choose. It should be fast and secure. That said, here my list of reasons why I didn't go with it:
- It's Promise based. For my work I started out with a promised based API, but switching to a synchronous API simplified the whole architecture.
- It lacks algorithms that I use or plan to use: XChaCha20-Poly1305, Ed25519, ML-KEM, Argon2
- For React Native you need to use a package to setup polyfills react-native-quick-crypto
Noble vs Libsodium.js
Audit
To me the most crutial factor is: Has the code been audited by someone credible. It provides a solid level of confidence that the code is secure and free of vulnerabilities.
Noble has only been audited partially. The hashes package has
been
audited by
Cure53 but a handfull of algorithms were excluded e.g. blake3
and argon2
. The curves package has been audited by by Trail
of
Bits
and Kudelski
Security,
but also only partially. The ciphers and post-quantum package
have not been audited.
Libsodium has been audited by Dr. Matthew Green of Cryptography Engineering, but also only a subset of all available algorithms.
Tests
I have not investigated a lot, but both seem to have a solid test-suite in place and both are using test-vectors to verify the correctness of the algorithms. Definitly a good sign.
Size
Note: These values are from the respective repositories and I did not verify them myself.
Noble packages are tree-shake-friendly. This means good bundler will only include the necessary code into the bundle.
Here are the actual sizes:
- @noble/hashes 17KB gzipped
- @noble/curves 87KB gzipped
- @noble/ciphers 8KB gzipped
- @noble/post-quantum 20KB gzipped including hashes (which it depends on)
The complete Libsodium.js library weighs 188KB (minified, gzipped).
Environments
Noble runs in every environment without any additional setup.
Libsodium.js runs in every environments except React Native. React Native doesn't support WebAssembly, but this isn't the issue here since Libsodium.js includes a fallback. The Emscripten JavaScript output just doesn't work with React Native. As an alternative you can use react-native-libsodium. It matches the Libsodium.js API, but it hasn't been audited and lacks features and algorithms compared to Libsodium.js. You can contribute to it and as it's author and maintainer I'm happy to accept Pull Requests.
There might be an easier alternative by creating a Libsodium.js build using react-native-webassembly, but I haven't tried and don't know if there are any caveats.
Performance
This section actually triggered this post. Over the years I heard plenty of people making claims about performance over various cryptography libraries, but rarely saw a benchmark with actual comparisons.
My goal was to compare the performance of both libraries in an actual browser environment. In addition I wanted to make sure inputs with different sizes are tested. I implemented only a handful of operations, but happy to accept Pull Requests.
You can try the benchmark yourself here: https://cryptography-benchmark.vercel.app/. The source is available at https://github.com/nikgraf/cryptography-benchmark.
Here the result of a benchmark run on a M1 MacBook Pro 2020 in Chrome 124:
Operation | Source | Noble ops/sec | Libsodium ops/sec | Factor faster |
---|---|---|---|---|
ed25519 keypair generation | 8769 | 15878 | 1.8 | |
ed25519 sign | 10 Bytes | 4451 | 32175 | 7.2 |
ed25519 sign | 1 KB | 4137 | 28653 | 6.9 |
ed25519 sign | 100 KB | 342 | 2260 | 6.6 |
ed25519 sign | 1 MB | 36 | 241 | 6.7 |
ed25519 verify | 10 Bytes | 951 | 11413 | 12.0 |
ed25519 verify | 1 KB | 922 | 11370 | 12.3 |
ed25519 verify | 100 KB | 414 | 3413 | 8.2 |
ed25519 verify | 1 MB | 67 | 462 | 6.9 |
xchacha20poly1305 key generation (32byte | 1000000 | 30750 | 32.5 | |
xchacha20poly1305 encrypt | 10 Bytes | 167224 | 675676 | 4.0 |
xchacha20poly1305 encrypt | 1 KB | 107527 | 303030 | 2.8 |
xchacha20poly1305 encrypt | 100 KB | 1908 | 4566 | 2.4 |
xchacha20poly1305 encrypt | 1 MB | 185 | 422 | 2.3 |
xchacha20poly1305 decrypt | 10 Bytes | 233100 | 1041667 | 4.5 |
xchacha20poly1305 decrypt | 1 KB | 99502 | 285714 | 2.9 |
xchacha20poly1305 decrypt | 100 KB | 1698 | 4149 | 2.4 |
xchacha20poly1305 decrypt | 1 MB | 173 | 424 | 2.5 |
Results
Libsodium.js is faster in all cases except the random byte generation. The difference is in the one to two-digit range in terms of the factor. Unless you need to run thousands of operations per second a user of a browser application probably won't notice the difference.
One additional important take-away is that the size of the input data has a significant impact on the performance, but the factor faster stays relatively the same (except for ed25519 verify in Libsodium.js). While there is a difference I argue for most applications where cryptography happens when new data is sent or received the difference is negligible.
Note that according to Frank Denis, the author of Libsodium and Libsodium.js, the WebAssembly version of Libsodium.js could be significantly faster if support for Safari on old iOS versions would be dropped.
Paul Miller, the author of Noble, pointed out that noble-hashes (not part of my benchmarks) could be about 4x+ faster, but it would conflict with the unsafe-eval
CSP policy.
Mobile performance
I ran the benchmark on an iPhone X (iOS 17) in Safari. The difference between Noble and Libsodium.js are very similar to the results on the MacBook Pro. In general on the Macbook Pro there were roughly 1.5 to 2.5x more ops/sec compared to the iPhone X.
Here the results of a benchmark run on an iPhone X (iOS 17) in Safari:
Operation | Source | Noble ops/sec | Libsodium ops/sec | Factor faster |
---|---|---|---|---|
ed25519 keypair generation | 4348 | 13405 | 3.1 | |
ed25519 sign | 10 Bytes | 2484 | 16892 | 6.8 |
ed25519 sign | 1 KB | 2265 | 14925 | 6.6 |
ed25519 sign | 100 KB | 252 | 1235 | 4.9 |
ed25519 sign | 1 MB | 28 | 132 | 4.8 |
ed25519 verify | 10 Bytes | 506 | 6878 | 13.6 |
ed25519 verify | 1 KB | 493 | 6757 | 13.7 |
ed25519 verify | 100 KB | 267 | 1905 | 7.1 |
ed25519 verify | 1 MB | 51 | 256 | 5.1 |
xchacha20poly1305 key generation (32byte | 1666667 | 80645 | 20.7 | |
xchacha20poly1305 encrypt | 10 Bytes | 92593 | 333333 | 3.6 |
xchacha20poly1305 encrypt | 1 KB | 50000 | 200000 | 4.0 |
xchacha20poly1305 encrypt | 100 KB | 1031 | 2667 | 2.6 |
xchacha20poly1305 encrypt | 1 MB | 104 | 267 | 2.6 |
xchacha20poly1305 decrypt | 10 Bytes | 153846 | 555556 | 3.6 |
xchacha20poly1305 decrypt | 1 KB | 62500 | 153846 | 2.5 |
xchacha20poly1305 decrypt | 100 KB | 1042 | 2632 | 2.5 |
xchacha20poly1305 decrypt | 1 MB | 104 | 270 | 2.6 |
Chrome Dev Tools slow down
While developing the benchmarks I discovered that having the Chrome DevTools open would impact the performance of libsodium.js. I wonder why this is the case and what else could have an impact. If you compare the results to above Noble becomes stays relatively the same and Libsodium.js becomes slower and it seems to get worse the larger the input data is.
Here the result of a benchmark run on my M1 MacBook Pro 2020:
Operation | Source | Noble ops/sec | Libsodium ops/sec | Factor faster |
---|---|---|---|---|
ed25519 keypair generation | 8687 | 10290 | 1.2 | |
ed25519 sign | 10 Bytes | 4620 | 13301 | 2.9 |
ed25519 sign | 1 KB | 4168 | 6766 | 1.6 |
ed25519 sign | 100 KB | 338 | 140 | 2.4 |
ed25519 sign | 1 MB | 35 | 14 | 2.4 |
ed25519 verify | 10 Bytes | 913 | 5575 | 6.1 |
ed25519 verify | 1 KB | 921 | 4614 | 5.0 |
ed25519 verify | 100 KB | 407 | 272 | 1.5 |
ed25519 verify | 1 MB | 66 | 28 | 2.3 |
xchacha20poly1305 key generation (32byte | 980392 | 29638 | 33.1 | |
xchacha20poly1305 encrypt | 10 Bytes | 137363 | 152905 | 1.1 |
xchacha20poly1305 encrypt | 1 KB | 103627 | 28169 | 3.7 |
xchacha20poly1305 encrypt | 100 KB | 1889 | 332 | 5.7 |
xchacha20poly1305 encrypt | 1 MB | 189 | 33 | 5.7 |
xchacha20poly1305 decrypt | 10 Bytes | 265252 | 157233 | 1.7 |
xchacha20poly1305 decrypt | 1 KB | 111732 | 28129 | 4.0 |
xchacha20poly1305 decrypt | 100 KB | 1901 | 331 | 5.7 |
xchacha20poly1305 decrypt | 1 MB | 190 | 33 | 5.7 |
Which one to choose?
As always it depends. In most cases I would go with Noble because:
- Well audited (mostly)
- Runs in every environment (since it's pure JavaScript)
- Tree-shakeable
- Fast enough for most use-cases
While I can't speak for his reasoning, I would like to add a quote from Frank Denis, the author of Libsodium and Libsodium.js
libsodium.js was a nice contribution, but honestly, for crypto in JavaScript today, I'd rather use WebCrypto when possible, and Noble cryptography for everything else.
Source: https://github.com/jedisct1/libsodium.js/issues/327#issuecomment-1793419292
Of course there are very valid reasons why you might still choose Libsodium.js.
- You really need the the performance advantage
- You already using Libsodium in other languages and this way your team only needs to learn one API and you reduce your attack surface
I hope this post helps you to make an informed decision. If you have any questions or feedback, feel free to reach out to me.
P.S: As a library author you could design your library in a way that the developer can inject the cryptography library of their choice and provide examples or helpers for Noble and Libsodium.js. But this is a topic for another post :)
Published at: 2024-05-21
Updated at: 2024-05-21