node 后端和浏览器前端,有关 RSA 非对称加密的完整实践, 前后端匹配的代码演示

前言

前天,写了一篇文章《我设计的一个安全的 web 系统用户密码管理流程》,里面提到了有关非对称加密 RSA 的一些事情。思想归思想,实践要重于理论,所以我想趁周末,来完成这个时间。

结果发现现实比理想要困难许多,这事儿折腾了我昨儿一天,今天,终于完成了。

主要困难点有如下:

  1. 之前我们的 RSA 一般使用 pkcs1 格式。但是在最新的 NODE v22 版本中,这种格式被认为是不安全的,所以要改。(或许之前的版本就更新了,这里我没去查升级日志,只确定 v22+ 肯定是不行)
  2. 之前我们前端代码中经常使用的 JSEncrypt 库已经好几年不更新了,对最新的规范支持不完整。
  3. 我一直使用的 1024 的秘钥,结果这里也出现了问题。

也就是说,我之前代码跑得好好的,经过这么一升级,代码烂了!!!

中间我踩过的坑太多了,字字泣血,这里只展示最终成果吧。

生成秘钥

bash 复制代码
# 生成私钥
openssl genrsa -out src/config/key/rsa_private_key.pem 2048
# 生成公钥
openssl rsa -in src/config/key/rsa_private_key.pem -pubout -out src/config/key/rsa_public_key.pem

NODE 后端

在后端时间库的选择上,我一开始选择的是 node-rsa 库,但是在不断的调整中,最后选择了 node-forge 库。

但,我并不确定 node-rsa 库是有问题的,因为最终我发现我的问题并不是后端的问题,而是我前端的问题。以下代码是可用的,如果我等下还有心情,我再尝试一下 node-rsa

js 复制代码
import { readFile } from 'node:fs/promises'
import { RSA_KEY } from '@/config'
import * as forge from 'node-forge'

const { RSA_PRIVATE_KEY_PATH, RSA_PUBLIC_KEY_PATH } = RSA_KEY

// 公钥加密方法
export const encrypt = (str: string) => {
  return new Promise((resolve, reject) => {
    readFile(RSA_PUBLIC_KEY_PATH, 'utf-8').then((data) => {
      try {
        const publicK = forge.pki.publicKeyFromPem(data)
        const res = publicK.encrypt(str, 'RSA-OAEP', {
          md: forge.md.sha256.create(),
          mgf1: forge.md.sha256.create(),
        })
        const Base64Res = forge.util.encode64(res)
        resolve(Base64Res)
      } catch (e) {
        reject(new Error(`RSA加密失败: ${e.message || e}`))
      }
    })
  })
}

// 私钥解密方法
export const decrypt = (str: string) => {
  return new Promise((resolve, reject) => {
    if (!str || typeof str !== 'string') {
      return reject(new Error('RSA解密失败: 无效的输入'))
    }

    readFile(RSA_PRIVATE_KEY_PATH, 'utf-8').then((data) => {
      try {
        const privateK = forge.pki.privateKeyFromPem(data)
        const base64Str = forge.util.decode64(str)
        const res = privateK.decrypt(base64Str, 'RSA-OAEP', {
          md: forge.md.sha256.create(),
          mgf1: forge.md.sha256.create(),
        })
        resolve(res)
      } catch (e) {
        reject(new Error(`RSA解密失败: ${e.message || e}`))
      }
    })
  })
}

浏览器前端

在JSEncrypt 确认不可用的前提下,我找了很多资料,遇到了很多坑。经过不断的研究,我决定,不使用任何库类,而是直接使用现代浏览器支持的 Web Crypto API 特性。

如上图所示,Web Crypto API 已经被广泛的现代浏览器所支持了。估计,这也是为什么 JSEncrypt 这个库不更新了的原因。

完整代码如下:

js 复制代码
async function importPublicKey(pem: string) {
  const pemHeader = "-----BEGIN PUBLIC KEY-----"
  const pemFooter = "-----END PUBLIC KEY-----"
  const pemContents = pem
    .replace(pemHeader, "")
    .replace(pemFooter, "")
    .replace(/\s+/g, "")

  const binaryDer = base64ToArrayBuffer(pemContents)

  return await window.crypto.subtle.importKey(
    "spki",
    binaryDer,
    {
      name: "RSA-OAEP",
      hash: { name: "SHA-256" }
    },
    true,
    ["encrypt"]
  )
}

function base64ToArrayBuffer(base64: string) {
  const binaryString = atob(base64)
  const bytes = new Uint8Array(binaryString.length)
  for (let i = 0; i < binaryString.length; i++) {
    bytes[i] = binaryString.charCodeAt(i)
  }
  return bytes.buffer
}

export async function encrypt(publicKeyStr: string, message: string) {
  const importedPublicKey = await importPublicKey(publicKeyStr)

  const encrypted = await crypto.subtle.encrypt(
    { name: "RSA-OAEP" },
    importedPublicKey,
    new TextEncoder().encode(message)
  )

  // 将加密后的二进制数据转换为 base64 字符串
  return btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)))
}

主要关键点是,从接口拿到后端的公钥后,需要去头去尾,转换一下格式,才能被使用。

好的,以上代码都是在 node v22 环境下验证可用的,预计短期几年内应该不会有太大变化。

后记

其实,以上代码写出来还好,我遇到把我卡住的关键点是我服务端使用的是 1024 的秘钥。而使用这个秘钥,用来加密 sha256 的 hash 值的时候一直报错。我一直以为是我前后端代码没搞好的问题。但是,经过不断的调试猛然发现,其在加密短字符串的时候是没问题的,而 sha256 的 hash 值是 64 位的长度。

在我抓耳挠腮的时候,我福至心灵,把服务端的秘钥换成 2048 的秘钥后,所有问题就都解决了,顿时间,拨云见日。

最终,我查看资料,知道 使用 ‌SHA-256‌ 时,1024 位 RSA-OAEP 加密支持单次最大 ‌62 字节明文‌。这里,多出了2个字节。

好吧,我今天可以睡个好觉了!祝各位看官,安康!

相关推荐
万少16 分钟前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL21 分钟前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl0237 分钟前
java web5(黑马)
java·开发语言·前端
Amy.Wang38 分钟前
前端如何实现电子签名
前端·javascript·html5
今天又在摸鱼41 分钟前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿43 分钟前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再1 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
jingling5551 小时前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架
拾光拾趣录1 小时前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css
莫空00001 小时前
深入理解JavaScript属性描述符:从数据属性到存取器属性
前端·面试