Mastering Cryptography Fundamentals with Node’s crypto module

By admin
Do you know that meme of

Homer Simpson
PERSON

trying to hide in the bushes? That’s how I used to feel when my coworkers would discuss asymmetric encryption, certificate signing, salting, and scary-sounding acronyms like

PBKDF2
GPE

. After

years
DATE

of trying to ignore this problem, I decided to study up on modern Cryptography via books and online videos. However it was important to me to see how the concepts and algorithms I was learning can be applied in real-world applications using

Javascript
PRODUCT

. Thankfully Node.js has a built-in module for cryptographic operations called the crypto module which is quite extensive. So I dug into the crypto module and examined how it can be used to put the principles I was learning into practice.

This post covers the fundamentals of Cryptography and shows how to put them to use with Node.js’ crypto module.


Meet Alice
PERSON

and

Bob
PERSON

. They’re going to help out through our journey.

Alice
PERSON

and

Bob
PERSON

want to communicate securely, but they know that

Eve
PERSON

wants to intercept or tamper with their correspondence.

💡 Don’t worry about copying code from the code samples in this post. More robust code samples are available in the accompanying repo

Encryption 🔒

Let’s start with Encryption!

Alice
PERSON

wants to send

Bob
PERSON

a message, so she will scramble the plaintext (original message) to a ciphertext (encrypted message) that will be illegible to

Eve
PERSON

. A shared key 🔑 known only to

Alice
PERSON

and

Bob
PERSON

allows to transform the plaintext to the ciphertext and vice-versa.

Choosing an Encryption Algorithm

First

Alice
PERSON

and

Bob
PERSON

must choose a specific encryption algorithm to use. The crypto module’s

getCiphers
ORG

function returns a list of the names of the supported cryptographic algorithms that are supported on your machine.

In this example

Alice
PERSON

and

Bob
PERSON

will choose to use the aes-256-cbc cipher. Let’s break down what we can learn about this particular algorithm from its name:

aes : The "Advanced Encryption Standard." As the name suggests this is a widely used encryption standard.

256
CARDINAL

This refers to the size of the key that will be used. In this case a

256
CARDINAL

bits key. Generally speaking the longer the key is the more you’ll be protected from brute-force attacks (where an attacker loops through all possible keys). cbc This stands for "

Cipher Block Chaining
WORK_OF_ART

" which is a mode of encryption that works by splitting the plaintext into fixed-size "blocks".

One
CARDINAL

of the things that makes this method secure is that it uses an

Initialization Vector
ORG

which we’ll cover next.

Using an Initialization Vector

In classic encryption algorithms passing the same plaintext and key always yields the same ciphertext. This repetition is a potential exploit for attackers like

Eve
PERSON

. To avoid this we add a random value called

the Initialization Vector
ORG

(IV) as an input to the encryption we’ll use. Because this value will be different every time, the ciphertext will be different every time.


The Encryption and Decryption Process
ORG

Let’s take a look at the encryption and decryption process:

To encrypt

Alice
PERSON

invokes the selected encryption algorithm with: The desired message (the plaintext) A generated random value as the IV The shared key (we’ll discuss what value is used as the key and how it’s shared later on)

Alice
PERSON

transmits the ciphertext (the output of the encryption algorithm), the IV, and the chosen algorithm to

Bob
PERSON

over the public channel. Even if

Eve
PERSON

manages to get a hold on this data, she can’t reverse the encryption since she doesn’t have the key.

Bob
PERSON

performs the decryption using the ciphertext, the IV, and his copy of the shared key (and

Eve
PERSON

‘s snooping attempts are thwarted! 💪)

Use this slide component to see this process visualized:


Alice
PERSON

performs the encryption Eve can intercept the ciphertext

Bob
PERSON

performs the decryption

Encryption \ Decryption with the crypto module

Let’s take a look at

Alice’s Encryption
ORG

code:

import crypto from "crypto"; import util from "util"; const randomBytes = util.promisify(crypto.randomBytes); const algorithm = "aes-256-cbc"; // We always use Buffers with the crypto module const plaintext = Buffer.from("yonatan.dev", "utf8"); // randomBytes let’s us generate random data, // but needs to be promisified const iv = await randomBytes(16); const key = loadKey(); const cipher = crypto.createCipheriv(algorithm, key, iv); const ciphertext = Buffer.concat([ // We have the option of composing the ciphertext in steps, // by calling update() several times

cipher.update(plaintext
PERSON

), cipher.final(), ]);

And here’s

Bob
PERSON

‘s decryption code (notice how similar it is to

Alice
PERSON

‘s encryption code):

import crypto from "crypto"; function receive(iv, ciphertext, algorithm) { const key = loadKey(); const decipher = crypto.createDecipheriv(algorithm, key, iv); const plaintext = Buffer.concat([ decipher.update(ciphertext), decipher.final(), ]);

console.log(plaintext.toString("utf8
PERSON

")); // yonatan.dev }

Key Derivation Functions 🔑

So far we’ve established that the confidentiality of the correspondence relies on the secrecy of the shared key. However, we’ve left the process somewhat vague regarding how to determine the value of the key.

Alice
PERSON

and

Bob
PERSON

might be tempted to use a human-memorable password they both can agree on and remember as their key, but this would ultimately give

Eve
PERSON

an advantage. Let’s understand why.

Low Entropy

The longer the key, the more challenging it becomes for an attacker to loop through and attempt all potential keys. However, when we confine ourselves to keys that are human-readable or memorable, we reduce the number of keys for the attacker to try. Keys derived from common words or easily guessable phrases are referred to as having low entropy. This low entropy means they contain less randomness, making them more vulnerable to brute force attacks.

Using a human-memorable password as a key

Slow by design?

If we still want to use a human-memorable password as the basis of our security, we can derive a stronger key from a human’s original password using a Key Derivation Function (

KDF
PERSON

). KDFs are similar to hashing functions in that they are deterministic (given the same input they will always return the same output). But unlike normal hashing functions, KDFs are purposefully made to be slow! They are designed to be compute and memory intensive so that it will be ineffective for attackers like

Eve
PERSON

to try to loop through potential passwords if they have to trigger the

KDF
ORG

on every iteration.

Using a Key Derivation Function to derive a key from a password

Protecting from pre-compute

Let’s think of what

Eve
PERSON

would have to do if she would like to find the key by assuming

Alice
PERSON

and

Bob
PERSON

use a common phrase as their password and then derive their key using a well-known

KDF
PERSON

. We might assume

Eve
PERSON

would find a pre-compiled list of common passwords, and for each one she would perform the costly task of applying the

KDF
ORG

to extract the potential key. But

Eve
PERSON

is smarter than that! She would instead find a pre-computed list of common passwords already paired with their output values when passed through well-known KDFs! With this approach the addition of the

KDF
ORG

isn’t slowing her down at all, and therefore negates its role in enhancing security.

Commonly used passwords and their pre-computed

KDF
PERSON

output


Salts
GPE

to the rescue 🧂

Fortunately it’s possible to upheld the effectiveness of KDFs and protect against pre-computation by adding a random value known as the "salt" to the key derivation process. Even if

Eve
PERSON

has access to our salt, she would have to go through the costly process of calculating keys using common passwords and our unique salt.

Adding a random salt value as an input to the KDF

Key Derivation Functions with the crypto module

Here’s how to use the popular scrypt

KDF
ORG

to derive a key from a password (with the addition of a salt of course!)

import crypto from "crypto"; import util from "util"; const randomBytes = util.promisify(crypto.randomBytes); const scrypt =

util.promisify(crypto.scrypt
PERSON

); const password = "qwerty"; const salt = await randomBytes(16); const key = await

scrypt(password
GPE

, salt,

32
DATE

);

console.log(`The
NORP

key is: ${key}`);

The crypto module also includes other KDFs such as

hkdf
PERSON

and

pbkdf2
PERSON

.

Randomness 🔮

As you might have noticed so far, we rely on generating random values quite a lot when working with cryptographic algorithms. This means that we have to do our best to generate values that are are close as possible to truly random. If an attacker can predict or influence the values we choose it can give them a serious advantage. In Javascript we might be tempted to use the familiar Math.random() function to generate randomness, but this would be a mistake. As the MDN Docs state:

Math.random() does not provide cryptographically secure random numbers. Do not use them for anything related to security.

So what can we use instead? The crypto modules has several methods for generating randomness:

import crypto from "crypto"; import util from "util"; const randomBytes = util.promisify(crypto.randomBytes); const

randomFill = util.promisify(crypto.randomFill
PERSON

); const randomInt = util.promisify(crypto.randomInt); // generate a new buffer of a given size and fill it with random data const buffer1 = await randomBytes(4); console.log(buffer1.toString("hex")); //

82e2e97d
PERSON

// fill an existing buffer (or subset of it) with random data const buffer2 = Buffer.alloc(4); await

randomFill(buffer2
PERSON

,

2
CARDINAL

,

1
CARDINAL

); console.log(buffer2.toString("hex")); //

00003500
CARDINAL

// generate a random integer within a certain range const int = await randomInt(18,

180
CARDINAL

); console.log(int); //

137
CARDINAL

// generate a random UUID const uuid = crypto.randomUUID(); console.log("uuid", uuid); //

0748d0c6-3641
CARDINAL

-4858-876e-ec8420ba261d

Key Distribution Problem 🤔

If we consider again the encryption process outlined above,

Alice
PERSON

and

Bob
PERSON

must exchange a shared key known only to them. But does this mean that

Alice
PERSON

and

Bob
PERSON

must meet in person? What happens if they live far apart? Is there a way for them to establish the key online and still maintain its secrecy? This was known as

the Key Distribution Problem
ORG

.

Luckily in

the 1970s
DATE

significant breakthroughs were made in solving

the Key Distribution Problem
FAC

. At

Stanford
ORG

, researchers

Diffie, Hellman
ORG

, and

Merkle
PERSON

introduced the Diffie-Hellman Key Exchange, while at

MIT
ORG

,

Rivest
ORG

,

Shamir
PERSON

, and

Adleman
PERSON

published the

RSA
ORG

method. Let’s take a look at how their work enabled distant parties like

Alice
PERSON

and

Bob
PERSON

to communicate securely without the need to physically exchange keys.


Diffie-Hellman
ORG

key exchange


The Diffie-Hellman
WORK_OF_ART

key exchange achieves something that seems intuitively impossible.

Alice
PERSON

and

Bob
PERSON

exchange information completely in the open, and yet manage to produce a key known only to both of them. To clarify how this can even be possible, a paint mixing analogy is often used. These videos do a good job at illustrating this analogy and explaining some of the math behind the protocol.

Let’s review the key exchange process:


Alice
PERSON

computes her public key alicePublicKey using: p : A large random prime number g : A (small) number

Alice
PERSON

‘s private key: A random number

Alice
PERSON

sends p ,

g
PERSON

, and alicePublicKey to

Bob Bob
PERSON

computes his pubic key

bobPublicKey
TIME

in the same way using the same p and g and a different private key of his choosing

Bob
PERSON

computes the shared key secretKey using his private key and

Alice
PERSON

‘s public key

Bob
PERSON

sends

Alice
PERSON

his public key

Alice
PERSON

computes the same shared key using her private key and

Bob
PERSON

‘s public key

Use this slide component to see this process visualized:


Alice
PERSON

computes her public key

Alice
PERSON

sends her prime, generator, and public key

Bob
PERSON

computes his public key

Bob
PERSON

computes the shared key

Bob
PERSON

sends his public key back

Alice
PERSON

computes the same shared key!

So now

Alice
PERSON

and

Bob
PERSON

can use this shared key for their correspondence, and

Eve
PERSON

is none the wiser 😌

Diffie-Hellman with the crypto module

Here’s

Alice
PERSON

‘s code for her parts of the key exchange

(fun fact: createDiffieHellman(2048) takes

20 seconds
TIME

to run on my machine!)

import { createDiffieHellman } from "crypto"; // return a

DiffieHellman
ORG

key exchange object // (generate a prime number with a length of

2048
CARDINAL

bits) const

alice = createDiffieHellman(2048)
PERSON

; // get the random prime used const prime = alice.getPrime(); // get the random generator used const generator = alice.getGenerator(); // generate both keys and return the public key const alicePublicKey = alice.generateKeys(); function

receive(bobPublicKey
NORP

) { const secretKey = alice.computeSecret(bobPublicKey); }

And here’s

Bob
PERSON

‘s

import { createDiffieHellman } from "crypto"; function

receive(prime
ORG

, generator, alicePublicKey) { // return a

DiffieHellman
ORG

key exchange object // using the same prime and generator used by

Alice
PERSON

const bob = createDiffieHellman(prime, generator); // generate both keys and return the public key const

bobPublicKey
TIME

= bob.generateKeys(); const secretKey = bob.computeSecret(alicePublicKey); }

The crypto module also exposes the ECDH class which implements another important

Diffie-Hellman
ORG

algorithm called

Elliptic Curve Diffie-Hellman
PRODUCT

(ECDH).

The RSA Method

Diffie-Hellman’s solution to

the Key Distribution Problem
ORG

is remarkable, but has some limitations.

Alice
PERSON

and

Bob
PERSON

have to go through the rigmarole of of the key exchange before they either party can send an encrypted message. When using the

RSA
ORG

method, if

Alice
PERSON

wants to send

Bob
PERSON

an encrypted message for the

first
ORDINAL

time, there’s no need to exchange keys. Instead,

Alice
PERSON

can just encrypt a message and send it to

Bob
PERSON

to decrypt. This becomes possible by using a different key for encryption than for decryption (which is why it’s referred to as asymmetric encryption). This video explores the reasoning and some of the math behind the

RSA
ORG

method.

Here’s an overview of

the Encryption \ Decryption
LAW

process with RSA:


Bob
PERSON

generates

two
CARDINAL

different keys A private key A public key (derived from the private key)

Bob
PERSON

shares his public key with the world

Notice that this key can be used by anyone who wants to send

Bob
PERSON

an encrypted message. There’s no need for

Bob
PERSON

to generate different public keys for different parties.

Alice
PERSON

uses

Bob
PERSON

‘s public key to encrypt her message

Alice
PERSON

sends the encrypted message to

Bob Bob
PERSON

can decrypt

Alice
PERSON

‘s message using his private key

Use this slide component to see this process visualized:


Bob
PERSON

computes his private and public key

Bob
PERSON

publishes his public key

Alice
PERSON

encrypts using

Bob
PERSON

‘s public key

Alice
PERSON

sends the ciphertext to

Bob Bob
PERSON

deciphers using his private key

RSA with the crypto module


First
ORDINAL

,

Bob
PERSON

creates his key pair (public and private keys). He can do this only once, and can use it to communicate with everyone, not just

Alice
PERSON

. In this code sample both keys are saved to disk, but in a real-world scenario

Bob
PERSON

must share his public key somehow.

import crypto from "crypto"; import util from "util"; import { writeFile } from "fs/promises"; const generateKeyPair = util.promisify(crypto.generateKeyPair); const keyPair = await generateKeyPair("rsa", { modulusLength:

4096
CARDINAL

, }); const publicKey =

keyPair.publicKey.export
PERSON

({ type: "spki", format: "pem", }); await writeFile("bob-public.pem", publicKey); const privateKey = keyPair.privateKey.export({ type: "pkcs8", format: "pem", }); await writeFile("bob-private.pem", privateKey);

Here’s

Alice
PERSON

‘s encryption code which uses

Bob
PERSON

‘s public key

import crypto from "crypto"; import { readFile } from "fs/promises"; const { RSA_PKCS1_OAEP_PADDING } = crypto.constants; const

bobPublicKey
TIME

= crypto.createPublicKey( await readFile("bob-public.pem") ); const plaintext = Buffer.from("Hello world!", "utf8"); const ciphertext = crypto.publicEncrypt( { key:

bobPublicKey
TIME

, padding: RSA_PKCS1_OAEP_PADDING, }, plaintext );

And here’s

Bob
PERSON

‘s decryption code which uses

Bob
PERSON

‘s private key

import crypto from "crypto"; import { readFile } from "fs/promises"; const { RSA_PKCS1_OAEP_PADDING } = crypto.constants; async function

receive(ciphertext
PERSON

) { const

bobPrivateKey
ORG

= crypto.createPrivateKey( await readFile("bob-private.pem") ); const plaintext = crypto.privateDecrypt( { key:

bobPrivateKey
ORG

, padding: RSA_PKCS1_OAEP_PADDING, }, ciphertext );

console.log(plaintext.toString("utf8
PERSON

")); // Hello world! }

Signing and Verification ✍️

Consider this scenario:

Alice
PERSON

is an acclaimed Cryptography expert.

Bob
PERSON

asks her to recommend a good introductory post about Cryptography. He receives a message from her recommending some blog (the message doesn’t necessarily have to be encrypted if it’s not sensitive information).

Bob
PERSON

wants to be able to verify the message came from

Alice
PERSON

. Otherwise it’s possible that

Eve
PERSON

intercepted the message and altered its content. This is where signing and verifying comes in.

To put

Bob
PERSON

at ease,

Alice
PERSON

can create a cryptographic signature of her original message.

Bob
PERSON

can then cryptographically verify that the signature is authentic. This process requires using

Alice
PERSON

‘s asymmetric keys. Let’s see how:


Alice
PERSON

computes a signature value using Her private key The message itself

Alice
PERSON

sends

Bob
PERSON

the message and the signature value

(

Bob
PERSON

has access to

Alice
PERSON

‘s public key since it’s published for all)

Bob
PERSON

is able to check if the signature is valid using The signature The message

Alice
PERSON

‘s public key

Use this slide component to see this process visualized:


Alice
PERSON

signs the message with her public key

Alice
PERSON

sends

Bob
PERSON

the message+signature

Bob
PERSON

is able to verify using

Alice
PERSON

‘s public key

Signing and Verification with the crypto module

Here’s

Alice
PERSON

‘s code for calculating the signature using her private key.

Notice that we need to supply a hash function to use as part of the signing process (we’re using sha256 in this case).

import crypto from "crypto"; import { readFile } from "fs/promises"; const

alicePrivateKey
NORP

= crypto.createPrivateKey( await readFile("alice-private.pem") ); const message =

Buffer.from("blog.yonatan.dev
PERSON

", "utf8"); const signature = crypto.sign( "sha256", message, { key:

alicePrivateKey
ORG

, } );

And here’s

Bob
PERSON

‘s code for verifying the signature using

Alice
PERSON

‘s public key

import crypto from "crypto"; import { readFile } from "fs/promises"; async function

receive(message
GPE

, signature) { const alicePublicKey = crypto.createPublicKey( await readFile("alice-public.pem") ); const isVerified = crypto.verify( "sha256", message, { key: alicePublicKey, }, signature ); console.log(isVerified); // true }

The crypto module also exposes the

Hmac
PERSON

(Hash-Based Message Authentication Code) class, which offers a different approach to authenticating and verifying data.

Public Key Certificates 🪪

Let’s re-examine the signing and verification example from the previous section. Did this process really help

Bob
PERSON

know for certain that the message really came from

Alice
PERSON

? Not really. All he knows for sure is that it was definitely signed by someone with access to the private key that matches the public key that

Bob
PERSON

associates with

Alice
PERSON

. How can we know for sure that a particular pubic key really belongs to a specific entity? This is where

Public Key Certificates
ORG

come in.

Detour: Let’s talk about certificates

Let’s put aside Cryptography and consider the concept and characteristics of certificates in general, like this one for example:

Is this a real certificate? How can we verify it? 🤔

Apparently this certificate was created to provide evidence that the blog post you’re reading is "the best blog post about Cryptography on the internet". To support this assertion, the certificate is signed by

Alan Turing
PERSON

(

one
CARDINAL

of the world’s most celebrated cryptographers) who evidently made this claim.

These characteristics are common to this certificate and most others:

The content the certificate establishes a claim over

e.g. "the best blog post about Cryptography on the internet" The subject

e.g. blog.yonatan.dev The issuer

e.g.

Alan Turing Issuer
PERSON

verification through a signature

e.g.

Alan Turing’s
PERSON

signature

Public Key Certificates are very similar to this. They provide evidence that a public key belongs to a particular subject. To support that claim, they are signed by the issuer. But unlike the fake certificate above, the validity of

digital Public Key Certificates
ORG

can (and should) be verified.

Issuing and Verifying Public Key Certificates

If

Alice
PERSON

wants to create a certificate that will satisfy

Bob
PERSON

,

two
CARDINAL

requirements must be met:

She must convince an issuer that

Bob
PERSON

already trusts to vouch for her. The issuer must add their cryptographic signature to the certificate.

Similarly, if

Bob
PERSON

wants to trust

Alice’s Public Key Certificate
ORG

,

two
CARDINAL

requirements must be met:

He must trust the issuer of

Alice
PERSON

‘s certificate. He must cryptographically verify the signature on the certificate using the issuer’s public key.

Let’s assume that

Alice
PERSON

and

Bob
PERSON

both know

Carol
PERSON

, who is

Alice
PERSON

‘s friend and

Bob
PERSON

‘s sister.

To create her certificate

Alice
PERSON

takes her public key and some other metadata about herself (name, country, etc.), and creates a Certificate Signing Request (CSR). Carol the issuer reviews this request and agrees to vouch for

Alice
PERSON

. So using her private key

Carol
ORG

creates a cryptographic signature of the "To Be Signed" (

TBS
ORG

) data that is contained within the request. The signature is added to the published certificate.

On

Bob
PERSON

‘s side, in order to verify the certificate, he extracts the signature from the certificate and cryptographically verifies that it matches the rest of the information in the certificate.

Use this slide component to see this process visualized:


Carol
PERSON

issues

Alice
PERSON

‘s certificate

Bob
PERSON

verifies

Alice
PERSON

‘s certificate using

Carol
ORG

‘s public key

Verifying Public Key Certificates with the crypto module

In order to read certificates we use the

X509Certificate
PRODUCT

class (this refers to X.509 which is the standard that defines the format of these certificates). We can check the metadata on the certificate like the subject, issuer, and when it’s set to expire. And crucially we can verify the certificate using the certificate of the issuer.

Here is the code

Bob
PERSON

uses to verify

Alice
PERSON

‘s certificate using

Carol
ORG

‘s certificate (which he already trusts)

import

{ X509Certificate }
PERSON

from "crypto"; import { readFile } from "fs/promises"; const aliceCert = new

X509Certificate(await
FAC

readFile("alice.cer"));

console.log(aliceCert.subject
ORG

); // CN=alice (Common Name=alice) console.log(aliceCert.issuer); // CN=carol (Common Name=carol) console.log(`${aliceCert.validFrom} – ${aliceCert.validTo}`); //

May 2
DATE


19:00:13
TIME

2023 GMT – Jul 31

19:00:12
TIME


2024 GMT
TIME

const carolCert = new

X509Certificate(await
ORG

readFile("carol.cer")); const isVerified = aliceCert.verify(carolCert.publicKey);

console.log(isVerified
PERSON

); // true


The Certificate Chain

PRODUCT

Now it’s clear how

Bob
PERSON

can trust that he’s really talking to

Alice
PERSON

. But since it all hinges on

Bob
PERSON

trusting

Carol
PERSON

, it bears asking: how did

Bob
PERSON

come to trust

Carol
PERSON

initially? Of course this is where

Doris
PERSON

,

Bob
PERSON

and

Carol
PERSON

‘s mom, enters the picture.

What this tries to illustrate is that there’s no magic formula to establishing trust with a new certificate. It always depends on knowing and trusting a different certificate. This means to trust a new certificate, you need to establish trust with a different certificate, which means following up with a different certificate, etc. This hierarchy is referred to as a certificate chain. When reviewing a Web site’s certificate in your browser, you can inspect the entire certificate chain.

nodejs.org’s certificate chain

At the root of the certificate chain is a certificate that can’t be verified by another certificate. This top-most certificate is called the root certificate. The reason your browser typically trusts this certificate is because its baked into your operating system.

The root certificate which is part of the OS

Conclusion

Hopefully this post helped you get a better understanding of Cryptography and the role it plays in our online world. All the code samples from this post are expanded upon in this github repo which includes code you can actually run.

Congrats on making it through the post! To commemorate this accomplishment, I would like to present with your own personalized diploma (including a scannable cryptographic signature of course

🤓
PERSON

). Click here to generate it:

If you feel like showing off your generated certificate, and\or share your feedback about this post, I would love to hear from you on Twitter or LinkedIn.