道路千万条,安全第一条 --- RSA 密文传输

361 阅读4分钟

很久很久以前。。

很久很久以前,我们的项目前后端交互数据都是明文传输的。
直到有一天,有一个甲方来势汹汹的说到:你们这样很不安全啊~

safe.jpg

在项目中添加使用 encryption 加密模块

与后端同事协商后,决定使用RSA加密方式来进行数据交互。并将 RSA 加密封装成独立的 vuex 仓库模块。
在项目中,我们可以根据环境不同来决定是否启用接口加密。并在路由拦截器中通过环境变量的判断来触发 vuexencryption 加密模块。

Encryption 初始化加密流程

1. 在路由拦截器中触发 vuex action 方法 ENCRYPTION_INIT 进行加密初始化;

2. ENCRYPTION_INIT 方法中首先会从本地的 Cookie 中获取加密信息,看近期是否进行过加密初始化工作;

    // ------- cookies/encryption.js --------------
    // 获取 Secret
    export function getSecret() {
      const value = Cookies.get('encrytion-secret')
      return value ? Base64.decode(value) : value
    }

    // 获取 Code
    export function getCode() {
      const value = Cookies.get('encrytion-code')
      return value ? Base64.decode(value) : value
    }

    // 获取 Code 和 Secret
    export function getSecretAndCode() {
      const code = getCode()
      const secret = getSecret()
      return {
        hasCookie: code && secret,
        code,
        secret
      }
    }


    // ------- Encryption.js --------------
    // 初始化AES的加密类
    !state.secretAesEncryptor && commit('INIT_SECRET_AES_ENCRYPTOR')
    const { hasCookie, code, secret } = getSecretAndCode()

    if (hasCookie && !flag) { // cookie 存在的情况下直接存储参数
        if (secret === state.secretAesEncryptor.encrypt(state.appSecret)) return
        // 存储加密信息
        commit('SET_APP_SECRET', state.secretAesEncryptor.decrypt(secret))
        commit('SET_APP_CODE', state.secretAesEncryptor.decrypt(code))
        return
    }

3. 加密方式我们采用的是: AESRSA 两个算法进行加密。第一步就是通过接口先获取服务端的 RSA公钥

4. 通过 node-rsa 插件来生成客户端的 RSA密钥对

    // 生成密钥对
    export function generateRsaKeys() {
      return new Promise((r, j) => {
        try {
          const key = new NODERSA({ b: 1024 }) // 生成1024位的密钥
          key.setOptions({ encryptionScheme: 'pkcs1' })
          const publicDer = key.exportKey('pkcs8-public') // 公钥
          const privateDer = key.exportKey('pkcs8-private') // 私钥

          r({
            PRIVATE_KEY: spliceKeyString(privateDer),
            PUBLIC_KEY: spliceKeyString(publicDer)
          })
        } catch (error) {
          j('密钥对生成失败' + error)
        }
      })
    }
    
    // ------- Encryption.js --------------
    async ENCRYPTION_INIT({ dispatch, commit, state }, flag) {
      // ...
      try {
        await dispatch('GET_SERVER_PUBLICKEY')
        // 生成客户端密钥对
        const { PRIVATE_KEY, PUBLIC_KEY } = await generateRsaKeys()
        commit('SET_CLIENT_PUBLICKEY', PUBLIC_KEY)
        commit('SET_CLIENT_PRIVATEKEY', PRIVATE_KEY)
        // 初始化客户端RSA加密类
        commit('INIT_CLIENT_RSA_ENCRYPTOR')

        // ...
      } catch (error) {
        commit('SET_ENCRYPTION_STATUS', false)
        console.log(error)
      }
    },
    // 获取服务端公钥
    async GET_SERVER_PUBLICKEY({ commit, dispatch }) {
      try {
        const { data: publicKey } = await getSeverPublicKey()
        commit('SET_SERVER_PUBLICKEY', publicKey)
      } catch (error) {
        throw Error('system error: 获取服务端公钥失败')
      }
    },

5. 通过获取到的 服务端RSA公钥 来加密我们生成 客户端RSA公钥,并发送请求传递给后端;

6. 后端通过自己的 服务端RSA私钥 解密后就获取到我们的 客户端RSA公钥 了;

7. 后端会通过我们的 客户端RSA公钥 来加密一段密文并在上一次的接口中返回给我们;

8. 我们使用 服务端RSA私钥 解密并获取到真实的密文信息;

    /**
     * 与后端约定好传参形式
     * 截取客户端公钥首尾字母,并用服务端公钥加密
     * @param {string 客户端公钥} publicKey
     * @returns Object<clientPubKey, enClientPubKey>
     */
    function splitClientPublicKey(publicKey, serverRsaEncryptor) {
      const keysArr = publicKey.split('')
      const clientPubKey = keysArr.splice(1, keysArr.length - 2)
        .join('')
      return {
        clientPubKey,
        enClientPubKey: serverRsaEncryptor.encrypt(keysArr.join(''))
      }
    }
    
    // ------------------------------
    // 获取客户端加密的应用code和secret
    async GET_ClIENT_SECRET({ state, commit, dispatch }) {
      try {
        // 用服务端公钥初始化服务端 RSA 加密类
        !state.serverRsaEncryptor && commit('INIT_SERVER_RSA_ENCRYPTOR')
        const {
          serverPublicKey,
          clientPublicKey,
          serverRsaEncryptor,
          clientRsaEncryptor
        } = state

        const { data } = await getClientSecret({
          servicePubKey: serverPublicKey,
          ...splitClientPublicKey(clientPublicKey, serverRsaEncryptor)
        })
        
        const { s, c } = JSON.parse(clientRsaEncryptor.decrypt(data))

        setCode(state.secretAesEncryptor.encrypt(c))
        setSecret(state.secretAesEncryptor.encrypt(s))

        commit('SET_APP_SECRET', s)
        commit('SET_APP_CODE', c)
        commit('SET_ENCRYPTION_STATUS', true)
      } catch (error) {
        console.log(error)
        throw Error('system error: 获取服务端公钥失败', error)
      }
    }

9. 将密文直接存储到 vuex 仓库中以便后续发送请求时携带密文信息,同时再使用 AES算法 将密文加密后存储到 Cookie 中防止后续重复进行加密初始化工作;

RSA 非对称加密原理

从上面的流程中,已经能看出 RSA 非对称加密的原理了。

  • 前后端分别生成了一对 RSA公钥、私钥

01.jpg

  • 并通过接口交换自己的 公钥

02.jpg

  • 在后续的请求交互中,分别使用对方的 公钥 进行加密发送信息和自己的 私钥 进行解密获取信息

03.jpg

RSA/AES 加密类的封装

  • RsaEncryptor 加/解密 构造类
    // RsaEncryptor 加/解密 构造类
    export class RsaEncryptor {
      constructor(opt) {
        const { PUBLIC_KEY, PRIVATE_KEY } = opt

        this.JSEncrypt = new JSEncrypt()
        this.JSDecrypt = new JSEncrypt()
        this.JSDecrypt.setPrivateKey(PRIVATE_KEY)
        this.JSEncrypt.setPublicKey(PUBLIC_KEY)

        this.PRIVATE_KEY = PRIVATE_KEY
        this.PUBLIC_KEY = PUBLIC_KEY
      }

      // 加密
      encrypt(msg) {
        if (!this.PUBLIC_KEY) throw Error('RsaEncryptor Error: missing PUBLIC_KEY')

        return this.JSEncrypt.encrypt(msg)
      }

      // 解密
      decrypt(data) {
        if (!this.PRIVATE_KEY) throw Error('RsaEncryptor Error: missing PRIVATE_KEY')

        return this.JSDecrypt.decrypt(data)
      }
    }
  • AesEncryptor 加/解密 构造类
    // AesEncryptor 加/解密 构造类
    export class AesEncryptor {
      constructor(key) {
        if (!key) throw Error('AesEncryptor Error: missing key')
        this.key = crypto.enc.Hex.parse(key)
      }

      // 加密
      encrypt(msg) {
        const srcs = crypto.enc.Utf8.parse(msg)
        const encrypted = crypto.AES.encrypt(srcs, this.key, {
          mode: crypto.mode.ECB,
          padding: crypto.pad.Pkcs7
        });
        return encrypted.toString()
      }

      // 解密
      decrypt(data) {
        const decrypt = crypto.AES.decrypt(data, this.key, {
          mode: crypto.mode.ECB,
          padding: crypto.pad.Pkcs7
        })

        return crypto.enc.Utf8.stringify(decrypt).toString()
      }
    }

传送门