node:crypto is underused
In this post, I’ll show you* the basics of node:crypto, the builtin
Node.js cryptography module. I find 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.
tsximport {promisify } from "node:util";import {generateKeyPair } from "node:crypto";const {privateKey ,publicKey } = awaitpromisify (generateKeyPair )("rsa", {modulusLength : 2048,});
tsximport {promisify } from "node:util";import {generateKeyPair } from "node:crypto";const {privateKey ,publicKey } = awaitpromisify (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.
tsxconstoriginalString = "my important message";constoriginalUint8Array = newUint8Array (originalString .split ("").map ((ch ) =>ch .charCodeAt (0)),);
tsxconstoriginalString = "my important message";constoriginalUint8Array = newUint8Array (originalString .split ("").map ((ch ) =>ch .charCodeAt (0)),);
Now anybody can encrypt it with our public key, but only we can decrypt it.
tsximport {publicEncrypt ,privateDecrypt } from "node:crypto";constencryptedBuffer =publicEncrypt (publicKey ,originalUint8Array );constdecryptedBuffer =privateDecrypt (privateKey ,encryptedBuffer );constdecryptedString =decryptedBuffer .toString ();
tsximport {publicEncrypt ,privateDecrypt } from "node:crypto";constencryptedBuffer =publicEncrypt (publicKey ,originalUint8Array );constdecryptedBuffer =privateDecrypt (privateKey ,encryptedBuffer );constdecryptedString =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.
tsximport {sign ,verify } from "node:crypto";constsignature =sign (null,originalUint8Array ,privateKey );constverified =verify (null,originalUint8Array ,publicKey ,signature );
tsximport {sign ,verify } from "node:crypto";constsignature =sign (null,originalUint8Array ,privateKey );constverified =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.
tsxconstsigningKeys = awaitpromisify (generateKeyPair )("ed25519");
tsxconstsigningKeys = awaitpromisify (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:
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