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:

OperationSourceNoble ops/secLibsodium ops/secFactor faster
ed25519 keypair generation8769158781.8
ed25519 sign10 Bytes4451321757.2
ed25519 sign1 KB4137286536.9
ed25519 sign100 KB34222606.6
ed25519 sign1 MB362416.7
ed25519 verify10 Bytes9511141312.0
ed25519 verify1 KB9221137012.3
ed25519 verify100 KB41434138.2
ed25519 verify1 MB674626.9
xchacha20poly1305 key generation (32byte10000003075032.5
xchacha20poly1305 encrypt10 Bytes1672246756764.0
xchacha20poly1305 encrypt1 KB1075273030302.8
xchacha20poly1305 encrypt100 KB190845662.4
xchacha20poly1305 encrypt1 MB1854222.3
xchacha20poly1305 decrypt10 Bytes23310010416674.5
xchacha20poly1305 decrypt1 KB995022857142.9
xchacha20poly1305 decrypt100 KB169841492.4
xchacha20poly1305 decrypt1 MB1734242.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.

Signing and verify performance chart

Encryption performance chart

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:

OperationSourceNoble ops/secLibsodium ops/secFactor faster
ed25519 keypair generation4348134053.1
ed25519 sign10 Bytes2484168926.8
ed25519 sign1 KB2265149256.6
ed25519 sign100 KB25212354.9
ed25519 sign1 MB281324.8
ed25519 verify10 Bytes506687813.6
ed25519 verify1 KB493675713.7
ed25519 verify100 KB26719057.1
ed25519 verify1 MB512565.1
xchacha20poly1305 key generation (32byte16666678064520.7
xchacha20poly1305 encrypt10 Bytes925933333333.6
xchacha20poly1305 encrypt1 KB500002000004.0
xchacha20poly1305 encrypt100 KB103126672.6
xchacha20poly1305 encrypt1 MB1042672.6
xchacha20poly1305 decrypt10 Bytes1538465555563.6
xchacha20poly1305 decrypt1 KB625001538462.5
xchacha20poly1305 decrypt100 KB104226322.5
xchacha20poly1305 decrypt1 MB1042702.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:

OperationSourceNoble ops/secLibsodium ops/secFactor faster
ed25519 keypair generation8687102901.2
ed25519 sign10 Bytes4620133012.9
ed25519 sign1 KB416867661.6
ed25519 sign100 KB3381402.4
ed25519 sign1 MB35142.4
ed25519 verify10 Bytes91355756.1
ed25519 verify1 KB92146145.0
ed25519 verify100 KB4072721.5
ed25519 verify1 MB66282.3
xchacha20poly1305 key generation (32byte9803922963833.1
xchacha20poly1305 encrypt10 Bytes1373631529051.1
xchacha20poly1305 encrypt1 KB103627281693.7
xchacha20poly1305 encrypt100 KB18893325.7
xchacha20poly1305 encrypt1 MB189335.7
xchacha20poly1305 decrypt10 Bytes2652521572331.7
xchacha20poly1305 decrypt1 KB111732281294.0
xchacha20poly1305 decrypt100 KB19013315.7
xchacha20poly1305 decrypt1 MB190335.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


Join the Newsletter

Thoughts on Software Engineering with a focus on React, Cryptography, CRDTs and Effect.