Update (2019-10-11): I also published a follow-up to this article that's a bit broader in scope.
If you ask around about implementing encryption or signatures in your apps, chances are someone will tell you to just use libsodium. And this is, truthfully, the correct answer for most people's problems.
However, the incumbent options for libsodium in the JavaScript ecosystem leave a lot to be desired.
In particular, there are two back-end libraries that implement libsodium in JavaScript that I'll be discussing:
- sodium-native, which is an unopinionated low-level binding of the C API
-
libsodium-wrappers (and the other packages in the
libsodium.js
repository) which is cross-platform but slightly slower than sodium-native
Encrypting a String in Sodium-Native
I bet you think you could just do this and call it a day?
const sodium = require('sodium-native');
// Initialize with random bytes:
let key = sodium.randombytes_buf(32);
let nonce = sodium.randombytes_buf(24);
let message = "This is just an example string. Hello dev.to readers!";
// Encrypt:
let encrypted = sodium.crypto_secretbox_easy(message, nonce, key);
// Decrypt:
let decrypted = sodium.crypto_secretbox_open_easy(encrypted, nonce, key);
console.log(message === decrypted.toString());
Short, sweet, and to the point, right? Nope. That code doesn't work at all.
That snippet of code needs to be written like this:
const sodium = require('sodium-native');
// Initialize with random bytes:
let key = sodium.randombytes_buf(32);
let nonce = sodium.randombytes_buf(24);
let message = Buffer.from("This is just an example string. Hello dev.to readers!");
// Encrypt:
let encrypted = Buffer.alloc(message.length + 16);
sodium.crypto_secretbox_easy(encrypted, message, nonce, key);
// Decrypt:
let decrypted = Buffer.alloc(encrypted.length - 16);
sodium.crypto_secretbox_open_easy(decrypted, encrypted, nonce, key);
console.log(message.toString() === decrypted.toString());
This API is terrible for JavaScript developers: Instead of returning a value, sodium-native overwrites one of the buffers you pass with the return value. Which means you have to allocate (and correctly size) buffers yourself.
Manual buffer allocation, especially to Node.js devs who learned before Buffer.alloc()
and Buffer.from()
became the norm, almost begs developers to write memory unsafe code. It also breaks if the user provides a string input instead of a Buffer
.
Encrypting a String in Libsodium.js
Fortunately, libsodium-wrappers
does a fairly good job of exposing a usable in most cases. Except one caveat:
const _sodium = require('libsodium-wrappers');
await _sodium.ready; // You can't use the library until it's ready
const sodium = _sodium;
Henceforth, the API consists entirely of synchronous functions.
const _sodium = require('libsodium-wrappers');
(async function() {
await _sodium.ready;
const sodium = _sodium;
// Initialize with random bytes:
let key = sodium.randombytes_buf(32);
let nonce = sodium.randombytes_buf(24);
let message = "This is just an example string. Hello dev.to readers!";
// Encrypt:
let encrypted = sodium.crypto_secretbox_easy(message, nonce, key);
// Decrypt:
let decrypted = sodium.crypto_secretbox_open_easy(encrypted, nonce, key);
console.log(message === decrypted.toString());
})();
Other Differences and Design Warts
Compared with sodium-native, libsodium-wrappers is slightly slower (sodium-native calls a C library, where as libsodium-wrappers compiles the library with emscripten), but it runs in more places (i.e. in web browsers) and doesn't need a C compiler to get running.
Both libraries suffer from a subtle risk with X25519 keypairs: It's easy to accidentally get the public and secret key arguments mixed up and make your protocol insecure (although the unit tests will still pass).
Neither library works well with IDE code completion.
Neither library is particularly well-documented, either.
Because of these grievances, if a developer asked me today which of the two to use in a greenfield development project, I wouldn't be able to recommend either. Which is really sad because the first two sentences of the official libsodium documentation state:
Sodium is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing and more.
It is a portable, cross-compilable, installable, packageable fork of NaCl, with a compatible API, and an extended API to improve usability even further.
So, with that in mind, I'd like to introduce Sodium-Plus to the world.
Introducing Sodium-Plus (Na+)
You can find sodium-plus on Github and install it from NPM.
Sodium-Plus is the libsodium API that JavaScript developers deserve.
const { SodiumPlus } = require('sodium-plus');
(async function() {
// Select a backend automatically
let sodium = await SodiumPlus.auto();
let key = await sodium.crypto_secretbox_keygen();
let nonce = await sodium.randombytes_buf(24);
let message = 'This is just a test message';
// Message can be a string, buffer, array, etc.
let ciphertext = await sodium.crypto_secretbox(message, nonce, key);
console.log(ciphertext);
try {
let decrypted = await sodium.crypto_secretbox_open(ciphertext, nonce, key);
console.log(decrypted.toString('utf-8'));
} catch (e) {
console.error("Invalid ciphertext throws instead of returning false.");
}
})();
It's pluggable. You can power it with either sodium-native
if you're strictly a Node shop and need the performance, or libsodium-wrappers
if you need cross-platform support. You can even install sodium-native
on some builds and Sodium-Plus will automatically use it in the default configuration.
It's asynchronous where ever possible.
It's fully type-safe. You'll never accidentally get your public and secret keys mixed up with Sodium-Plus.
const { SodiumPlus } = require('sodium-plus');
(async function() {
// Select a backend automatically
let sodium = await SodiumPlus.auto();
console.log("Selected backend: " + sodium.getBackendName());
let aliceKeypair = await sodium.crypto_box_keypair();
let aliceSecret = await sodium.crypto_box_secretkey(aliceKeypair);
let alicePublic = await sodium.crypto_box_publickey(aliceKeypair);
// This works:
let ciphertext = await sodium.crypto_box_seal(plaintext, alicePublic);
let decrypted = await sodium.crypto_box_seal_open(ciphertext, alicePublic, aliceSecret);
// These do not:
try {
ciphertext = await sodium.crypto_box_seal(plaintext, aliceSecret);
} catch (e) {
decrypted = await sodium.crypto_box_seal_open(ciphertext, aliceSecret, alicePublic);
console.log(e); // TypeError { ... }
}
})();
Feel free to run this code yourself, both with and without sodium-native
.
In virtually every way, we want Sodium-Plus to be an significant usability improvement over the existing libsodium implementations.
Additionally, we want to make sure it's easier to use Sodium-Plus than any other JavaScript cryptography libraries.
What's the Project Status?
As of 2019-10-07:
- Version 0.1.0 (the first alpha) has been released, which only contains the most common features of libsodium.
- Many APIs (
generichash
,secretstream
, etc.) have not yet been implemented in our library. The documentation has not been completed yet for what is implemented. - However, your IDE will autocomplete correctly (due to our use of docblocks).
Our development roadmap is as follows:
- Initial release. We are here.
- Collect feedback from developers. (This is where I'd love your help!)
- API completeness. (a.k.a. Finish wrapping the other libraries)
- Complete the documentation.
- Ensure 100% unit test coverage.
- Ensure a
@types
bundle is available for TypeScript users. - Maybe get a security audit? (Not sure if the funding exists for this yet.)
- Version 1.0.0. (Expected late 2019.)
Top comments (1)
This is awesome!