How to Encrypt Data in Node.js with AES-256-GCM

· 6 min read

Node.js ships everything you need to encrypt data in the built-in node:crypto module, so you don't need a third-party library. This guide gives you a complete encrypt and decrypt pair using AES-256-GCM, explains each choice, and covers when to move key handling out of your app.

Which algorithm to use

Use AES-256-GCM. Besides hiding the data, it produces a 16-byte authentication tag, and decryption fails if anyone changes a single bit of the ciphertext. Older modes such as AES-CBC don't check integrity on their own; you would have to add an HMAC yourself.

Avoid crypto.createCipher (the version without "iv" in the name). It derived the key and IV from a password in an unsafe way and was removed in Node.js 22. Use createCipheriv with a real key and a random IV, as below.

The code

encrypt.mjs
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";

// 32 random bytes = a 256-bit key. Load it from a secret manager in production.
const key = Buffer.from(process.env.DATA_KEY_BASE64, "base64");

export function encrypt(plaintext) {
  const iv = randomBytes(12); // new 96-bit IV for every message, never reused
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag(); // 16 bytes; proves the data wasn't changed
  return Buffer.concat([iv, tag, ciphertext]).toString("base64");
}

export function decrypt(payload) {
  const buf = Buffer.from(payload, "base64");
  const iv = buf.subarray(0, 12);
  const tag = buf.subarray(12, 28);
  const ciphertext = buf.subarray(28);
  const decipher = createDecipheriv("aes-256-gcm", key, iv);
  decipher.setAuthTag(tag);
  // final() throws if the key is wrong or the data was tampered with
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
}

const sealed = encrypt("[email protected]");
console.log(decrypt(sealed)); // [email protected]

The output packs the IV (12 bytes), the tag (16 bytes) and the ciphertext into one base64 string, so each encrypted value is a single column in your database.

Three rules that keep it secure

  1. Never reuse an IV with the same key. With GCM, a repeated IV leaks the relationship between two messages and lets an attacker forge tags. A random 12-byte IV per message is safe for about 2^32 messages per key (NIST SP 800-38D), so rotate the key well before that.
  2. Keep the key out of your code and your repository. Load it from a secret manager, or an environment variable that only the production process can read.
  3. Treat a decryption error as an attack or a bug. decipher.final() throws when the tag doesn't match: log it and stop, never fall back to returning the data.

Generating the key

Create the key once with node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" and put the output in your secret store as DATA_KEY_BASE64. Don't turn a password into a key with SHA-256. If you have to start from a password, use crypto.scrypt with a random salt.

When to stop managing keys yourself

The code above works well for one service and one key. It gets harder once you need to rotate keys without re-encrypting everything at once, share access between several services, or show an auditor who decrypted what. That is the job of a key management service.

With AMARACUS the key stays on our server and your app makes one HTTP request. You store the returned envelope instead of the output above:

encrypt with the AMARACUS API (Node.js 18+)
const res = await fetch("https://api.amaracus.tech/v1/encrypt", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AMARACUS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ key: process.env.AMARACUS_KEY_ID, data: "[email protected]" }),
});
const { data } = await res.json();
// Store data.envelope. Send it to /v1/decrypt with the same key to read the value back.

Each call is one operation; Starter includes 15,000 a month for $2. The encryption API guide covers the full request and response.

Frequently asked questions

Is AES-256-GCM secure enough for customer data?

Yes. AES-256-GCM is approved by NIST (SP 800-38D) and is one of the cipher suites of TLS 1.3. When encrypted data leaks, the cause is almost always key or IV handling, not the cipher.

Can I reuse an IV if the data is different?

No. With GCM, reusing an IV under the same key leaks information about both messages and lets an attacker forge the authentication tag. Generate a fresh random IV for every encryption.

Should I encrypt passwords with AES?

No. Hash passwords with a slow password hash such as Argon2id, so that nobody, including you, can recover them. See how to store user passwords.

Do I need a library such as crypto-js?

No. node:crypto wraps OpenSSL and is maintained as part of Node.js. Pure-JavaScript libraries are slower, and crypto-js is no longer developed.