crypto.createCipheriv()
Creates a Cipher object that encrypts data with a given algorithm, key and initialization vector.
crypto.createCipheriv(algorithm, key, iv[, options])- Added in Node.js v0.1.94
Parameters
| Parameter | Type | Description |
|---|---|---|
$algorithm | string | The cipher name, for example 'aes-256-gcm', 'aes-128-cbc' or 'chacha20-poly1305'. crypto.getCiphers() lists what is available. |
$key | string | ArrayBuffer | Buffer | TypedArray | DataView | KeyObject | The raw key. Its length is fixed by the algorithm: 32 bytes for aes-256, 16 bytes for aes-128. |
$iv | string | ArrayBuffer | Buffer | TypedArray | DataView | null | The initialization vector, or null for ciphers that need none. GCM expects 12 bytes; CBC expects the block size, 16 bytes for AES. |
$optionsoptional | Object | stream.Transform options plus cipher-specific settings. Required for CCM and OCB modes, where authTagLength must be given. |
$options.authTagLengthoptional | number= 16 for GCM and chacha20-poly1305 | Authentication tag length in bytes. Required for CCM and OCB; in GCM it sets the length returned by getAuthTag(). |
$options.encodingoptional | string= 'utf8' | The encoding used when key is a string. It does not affect a string iv, which is always read as UTF-8. |
Return value
CipherivA Cipheriv object, also a stream.Transform, with update(), final() and — for authenticated modes — getAuthTag().
Errors
Throws ERR_CRYPTO_INVALID_KEYLEN or ERR_CRYPTO_INVALID_IV if the key or IV length is wrong for the algorithm, and ERR_CRYPTO_UNKNOWN_CIPHER for an unrecognised algorithm.
Changelog
- PHP v17.9.0, v16.17.0authTagLength became optional for chacha20-poly1305, defaulting to 16 bytes.
- PHP v15.0.0The key and iv arguments can be an ArrayBuffer and are each limited to 2**31 - 1 bytes.
- PHP v11.6.0The key argument can now be a KeyObject.
- PHP v10.10.0Ciphers in OCB mode are now supported.
crypto.createCipheriv() is Node's symmetric encryption entry point. You supply an algorithm, a raw key and an initialization vector; you get back a stream.Transform that turns plaintext into ciphertext through update() and final().
Pick an authenticated mode
Use 'aes-256-gcm' unless you have a specific reason not to. GCM gives you both confidentiality and an authentication tag, retrieved with getAuthTag() after final(), which lets the decrypting side detect any tampering. Unauthenticated modes such as 'aes-256-cbc' encrypt happily but cannot tell you that a byte was flipped in transit — an attacker can corrupt the plaintext in controlled ways and you will never know.
The rules on keys and IVs
The key is raw bytes of exactly the right length: 32 for AES-256, 16 for AES-128. A passphrase is not a key — run it through crypto.scryptSync() first. The IV must be unique for every message under a given key, but it need not be secret, so generate it with crypto.randomBytes(12) and store it next to the ciphertext.
Reusing a GCM key/IV pair is catastrophic, not merely sloppy: it leaks the XOR of the two plaintexts and can expose the authentication subkey, letting an attacker forge tags. That single rule causes more real-world breaks than any other misuse of this API.
Example
When to use it
- Encrypting a sensitive column such as an access token before writing it to the database
- Protecting a config file that must sit on disk in a shared environment
- Encrypting a session payload stored in a client-side cookie
- Wrapping a file with AES-GCM so any tampering is detected on read
Frequently asked questions
Which cipher algorithm should I use in Node.js?
Use 'aes-256-gcm' for almost everything: it encrypts and authenticates in one pass, so tampering is detected on decryption. 'chacha20-poly1305' is a good alternative on hardware without AES acceleration, while plain CBC modes are best avoided because they detect no tampering at all.
Can I reuse the same IV for AES-GCM?
No — reusing a key and IV pair in GCM is a complete break, leaking the XOR of the two plaintexts and potentially allowing an attacker to forge authentication tags. Generate a fresh crypto.randomBytes(12) IV for every message and store it in the clear next to the ciphertext.
Why do I get ERR_CRYPTO_INVALID_KEYLEN?
Because the key is not the exact byte length the algorithm requires — 32 bytes for aes-256, 16 for aes-128 — which usually means a passphrase string was passed directly. Derive a proper key first with crypto.scryptSync(passphrase, salt, 32).
More examples
A reusable encrypt helper with a random IV
Packing the IV and tag in front of the ciphertext means one opaque string is all you have to store, and decrypt() can slice out everything it needs. The last line shows the random IV working: identical plaintext produces different ciphertext every time, so an observer cannot tell that two rows hold the same value.
const crypto = require('node:crypto');
// 32 raw bytes. In production, load this from a secret manager.
const KEY = crypto.randomBytes(32);
function encrypt(plaintext) {
const iv = crypto.randomBytes(12); // fresh for EVERY message
const cipher = crypto.createCipheriv('aes-256-gcm', KEY, iv);
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
// Pack iv + tag + ciphertext into one storable blob.
return Buffer.concat([iv, tag, ct]).toString('base64');
}
function decrypt(blob) {
const raw = Buffer.from(blob, 'base64');
const iv = raw.subarray(0, 12);
const tag = raw.subarray(12, 28);
const ct = raw.subarray(28);
const decipher = crypto.createDecipheriv('aes-256-gcm', KEY, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
}
const blob = encrypt('sk_live_abc123');
console.log('round trip ok :', decrypt(blob) === 'sk_live_abc123');
console.log('two encryptions differ:', encrypt('same') !== encrypt('same'));Deriving a key from a passphrase
The first attempt throws ERR_CRYPTO_INVALID_KEYLEN because the passphrase is 28 bytes, not 32. Padding or truncating it to fit would be worse than the error — scryptSync turns a low-entropy phrase into a full-strength key, and the salt must be stored so the same key can be rederived later.
const crypto = require('node:crypto');
const passphrase = 'correct horse battery staple';
// WRONG: a passphrase is not 32 raw bytes.
try {
crypto.createCipheriv('aes-256-gcm', passphrase, crypto.randomBytes(12));
} catch (err) {
console.log('raw passphrase ->', err.code);
}
// RIGHT: stretch it into a key with a slow KDF and a stored salt.
const salt = crypto.randomBytes(16);
const key = crypto.scryptSync(passphrase, salt, 32);
console.log('derived key bytes:', key.length);
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update('secret data', 'utf8'), cipher.final()]);
console.log('encrypted bytes :', ct.length);
console.log('store alongside : salt, iv, tag, ciphertext');Encrypting a file as a stream
Streaming keeps memory constant no matter how large the file is. The critical detail is that getAuthTag() returns nothing useful until the pipeline resolves, and the tag is not written into the output file — losing it makes the ciphertext permanently undecryptable in GCM mode.
import crypto from 'node:crypto';
import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
fs.writeFileSync('plain.txt', 'sensitive file contents');
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(12);
// A Cipheriv is a Transform stream, so it drops straight into a pipeline.
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
await pipeline(fs.createReadStream('plain.txt'), cipher, fs.createWriteStream('cipher.bin'));
// getAuthTag() is only valid once the stream has finished.
const tag = cipher.getAuthTag();
console.log('encrypted size:', fs.statSync('cipher.bin').size, 'bytes');
console.log('tag size :', tag.length, 'bytes');
console.log('tag must be stored separately — it is not in the file');
fs.unlinkSync('plain.txt');
fs.unlinkSync('cipher.bin');
Discussion