Encryption with JavaScript — Part 1

A while back, while working at Po.et, I undertook a migration that required implementing encryption into the main API.

Back then I didn't really know much about encryption, so I started by doing some research, and then building a PoC. Eventually I implemented a working encrypt function, which I could use as follows:

const secret = 'my secret'
const password =  'my password'
const encrypted = encrypt(secret, password)
console.log(encrypted) // prints random gibberish: ���ա(��...

Thanks to the crypto NodeJS module, the implementation of encrypt is actually very small, consisting of just 5 lines of code:

import { createCipheriv, randomBytes } from 'crypto'

export const encrypt = (text: string, key: string): string => {
  const iv = randomBytes(96)
  const cipher = createCipheriv('id-aes256-GCM', Buffer.from(key, 'hex'), iv)
  const ciphertext = cipher.update(text, 'utf8', 'hex') + cipher.final('hex')
  const authTag = cipher.getAuthTag().toString('hex')
  return ciphertext + '|' + authTag + '|' + iv.toString('hex')
}

But, even though it may not look like it, there's a lot going on in it.

It didn't take me very long to get to this implementation and feel relatively comfortable with it, but it did take me a long time to understand it.

I hit a few bumps on the road and the research was very scattered all around until I managed to get a good picture. I couldn't really find any free, online resources that covered everything in a friendly manner, so I decided to shape my learnings into a blog post, hoping it may help others.

In this series we'll go over how this function works, what decisions were made and what alternatives there are. No previous cryptography knowledge is required, just basic JavaScript to follow examples.

Try it Out

Let's start by actually running this function and seeing it work.

If you run that repl you should see an output like the following:

secret: my super secret, do not tell anyone!
password: passwordpasswordpasswordpassword
encrypted text in hex: 5382ced1e55b88c7e7d73d1dea2f0f20e280af5c6b72b01547821a905ea902843a79f1fa|3deb02fd01bc993acf07d9d3c5baf4bb|8a79abd2768fe50b92642ee8662844d98c788fdd80040b9cc9a7d9c8dbe410c265a74ca5948ec5c86a9d0060f1d3bc6523da4c289ce081c745df89ffd4d13730ff395a0f951cf6e0f6a355270af9e28265342892e05a6737516a0c7e7fb085c9
encrypted text in utf: S����[����=�/  \kr�G��^��:y��

The encrypted text, both in hex and utf, will be different from my example and also change with each run, thanks to the random IV. If you try replacing const iv = randomBytes(96) with const iv = Buffer.from(Array(96).fill(0)), you should consistently get the following output:

encrypted text in hex: c4e5a5f602b87320cbc4216dd3cfa5b7970777701666738f6ef0e376c7d913d3cb939a02|ec585ae3b8a3f53aff722da2971ba5dd|000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Now nobody can read our secrets anymore! To be sure we still can, let's try decrypting it:

Running this repl should output the following:

encryptedSecret: 5382ced1e55b88c7e7d73d1dea2f0f20e280af5c6b72b01547821a905ea902843a79f1fa|3deb02fd01bc993acf07d9d3c5baf4bb|8a79abd2768fe50b92642ee8662844d98c788fdd80040b9cc9a7d9c8dbe410c265a74ca5948ec5c86a9d0060f1d3bc6523da4c289ce081c745df89ffd4d13730ff395a0f951cf6e0f6a355270af9e28265342892e05a6737516a0c7e7fb085c9
password: passwordpasswordpasswordpassword
decryptedSecret: my super secret, do not tell anyone!

Phew! The secret's safe. As long as we and only we hold the password, no one else can know it.

If you're feeling a bit exited now and ready to copy and paste these functions into your application, you're welcome to! But do you know exactly what's going on inside these encrypt and decrypt functions? And, above all, are they really safe?

In the following sections I'll explore each of the decisions made in those few lines of code.

Libraries

Let's start with the first line of code: the imports.

import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'

Here we're importing Node's crypto library. This obviously works well in Node but isn't available in the browser. There are a few alternatives to implement encryption in JavaScript. Some only work in NodeJS, some exclusively in the browser and others both in NodeJS and the browser.

Let's start by reviewing the one we're actually using in our example first, and then we can go over a couple of alternatives.

NodeJS Crypto Module

NodeJS' Crypto module is the official encryption library for NodeJS, which comes bundled with the node binary.

The Crypto module has been around since the beginnings of NodeJS, at least 7 years before the Web Crypto API was standardized by W3C, and as a stand-alone package even before that. Even though it has seen some changes over the years, it has mostly been kept backwards-compatible due to the large ecosystem that relies on it, and for the longest time NodeJS hadn't yet implemented the W3C standard. More recently, a long time effort to implement a standard-compliant Web Crypto API module in NodeJS, landed in Node 15.

Still, the Crypto module doesn't differ much from the Web Crypto API.

In practice, the module is a thin wrapper for OpenSSL. What encryption/decryption algorithms are available depends on the version of openssl being used by NodeJS.

We'll cover the NodeJS Crypto module in more detail later in this article.

Web Crypto API

The Web Crypto API is a standard formalized and recommended by W3C on 2017.

As of January 2020 it's fully supported by all major browsers.

Among other improvements, this new API is aimed at solving a long-standing issue: JS cryptography (in the browser) was always weak at best (see Javascript Cryptography Considered Harmful for example). But even with this new API there still are ongoing discussions regarding whether it's safe to do cryptography in the browser, as it's an environment we don't fully control and subject to different attacks.

For in-browser cryptography it's preferred to use a browser plugin such as MetaMask, a hardware device with a secure element such as Ledger and YubiKey or a service like Fortmatic and Magic.

OpenPGPJS

OpenPGPJS is a pure JavaScript implementation of OpenPGP, maintained by ProtonMail, a company well known for the email security they offer.

It uses NodeJS Crypto Module in NodeJS environments and WebCrypto in browsers under the hood, but falls back to using asm.js implementations of AES, SHA-1, and SHA-256 if unavailable.

It has undergone two complete security audits from Cure53 and is used in production by ProtonMail, so it should be fairly secure.

libsodium.js

This one is a JavaScript version of the popular sodium crypto library compiled to WebAssembly and pure JavaScript using Emscripten.

Sodium is a portable, cross-compilable, installable, packageable fork of NaCl, that extends and improves the original while keeping a compatible API.

Both the C and JS versions of the library are maintained by the same person, so it's unlikely for the JS version to be abandoned as long as the C version keeps being worked on.

Tink

Tink is a multi-language, cross-platform library developed by Google, aimed at being easy to use correctly and hard to misuse.

As of September 2022, a JavaScript version of this library is in active development but not yet available for production, so we can't use it right now. It does look very interesting, and worth a try as soon as it's released.

Conclusion

For the examples in this series I've decided to use NodeJS's traditional Crypto module. In future entries I'll provide Web Crypto API examples too, as that option is probably the most future-proof one.

In the next entry in this series we'll go deeper into the actual cryptography and explore what cryptographic algorithms there are and why I've chosen aes256-GCM.

Next: Encryption with JavaScript — Part 2.

Acknowledgments

Thanks to Jero and my awesome wife for an early review of this article.

A newsletter for programmers

Yo! This is Taro. I've been doing JavaScript for years and TypeScript for years. I have experience with many programming languages, libraries, frameworks; both backend and frontend, and in a few company roles/positions.

I learned a few things over the years. Some took more effort than I wish they had. My goal with this blog and newsletter is to help frontend and backend developers by sharing what I learned in a friendlier, more accessible and thorough manner.

I write about cool and new JavaScript, TypeScript and CSS features, architecture, the human side of working in IT, my experience and software-related things I enjoy in general.

Subscribe to my newsletter to receive notifications when I publish new articles, as well as some newsletter-exclusive content.

No spam. Unsubscribe at any time. I'll never share your details with anyone. 1 email a week at most.

Success!
You have subscribed to Taro's newsletter
Shoot!
The server blew up. I'll go get my fire extinguisher — please check back in 5.