在 TypeScript 中实现确定性 RSA-OAEP 加密(固定密文输出)

1次阅读

在 TypeScript 中实现确定性 RSA-OAEP 加密(固定密文输出)

本文详解如何在 node-forge 库中通过显式指定 seed 参数,实现与 Go 的 rsa.EncryptOAEP 行为一致的确定性 RSA-OAEP 加密,确保相同明文、密钥、标签和种子始终生成完全相同的密文。

本文详解如何在 node-forge 库中通过显式指定 `seed` 参数,实现与 go 的 `rsa.encryptoaep` 行为一致的确定性 rsa-oaep 加密,确保相同明文、密钥、标签和种子始终生成完全相同的密文。

RSA-OAEP 是一种带随机化填充的标准化加密方案,其安全性依赖于每次加密时使用的不可预测的随机种子(seed)。但某些场景(如可重现的测试、区块链签名预计算、确定性密钥派生辅助等)需要「稳定输出」——即相同输入恒得相同密文。Go 的 crypto/rsa 允许传入自定义 io.Reader 控制种子生成;而 typescript 生态中主流的 node-forge 同样支持该能力,只是需正确构造并传入 seed 字节数组。

关键在于:forge.pki.rsa.PublicKey.encrypt() 的第三个参数(options)支持 seed 字段,类型为 ArrayBuffer | Uint8Array | number[],其长度必须严格匹配所用哈希算法的输出长度(如 SHA-256 为 32 字节)。若未提供 seed,node-forge 将内部调用 forge.random.getBytesSync() 生成真随机数,导致结果不可重现。

以下是一个完整、可运行的示例,使用 HMAC-SHA256 派生确定性种子(与 Go 示例逻辑完全对齐):

// 引入 forge(CDN 或 npm install node-forge) // <script src="https://cdn.jsdelivr.net/npm/node-forge@1.3.1/dist/forge.min.js"></script>  const forge = window.forge || require('node-forge');  // 1. 准备公钥(PEM 格式) const publicKeyPem = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoZ67dtUTLxoXnNEzRBFB mwukEJGC+y69cGgpNbtElQj3m4Aft/7cu9qYbTNguTSnCDt7uovZNb21u1vpZwKH yVgFEGO4SA8RNnjhJt2D7z8RDMWX3saody7jo9TKlrPABLZGo2o8vadW8Dly/v+I d0YDheCkVCoCEeUjQ8koXZhTwhYkGPu+vkdiqX5cUaiVTu1uzt591aO5Vw/hV4DI hFKnOTnYXnpXiwRwtPyYoGTa64yWfi2t0bv99qz0BgDjQjD0civCe8LRXGGhyB1U 1aHjDDGEnulTYJyEqCzNGwBpzEHUjqIOXElFjt55AFGpCHAuyuoXoP3gQvoSj6RC sQIDAQAB -----END PUBLIC KEY-----`;  const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);  // 2. 使用 HMAC-SHA256 派生确定性 seed(与 Go 示例完全一致) const hmac = forge.hmac.create(); hmac.start('sha256', 'seed'); // key = "seed" hmac.update(''.repeat(32)); // message = 32 bytes of 0x00 const seedBytes = hmac.digest().getBytes(); // Uint8Array of length 32  // 3. 执行确定性 RSA-OAEP 加密 const plaintext = 'sai'; const label = 'label';  const encryptedBytes = publicKey.encrypt(plaintext, 'RSA-OAEP', {   md: forge.md.sha256.create(), // OAEP 主哈希   mgf1: {     md: forge.md.sha256.create() // MGF1 掩码生成函数哈希   },   label,   seed: seedBytes // ⚠️ 核心:显式传入确定性 seed });  // 输出十六进制密文(可用于比对 Go 结果) console.log(forge.util.bytesToHex(encryptedBytes)); // → "7eb92c8cde81e2495e2835b19d24b00d1424394adfe3164af0822d5dd0d1b286..."

验证要点

  • 种子长度必须为 32 字节(SHA-256 输出),否则 encrypt() 会抛出错误;
  • label 必须为字符串(非 Uint8Array),且内容需与 Go 端完全一致(包括编码);
  • OAEP 和 MGF1 使用的哈希算法必须匹配(本例均为 SHA-256);
  • 使用 forge.util.bytesToHex() 而非 encode64() 可避免 Base64 填充差异,便于跨语言十六进制比对。

⚠️ 重要安全提醒(务必阅读)
RFC 8017 明确规定,RSA-OAEP 的安全性强依赖于种子的随机性。禁用随机性将使加密退化为确定性模式,可能遭受选择明文攻击(CPA)、重放攻击或密文碰撞分析。本文仅适用于明确接受安全降级的封闭场景(如单元测试、离线密钥推导辅助)。生产环境请始终使用真随机种子,并优先考虑更现代的密钥封装机制(如 HKDF + AES-GCM)。

? 最佳实践建议

  • 若需“可控重复性”,优先在应用层设计(例如:对输入加唯一 nonce 后哈希,再加密哈希值);
  • 避免在传输层直接暴露确定性密文;
  • 在代码中添加清晰注释,标明 // DETERMINISTIC MODE — NOT FOR PRODUCTION;
  • 定期审计此类逻辑,确保未意外泄露至用户可控输入路径。

text=ZqhQzanResources