错误信息
InvalidAccessError: Failed to execute 'exportKey' on 'SubtleCrypto': key is not extractable
也可能输出为
- InvalidAccessError: The CryptoKey is nonextractable
- InvalidAccessError: A parameter or an operation is not supported by the underlying object
上面是 Chromium 的文本;Safari/WebKit 为“The CryptoKey is nonextractable”;Firefox 为“A parameter or an operation is not supported by the underlying object”。它们都是 `InvalidAccessError` 类型的 DOMException。
WebCrypto SubtleCrypto.exportKey / wrapKey — DOMException InvalidAccessError (Chromium / Firefox / WebKit)
含义
该密钥创建时指定了 `extractable: false`,因此它的密钥材料永远无法读出——`exportKey` 不行,`wrapKey` 也不行。这正是这个标志的意义:私钥始终留在浏览器的加密实现内部,脚本(包括被注入的脚本)可以使用它,却无法窃取它。生成的密钥对中的公钥始终可以导出;这个错误针对的是私钥和对称密钥。
原因
密钥在生成时就有意设为不可导出
常见库和类似通行密钥的设计会以这种方式创建与设备绑定的密钥。之后新增的功能如果要显示、备份或同步私钥,就会与这一设计决定相冲突。
从 IndexedDB 恢复的密钥会保留原来的标志
偶见存储在 IndexedDB 中的 CryptoKey 取出时与创建时完全一样;之后无法再把它们改为可导出。
库的默认设置
偶见一些 JOSE 库和端到端加密(E2E)库默认用 `extractable: false` 生成密钥;于是它们的导出辅助函数在处理私钥时会失败。
修复方法
- 1.
私钥保留为对象,导出公钥
把不可导出的密钥以 `CryptoKey` 对象的形式存入 IndexedDB,只对外提供公钥(SPKI 或 JWK)。这样可以保留该标志提供的保护。
js const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]); // Persist the non-extractable private key as a CryptoKey object (structured clone), never as bytes: await idbPut("signing-key", pair.privateKey); // Share only the public key: const jwk = await crypto.subtle.exportKey("jwk", pair.publicKey); - 2.
仅在确实需要导出时才创建可导出的密钥
如果私钥确实必须离开浏览器,就将其生成为可导出的密钥,并且只在用另一个密钥包装之后再导出。
js // Only if the private key must leave the browser (e.g. a user-initiated, encrypted backup): const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); const wrapped = await crypto.subtle.wrapKey("pkcs8", pair.privateKey, backupKey, { name: "AES-GCM", iv });