Generate a 32 byte random key, create a fresh 12 byte random IV for every message, encrypt with createCipheriv using aes-256-gcm, read the authentication tag after final, and store IV, ciphertext and tag together. On decrypt, set the tag first and treat any thrown error as a failed message.
The code
The Node.js crypto documentation describes GCM among its authenticated modes, with getAuthTag and setAuthTag. This example follows that API.
const { randomBytes, createCipheriv, createDecipheriv } = require('node:crypto');
function encrypt(key, plaintext) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 });
const data = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, data]);
}
function decrypt(key, blob) {
const iv = blob.subarray(0, 12);
const tag = blob.subarray(12, 28);
const data = blob.subarray(28);
const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 });
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
}
Why each step is there
- Key of 32 bytes. OWASP recommends AES keys of at least 128 bits, ideally 256. Make it from a secure random source, never from a typed phrase.
- Fresh IV per message. Reusing an IV with the same key under GCM breaks its guarantees. Random 12 byte values are the common choice, and OWASP notes IV handling should be automatic.
- authTagLength set. The Node docs say that if the tag length is invalid or does not match the authTagLength option, setAuthTag throws. Setting it explicitly on both sides removes ambiguity.
- getAuthTag after final. The docs say getAuthTag should only be called after encryption is complete with final.
- Tag set before final on decrypt. The docs say that if no tag is provided, or the ciphertext was tampered with, final will throw, and the ciphertext should be discarded.
Where the key comes from
Do not hard code it. OWASP says not to hard code keys in source code or check them into version control, and to use platform secure storage such as an HSM, cloud key vault or secrets manager where available. A common design wraps a data key with a key held in a key management service.
Limits to remember
The OWASP key management page gives an AES-GCM example of limiting each key to at most 2 to the power of 32 encryption operations across all devices sharing that key. Plan rotation before you approach it, and store a key identifier next to each blob so old data stays readable.
Common mistakes
- A constant IV copied from an example.
- Catching the decrypt error and using the partial output anyway.
- Sending the tag in a different channel and losing it.
- Using the same key for encryption and for an unrelated signing job.
- Logging plaintext or keys during debugging.
Checklist before you ship
- Unit test: tamper with one byte, expect an error.
- Unit test: two encryptions of the same text give different outputs.
- A version byte in front of the blob so you can change the format later.
- A key id so rotation works.
Safeguard can generate an SBOM and scan your dependencies, so outdated cryptography libraries with known advisories show up in one inventory, and policies can gate a build on them. A free scan is at gold.safeguard.sh/scan.
Quick check
Why 12 bytes for the IV?
It is the common GCM size and the one most libraries expect. Check your library documentation before using other sizes.
Should I use createCipher instead?
No. Use createCipheriv, which takes an explicit key and IV. The older createCipher call does not, so check the Node release notes for its status in your runtime.
Sources
- Node.js crypto documentation
- OWASP Cryptographic Storage Cheat Sheet
- OWASP Key Management Cheat Sheet
Last reviewed: 19 October 2026.