Skip to content

πŸ› Bug Report β€” Runtime APIs: node:crypto GCM decipher requires setAuthTag() before update(); Node allows it any time before final()Β #8123

Open

Description

@dial481

Summary

With nodejs_compat, an aes-128-gcm / aes-256-gcm Decipheriv throws No auth tag provided if setAuthTag() is called after update(). Node.js documents that for GCM the tag only has to be set before final():

The decipher.setAuthTag() method must be called before decipher.update() for CCM, SIV, and GCM-SIV modes or before decipher.final() for GCM and OCB modes and chacha20-poly1305.
(https://nodejs.org/api/crypto.html, decipher.setAuthTag)

Streaming decryption depends on this, because the tag is the last thing on the wire. The ssh2 library's aes*-gcm@openssh.com support does exactly that (AESGCMDecipherNative in lib/protocol/crypto.js: setAAD, update as the packet arrives, then setAuthTag, then final), so every GCM SSH connection from a Worker fails on the first encrypted packet.

Repro (fixed inputs; workerd 2026-10-23 via wrangler 4.156.0, 2026-10-30 and 2026-11-01, compatibility_date 2026-10-01, nodejs_compat):

import * as crypto from 'node:crypto';
const key = Buffer.alloc(32, 0x4c), iv = Buffer.alloc(12, 0x61), aad = Buffer.from('header');
const enc = crypto.createCipheriv('aes-256-gcm', key, iv);
enc.setAAD(aad);
const ct = Buffer.concat([enc.update(Buffer.from('attack at dawn')), enc.final()]);
const tag = enc.getAuthTag();

const dec = crypto.createDecipheriv('aes-256-gcm', key, iv);
dec.setAAD(aad);
const head = dec.update(ct);   // workerd throws here: "No auth tag provided"
dec.setAuthTag(tag);
Buffer.concat([head, dec.final()]).toString();
// Node 22: 'attack at dawn'

Setting the tag before update() works in workerd.

Where

src/workerd/api/node/crypto.c++, CryptoImpl::CipherHandle::update (lines 813-816 at 6472e6e):

if (mode == CipherMode::DECIPHER && isAuthenticatedMode(ctx) && !authTagPassed) {
  authTagPassed = true;
  auto& tagRef = JSG_REQUIRE_NONNULL(maybeAuthTag, Error, "No auth tag provided");
  ...

This applies to every authenticated mode, GCM included. Node passes the tag to OpenSSL when setAuthTag() is called (CipherBase::SetAuthTag calls setAeadTag straight away) and does not require it in Update.

Suggested fix (not built or tested on our side): in update(), require the tag only for the modes that need it up front (CCM, and SIV / GCM-SIV if supported). For GCM and OCB, leave it to final(), which already passes the tag if it has not been passed yet.

Workaround we use: wrap createDecipheriv for GCM so the decipher holds the AAD and ciphertext until final(), then runs the native decipher with the tag set first. No plaintext is released before the tag is verified.

Docs: https://developers.cloudflare.com/workers/runtime-apis/nodejs/crypto/ says "All node:crypto APIs are fully supported in Workers" with exceptions that do not cover this.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions