You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Nodejs AES-256-GCM加密后 浏览器WebCrypto解密报DOMException排查

解密失败的核心原因

1. 缺失GCM认证标签

AES-256-GCM是带认证的加密模式,加密后会生成16字节的认证标签用于校验密文完整性,你当前服务端加密逻辑完全没有处理、返回该标签,客户端WebCrypto API解密时强制校验标签,缺失直接抛出异常。

2. IV未随密文返回给客户端

服务端加密生成的12字节IV仅在本地使用,没有拼入返回给客户端的加密数据中,客户端从加密数据头部截取的12字节IV实际是密文内容,参数完全不匹配。

3. 密文拼接逻辑错误

服务端加密时用+拼接两个Buffer对象,会自动将Buffer转为UTF-8字符串,破坏二进制密文结构,导致密文本身损坏。

4. 共享密钥派生逻辑可能不一致

客户端生成AES密钥时,对ECDH协商出的共享密钥做了SHA-256哈希,需要确认服务端的sharedKey参数也是ECDH共享密钥经SHA-256哈希后转的base64值,两端密钥不一致也会解密失败。

修复方案

第一步:修改服务端加密逻辑

export function encrypt(sharedKey: string, message: string) {
  const iv = getRandomIV();
  const cipher = crypto.createCipheriv(
    'aes-256-gcm',
    Buffer.from(sharedKey, 'base64'),
    iv
  );

  // 正确拼接Buffer,避免二进制损坏
  const encrypted = Buffer.concat([cipher.update(message, 'utf8'), cipher.final()]);
  // 获取GCM认证标签
  const authTag = cipher.getAuthTag();
  // 拼接结构:12字节IV + 密文 + 16字节认证标签,再转base64返回
  return Buffer.concat([iv, encrypted, authTag]).toString('base64');
}
function getRandomIV() {
  return crypto.randomBytes(12);
}

同时确认服务端生成sharedKey的逻辑:ECDH协商出共享密钥后,必须做SHA-256哈希,再转为base64传入encrypt函数,和客户端密钥派生逻辑对齐。

第二步:修改客户端解密逻辑

const SERVER_ENCRYPTION_IV_LENGTH = 12;
const GCM_AUTH_TAG_LENGTH = 16;

async function decrypt(base64EncryptedData: string) {
    const aesKey = await generateAesKey();
    // 先把服务端返回的base64转为Uint8Array
    const encryptedData = Uint8Array.from(atob(base64EncryptedData), c => c.charCodeAt(0));
    // 拆分数据:前12位是IV,最后16位是认证标签,中间是密文
    const nonce = encryptedData.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
    const authTag = encryptedData.subarray(encryptedData.length - GCM_AUTH_TAG_LENGTH);
    const data = encryptedData.subarray(SERVER_ENCRYPTION_IV_LENGTH, encryptedData.length - GCM_AUTH_TAG_LENGTH);

    const decrypted = await crypto.subtle.decrypt(
      {
        name: 'AES-GCM',
        iv: nonce,
        tag: authTag // 传入认证标签
      },
      aesKey,
      data
    );
    return {
      decrypted: new Uint8Array(decrypted),
      decryptedString: new TextDecoder().decode(decrypted),
    };
}

// generateAesKey逻辑保持不变即可,前提是服务端密钥派生逻辑和此处对齐
async function generateAesKey() {
    const publicKey = await getServerPublicKey();
    const privateKey = await getPrivateKey();
    const sharedSecret = await crypto.subtle.deriveBits(
      {
        name: 'ECDH',
        public: publicKey!,
      },
      privateKey,
      256
    );

    const aesSecret = await crypto.subtle.digest('SHA-256', sharedSecret);
    return crypto.subtle.importKey('raw', aesSecret, 'AES-GCM', true, [
      'encrypt',
      'decrypt',
    ]);
}

内容的提问来源于stack exchange,提问作者Ali Torki

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 07:39:02