JavaScript中能否实现双密钥加密:任一密钥均可解密同一密文?
在JavaScript中实现双密钥任意解密的加密方案
当然可以实现这种1-out-of-2双密钥加密方案——即两个密钥中的任意一个都能单独解密密文。下面是两种实用的实现思路,结合浏览器原生Web Crypto API(安全性更高,无需第三方库)给出代码示例:
方案一:对称加密+双公钥加密会话密钥
这是最常用且安全的方案,核心逻辑是:用随机生成的对称密钥加密明文,再用两个独立的非对称公钥分别加密这个对称密钥。解密时,用任意一个对应的私钥解密出对称密钥,即可解密密文。
代码实现
// 生成AES-GCM对称密钥并加密明文 async function encryptWithSessionKey(plaintext) { const encoder = new TextEncoder(); const data = encoder.encode(plaintext); // 生成256位AES-GCM密钥(支持加密和解密) const aesKey = await window.crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); // 生成12字节随机IV(AES-GCM推荐长度) const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 加密明文 const ciphertext = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, aesKey, data ); // 导出AES密钥为原始二进制数据,用于后续非对称加密 const exportedAesKey = await window.crypto.subtle.exportKey("raw", aesKey); return { ciphertext, iv, exportedAesKey }; } // 生成RSA-OAEP密钥对(用于加密/解密会话密钥) async function generateRSAKeyPair() { return window.crypto.subtle.generateKey( { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: "SHA-256", }, true, ["encrypt", "decrypt"] ); } // 创建支持双密钥解密的加密包 async function createDualKeyEncryptedPackage(plaintext) { // 生成会话密钥和明文密文 const { ciphertext, iv, exportedAesKey } = await encryptWithSessionKey(plaintext); // 生成两个独立的RSA密钥对 const keyPair1 = await generateRSAKeyPair(); const keyPair2 = await generateRSAKeyPair(); // 用两个公钥分别加密会话密钥 const encryptedKey1 = await window.crypto.subtle.encrypt( { name: "RSA-OAEP" }, keyPair1.publicKey, exportedAesKey ); const encryptedKey2 = await window.crypto.subtle.encrypt( { name: "RSA-OAEP" }, keyPair2.publicKey, exportedAesKey ); // 将二进制数据转为Base64,方便存储/传输 return { ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertext))), iv: btoa(String.fromCharCode(...new Uint8Array(iv))), encryptedKey1: btoa(String.fromCharCode(...new Uint8Array(encryptedKey1))), encryptedKey2: btoa(String.fromCharCode(...new Uint8Array(encryptedKey2))), // 注意:私钥需安全存储(如浏览器IndexedDB+加密,或系统密钥链) privateKey1: keyPair1.privateKey, privateKey2: keyPair2.privateKey }; } // 使用任意一个私钥解密 async function decryptWithEitherKey(encryptedPackage, privateKey) { const decoder = new TextDecoder(); // 将Base64转回二进制数据 const ciphertext = Uint8Array.from(atob(encryptedPackage.ciphertext), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(encryptedPackage.iv), c => c.charCodeAt(0)); // 匹配对应私钥的加密会话密钥 const targetEncryptedKey = privateKey === encryptedPackage.privateKey1 ? Uint8Array.from(atob(encryptedPackage.encryptedKey1), c => c.charCodeAt(0)) : Uint8Array.from(atob(encryptedPackage.encryptedKey2), c => c.charCodeAt(0)); // 解密会话密钥 const exportedAesKey = await window.crypto.subtle.decrypt( { name: "RSA-OAEP" }, privateKey, targetEncryptedKey ); // 导入AES密钥 const aesKey = await window.crypto.subtle.importKey( "raw", exportedAesKey, { name: "AES-GCM", length: 256 }, true, ["decrypt"] ); // 解密密文得到明文 const decryptedData = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, aesKey, ciphertext ); return decoder.decode(decryptedData); }
方案二:基于ECC的密钥拆分(进阶)
如果你想更精简的实现,可以用椭圆曲线加密(ECC)的特性:生成一个主私钥,派生出两个子私钥,每个子私钥都能独立解密用主公钥加密的内容。不过这种方案需要对ECC密钥派生有一定了解,适合有密码学基础的场景。
关键注意事项
- 优先使用浏览器原生
Web Crypto API,避免第三方库的潜在安全风险。 - 私钥必须安全存储:禁止明文存储,可使用浏览器的
Web Crypto将私钥标记为不可提取,或结合IndexedDB加密存储。 - 加密算法选择:AES-GCM提供内置的完整性校验,比ECB/CBC更安全;RSA-OAEP比RSA-PKCS#1 v1.5更抗攻击。
内容的提问来源于stack exchange,提问作者MrRav3n
相关产品推荐
相关产品推荐

