← back

node:crypto is underused

3 min read ·

In this post, I’ll show you* the basics of node:crypto, the builtin Node.js cryptography module. I it pretty cool, but not well known.

I’m not saying everybody needs to be a security expert. I’m def not one! But I feel that many engineers I know are needlessly scared of cryptography. With just a tiny sprinkle of it, we can do amazing things: like keeping our users’ data private with encryption or signing stuff we give out to the public to guarantee it hasn’t been tampered with. It’s been a while since I’ve written a tutorial-style blog post, so here it goes.

You can find all code snippets from this on CodeSandbox.

First things first, we need to generate a key pair.

tsx
import { promisify } from "node:util";
import { generateKeyPair } from "node:crypto";
 
const { privateKey, publicKey } = await promisify(generateKeyPair)("rsa", {
modulusLength: 2048,
});
tsx
import { promisify } from "node:util";
import { generateKeyPair } from "node:crypto";
 
const { privateKey, publicKey } = await promisify(generateKeyPair)("rsa", {
modulusLength: 2048,
});

Easy, right? We’ve got ourselves an RSA public and private key.

Rivest–Shamir–Adleman public-key cryptosystem was discovered in the late 70s, just like hip-hop. You need to now pause reading this, play the “Rapper’s Delight” by The Sugarhill Gang, while reading about RSA Security’s relationship with NSA.

Now back to JavaScript.

Let’s write and encode a short message, so we have something to work with.

tsx
const originalString = "my important message";
const originalUint8Array = new Uint8Array(
originalString.split("").map((ch) => ch.charCodeAt(0)),
);
tsx
const originalString = "my important message";
const originalUint8Array = new Uint8Array(
originalString.split("").map((ch) => ch.charCodeAt(0)),
);

Now anybody can encrypt it with our public key, but only we can decrypt it.

tsx
import { publicEncrypt, privateDecrypt } from "node:crypto";
 
const encryptedBuffer = publicEncrypt(publicKey, originalUint8Array);
const decryptedBuffer = privateDecrypt(privateKey, encryptedBuffer);
const decryptedString = decryptedBuffer.toString();
tsx
import { publicEncrypt, privateDecrypt } from "node:crypto";
 
const encryptedBuffer = publicEncrypt(publicKey, originalUint8Array);
const decryptedBuffer = privateDecrypt(privateKey, encryptedBuffer);
const decryptedString = decryptedBuffer.toString();

The functions for signing and verifying have a bit of a peculiar API, where we can pass null or undefined as the first argument if we already have our key in a KeyObject instead of a raw string. I’m pretty used to JSON.stringify(object, null, 2), so I think I actually prefer it over repeating the algorithm name.

tsx
import { sign, verify } from "node:crypto";
 
const signature = sign(null, originalUint8Array, privateKey);
const verified = verify(null, originalUint8Array, publicKey, signature);
tsx
import { sign, verify } from "node:crypto";
 
const signature = sign(null, originalUint8Array, privateKey);
const verified = verify(null, originalUint8Array, publicKey, signature);

If you’re just interested in signing, you should probably use Ed25519. It’s faster than RSA, twisted Edwards curves sound like a band name, and GitHub recommends it for your SSH keys.

tsx
const signingKeys = await promisify(generateKeyPair)("ed25519");
tsx
const signingKeys = await promisify(generateKeyPair)("ed25519");

Both RSA and Ed25519 can be used to sign and verify messages, but Ed25519 is just a digital signature scheme, so you can’t use it for encryption.

If you change rsa to ed25519 in the sandbox, one of the tests will fail with a friendly message:

Error: error:03000096:digital envelope routines::operation not supported for this keytype
Error: error:03000096:digital envelope routines::operation not supported for this keytype

A bit annoying that it doesn’t tell us about it on the type level, but without redesigning the API with types in mind, the type error would also be a bit nasty.

I remember thinking “Why shouldn’t I just use RSA for everything and limit the number of keys flying around?” sometime in the past. Apart from the performance, and some possible security reasons, there’s apparently some legislation stating that digital signatures are legally binding. I probably wouldn’t interpret hashing a message and then raising the hash to the power of a number that’s kept in secret as a signature, but I am not a lawyer, and I am unfortunately doomed to focus on the implementation.

Further Reading