如何用Web Crypto API的SubtleCrypto解密Node.js AES-GCM加密数据
问题:React Native服务端AES-GCM加密,WebView客户端SubtleCrypto解密失败
场景说明
需实现React Native服务端AES-256-GCM加密,WebView客户端通过SubtleCrypto解密的跨端加解密流程,目前客户端解密时持续抛出OperationalError,已确认密钥一致、加密参数(IV、密文、标签)已正确传递到客户端,服务端已调整IV为12字节并返回认证标签。
服务端(React Native)原加密代码
const encrypt = (str: string) => { const iv = crypto.randomBytes(12); const myHexToken = "0x...." const cipher = crypto.createCipheriv('aes-256-gcm', myHexToken.slice(0,32), iv) let encrypted = cipher.update(str, 'utf8', 'hex') encrypted += cipher.final('hex'); const tag = cipher.getAuthTag(); return { message: encrypted, tag: tag.toString('hex'), iv: iv.toString('hex'), }; };
客户端(WebView)原接收代码
var myHexToken = "0x...."; window.addEventListener("message", async function (event) { var responseData = JSON.parse(event.data); try { var decryptedData = await decrypt(responseData.iv, responseData.message, responseData.tag); } catch (e) { alert(e); } // 后续处理逻辑 });
客户端原解密代码(报错OperationalError)
function fromHex(hexString) { return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); } function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } function fromBase64(base64String) { return Uint8Array.from(window.atob(base64String), c => c.charCodeAt(0)); } async function importKey(rawKey) { var key = await crypto.subtle.importKey( "raw", rawKey, "AES-GCM", true, ["encrypt", "decrypt"] ); return key; } async function decrypt(iv, data, tag) { var rawKey = fromHex(myHexToken.slice(0,32)); var iv = fromHex(iv); var ciphertext = str2ab(data + tag); var cryptoKey = await importKey(rawKey) var decryptedData = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, cryptoKey, ciphertext ) var decoder = new TextDecoder(); var plaintext = decoder.decode(decryptedData); return plaintext; }
问题诊断与修复方案
核心错误1:服务端密钥解析错误
原服务端代码直接将十六进制格式的密钥字符串(myHexToken)传递给createCipheriv,Node.js的crypto模块会默认以UTF-8编码解析该字符串生成密钥,而非按照十六进制格式解码,导致实际使用的密钥与预期完全不符。
修复后的服务端加密代码
const encrypt = (str: string) => { const iv = crypto.randomBytes(12); const myHexToken = "0x...."; // 去掉0x前缀,将十六进制字符串解码为32字节的Buffer(AES-256需要32字节密钥) const keyBuffer = Buffer.from(myHexToken.slice(2), 'hex').slice(0, 32); const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv); let encrypted = cipher.update(str, 'utf8', 'hex'); encrypted += cipher.final('hex'); const tag = cipher.getAuthTag(); return { message: encrypted, tag: tag.toString('hex'), iv: iv.toString('hex'), }; };
核心错误2:客户端密文+标签拼接方式错误
原客户端代码将密文和标签的十六进制字符串直接拼接后,通过str2ab将字符串转为字符数组,这相当于把十六进制字符的ASCII编码当作密文二进制数据,完全不符合AES-GCM的解密要求。正确的做法是将密文和标签的十六进制字符串分别解码为二进制数组,再合并为一个完整的二进制数据块。
修复后的客户端解密代码
function fromHex(hexString) { return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); } async function importKey(rawKey) { return await crypto.subtle.importKey( "raw", rawKey, { name: "AES-GCM" }, true, ["decrypt"] // 仅需解密权限,缩小权限范围 ); } async function decrypt(ivHex, dataHex, tagHex) { // 处理密钥:去掉0x前缀,解码为32字节二进制数组,与服务端逻辑一致 const rawKey = fromHex(myHexToken.slice(2)).slice(0, 32); const iv = fromHex(ivHex); // 将密文和标签分别解码为二进制数组,再合并 const ciphertext = fromHex(dataHex); const tag = fromHex(tagHex); const combinedData = new Uint8Array(ciphertext.length + tag.length); combinedData.set(ciphertext, 0); combinedData.set(tag, ciphertext.length); const cryptoKey = await importKey(rawKey); const decryptedBuffer = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, cryptoKey, combinedData ); return new TextDecoder().decode(decryptedBuffer); }
额外注意事项
- 确保
myHexToken的长度足够:AES-256需要32字节密钥,对应64个十六进制字符(加上0x前缀共66字符),如果密钥长度不足,会自动截断或补全,导致加解密不一致。 - IV使用12字节符合NIST推荐标准,无需修改。
- 传递参数时确保
iv、message、tag均为完整的十六进制字符串,无截断或编码错误。
内容的提问来源于stack exchange,提问作者xfscrypt
相关产品推荐
相关产品推荐

