Node.js crypto module for AES-256-GCM encryption and decryption

The Node.js crypto module can encrypt data that your application must later recover. AES-256-GCM gives you ciphertext plus an authentication tag, so a modified payload fails instead of returning data you should trust. The executed command below shows both the recovered text and the authentication failure.

Use node:crypto with AES-256-GCM

Node provides the crypto module with Node.js, so this example needs no npm package. AES is a symmetric cipher, which means the same secret key encrypts and decrypts the payload.

Galois/Counter Mode (GCM) is an authenticated encryption mode. Along with the ciphertext, it produces an authentication tag that the decryptor verifies before it accepts the plaintext. The Node.js crypto documentation defines the createCipheriv, createDecipheriv, getAuthTag, and setAuthTag calls used here.

Build a payload that can be decrypted and verified

Keep the IV, ciphertext, and authentication tag together. The IV is an initialization vector, a per-encryption value that makes repeated encryption with the same key produce different ciphertext. Generate a new IV for every encryption and keep it with the payload.

Create the ciphertext and tag

The encrypt function starts with plaintext and creates a fresh 12-byte IV. It returns Base64 strings so the payload can move through JSON, a database field, or a message queue without losing the binary values.

const { randomBytes, createCipheriv, createDecipheriv } = require('node:crypto');

const algorithm = 'aes-256-gcm';
const key = randomBytes(32);

function encrypt(plaintext) {
  const iv = randomBytes(12);
  const cipher = createCipheriv(algorithm, key, iv);
  const ciphertext = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final(),
  ]);

  return {
    iv: iv.toString('base64'),
    ciphertext: ciphertext.toString('base64'),
    authTag: cipher.getAuthTag().toString('base64'),
  };
}

function decrypt(payload) {
  const decipher = createDecipheriv(
    algorithm,
    key,
    Buffer.from(payload.iv, 'base64'),
  );
  decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'));

  const plaintext = Buffer.concat([
    decipher.update(Buffer.from(payload.ciphertext, 'base64')),
    decipher.final(),
  ]);
  return plaintext.toString('utf8');
}

const payload = encrypt('Ship the report at 09:00 UTC');
console.log('Ciphertext:', payload.ciphertext);
console.log('Decrypted:', decrypt(payload));

const tampered = { ...payload, authTag: Buffer.alloc(16).toString('base64') };
try {
  decrypt(tampered);
} catch (error) {
  console.log('Tampered payload:', error.message);
}

AES-256 needs a 32-byte key, which is why the example calls randomBytes(32). The generated key only exists for the life of this process, which keeps the example focused on the crypto API rather than pretending it has solved key management.

Decrypt only after GCM verifies the tag

The decrypt function rebuilds the IV and ciphertext from Base64, then passes the authentication tag to setAuthTag before final(). That final call performs GCM verification. If the tag, IV, key, or ciphertext does not match, Node throws instead of returning plaintext.

Terminal output showing AES-256-GCM decryption and tamper rejection in Node.js
AES-GCM returns the original text and rejects a modified authentication tag.

The output shows both states that matter. The original payload decrypts to the sentence passed to encrypt, and the changed tag produces an authentication error that the example catches and reports.

Keep encryption and password storage separate

Use reversible encryption for data that your application must read again, such as a protected integration secret or a field with a defined recovery path. Store password verifiers with a password-hashing design instead, because an application should not need to decrypt a user’s password.

The in-memory key is the deliberate boundary of this example. A production service needs a protected key source, access controls, rotation, and a documented recovery path. The OWASP Cryptographic Storage Cheat Sheet covers the architecture and key-management questions that sit outside a short API example.

Run the example, then replace the temporary key

Save the code as aes-gcm.js and run node aes-gcm.js. Keep the IV, ciphertext, and authentication tag as one payload, then replace randomBytes(32) with the protected key source your application already controls.

Aditya Gupta
Aditya Gupta

Aditya Gupta is a founding member and editor at CodeForGeek. He first found his way into tech by reading articles, and now writes approachable guides to Node.js security, authentication, AI tools, coding agents, and web scraping.

Articles: 529