小程序加密网络通道,小程序aes加密

为了防止小程序的接口被盗用,用了小程序的加密网络通道,前端使用cryptojs加密,报错Native crypto module could not be used to get secure random number.

结论

  1. 修改package.json,版本降级,"crypto-js": "3.1.9-1",
  2. 或者改成使用https://github.com/ricmoo/aes-js

原因

刚开始用的官方文档推荐的https://github.com/ricmoo/aes-js ,结果aes-cbc模式的还要自己实现Pkcs7填充,

改用了https://github.com/brix/crypto-js这个包

这个包的新版本web开发时是没问题的,但是小程序环境会报Native crypto module could not be used to get secure random number.这个错误

var CryptoJS = CryptoJS || (function (Math, undefined) {

  var crypto;

  // Native crypto from window (Browser)
  if (typeof window !== 'undefined' && window.crypto) {
    crypto = window.crypto;
  }

  // Native crypto in web worker (Browser)
  if (typeof self !== 'undefined' && self.crypto) {
    crypto = self.crypto;
  }

  // Native crypto from worker
  if (typeof globalThis !== 'undefined' && globalThis.crypto) {
    crypto = globalThis.crypto;
  }

  // Native (experimental IE 11) crypto from window (Browser)
  if (!crypto && typeof window !== 'undefined' && window.msCrypto) {
    crypto = window.msCrypto;
  }

  // Native crypto from global (NodeJS)
  if (!crypto && typeof global !== 'undefined' && global.crypto) {
    crypto = global.crypto;
  }

  // Native crypto import via require (NodeJS)
  if (!crypto && typeof require === 'function') {
    try {
      crypto = require('crypto');
    } catch (err) {
    }
  }

  /*
   * Cryptographically secure pseudorandom number generator
   *
   * As Math.random() is cryptographically not safe to use
   */
  var cryptoSecureRandomInt = function () {
    if (crypto) {
      // Use getRandomValues method (Browser)
      if (typeof crypto.getRandomValues === 'function') {
        try {
          return crypto.getRandomValues(new Uint32Array(1))[0];
        } catch (err) {
        }
      }

      // Use randomBytes method (NodeJS)
      if (typeof crypto.randomBytes === 'function') {
        try {
          return crypto.randomBytes(4).readInt32LE();
        } catch (err) {
        }
      }
    }

    throw new Error('Native crypto module could not be used to get secure random number.');
  };
}(Math));

正常的浏览器环境会实现Crypto接口, 里面有个getRandomValues 方法生成加密用的随机数,crypto-js通过从3.2.0开始新增了cryptoSecureRandomInt方法,通过window.crypto.getRandomValues来调用, 小程序虽然实现了这个方法,但是要通过wx.getUserCryptoManager().getRandomValues 来调用,crypto-js也没有提供设置getRandomValues属性的方法.因此出现了开头的错误.那我们就退回之前的好了。

示例代码

小程序tarojs加密发送数据


const userCryptoManager = Taro.getUserCryptoManager()
const str = "data to be sent"

userCryptoManager.getLatestUserKey({
  success: (res: Taro.UserCryptoManager.getLatestUserKey.SuccessCallbackResult) => {
    
    const {encryptKey, iv, version, expireTime} = res
    
    const data = Crypto.AES.encrypt(str, Crypto.enc.Utf8.parse(encryptKey), {iv: Crypto.enc.Utf8.parse(iv)}).toString()

    Taro.request({
      url: 'https://xx.x.com/api/v1/check',
      data: {version, data},
      header: {
        'content-type': 'application/x-www-form-urlencoded'
      },
      success: function (res) {

      }
    })
  }
})

服务端golang解密

  1. 先获取前端加密用的key

用到的包 github.com/ArtisanCloud/PowerWeChat,github.com/tidwall/gjson

func AesDecryptByCBC(encrypted, key string, iv ...string) string {
	// 判断key长度
	keyLenMap := map[int]struct{}{16: {}, 24: {}, 32: {}}
	if _, ok := keyLenMap[len(key)]; !ok {
		panic("key长度错误")
	}
	// encrypted密文反解base64
	decodeString, _ := base64.StdEncoding.DecodeString(encrypted)
	// key 转[]byte
	keyByte := []byte(key)
	// 创建一个cipher.Block接口。参数key为密钥,长度只能是16、24、32字节
	block, _ := aes.NewCipher(keyByte)
	// 获取秘钥块的长度
	blockSize := block.BlockSize()
	// 选择加密模式
	var blockMode cipher.BlockMode
	if len(iv) > 0 {
		blockMode = cipher.NewCBCDecrypter(block, []byte(iv[0]))
	} else {
		blockMode = cipher.NewCBCDecrypter(block, keyByte[:blockSize])
	}
	// 创建数组,存储解密结果
	decodeResult := make([]byte, 256)
	// 解密
	blockMode.CryptBlocks(decodeResult, decodeString)
	// 解码
	padding := PKCS7UNPadding(decodeResult)
	return string(padding)
}
func AesEncryptByCBC(str, key string, iv ...string) string {
	// 判断key长度
	keyLenMap := map[int]struct{}{16: {}, 24: {}, 32: {}}
	if _, ok := keyLenMap[len(key)]; !ok {
		panic("key长度错误")
	}
	// 待加密字符串转成byte
	originDataByte := []byte(str)
	// 秘钥转成[]byte
	keyByte := []byte(key)
	// 创建一个cipher.Block接口。参数key为密钥,长度只能是16、24、32字节
	block, _ := aes.NewCipher(keyByte)
	// 获取秘钥长度
	blockSize := block.BlockSize()
	// 补码填充
	originDataByte = PKCS7Padding(originDataByte, blockSize)
	// 选用加密模式
	var blockMode cipher.BlockMode
	if len(iv) > 0 {
		blockMode = cipher.NewCBCEncrypter(block, []byte(iv[0]))
	} else {
		blockMode = cipher.NewCBCEncrypter(block, keyByte[:blockSize])
	}
	// 创建数组,存储加密结果
	encrypted := make([]byte, len(originDataByte))
	// 加密
	blockMode.CryptBlocks(encrypted, originDataByte)
	// []byte转成base64
	return base64.StdEncoding.EncodeToString(encrypted)
}
func PKCS7UNPadding(originDataByte []byte) []byte {
	length := len(originDataByte)
	unpadding := int(originDataByte[length-1])
	return originDataByte[:(length - unpadding)]
}
func PKCS7Padding(originByte []byte, blockSize int) []byte {
	// 计算补码长度
	padding := blockSize - len(originByte)%blockSize
	// 生成补码
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
	// 追加补码
	return append(originByte, padText...)
}

func DecryptMsg(openid, sessionKey, msg string) string {
    //从internet.getUserEncryptKey获取最近的key
    
    //构造签名
	h := hmac.New(sha256.New, []byte(sessionKey))
	h.Write([]byte(""))
	sign := hex.EncodeToString(h.Sum(nil))
	var m = make(map[string]interface{})
	params := &object.StringMap{
		"openid":     openid,
		"signature":  sign,
		"sig_method": "hmac_sha256",
	}

    //发起请求
	Mp.Internet.HttpPostJson("wxa/business/getuserencryptkey", nil, params, nil, &m)
	
	//解析微信返回的数据,取最新一条
	keyI := m["key_info_list"].([]interface{})[0]
	firstKey := keyI.(map[string]interface{})
	
	//返回的就是解密的数据字符串 "data to be sent"
	return utils.AesDecryptByCBC(msg, firstKey["encrypt_key"].(string), firstKey["iv"].(string))
}