总算通过了,在网上找了很多人的实现方法,都有这样那样的问题。本人对加密完全是小白一个,微信官方的加密解密技术方案看了很久才勉强弄明白,总结一下需要注意的地方。
- 数据使用
PKCS#7
填充,填充规则看文档。通俗一点的解释是你用来加密的buf
必须是 32 位的整数倍,如果加密前的buf
长度不是整数倍怎么办呢?就要给它补上,补(32 - N%32)
个(32-N%32)
, 比如你长度是 30,后面就要2个02,29就是补3个 03。这里有个问题是 crypto 模块默认是自动填充的,而且填充的内容规则不同,需要自己来实现填充规则,而且要设置setAutoPadding(false)
。下面代码有实现,可以参考。 - 解密格式为 'aes-256-cbc' ,官方说法是‘AES采用CBC模式,秘钥长度为32个字节(256位)’,琢磨了很久。用通俗一点的说法是,
- 下面代码中,消息加密的方法,传入的参数是明文回复的消息文本
<xml>...</xml>
,返回的是一段字符串文本,需要自己包装,最后回复给用户的内容是<xml><ToUserName><![CDATA[toUser]]</ToUserName> <Encrypt><![CDATA[ 加密方法返回的字符串文本 ]]</Encrypt> </xml>
,解密的方法则相反,传入的是加密字符串文本,返回的是加密前的xml
消息文本,和官方文档中的消息格式说明是一样的。
'use strict'
/**
* WxCrypt 微信消息加解密的方法
* @return new WxCrypt(wxConfig)
* 返回一个单例即可,不需要重复引用
*/
const crypto = require('crypto');
const wxConfig = {
// 传入配置信息
token: 'xxx',
appid: 'xxx',
msg_signature: 'xxx',
encodingAESKey: 'xxx'
}
class WxCrypt {
constructor(opts){
//初始化需要用到的属性
this.token = opts.token;
this.appid = opts.appid;
this.msg_signature = opts.msg_signature; //query 传进来签名
this.aesKey = new Buffer(opts.encodingAESKey + '=', 'base64');
this.IV = this.aesKey.slice(0, 16)
}
encrypt (xmlMsg) {
/*
*@params String xmlMsg 格式化后的 xml 字符串
*@return String 加密后的字符串 填入到 Encrypt 节点中
* 参照官方文档 需要返回一个buf: 随机16字节 + xmlMsg.length(4字节)+xmlMsg+appid。
* buf的字节长度需要填充到 32的整数,填充长度为 32-buf.length%32, 每一个字节为 32-buf.length%32
*/
let random16 = crypto.pseudoRandomBytes(16);
let msg = new Buffer(xmlMsg);
let msgLength = new Buffer(4);
msgLength.writeUInt32BE(msg.length, 0);
let corpId = new Buffer(this.appid);
let raw_msg = Buffer.concat([random16,msgLength,msg ,corpId]);
let cipher = crypto.createCipheriv('aes-256-cbc', this.aesKey, this.IV);
cipher.setAutoPadding(false);//重要,autopadding填充的内容无法正常解密
raw_msg = this.PKCS7Encode(raw_msg);
let cipheredMsg = Buffer.concat([cipher.update(/*encoded*/raw_msg), cipher.final()]);
return cipheredMsg.toString('base64');
}
decrypt(text) {
/*
*@params String text 需要解密的字段(Encrypt节点中的内容)
* @return String msg_content 返回消息内容(xml字符串)
*/
let plain_text;
let decipher = crypto.Decipheriv('aes-256-cbc', this.aesKey, this.IV)
// crypto.Decipheriv == crypto.createDecipheriv 两个方法是一样的
decipher.setAutoPadding(false);//重要
let decipheredBuff = Buffer.concat([decipher.update(text, 'base64'), decipher.final()])
decipheredBuff = this.PKCS7Decode(decipheredBuff)
let len_netOrder_corpid = decipheredBuff.slice(16)
//切割掉16个随机字符,剩余为 (4字节的 msg_len) + msg_content(长度为 msg_len ) + msg_appId
let msg_len = len_netOrder_corpid.slice(0, 4).readUInt32BE(0)
let msg_content = len_netOrder_corpid.slice(4, msg_len+4).toString('utf-8')
// let msg_appId =len_netOrder_corpid.slice(msg_len+4).toString('utf-8')
return msg_content
}
PKCS7Decode(buff) {
/*
*去除尾部自动填充的内容
*/
let padContent = buff[length -1]
if(padContent<1 || padContent >32){
padContent = 0
}
let padLen = padContent;//根据填充规则,填充长度 = 填充内容,这一步赋值可以省略
return buff.slice(0, buff.length - padLen)
}
PKCS7Encode(buff) {
let blockSize = 32;
let needPadLen = 32 - buff.length % 32
if( needPadLen == 0) {
needPadLen = blockSize
}
let pad = new Buffer(needPadLen)
pad.fill(needPadLen)
let newBuff = Buffer.concat([buff, pad])
return newBuff
}
}
var wxCrypt = new WxCrypt(wxConfig);
module.exports = wxCrypt;