skip to content
ainoya.dev

Using the Web Crypto API for Secure Encryption and Decryption

/ 3 min read

Securely managing sensitive information such as API keys in web services is crucial. My focus was on encrypting this data in a decryptable manner without depending on Node.js libraries, particularly in environments like Cloudflare Workers. This led me to explore the Web Crypto API, which provides a robust solution for secure cryptographic operations directly in the browser, free from Node.js Library dependencies.

Why Web Crypto API?

I chose the Web Crypto API for its strong security features and broad support across modern browsers. Importantly, it operates in restricted environments like Cloudflare Workers, making it ideal for my requirements.

For encryption and decryption, I implemented the AES-256-GCM method. This choice was influenced by GitLab’s cryptographic standards, which suggests AES-256 for “future-proofing” despite potential performance impacts.

Implementation Code

Below is the TypeScript code for encryption and decryption, which can be included in your projects:

// ~/utils/encryptor.server.ts
// Purpose: Encryptor utility for encrypting and decrypting data.
const algorithm = "AES-GCM";

function base64ToArrayBuffer(base64: string): ArrayBuffer {
  const binaryString = atob(base64);
  const len = binaryString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes.buffer;
}

function arrayBufferToBase64(buffer: ArrayBuffer): string {
  let binary = "";
  const bytes = new Uint8Array(buffer);
  const len = bytes.byteLength;
  for (let i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

export async function encrypt(
  data: string,
  keyBase64Encoded: string
): Promise<string> {
  const keyBuffer = base64ToArrayBuffer(keyBase64Encoded);

  const cryptoKey = await crypto.subtle.importKey(
    "raw",
    keyBuffer,
    { name: algorithm, length: 256 },
    false,
    ["encrypt"]
  );

  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encryptedData = await crypto.subtle.encrypt(
    { name: algorithm, iv: iv },
    cryptoKey,
    new TextEncoder().encode(data)
  );

  const combined = new Uint8Array(iv.length + encryptedData.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(encryptedData), iv.length);

  return arrayBufferToBase64(combined);
}

export async function decrypt(
  data: string,
  keyBase64Encoded: string
): Promise<string> {
  const keyBuffer = base64ToArrayBuffer(keyBase64Encoded);
  const dataBuffer = base64ToArrayBuffer(data);

  const iv = dataBuffer.slice(0, 12);
  const encrypted = dataBuffer.slice(12);

  const cryptoKey = await crypto.subtle.importKey(
    "raw",
    keyBuffer,
    { name: algorithm, length: 256 },
    false,
    ["decrypt"]
  );

  const decrypted = await crypto.subtle.decrypt(
    { name: algorithm, iv: new Uint8Array(iv) },
    cryptoKey,
    encrypted
  );

  return new TextDecoder().decode(decrypted);
}

Testing the Implementation

Here’s a test script to ensure the functionality works as expected:

// test/util/encryptor.server.test.ts
// test encrypt and decrypt

import { expect, test } from "vitest";

import { encrypt, decrypt } from "~/utils/encryptor.server";
import crypto from "crypto";

test("encrypt and decrypt", async () => {
  // random 32 bytes key
  const key = crypto.randomBytes(32);
  const data = "test_data";

  const encrypted = await encrypt(data, key.toString("base64"));

  const decrypted = await decrypt(encrypted, key.toString("base64"));

  expect(decrypted).toBe(data);

  // data is encrypted
  expect(encrypted).not.toBe(data);

  // cannot decrypt with wrong key
  const wrongKey = crypto.randomBytes(32);
  // wrong decryption will throw error
  await expect(
    decrypt(encrypted, wrongKey.toString("base64"))
  ).rejects.toThrow();
});

Conclusion

The Web Crypto API provides a powerful, platform-independent tool for encrypting and decrypting sensitive information, even within the limited environments like Cloudflare Workers.

Note: The code provided in this article does not guarantee operational security. Please use it at your own risk.

References