Migrating from Node.js crypto to Web Crypto API: A guided experience
Published: 2023-09-13 06:40:10

## Introduction

The Web Crypto API is a new Javascript tool for cryptography. It is compatible with modern browsers and leading platforms, including [Cloudflare Workers](https://workers.cloudflare.com/) and [Vercel](https://vercel.com), and also Node.js. Such interoperability implies a developer's dream - write your cryptography code once and run it across numerous platforms using the Web Crypto API.

However, Node.js already has a "crypto" module with a long history, leaving a significant amount of legacy code in need of migration. In this article, we will delve into the transition experience, providing a comprehensive guide focusing on 3 common scenarios. We aim to illuminate the pathway to successful migration.

## What is Web Crypto API

Web Crypto API, an open standard by W3C for JavaScript, is a collection of standardised cryptography primitives defined in the [Web Cryptography API specification](https://www.w3.org/TR/WebCryptoAPI/). It was created after several browsers and platforms began adding their own non-interoperable cryptography functions.

The API provides primitives for key generation, encryption and decryption, digital signatures, key and bit derivation, and cryptographic digest. It is centered around an interface called SubtleCrypto, you can find more details and tutorials in the Mozilla [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) documentation.

## But Node.js already has crypto module?

Node.js developers are typically familiar with the [crypto module](https://nodejs.org/api/crypto.html). It offers a comprehensive set of cryptographic primitives. This module not only provides mechanisms for the same cryptographic operations defined in the Web Crypto API but often includes a broader range of cryptographic algorithms.

So why do we still need Web Crypto API? As Javascript becomes popular across many platforms and environments , from the client-side to servers and especially on the edge, it's important to have a cross-platform cryptographic tool to simplify processes.

In addition, the functions in Web Crypto API standard all return promises and support the async/await syntax. This is a significant advantage over the crypto module, which is synchronous and can block the event loop.

And there is a interesting thing that Node.js adds [its support](https://nodejs.org/api/webcrypto.html#web-crypto-api) for Web Crypto API, that means, in most cases, Web Crypto API is perfect in most known platforms.

## Using Web Crypto API

On most platforms, the collection of Web Crypto APIs is accessible via the global `crypto` object, which includes 3 top-level utilities: `getRandomValues`, `randomUUID` and `subtle`.

There are many differences compared to the traditional crypto module. They can be summarized into 3 most common parts. Let’s go through them and see how to migrate from existing code.

## #1 Generate random values

In crypto module, you can generate random values by calling `randomBytes`

```ts
import { randomBytes } from 'crypto';

const generateRandomString = (length = 64) => randomBytes(length).toString('hex');
```

There is a simalar function called `getRandomValues` in Web Crypto, but the return value is an `ArrayBuffer`, so we need an another step to convert it to string.

```ts
const generateRandomString = (length = 64) => {
  const array = new Uint8Array(10);
  crypto.getRandomValues(array);
  return Array.from(array)
    .map((byte) => byte.toString(16).padStart(2, '0'))
    .join('');
};
```

## #2 Hasing (or digest)

`createHash` is easy to use in crypto module:

```ts
export const sha256 = (text: string): string => {
  return createHash('sha256').update(text).digest('hex');
};
```

In Web Crypto, we can use `subtle.createHash`

```ts
export const sha256 = async (text: string): Promise<string> => {
  const encoder = new TextEncoder();
  const data = encoder.encode(text);
  const hash = await crypto.subtle.digest('SHA-256', data);

  return Array.from(new Uint8Array(hash))
    .map((byte) => byte.toString(16).padStart(2, '0'))
    .join('');
};
```

As you can see, the transformation from `ArrayBuffer` to hex string is also needed.

## #3 Encryption and decryption

In crypto module, AES encryption and decryption can be implemented with:

```ts
export const encrypt = (text: string, password: string) => {
  const iv = randomBytes(16);
  const cipher = createCipheriv('aes-256-gcm', password, iv);
  const encrypted = cipher.update(text, 'utf8', 'base64');
  return {
    ciphertext: `${encrypted}${cipher.final('base64')}`,
    iv: iv.toString('base64'),
  };
};

export const decrypt = (ciphertext: string, iv: string, password: string) => {
  const decipher = createDecipheriv('aes-256-gcm', password, iv);
  const decrypted = decipher.update(encrypted, 'base64', 'utf8');
  return `${decrypted}${decipher.final('utf8')}`;
};
```

In Web Crypto, we need to create key by `importKey` first:

```ts
async function encrypt(text: string, password: string) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encodedPlaintext = new TextEncoder().encode(text);

  const secretKey = await crypto.subtle.importKey(
    'raw',
    Buffer.from(await getKeyFromPassword(password, crypto), 'hex'),
    {
      name: 'AES-GCM',
      length: 256,
    },
    true,
    ['encrypt', 'decrypt']
  );

  const ciphertext = await crypto.subtle.encrypt(
    {
      name: 'AES-GCM',
      iv,
    },
    secretKey,
    encodedPlaintext
  );

  return {
    ciphertext: Buffer.from(ciphertext).toString('base64'),
    iv: Buffer.from(iv).toString('base64'),
  };
}

async function decrypt(ciphertext: string, iv: string, password: string) {
  const secretKey = await crypto.subtle.importKey(
    'raw',
    Buffer.from(await getKeyFromPassword(password, crypto), 'hex'),
    {
      name: 'AES-GCM',
      length: 256,
    },
    true,
    ['encrypt', 'decrypt']
  );

  const cleartext = await crypto.subtle.decrypt(
    {
      name: 'AES-GCM',
      iv: Buffer.from(iv, 'base64'),
    },
    secretKey,
    Buffer.from(ciphertext, 'base64')
  );

  return new TextDecoder().decode(cleartext);
}
```

## Conclusion

As you can see, the migration is not hard, the main job is to change the syntax to fit the new API and resolve `ArrayBuffer` with `TextEncoder`.

As an identity product, Logto uses cryptography in many places. We have migrated from the crypto module to the Web Crypto API. This transition enables us to better adapt to edge environments and makes it possible to execute SDK code securely in the browser.