Encryption operations can be tricky, so much that paid encryption-as-a-service companies exist just to ensure that cryptographic operations are implemented correctly in codebases. The good news is that, with some little learning, you can make do with proper encryption for free with Node’s built-in crypto module.
In this guide, we explore how you can use Node’s built-in crypto module to correctly perform the (symmetric) encryption/decryption operations to secure data for your applications.
To begin, we need to understand the concept of symmetric encryption.
Symmetric Encryption
When people talk about “encryption,” they tend to mean symmetric encryption which is useful for encrypting text into a random string of characters. A common scenario where this is relevant is encrypting user data on a server so that it’s stored “encrypted at rest” in a database.
In layman terms, symmetric encryption is when you take the text you want to encrypt (called the plaintext) and use a secret key with an encryption algorithm to output the encrypted text (called the ciphertext). The operation is reversible and so decryption is when we can use the same secret with the plaintext
Looks easy right?
Unfortunately, when it comes time to implement symmetric encryption, developers get it wrong all the time oftentimes because there’re a lot to understand:
- Encoding formats: Data can be encoded/decoded in many ways like
base64
,hex
, etc. These different representations often confuse developers when converting from one format to another. - Algorithms and configuration: There are many encryption algorithms to consider from like
aes-256-gcm
oraes-256-cbc
, each with their own requirements. - Randomness: Keys used in encryption procedures should be generated randomly to ensure high entropy. Often times, developers think they’re generating sufficiently-random keys but they’re not.
- Complexity: Encryption in Node.js involves a few steps that aren’t always intuitive to developers and some concepts are genuinely puzzling at first. Take, for instance, the concept of an initialization vector (IV); developers new to cryptography often re-use these across their encryption processes and this is a big no-no.
Anyways, in this article I won’t cover the above nuances since they’re more crypto-heavy (we’ll save this discussion for another time) but focus more on how we can perform encryption correctly with Node.js by example.
The best way to explain how to correctly perform the encryption is to demonstrate a proper implementation using the aes-256-gcm
algorithm. Let’s start with encryption.
Encryption
const crypto = require('crypto');
const encryptSymmetric = (key, plaintext) => {
const iv = crypto.randomBytes(12).toString('base64');
const cipher = crypto.createCipheriv(
"aes-256-gcm",
Buffer.from(key, 'base64'),
Buffer.from(iv, 'base64')
);
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
ciphertext += cipher.final('base64');
const tag = cipher.getAuthTag()
return { ciphertext, tag }
}
const plaintext = "encrypt me";
const key = crypto.randomBytes(32).toString('base64');
const { ciphertext, iv, tag } = encryptSymmetric(key, plaintext);
To perform the encryption, we need two items:
plaintext
: The text that you want to encrypt.key
: A 256-bit encryption key.
We get the following outputs from the encryption:
ciphertext
: The encrypted text.iv
: A 96-bit initialization vector to provide the initial state for the encryption and allow the samekey
to be re-used with a differentiv
for future encryption operations.tag
: A piece of data generated during the encryption process to help verify that the encrypted text was not tampered with later during the decryption process.
Let’s walk through the code:
const plaintext = "encrypt me";
const key = crypto.randomBytes(32).toString('base64');
Here we define the input variables needed to perform the encryption. Beyond the text that you want to encrypt, it’s important to generate the key
randomly to ensure security via higher entropy; I prefer using the built-in crypto.randomBytes()
for generation. I also convert encode everything in base64
format since its easy to store conceptually.
Next, let’s dissect the encryption function:
const encryptSymmetric = (key, plaintext) => {
// create a random initialization vector
const iv = crypto.randomBytes(12).toString('base64');
// create a cipher object
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
// update the cipher object with the plaintext to encrypt
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
// finalize the encryption process
ciphertext += cipher.final('base64');
// retrieve the authentication tag for the encryption
const tag = cipher.getAuthTag();
return { ciphertext, iv, tag };
}
Here, the encryption function creates a new cipher during crypto.createCipheriv
initialized with the algorithm aes-256-gcm
(you can think of it as a type of encryption algorithm) and the key
and iv
. The next part loads the text that you want to encrypt plaintext
in and performs the encryption and retrieves an authentication tag that you can use to check that the ciphertext
was not tampered with during the decryption process.
The last thing you should know about encryption is how to handle the data. In the context of encrypting user data and storing it at rest, you’d want to store the encrypted data ciphertext
, iv
, and tag
in the database and keep the secret key stored somewhere else securely such as as an environment variable on the server or better yet in a a dedicated secret manager like Infisical. When you want to retrieve the data, you can query for it from the database and decrypt it using the secret key.
Speaking of which, let’s dive into decryption. As before, let’s start with a proper implementation.
Decryption
const decryptSymmetric = (key, ciphertext, iv, tag) => {
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
Buffer.from(key, 'base64'),
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
plaintext += decipher.final('utf8');
return plaintext;
}
const plaintext = decryptSymmetric(key, ciphertext, iv, tag);
To perform the decryption, we need four items:
key
: The 256-bit encryption key used to encrypt the original text.ciphertext
: The encrypted text that you want to decrypt.iv
: The 96-bit initialization vector used during the encryption.tag
: The tag generated during the encryption.
We get the following outputs from the encryption:
plaintext
: The original text that we encrypted.
Let’s walk through the decryption function:
const decryptSymmetric = (key, ciphertext, iv, tag) => {
// create a decipher object
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
Buffer.from(key, 'base64'),
Buffer.from(iv, 'base64')
);
// set the authentication tag for the decipher object
decipher.setAuthTag(Buffer.from(tag, 'base64'));
// update the decipher object with the base64-encoded ciphertext
let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
// finalize the decryption process
plaintext += decipher.final('utf8');
return plaintext;
}
Here, the decryption function creates a decipher object during crypto.createDecipheriv
initialized with the algorithm aes-256-gcm
, the key
created earlier, and iv
also created earlier. The next part sets the authentication tag to be used for checking to see if the ciphertext
was tampered with and finally we perform the decryption to obtain the original text.
Horray!
While this all sounds complicated, I would encourage you to take some time to understand it and define helper functions for encryptSymmetric
and decryptSymmetric
like above so you can reuse them consistently across your codebase.
Lastly, your data is only as secure as the keys used to encrypt the data. As such, I strongly recommend securely storing encryption keys and accessing them in your application as environment variables or fetching them back from a secret manager like Infisical at runtime. This is simple to set up and worthwhile to learn.
Resources
Encryption/decryption can be a tricky subject, especially for folks with limited prior cryptography experience. So, before we part ways, I wanted to leave you with some resources:
- The Node Crypto Documentation
- Crypto 101: A great introductory book about cryptography for folks who want to dive deeper.
— -
Infisical — The open source secret management platform
Infisical (9.7K+ ⭐️)helps thousands of teams and organizations store and sync secrets across their team and infrastructure.
GitHub repo: https://github.com/Infisical/infisical