WebCrypto crypto.subtleWebCrypto
错误信息
OperationError: The operation failed for an operation-specific reason
WebCrypto crypto.subtle — browsers & Node
含义
这是 WebCrypto 有意含糊的兜底错误:密码原语本身失败了。对于使用 AES-GCM 的 decrypt(),它表示认证标签未通过验证——密钥错误、IV 错误、附加数据错误,或者密文被修改。规范有意让消息保持笼统,以免实现泄露密码学检查失败的原因。对于 sign() 和 deriveBits(),同一个名称涵盖原语内部的各种失败,因此堆栈跟踪中是哪个操作出错,比消息文本更重要。
原因
AES-GCM 认证失败
常见解密时使用的密钥与加密时不同、IV 不匹配、additionalData 缺失或不同,或者密文在传输中丢失了附加在末尾的 16 字节标签。GCM 是认证加密:上述任何一种情况都会让整个操作失败,而不是返回乱码。
密文被截断或被重新编码
常见Base64 处理方式不一致(URL 安全字母表与标准字母表混用)、数据库列截掉了字节,或者字符串与二进制之间的转换损坏了缓冲区。标签检查能发现每一处这样的改动——这正是它的职责。
派生参数超出范围
偶见deriveBits/deriveKey 请求了无法满足的长度、PBKDF2 的迭代次数为零,或者 ECDH 计算的输入彼此不一致——在多个运行时中,这些都会表现为 OperationError。
修复方法
- 1.
IV 与密文一起传输,两端对称解码
把 IV 放在密文前面,解密时再拆开,这样两端始终一致;所有地方都使用同一种 base64 变体。
js // encrypt const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data); const packed = new Uint8Array([...iv, ...new Uint8Array(ct)]); // decrypt const buf = packedFromWire; const pt = await crypto.subtle.decrypt( { name: "AES-GCM", iv: buf.slice(0, 12) }, key, buf.slice(12), ); - 2.
把它当作完整性判定,而不是数据损坏
decrypt 抛出 OperationError 时,正确的响应是认定“这段密文在这个密钥下未通过认证”,并拒绝它。不要通过调整填充来重试,也不要退回到无认证的模式;检查失败正是这项安全特性在起作用。
- 3.
对比两端的密钥指纹
导出两端的密钥并分别计算哈希;如果摘要不同,问题一步就能查明。
js const raw = await crypto.subtle.exportKey("raw", key); const fp = await crypto.subtle.digest("SHA-256", raw); console.log(Buffer.from(fp).toString("hex").slice(0, 16)); // compare on both sides