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.
tsx
import {promisify } from "node:util";import {generateKeyPair } from "node:crypto";const {privateKey ,publicKey } = awaitpromisify (generateKeyPair )("rsa", {modulusLength : 2048,});
tsx
import {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.
tsx
constoriginalString = "my important message";constoriginalUint8Array = newUint8Array (originalString .split ("").map ((ch ) =>ch .charCodeAt (0)),);
tsx
constoriginalString = "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.
tsx
import {publicEncrypt ,privateDecrypt } from "node:crypto";constencryptedBuffer =publicEncrypt (publicKey ,originalUint8Array );constdecryptedBuffer =privateDecrypt (privateKey ,encryptedBuffer );constdecryptedString =decryptedBuffer .toString ();
tsx
import {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.
tsx
import {sign ,verify } from "node:crypto";constsignature =sign (null,originalUint8Array ,privateKey );constverified =verify (null,originalUint8Array ,publicKey ,signature );
tsx
import {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.
tsx
constsigningKeys = awaitpromisify (generateKeyPair )("ed25519");
tsx
constsigningKeys = 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