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
相关产品推荐
相关产品推荐

