Mastering Cryptography Fundamentals with Node’s crypto module

Created on November 12, 2023 at 11:49 am

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.

Connecting to blog.lzomedia.com... Connected... Page load complete