为了防止小程序的接口被盗用,用了小程序的加密网络通道,前端使用cryptojs加密,报错Native crypto module could not be used to get secure random number.
结论
- 修改
package.json,版本降级,"crypto-js": "3.1.9-1", - 或者改成使用
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解密
- 先获取前端加密用的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))
}