crypto.createDecipheriv()
Creates a Decipher object that decrypts data using the same algorithm, key and IV used to encrypt it.
crypto.createDecipheriv(algorithm, key, iv[, options])- Added in Node.js v0.1.94
Parameters
| Parameter | Type | Description |
|---|---|---|
$algorithm | string | The cipher name. It must be exactly the algorithm used to encrypt, for example 'aes-256-gcm'. |
$key | string | ArrayBuffer | Buffer | TypedArray | DataView | KeyObject | The raw key, identical to the one used for encryption and of the length the algorithm requires. |
$iv | string | ArrayBuffer | Buffer | TypedArray | DataView | null | The initialization vector used during encryption, or null for ciphers that need none. |
$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; must be set for GCM only when a non-default tag length was used. |
$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
DecipherivA Decipheriv object, also a stream.Transform, with update(), final() and — for authenticated modes — setAuthTag() and setAAD().
Errors
Throws on final() when an authenticated cipher's tag does not verify, reported as "Unsupported state or unable to authenticate data"; also throws ERR_CRYPTO_INVALID_KEYLEN or ERR_CRYPTO_INVALID_IV for wrong-sized inputs.
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.2.0The authTagLength option can be used to restrict accepted GCM authentication tag lengths.
crypto.createDecipheriv() is the mirror of createCipheriv() and needs the same three inputs: identical algorithm, identical key, identical IV. Get any one of them wrong and decryption fails — which is the intended behaviour, not a bug.
The GCM contract
With an authenticated cipher you must call setAuthTag() with the tag produced during encryption before calling final(). That call is where verification happens: if the ciphertext, the IV, the key, any additional authenticated data or the tag itself has been altered, final() throws "Unsupported state or unable to authenticate data". If you also called setAAD() when encrypting, you must call it again with the same value, before update().
This makes GCM safe in a way CBC is not: a wrong key or a flipped byte produces a clean exception rather than plausible-looking garbage.
What trips people up
That error message is the single most-searched Node crypto string, and it almost never means the crypto is broken. In order of likelihood: the auth tag was never stored, the IV was regenerated instead of retrieved, the ciphertext was round-tripped through a lossy encoding such as latin1 or a UTF-8 text column, or setAAD() was omitted.
Treat any failure as final. Do not fall back to returning partial plaintext — the tag failed for a reason, and output from update() before a failed final() is unverified data that must be discarded.
Example
When to use it
- Reading back an encrypted API token from a database row
- Decrypting a session cookie and rejecting it if the tag does not verify
- Restoring an encrypted config file at application boot
- Detecting that a stored ciphertext was tampered with before using its contents
Frequently asked questions
What causes "Unsupported state or unable to authenticate data"?
The GCM authentication tag did not verify, meaning the key, IV, ciphertext, AAD or tag differs from what was used to encrypt. In practice it is nearly always a storage problem — a lost auth tag, a regenerated IV, or ciphertext corrupted by a text column — rather than a fault in the crypto.
Do I have to call setAuthTag() before final()?
Yes, for GCM, CCM, OCB and chacha20-poly1305 the tag must be supplied before final() runs, because final() is where verification happens. Omitting it makes final() throw, and setAAD() likewise must be called before the first update() if AAD was used when encrypting.
Can I decrypt if I lost the IV or the auth tag?
No. The IV is required to decrypt at all and the tag is required to authenticate, and neither can be recovered or brute-forced. Both are non-secret, so store them alongside the ciphertext — packing iv + tag + ciphertext into one blob is the usual approach.
More examples
Every way GCM decryption fails
Only the first line succeeds; all four failure modes produce the same generic message, which is deliberate — a more specific error would tell an attacker which part of their guess was close. When debugging, check your storage layer rather than reading meaning into the message.
const crypto = require('node:crypto');
const key = Buffer.alloc(32, 7);
const iv = Buffer.alloc(12, 3);
const aad = Buffer.from('user:42');
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
cipher.setAAD(aad);
const ct = Buffer.concat([cipher.update('secret', 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
function attempt(label, { k = key, i = iv, a = aad, t = tag, c = ct }) {
try {
const d = crypto.createDecipheriv('aes-256-gcm', k, i);
d.setAAD(a);
d.setAuthTag(t);
console.log(label, '->', Buffer.concat([d.update(c), d.final()]).toString('utf8'));
} catch (err) {
console.log(label, '-> FAILED:', err.message);
}
}
attempt('correct ', {});
attempt('wrong key ', { k: Buffer.alloc(32, 8) });
attempt('wrong iv ', { i: Buffer.alloc(12, 4) });
attempt('wrong aad ', { a: Buffer.from('user:99') });
attempt('wrong tag ', { t: Buffer.alloc(16, 0) });Decrypting a packed blob from the database
The last two lines are the point: base64 survives a text column intact, while writing raw ciphertext bytes into a UTF-8 column silently replaces invalid sequences and changes the length, permanently destroying the data. That corruption surfaces later as an authentication failure that looks like a crypto bug.
const crypto = require('node:crypto');
const KEY = crypto.randomBytes(32);
function encrypt(text) {
const iv = crypto.randomBytes(12);
const c = crypto.createCipheriv('aes-256-gcm', KEY, iv);
const ct = Buffer.concat([c.update(text, 'utf8'), c.final()]);
return Buffer.concat([iv, c.getAuthTag(), ct]); // 12 + 16 + n bytes
}
function decrypt(blob) {
if (blob.length < 28) throw new Error('ciphertext too short');
const d = crypto.createDecipheriv('aes-256-gcm', KEY, blob.subarray(0, 12));
d.setAuthTag(blob.subarray(12, 28));
return Buffer.concat([d.update(blob.subarray(28)), d.final()]).toString('utf8');
}
const blob = encrypt('sk_live_abc123');
console.log('blob bytes :', blob.length);
console.log('decrypted :', decrypt(blob));
// Store as BYTEA/BLOB, or base64 — never as raw text in a UTF-8 column.
const safe = blob.toString('base64');
console.log('base64 survives round trip:', decrypt(Buffer.from(safe, 'base64')) === 'sk_live_abc123');
const lossy = Buffer.from(blob.toString('utf8'), 'utf8');
console.log('utf8 column corrupts it :', lossy.length !== blob.length);Decrypting a file as a stream
Writing to a temporary name and renaming only after the pipeline resolves is the safe pattern for streamed GCM. Because the tag is checked at the very end, a consumer reading the output as it is produced would be processing unverified plaintext from an attacker-supplied file.
import crypto from 'node:crypto';
import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(12);
fs.writeFileSync('secret.txt', 'file contents to protect');
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
await pipeline(fs.createReadStream('secret.txt'), cipher, fs.createWriteStream('secret.enc'));
const tag = cipher.getAuthTag();
// Decrypt to a TEMP file: the tag is only verified when the stream ends,
// so nothing before that point should be treated as trustworthy.
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
await pipeline(fs.createReadStream('secret.enc'), decipher, fs.createWriteStream('secret.tmp'));
fs.renameSync('secret.tmp', 'secret.out'); // promote only after success
console.log('recovered:', fs.readFileSync('secret.out', 'utf8'));
for (const f of ['secret.txt', 'secret.enc', 'secret.out']) fs.unlinkSync(f);
Discussion