crypto.createDecipheriv()

Creates a Decipher object that decrypts data using the same algorithm, key and IV used to encrypt it.

Signature
crypto.createDecipheriv(algorithm, key, iv[, options])
  • Added in Node.js v0.1.94

Parameters

ParameterTypeDescription
$algorithmstringThe cipher name. It must be exactly the algorithm used to encrypt, for example 'aes-256-gcm'.
$keystring | ArrayBuffer | Buffer | TypedArray | DataView | KeyObjectThe raw key, identical to the one used for encryption and of the length the algorithm requires.
$ivstring | ArrayBuffer | Buffer | TypedArray | DataView | nullThe initialization vector used during encryption, or null for ciphers that need none.
$optionsoptionalObjectstream.Transform options plus cipher-specific settings. Required for CCM and OCB modes, where authTagLength must be given.
$options.authTagLengthoptionalnumber= 16 for GCM and chacha20-poly1305Authentication tag length in bytes. Required for CCM and OCB; must be set for GCM only when a non-default tag length was used.
$options.encodingoptionalstring= '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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · javascript
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.

Example · javascript
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.

Example · javascript
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

  • Be the first to comment on this lesson.