KernelRanch
返回文章列表

AES-GCM 解密已经读到明文,为什么还要等 final()?

2026-11-13 #security#cryptography#nodejs

使用 Node.js 解密 AES-GCM 数据时,decipher.update() 返回的字节要等 decipher.final() 成功后才能使用。若提前解析、写入数据库或把内容返回给调用方,篡改的数据可能在认证失败前就产生副作用。

1. 用篡改样本观察认证时机

保存为 gcm_auth_lab.mjs,运行 node gcm_auth_lab.mjs。样本使用随机密钥和 12 字节随机 nonce,仅加密一条短消息。AAD 表示接收方预期的租户上下文。

import assert from 'node:assert/strict';
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const algorithm = 'aes-256-gcm';
const key = randomBytes(32);
const nonce = randomBytes(12);
const aad = Buffer.from('tenant=42');
const message = Buffer.from('status=paid');
const options = { authTagLength: 16 };

const cipher = createCipheriv(algorithm, key, nonce, options);
cipher.setAAD(aad);
const encrypted = Buffer.concat([cipher.update(message), cipher.final()]);
const tag = cipher.getAuthTag();

function decrypt(ciphertext, associatedData, observe = () => {}) {
  const decipher = createDecipheriv(algorithm, key, nonce, options);
  decipher.setAAD(associatedData);
  decipher.setAuthTag(tag);
  const pending = decipher.update(ciphertext);
  observe(pending.length);
  return Buffer.concat([pending, decipher.final()]);
}

assert.deepEqual(decrypt(encrypted, aad), message);
console.log('original: accepted');

const tampered = Buffer.from(encrypted);
tampered[0] ^= 1;
for (const [label, ciphertext, context] of [
  ['changed ciphertext', tampered, aad],
  ['wrong AAD', encrypted, Buffer.from('tenant=43')],
]) {
  let pendingBytes;
  assert.throws(() => decrypt(ciphertext, context, n => { pendingBytes = n; }));
  assert.equal(pendingBytes, message.length);
  console.log(label + ': update returned ' + pendingBytes + ' bytes; final rejected');
}
original: accepted
changed ciphertext: update returned 11 bytes; final rejected
wrong AAD: update returned 11 bytes; final rejected

2. 把认证成功作为交付边界

短消息可以先把待认证的字节缓存在内存,final() 成功后再一起返回。大文件若使用单个 GCM 标签,可以把结果写到应用私有的临时文件,成功后才对读者可见,失败时清理临时文件。需要边接收边交付时,采用定义了分块认证、块序号和完整性规则的协议。

格式还需要约定 nonce、标签和 AAD 的来源。AAD 必须和加密时的字节一致,例如从当前已验证的租户上下文构造。生产系统需要确保同一密钥下的每次加密使用唯一 nonce。样本每次生成新密钥只加密一次,重复解密同一个密文用于对照认证结果。

小结

把 update() 的结果留在内部。认证成功后才交付明文,失败时丢弃待认证数据。

数据来源与检索时间

来源URL检索日期对应内容
Node.js 24.19.0 Cryptohttps://nodejs.org/download/release/v24.19.0/docs/api/crypto.html#decipherfinaloutputencoding2026-11-13GCM 的认证时机和解密输出边界
RFC 5116 §5.1.1https://www.rfc-editor.org/rfc/rfc5116.html#section-5.1.12026-11-13GCM 的 nonce 重用后果

核验说明

  • Windows、Node.js 24.19.0 实测有效输入、密文变动和 AAD 不匹配。验证范围限于短消息 API,未执行大文件或密钥管理流程。