Skip to main content

Command Palette

Search for a command to run...

Secure Your Data with Web Crypto API: Browser Encryption Guide

Published
8 min read
Secure Your Data with Web Crypto API: Browser Encryption Guide
I

Full stack developer passionate about creating functional and interactive web applications and technical writer that just want to share knowledge gained while building web applications

Encryption is an essential process in cryptography where readable information, known as plaintext, is converted into an unreadable format known as ciphertext. This ensures that only authorised parties who have access to the decryption key can read the original data. To encrypt data in the browser, we utilise the Web Crypto API.
Web Crypto API is a browser interface that allows web applications to perform cryptographic operations using various algorithms. The best algorithm to be used must meet the basic cryptographic security requirements

  • Confidentiality: Unauthorised users cannot read the data
  • Integrity: The data has not been tampered with during transmission or storage
  • Authenticity: The identity of the sender is verified

The encryption algorithm that satisfies the conditions above is Advanced Encryption Method with Galois/Counter Mode (AES-GCM). To access the web crypto API, we will use the crypto.subtle interface, this gives us access to cryptographic functions like encrypt(), decrypt(), etc. This allows us to perform cryptographic operations directly within a web application.

Random Values

crypto.getRandomValues() generates cryptographically secure random numbers; this is more secure than math.random , the random numbers generated would be used as the initialization vector.

Initialization Vector (IV)

Initialization vector (IV) is an important, non-secret key required by AES-GCM and other encryption algorithms. Its primary purpose is to make sure that the encryption produces different ciphered text each time the same plain text is encrypted. The standard method for generating a cryptographically secure IV in the browser is using the crypto.getRandomValues method, for AES-GCM the recommended IV size is 12 bits

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

Crypto Key

The crypto key is an essential part of the Web Crypto API; it is the secret material used for encryption, decryption, and other cryptographic operations. To generate a crypto key crypto.subtle.generateKey() is used. The generateKey() accepts four arguments: type, algorithm, extractable and usages.

const key = await window.crypto.subtle
    .generateKey(
      {
        name: "AES-GCM",
        length: 256,
      },
      true,
      ["encrypt", "decrypt"]
    )

The type argument specifies the category of the key, which can be either “public” or “secret”. The algorithm object indicates the type of algorithm used, along with its key and other properties, such as length and hash function. Extractable tells if the key can be exported or not. Usages is an array of strings defining the allowed crypto operations for the key.

Encrypt

encrypt() This is the method of the CryptoSubtle interface that encrypts the data. It accepts three arguments: the algorithm and its parameters (mostly the initialization vector), the crypto key, and the data to be encrypted.

const encode = (text) => {
  return new TextEncoder().encode(text);
};

const encrypt = async (text, cryptoKey) => {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encryptedBuffer = await window.crypto.subtle.encrypt(
        {
            name: CRYPTO_ALGO_NAME,
            iv,
        },
        cryptoKey,
        encode(text)
    );
}

Decrypt

decrypt(). This method of the CryptoSubtle interface turns the unreadable encrypted data into readable plaintext. It accepts as arguments the algorithm used to encrypt the data, the crypto key and the encrypted data.

await window.crypto.subtle.decrypt(
    { name: "AES-GCM", iv: initializationVector },
    cryptoKey,
    cipheredText
  );

Import and Export Key

In some cases, the crypto key needs to be stored or transmitted; in this case, the exportKey() method is used to extract a cryptographic key from the crypto key, which can now be stored or transmitted. The exportKey() accepts two arguments: the export format and the crypto key. The export format is the format in which the crypto key can be exported. There are four export formats: raw, JSON Web Key (JWK), Standard public key info (SPKI), and Public Key Cryptographic Standard 8 (PKCS8).

const exportedkey = await window.crypto.subtle.exportKey("jwk", cryptoKey);

To use the exported key, the importKey() function is used to convert the exported key back to a crypto key that can be used for decryption. The importKey() accepts five arguments: format, keyData, algorithm, extractable, and keyUsages.

await window.crypto.subtle.importKey(
    "jwk",
    {
      alg: "A128GCM",
      ext: true,
      k: encryptionKey,
      key_ops: ["encrypt", "decrypt"],
      kty: "oct",
    },
    {
      name: "AES-GCM",
      length: 128,
    },
    false,
    ["decrypt"]
  );

Example

For this example, we are going to build a simple page with inputs to enter text that we want to encrypt, then copy the encryption details and paste them inside the decryption inputs to decrypt. The main focus of this is on the JavaScript side of things.

This is the HTML markup

 <main>
        <div>
            <div class="encrypt">
                <h2>Web Data Encryption</h2>
                <p>Enter Text that you want to encrypt</p>
                <textarea placeholder="Enter text"></textarea>
                <button id="encrypt">Encrypt</button>
            </div>
            <div class="encryption-details">
                <h3>Encryption Details</h3>
                <span>
                    <h3>Ciphered Text</h3>
                    <p class="ciphered-text" id="ciphered-text"></p>
                    <button id="copy">Copy</button>
                </span>
                <span>
                    <h3>Initialization Vector (IV)</h3>
                    <p class="iv" id="iv"></p>
                    <button id="copyIV">Copy</button>
                </span>

                <span>
                    <h3>Crypto Key</h3>
                    <p class="ciphered-text" id="cryptoKey"></p>
                    <button id="copyKey">Copy</button>
                </span>
            </div>
        </div>
        <div>
            <div class="encrypt">
                <h2>Web Data Encryption</h2>
                <p>Enter Ciphered Text to decrypt</p>
                <textarea placeholder="Enter ciphered text" id="decrypted-text-input"></textarea>
                <input type="text" id="ivInput" class="ivInput" placeholder="Enter Initialization Vector (IV)">
                <input type="text" id="cryptoKeyInput" class="ivInput" placeholder="Enter cryptoKey">
                <p class="error" id="error">Could not decrypt text</p>
                <button id="decrypt">Decrypt</button>
            </div>
            <div class="encryption-details">
                <!-- <h3>Decryption Details</h3>xx -->
                <span>
                    <h3>Decrypted Text</h3>
                    <p class="ciphered-text" id="decrypted-text"></p>
                </span>
            </div>
        </div>
    </main>

Then the styling

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

main {
  padding: 16px;
  margin: auto;
  display: flex;
  gap: 20px;
}

main div {
  width: 100%;
  max-width: 100%;
}

textarea {
  /* margin-top: 10px; */
  padding: 16px;
}

.error {
  color: red;
  display: none;
}

.encrypt {
  /* width: 50%; */
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.encrypt button {
  border: none;
  background-color: cornflowerblue;
  color: white;
  padding: 10px 0;
  cursor: pointer;
}

.encryption-details {
  margin-top: 20px;
}

.encryption-details span p {
  word-break: break-all;
  word-wrap: break-word;
}

.encryption-details span button {
  border: none;
  padding: 10px 20px;
  margin-top: 10px;
  background-color: cornflowerblue;
  color: white;
  cursor: pointer;
  margin: 10px;
}

.ivInput {
  padding: 10px;
}

Then, to the JavaScript where we do the encryption and decryption
First, we bring in all the DOM elements that we need.

const ivOutput = document.getElementById("iv");
const cipheredTextOutput = document.getElementById("ciphered-text");
const decryptedTextTextOutput = document.getElementById("decrypted-text");
const textArea = document.querySelector("textarea");
const cipheredtextArea = document.querySelector("#decrypted-text-input");
const encrypt = document.getElementById("encrypt");
const decrypt = document.getElementById("decrypt");
const copy = document.querySelector("#copy");
const copyIV = document.querySelector("#copyIV");
const copyKey = document.querySelector("#copyKey");
const error = document.getElementById("error");
const ivInput = document.getElementById("ivInput");
const cryptoKeyOutput = document.getElementById("cryptoKey");
const cryptoKeyInput = document.getElementById("cryptoKeyInput");

Then we initialise variables for the cryptoKey and initialization vector (IV), and also declare the encodeText() and decodeText() functions.

let iv;
let key;
function encodeText(text) {
  let enc = new TextEncoder();
  return enc.encode(text);
}

function decodeText(text) {
  let dec = new TextDecoder();
  return dec.decode(text);
}

The encodeText() function takes a string and returns a Uint8Array containing the string encoded. The decodeText() takes an encoded byte and returns a plain JavaScript string.

The next step is to generate the IV and the crypto key

function generateIV() {
  iv = window.crypto.getRandomValues(new Uint8Array(12));
}

async function generateKey() {
  const key = await window.crypto.subtle.generateKey(
          {
            name: "AES-GCM",
            length: 128,
          },
          true,
          ["encrypt", "decrypt"]
  );
  return key;
}

Two helper functions are needed: a function to capture any text enclosed inside the square brackets, which we would call sanitizeInput and another function to convert data to Uint8Array, which we would call dataToBytes.

const REGEX_FOR_EXTRACTING_TEXT_FROM_INPUT = /\[(.*?)\]/;

const sanitizeInput = (input) => {
  const val = input.match(REGEX_FOR_EXTRACTING_TEXT_FROM_INPUT)?.[1].trim();
  return val.split(",");
};

const dataToBytes = (data) => {
  const arr = Array.from(data);
  const bytes = new Uint8Array(arr);

  return bytes;
};

The next step is to set up the import and export key functions. The exportCryptoKey() function takes the crypto key as an argument and exports it in JSON Web Key (JWK) format using the exportKey method of crypto.subtle, returning the k value. The importCryptoKey function takes the exported crypto key as an argument, which is named encryptionKey. It turns the encryptionKey back into a usable crypto key using the importKey method of crypto.subtle.

async function exportCryptoKey(cryptoKey) {
  const exportedkey = await window.crypto.subtle.exportKey("jwk", cryptoKey);
  return exportedkey.k;
}

async function importCryptoKey(encryptionKey) {
  const cryptoKey = await window.crypto.subtle.importKey(
    "jwk",
    {
      alg: "A128GCM",
      ext: true,
      k: encryptionKey,
      key_ops: ["encrypt", "decrypt"],
      kty: "oct",
    },
    {
      name: "AES-GCM",
      length: 128,
    },
    false,
    ["decrypt"]
  );
  return cryptoKey;
}

To encrypt and decrypt the text input, we declare the functions that would do the work: the encryptText and decryptText functions.

async function encryptText(encodedText) {
  key = await generateKey();
  const exportedkey = await exportCryptoKey(key);
  cryptoKeyOutput.textContent = exportedkey;
  let encrypt = await window.crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    encodedText
  );
  let buffer = new Uint8Array(encrypt);
  return buffer;
}

async function decryptText(cipheredText, initializationVector) {
  const someKey = await importCryptoKey(cryptoKeyOutput.textContent);
  let decryptedText = await window.crypto.subtle.decrypt(
    { name: "AES-GCM", iv: initializationVector },
    someKey,
    cipheredText
  );
  return decryptedText;
}

The last step is to add event listeners to the encrypt and decrypt buttons. The ecrypt button event listener encodes the text input using the encodeText function that we declared earlier, generates the IV, encrypts the message and displays the encrypted message and IV to the user. The decrypt event listener cleans up the IV and ciphered text and converts them into Uint8Array, the decryptText function decrypts the message and returns bytes which is then turned into a plain text using the decodeText function and then displayed to the user.

encrypt.addEventListener("click", async () => {
  const message = encodeText(textArea.value);
  generateIV();
  const encryptedText = await encryptText(message);
  cipheredTextOutput.textContent = `[${encryptedText}]`;
  ivOutput.textContent = `[${iv}]`;
});

decrypt.addEventListener("click", async () => {
  try {
    const ivInputValue = dataToBytes(sanitizeInput(ivInput.value));
    const message = cipheredtextArea.value;
    const san = dataToBytes(sanitizeInput(message));
    const decryptedMessage = await decryptText(san, ivInputValue);
    const decodedText = decodeText(decryptedMessage);
    decryptedTextTextOutput.textContent = decodedText;

    error.style.display = "none";
  } catch (e) {
    console.error(e);
    error.style.display = "block";
  }
});

Just to improve the user experience, we would add copy to clipboard functionality for each of the data that we need to copy; The cipheredText, initialization vector (IV), and crypto key.

copy.addEventListener("click", async () => {
  await navigator.clipboard.writeText(cipheredTextOutput.textContent);
});

copyKey.addEventListener("click", async () => {
  await navigator.clipboard.writeText(cryptoKeyOutput.textContent);
});

copyIV.addEventListener("click", async () => {
  await navigator.clipboard.writeText(ivOutput.textContent);
});