DEV Community

Thomas Dudek
Thomas Dudek

Posted on • Originally published at sharesecure.link

The Ultimate Developer’s Guide to AES-GCM: Encrypt and Decrypt with JavaScript and the Web Cryptography API

Hey developers 👋,

ever wondered about how secure your application really is? This guide will show you how to leverage the Web Cryptography API to protect your data effectively, focusing on AES-GCM encryption. We'll break down the essentials of key management, encryption processes, and integrity checks, giving you a straightforward path to robust data security.
Ready to elevate your security approach? Let's dive in.

How it works


We will use the Web Cryptography API because it provides secure, standards-based methods for generating keys, hashing, signing, encrypting and decrypting data built directly into browsers. This enables a wide range of cryptographic operations that are essential for modern web applications.
For the encryption algorithm, we'll use AES-GCM, which is known for its efficiency and security.

AES-GCM (Advanced Encryption Standard - Galois/Counter Mode) is a symmetric key encryption algorithm that inherently requires a key to encrypt and decrypt data. The key could technically be any string. However, for enhanced security and to ensure data is secured with an additional password, we use PBKDF2 (Password-Based Key Derivation Function 2) for key derivation. PBKDF2 takes a password as input and produces a cryptographic key. It incorporates a salt to prevent rainbow table attacks and can perform many iterations to increase the computation time, thereby enhancing resistance against brute-force attacks.


Understanding Encryption: How It All Works

Before we deep dive into each component, let's look at the process shown above and summarize the relevant components involved in securing data:

Key Derivation

Directly using user passwords as encryption keys is not advisable due to their predictability and vulnerability to being guessed or cracked through brute-force attacks. Instead, we use a key derivation process to transform these potentially weak passwords into strong, cryptographic keys.

  • Password: Used as input for PBKDF2. Typically, user passwords may not be complex or random enough to serve as strong keys on their own. They often contain predictable patterns or are reused across different services, making them susceptible to attacks.
  • Salt: Used as input for PBKDF2. The salt is a random value added to the password before key derivation. The salt ensures that even if two users have the same password, their derived keys will be different. It also protects against precomputed attacks, such as rainbow table attacks, where attackers use pre-generated hashes to crack passwords quickly.
  • PBKDF2: This key derivation function is specifically designed to make deriving the key from the password computationally expensive and time-consuming. By incorporating the salt and performing many iterations, PBKDF2 effectively protects from brute-force and other speed-based cracking attempts. The use of a hashing function like SHA-256 within PBKDF2 also ensures the integrity and randomness of the derived key.
  • Encryption Key: The result of the key derivation process, used to encrypt the plaintext data. This key is significantly harder to reverse-engineer or guess compared to the original user password.

Encryption

With a robust encryption key in hand from our key derivation process, we move on to the encryption phase where this key is used to secure our data.

  • Plaintext Data: The original data that needs to be encrypted. This data is combined with the encryption key to ensure it remains confidential.
  • Initialization Vector (IV): A unique sequence used for each encryption operation to ensure that identical plaintext results in different ciphertexts every time.
  • AES-GCM: This is the encryption algorithm we use.
  • Encrypted Data (Ciphertext): The output of the encryption process, representing the encrypted version of the plaintext.
  • Authentication Tag: Generated during the encryption, this tag helps verify the integrity and authenticity of the data upon decryption, ensuring the data has not been tampered with during transmission or storage.

Now, with a clear understanding of what each component does and how they interact, we can walk each step in more detail to understand their roles in the encryption and decryption processes.

How to create a secure Encryption Key?

The secure cryptographic key generation starts with the user's password, processed through the PBKDF2 algorithm using SHA-256 hashing. This method enhances security by deterring brute-force attacks through computationally intensive operations and requires two steps:

Salt Generation

We use a cryptographically secure random number generator from the Web Cryptography API to create a random salt. This ensures that identical passwords do not produce the same encryption key.

const salt = window.crypto.getRandomValues(new Uint8Array(16)); // 128-bit salt
Enter fullscreen mode Exit fullscreen mode

A 128-bit salt provides a high level of randomness, which is crucial for ensuring that each derived key is unique. With 2^128 possible combinations, the likelihood that two salts are the same is extremely low, even across millions of encryption instances. While providing robust security, a 128-bit salt also balances performance. It doesn't significantly slow down the key derivation process but offers enough complexity to provide security against most attack vectors.

Key Derivation

First, we need to prepare the user's password for cryptographic use by converting it into a format suitable for key derivation. We achieve this using the importKey() function from the Web Cryptography API:

const baseKey = await window.crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode(password),
  { name: "PBKDF2" },
  false,
  ["deriveKey"],
);
Enter fullscreen mode Exit fullscreen mode

Now, we apply the PBKDF2 function to the base key to obtain the final encryption key. This step uses the provided salt, iteration count, and hash function:

const derivedKey = window.crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt,
    iterations: 600000,
    hash: "SHA-256",
  },
  baseKey,
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"],
);
Enter fullscreen mode Exit fullscreen mode

Using 600,000 iterations for key derivation in PBKDF2 is recommended by NIST to balance security and performance. This high number of iterations increases the computational effort required, making brute-force attacks more difficult and time-consuming by modern hardware, which can quickly process lower iterations.

How to encrypt data with AES-GCM?

When it comes to encrypting data with AES-GCM, there are essential steps to follow:

  1. Key Importation: This step prepares the derived encryption key for use in encryption operations, ensuring that the cryptographic process can securely access and utilize the key.
  2. Initialization Vector Generation: To ensure each encryption operation's uniqueness and security, we generate the Initialization Vector (IV) using a secure random number generator.
  3. Data Encryption: Once we have the key prepared, we can proceed with encrypting the data. This involves specifying the Initialization Vector (IV) and the length of the authentication tag. This encryption process not only secures the data but also generates an authentication tag, which is required for verifying data integrity and authenticity upon decryption.

Initialization Vector (IV)

We generate a 12-byte Initialization Vector (IV) using a secure random number generator. This method ensures that each encryption session starts with a fresh, secure IV.

const iv = window.crypto.getRandomValues(new Uint8Array(12)); // optimal length for AES-GCM
Enter fullscreen mode Exit fullscreen mode

Here's why a 12-byte IV is optimal:

  • Alignment with AES Block Size: This size directly aligns with AES's block size, simplifying the encryption process by eliminating the need for additional hashing or padding.
  • Enhanced Security and Efficiency: The 12-byte length helps avoiding IV reuse, relevant for maintaining data confidentiality and integrity. It allows AES-GCM to use the IV as the initial counter block, streamlining encryption operations.
  • Compliance with NIST Recommendations: According to NIST, a 12-byte IV is recommended for balancing security with performance, suitable for up to (2^{32}) encryptions with the same key.

Encrypting the Data

Once the encryption key is prepared and the Initialization Vector (IV) is generated, we're ready to encrypt the data. This process involves transforming the plaintext data into ciphertext using the AES-GCM encryption algorithm.
By specifying the IV and the desired tag length, we ensure both confidentiality and integrity of the encrypted data:

const key = await deriveKey(password, salt);
const encrypted = await window.crypto.subtle.encrypt(
  { name: "AES-GCM", iv, tagLength: 128 },
  key,
  new TextEncoder().encode(data),
);
Enter fullscreen mode Exit fullscreen mode

Extracting the Authentication Tag

When encrypting data using the Web Cryptography API, the output of the encryption process contains both the ciphertext and the authentication tag combined. This means that upon encryption, the resulting encrypted value already includes the authentication tag.

const ciphertext = encrypted.slice(0, encrypted.byteLength - 16);
const authTag = encrypted.slice(encrypted.byteLength - 16);
Enter fullscreen mode Exit fullscreen mode

Extracting the authentication tag is especially recommended when using encryption in environments with different technology stacks and polyglot setups. For instance, in Node.js's crypto module, the authentication tag is handled separately from the ciphertext: const cipher = createCipheriv("aes-256-gcm", encryptionKey, iv); /* ... */ cipher.getAuthTag(), unlike some other environments where it might be automatically appended to the output. Manually extracting the tag ensures compatibility across different cryptographic libraries and frameworks, allowing for more control over how encryption is implemented and verified.

How to decrypt the data with AES-GCM?

Once our data is securely encrypted, our next step is to unlock it and retrieve the original information. The decryption process reverses the encryption steps, using the same key, IV, and authentication tag to ensure the data's integrity has not been compromised.

// re-derive the key from the password and salt used during encryption
const key = await deriveKey(password, salt);

// re-combine the ciphertext and the authentication tag
const encryptedContent = new Uint8Array(ciphertext.length + authTag.length);
encryptedContent.set(ciphertext, 0);
encryptedContent.set(authTag, ciphertext.length);

const decryptedContent = await window.crypto.subtle.decrypt(
  { name: "AES-GCM", iv, tagLength: 128 },
  key,
  encryptedContent,
);

const decryptedData = new TextDecoder().decode(decryptedContent);
Enter fullscreen mode Exit fullscreen mode

This function retrieves the original plaintext data securely and confirms that the data's integrity has not been compromised by verifying the authentication tag before decryption.

Full Code Example: AES-GCM encryption and decryption

Here's a complete example demonstrating the AES-GCM encryption and decryption process using the Web Cryptography API:

async function deriveKey(password, salt) {
  const encodedPassword = new TextEncoder().encode(password);
  const baseKey = await window.crypto.subtle.importKey(
    "raw",
    encodedPassword,
    { name: "PBKDF2" },
    false,
    ["deriveKey"],
  );

  const derivedKey = await window.crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: salt,
      iterations: 600000,
      hash: "SHA-256",
    },
    baseKey,
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"],
  );

  return derivedKey;
}

async function encryptData(data, password) {
  const salt = window.crypto.getRandomValues(new Uint8Array(16)); // 128-bit salt
  const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 12 bytes for AES-GCM
  const key = await deriveKey(password, salt);
  const encodedData = new TextEncoder().encode(data);

  const encryptedContent = await window.crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv: iv,
      tagLength: 128, // 128-bit tag length
    },
    key,
    encodedData,
  );

  // extract the ciphertext and authentication tag
  const ciphertext = encryptedContent.slice(
    0,
    encryptedContent.byteLength - 16,
  );
  const authTag = encryptedContent.slice(encryptedContent.byteLength - 16);

  return {
    ciphertext: new Uint8Array(ciphertext),
    iv: iv,
    authTag: new Uint8Array(authTag),
    salt: salt,
  };
}

async function decryptData(encryptedData, password) {
  const { ciphertext, iv, authTag, salt } = encryptedData;
  const key = await deriveKey(password, salt);

  // re-combine the ciphertext and the authentication tag
  const dataWithAuthTag = new Uint8Array(ciphertext.length + authTag.length);
  dataWithAuthTag.set(ciphertext, 0);
  dataWithAuthTag.set(authTag, ciphertext.length);

  const decryptedContent = await window.crypto.subtle.decrypt(
    { name: "AES-GCM", iv: iv, tagLength: 128 },
    key,
    dataWithAuthTag,
  );

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

// Example usage
const data = "Hello, world!";
const password = "securePassword123";

const encryptedData = await encryptData(data, password);
console.log("Encrypted Data:", encryptedData);

const decryptedData = await decryptData(encryptedData, password);
console.log("Decrypted Data:", decryptedData);
Enter fullscreen mode Exit fullscreen mode

Best Practices for Storage and Transmission

  1. Ciphertext: This is the encrypted version of your plaintext data.

    ✅ Can be stored or transmitted publicly as it requires the corresponding decryption key to unlock, making it meaningless without access to that key.

  2. Initialization Vector (IV): Used to ensure that identical plaintext encrypts to different ciphertext under the same key.

    ✅ Can be stored or transmitted alongside the ciphertext. The IV does not need to be kept secret as its sole purpose is to provide cryptographic randomness and prevent repeatable patterns in the encryption.

  3. Salt: Used in the key derivation process to prevent the generation of identical keys from the same password.

    ✅ Like the IV, the salt can be stored or transmitted publicly. It is not sensitive but is essential for deriving the same encryption key during the decryption process.

  4. Authentication Tag: Provides a way to verify the integrity and authenticity of the encrypted data when decrypting.

    ✅ Should be stored or transmitted with the ciphertext. While it does not compromise the security of the ciphertext if exposed, its integrity verification purpose requires it to accompany the ciphertext.

  5. Encryption Key: The key derived from the password that is used to encrypt and decrypt data.

    ⚠️ Should never be stored or transmitted alongside the ciphertext. The security of the encrypted data relies on the secrecy and security of this key. It must be protected and managed with strong security measures, typically kept in secure storage accessible only to authorized systems or personnel.

Browser Compatibility

The Web Cryptography API is supported in the following browsers:

  • Google Chrome 37+
  • Mozilla Firefox 34
  • Internet Explorer 11+
  • Microsoft Edge 12+
  • Safari 10.1+

Conclusion

Using AES-GCM for encryption and decryption with the Web Cryptography API provides a robust framework for securing data. This guide has detailed the steps for encrypting data securely and decrypting it to ensure integrity, showcasing the necessity of handling encryption components like the authentication tag with care. By following these procedures, developers can protect sensitive information effectively in a variety of technical environments.

Resources


Try It Out

Curious to see how this encryption is used in practice? We use AES-GCM with PBKDF2 to create one-time links to securly share data encrypted within the browser before sending it over the wire.

ShareSecure

Go ahead and create your first secret link: https://www.sharesecure.link

Top comments (0)