jonelantha: Blog


How-To: Perform password based AES encryption in the browser with zero dependencies

15th November 2020

This post contains a code snippet demonstrating the use of the browser's SubtleCrypto API to encrypt and decrypt string content using a text-based password

SubtleCrypto? 🤔

Modern browsers ship with the SubtleCrypto API - a comprehensive API for performing encryption, decryption & hashing operations natively (without the need for any additional libraries). SubtleCrypto is a powerful yet relatively low-level API - so it does need some orchestration to perform higher level tasks. This article will take a look at one particular high-level task: AES encryption of text using just a password.

The code

Below is the encrypt and matching decrypt functions for working with string based content using a password or passphrase. One thing to note is that the result of the encryption is actually a JavaScript object containing three string values, in the next section there's a usage example showing one way to work with the returned object.

async function encrypt(content, password) {
  const salt = crypto.getRandomValues(new Uint8Array(16));

  const key = await getKey(password, salt);

  const iv = crypto.getRandomValues(new Uint8Array(12));

  const contentBytes = stringToBytes(content);

  const cipher = new Uint8Array(
    await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, contentBytes)
  );

  return {
    salt: bytesToBase64(salt),
    iv: bytesToBase64(iv),
    cipher: bytesToBase64(cipher),
  };
}

async function decrypt(encryptedData, password) {
  const salt = base64ToBytes(encryptedData.salt);

  const key = await getKey(password, salt);

  const iv = base64ToBytes(encryptedData.iv);

  const cipher = base64ToBytes(encryptedData.cipher);

  const contentBytes = new Uint8Array(
    await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, cipher)
  );

  return bytesToString(contentBytes);
}

async function getKey(password, salt) {
  const passwordBytes = stringToBytes(password);

  const initialKey = await crypto.subtle.importKey(
    "raw",
    passwordBytes,
    { name: "PBKDF2" },
    false,
    ["deriveKey"]
  );

  return crypto.subtle.deriveKey(
    { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" },
    initialKey,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"]
  );
}

// conversion helpers

function bytesToString(bytes) {
  return new TextDecoder().decode(bytes);
}

function stringToBytes(str) {
  return new TextEncoder().encode(str);
}

function bytesToBase64(arr) {
  return btoa(Array.from(arr, (b) => String.fromCharCode(b)).join(""));
}

function base64ToBytes(base64) {
  return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}

(Some details in the above code derived from the MDN Crypto examples)

Usage

Encryption example

Example of using the above code to encrypt a string:

const content = "text to encrypt";

const password = "super secret password";

const encryptedData = await encrypt(content, password);

// encryptedData will be an object with three string fields:
// `salt`, `iv` & `cipher`

const serializedEncryptedData = JSON.stringify(encryptedData);

// serializedEncryptedData is a string which can be POSTed
// to an api (or similar)

Decryption example

Decrypting the output from the encryption example above:

const password = "super secret password";

// fetch the previously stored data from a GET api (or similar)
const serializedEncryptedData = fetchFromApi(...);

const encryptedData = JSON.parse(serializedEncryptedData);

const decryptedContent = await decrypt(encryptedData, password);

More information for the curious

So what are salt and iv for?

The encryption key is derived from the password as expected, what's not so clear is how salt and iv feature.

The salt is effectively a random number which is used when deriving the encryption key from the password. Without a random element the derived key will always be the same for any particular password and this would give a potential hacker an advantage. This does mean that the same salt value must be available when decrypting, that's why the salt needs to be stored alongside the encrypted cipher data.

Likewise the iv performs a similar role when using the key to encrypt the content. Without a random element the same key and content will always generate the same encrypted cipher data - again this would make a hacker's life much easier.

Further reading: Using Salts, Nonces, and Initialization Vectors (oreilly.com)

What are all those parameters for deriveKey?

Most of the parameters used for the SubtleCrypto functions should be fairly self explanatory but some of the deriveKey parameters are less obvious, particular the first one:

return crypto.subtle.deriveKey(
  { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" },  initialKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

Why would SHA-256 feature in AES encryption and what are the number of iterations used for? Basically this first parameter is describing the method deriveKey should use to turn the text password into the encryption key. PBKDF2 is an algorithm for generating the key from the password, here it's configured to apply SHA-256 100,000 times to the password to generate the key. More iterations are better here; if a hacker wants to perform a dictionary attack they'd have to repeat the above process for every guess, so it's best to make the process as slow as possible.

A more detailed explanation about how iterations feature can be found at 1Password

Summing up

Most of the time it will make sense to perform encryption on the server but encryption in the browser offers one key advantage: the decrypted content (and password) never leave the browser. That means the sensitive content doesn't travel across the internet and it won't ever be available on the web server (which could potentially be compromised).

Support for SubtleCrypto is pretty good (See Can I use stats) so it should be possible to start using browser based encryption without any dependencies.

Hope this has been useful, thanks for reading!


© 2003-2024 jonelantha