Node.js(TypeScript)中如何实现单密钥加密、多密钥解密?
需求说明
需要实现单密钥加密、多把独立密钥均可解密同一份密文的功能,当前基于Node.js内置crypto库开发,使用aes-256-ctr算法,原有代码仅支持加解密使用相同密钥,无法满足多密钥解密的业务要求,开发环境为Node.js + TypeScript,原有代码如下:
const crypto = require("crypto"); const algorithm = "aes-256-ctr"; const secretKey = "vOVH6sdmpNWjRRIqCc7rdxs01lwHzfr3"; encrypt = (text: string) => { let iv = crypto.randomBytes(16); let cipher = crypto.createCipheriv(algorithm, secretKey, iv); let encrypted: any = Buffer.concat([cipher.update(text), cipher.final()]); let hash: any = { iv: iv.toString("hex"), content: encrypted.toString("hex"), }; return hash; }; decrypt = (hash: any) => { if (hash.iv) { const decipher = crypto.createDecipheriv(algorithm, secretKey,Buffer.from(hash?.iv, "hex")); const decrpyted = Buffer.concat([ decipher.update(Buffer.from(hash.content, "hex")),decipher.final(),]); return decrpyted.toString(); } };
实现方案
对称加密的原生逻辑就是加解密必须使用相同密钥,不存在直接配置多把解密密钥的AES模式,要实现需求最稳妥、性能最好的方案是采用数字信封结构,核心逻辑如下:
- 每次加密新数据时,首先随机生成一个一次性的32字节AES数据密钥,用这个数据密钥走原有aes-256-ctr逻辑加密明文,得到密文和对应iv
- 遍历所有允许解密该密文的业务密钥,每把业务密钥单独加密这个随机生成的数据密钥,生成对应的数据密钥密文副本,和明文密文、iv存在一起
- 解密时,拿当前持有的业务密钥,逐个尝试解密存储的数据密钥副本,只要能成功解密出数据密钥,就可以用这个数据密钥解出原始明文
这个方案的优势是原有AES加解密逻辑几乎不需要改动,额外开销只有32字节长度的数据密钥的多次加密,性能损耗可以忽略;同时每段密文使用独立的随机数据密钥,符合密码学安全规范,不会出现一钥泄露全量密文崩溃的问题。
改造后代码
import crypto from "crypto"; const algorithm = "aes-256-ctr"; const KEY_LENGTH = 32; // aes-256要求密钥长度为32字节 interface EncryptResult { iv: string; content: string; encryptedDataKeys: Array<{ keyIv: string; encryptedKey: string; }>; } const encrypt = (text: string, allowedDecryptKeys: string[]): EncryptResult => { // 生成本次加密专用的一次性数据密钥 const dataKey = crypto.randomBytes(KEY_LENGTH); // 用数据密钥加密明文,和原有逻辑完全一致 const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, dataKey, iv); const encryptedContent = Buffer.concat([cipher.update(text), cipher.final()]); // 用所有授权的业务密钥分别加密数据密钥,生成可解密的密钥副本 const encryptedDataKeys = allowedDecryptKeys.map((secretKey) => { if (Buffer.byteLength(secretKey) !== KEY_LENGTH) { throw new Error("密钥长度必须为32字节,适配aes-256算法要求"); } const keyIv = crypto.randomBytes(16); const keyCipher = crypto.createCipheriv(algorithm, Buffer.from(secretKey), keyIv); const encryptedKey = Buffer.concat([keyCipher.update(dataKey), keyCipher.final()]); return { keyIv: keyIv.toString("hex"), encryptedKey: encryptedKey.toString("hex") }; }); return { iv: iv.toString("hex"), content: encryptedContent.toString("hex"), encryptedDataKeys }; }; const decrypt = (hash: EncryptResult, currentKey: string): string | null => { if (!hash.iv || !hash.encryptedDataKeys?.length) return null; if (Buffer.byteLength(currentKey) !== KEY_LENGTH) { throw new Error("密钥长度必须为32字节,适配aes-256算法要求"); } // 尝试用当前密钥解密数据密钥 let dataKey: Buffer | null = null; for (const encKeyItem of hash.encryptedDataKeys) { try { const keyDecipher = crypto.createDecipheriv( algorithm, Buffer.from(currentKey), Buffer.from(encKeyItem.keyIv, "hex") ); dataKey = Buffer.concat([ keyDecipher.update(Buffer.from(encKeyItem.encryptedKey, "hex")), keyDecipher.final() ]); break; } catch (e) { // 当前密钥不匹配该副本,继续尝试下一个 continue; } } // 所有副本都解密失败,说明当前密钥无解密权限 if (!dataKey) return null; // 用解密得到的数据密钥解明文,和原有逻辑一致 const decipher = crypto.createDecipheriv(algorithm, dataKey, Buffer.from(hash.iv, "hex")); const decrypted = Buffer.concat([ decipher.update(Buffer.from(hash.content, "hex")), decipher.final() ]); return decrypted.toString(); }; // 用法示例 const key1 = "vOVH6sdmpNWjRRIqCc7rdxs01lwHzfr3"; const key2 = "xY83kP9sW2qA5dF7gH0jL3zX1cV6bN4m"; const key3 = "pO2iU8yT7rE5wQ9aS4dF6gH3jK1lZ0xC"; // 加密时指定key1、key2为授权解密密钥 const cipherText = encrypt("测试密文内容", [key1, key2]); console.log(decrypt(cipherText, key1)); // 正常输出明文 console.log(decrypt(cipherText, key2)); // 正常输出明文 console.log(decrypt(cipherText, key3)); // 返回null,无权限
其他可选方案说明
- 非对称数字信封:如果需要加密后动态新增解密方,可以使用非对称公钥加密数据密钥,持有对应私钥的主体均可解密,但非对称运算性能远低于对称运算,长文本加密场景下效率差,且如果需要不同解密方持有完全独立的密钥,嵌套逻辑复杂度更高
- Shamir秘密共享:如果需要实现N个密钥中凑够M个即可解密的阈值场景,可以把数据密钥拆成N个分片分发给各解密方,凑够阈值数量的分片即可重组数据密钥解密,该方案实现复杂度高,适合有特殊阈值要求的场景,普通多密钥解密需求不需要用到
内容的提问来源于stack exchange,提问作者fk me
相关产品推荐
相关产品推荐

