Hi I'm Shendi
微信公众号消息加解密
微信文档
微信公众平台提供了c++, php, java, python, c# 5种语言的示例代码,我开发微信公众号使用的Node.js,于是需要查阅上方文档进行代码编写
密文示例如下
<xml>
<ToUserName><![CDATA[toUser]]</ToUserName>
<Encrypt><![CDATA[msg_encrypt]]</Encrypt>
</xml>
xml中Encrypt的内容即是密文
验证消息合法性
为了验证消息体的合法性,开放平台新增消息体签名,开发者可用以验证消息体的真实性,并对验证通过的消息体进行解密。具体做法如下:在微信服务器向公众号插件推送消息时,将会在其消息接收URL(创建时填写)上增加参数:msg_signature msg_signature=sha1(sort(Token、timestamp、nonce, msg_encrypt))
这部分与服务器配置验证类似
参数 | 描述 |
---|---|
Token | 微信开放平台上,服务方设置的接收消息的校验token |
timestamp | URL上原有参数,时间戳 |
nonce | URL上原有参数,随机数 |
msg_encrypt | 前文描述密文消息体 |
消息体验证和解密
开发者先验证消息体签名的正确性,验证通过后,再对消息体进行解密。
验证方式:
- 开发者计算签名,dev_msg_signature=sha1(sort(Token、timestamp、nonce, msg_encrypt))
- 比较dev_msg_signature和URL上带的msg_signature是否相等,相等则表示验证通过
因要拿到密文,所以需要解析XML,我使用的nodejs,解析xml可以参考这篇文章:https://sdpro.top/blog/html/article/1099.html
代码如下
var xmlreader = require('xmlreader');
// 解析xml字符串
xmlreader.read(readData, function (err, xmlData) {
let encData = xml.Encrypt,
timestamp = req.query.timestamp,
nonce = req.query.nonce,
msgSignature = req.query.msg_signature;
res.statusCode = 404;
if (encData != null && timestamp != null && nonce != null && msgSignature != null) {
var data = [encData, timestamp, nonce, "服务器配置的Token"].sort().join("");
var sha1 = crypto.createHash("sha1");
if (msgSignature == sha1.update(data).digest("hex")) {
// 校验成功
res.statusCode = 200;
}
}
if (res.statusCode == 404) {
// 校验失败
res.send();
return;
}
});
消息加解密
AES密钥=Base64_Decode(EncodingAESKey + “=”),EncodingAESKey尾部填充一个字符的“=”, 用Base64_Decode生成32个字节的AESKey
AES采用CBC模式,秘钥长度为32个字节(256位),数据采用PKCS#7填充 ; PKCS#7:K为秘钥字节数(采用32),buf为待加密的内容,N为其字节数。Buf 需要被填充为K的整数倍。在buf的尾部填充(K-N%K)个字节,每个字节的内容 是(K- N%K)
尾部填充 | |
---|---|
01 | if ( N%K==(K-1)) |
0202 | if ( N%K==(K-2)) |
030303 | if ( N%K==(K-3)) |
... | ... |
KK....KK (K个字节) | if ( N%K==0) |
数据填充代码如下
/**
* PKCS#7填充
* @param {Buffer} buf 数据
*/
PKCS7Encode(buf) {
// 填充的字节 (K-N%K) k=32,buf为待加密的内容,N为其字节数
let padLen = 32 - (buf.length % 32);
let fillByte = padLen;
let padBuf = Buffer.alloc(padLen, fillByte);
return Buffer.concat([buf, padBuf]);
}
BASE64采用MIME格式,字符包括大小写字母各26个,加上10个数字,和加号“+”,斜杠“/”,一共64个字符,等号“=”用作后缀填充;
解密方式如下:
-
aes_msg=Base64_Decode(msg_encrypt)
-
rand_msg=AES_Decrypt(aes_msg)
-
验证尾部
-
去掉rand_msg头部的16个随机字节,4个字节的msg_len,和尾部的
加密消息的格式
<xml>
<Encrypt></Encrypt>
<MsgSignature></MsgSignature>
<TimeStamp></TimeStamp>
<Nonce></Nonce>
</xml>
其中,msg_encrypt=Base64_Encode(AES_Encrypt [random(16B)+ msg_len(4B) + msg + ])
random(16B)为16字节的随机字符串;msg_len为msg长度,占4个字节(网络字节序);
AESKey =Base64_Decode(EncodingAESKey + “=”),32个字节msg_signature=sha1(sort(Token、timestamp、nonce, msg_encrypt))timestamp、nonce回填请求中的值即可。
直接上代码,封装的代码如下
/**
* 微信消息加解密
* @author 砷碲(sdpro.top)
*/
class WxEncrypt {
/** 加密算法 */
static ALGORITHM = "aes-256-cbc";
/** 消息大小,字节 */
static MSGSIZE = 4;
/** 随机数大小,字节 */
static RANSIZE = 16;
// AES 密钥
static key = Buffer.from(encodingAESKey + '=', "base64");
// 向量为密钥的前16字节
static iv = WxEncrypt.key.slice(0, 16);
/**
* 用于验证的签名,dev_msg_signature=sha1(sort([元素列表]))
* @param list 用于签名的数组列表,一般为[encData, timestamp, nonce, "配置的Token"]
*/
static signature(list) {
return crypto.createHash("sha1").update(list.sort().join("")).digest("hex");
}
/** 加密 */
static encode(data) {
// msg_encrypt=Base64_Encode(AES_Encrypt [random(16B)+ msg_len(4B) + msg + ])
let ran = crypto.randomBytes(WxEncrypt.RANSIZE);
let msg = Buffer.alloc(WxEncrypt.MSGSIZE);
msg.writeUInt32BE(Buffer.byteLength(data), 0);
let buf = Buffer.concat([ran, msg, Buffer.from(data), Buffer.from(appId)]);
let cipher = crypto.createCipheriv(WxEncrypt.ALGORITHM, WxEncrypt.key, WxEncrypt.iv);
cipher.setAutoPadding(false);
buf = WxEncrypt.encodePKCS7(buf);
return Buffer.concat([cipher.update(buf), cipher.final()]).toString('base64');
}
/** 解密 */
static decode(data) {
let buf = Buffer.from(data, 'base64');
let decipher = crypto.createDecipheriv(WxEncrypt.ALGORITHM, WxEncrypt.key, WxEncrypt.iv);
// 禁用默认的数据填充方式
decipher.setAutoPadding(false);
// 解密,去除填充的数据
let dec = WxEncrypt.decodePKCS7(Buffer.concat([decipher.update(buf), decipher.final()]));
// 拿到具体消息
let startPos = WxEncrypt.RANSIZE + WxEncrypt.MSGSIZE;
return dec.slice(
startPos,
startPos + dec.readUInt32BE(WxEncrypt.RANSIZE)).toString();
}
/**
* PKCS#7填充还原
* @param {Buffer} buf 数据
*/
static decodePKCS7(buf) {
let padLen = buf[buf.length - 1];
return buf.slice(0, buf.length - padLen);
}
/**
* PKCS#7填充
* @param {Buffer} buf 数据
*/
static encodePKCS7(buf) {
// 填充的字节 (K-N%K) k=32,buf为待加密的内容,N为其字节数
let padLen = 32 - (buf.length % 32);
let fillByte = padLen;
let padBuf = Buffer.alloc(padLen, fillByte);
return Buffer.concat([buf, padBuf]);
}
}
使用 WxEncrypt.signature([timestamp,nonce,...])
获取验证数据
使用 WxEncrypt.encode(data)
加密 data数据
使用 WxEncrypt.decode(data)
解密 data数据
END
本文链接:https://sdpro.top/blog/html/article/1100.html♥ 赞助 ♥
尽管去做,或许最终的结果不尽人意,但你不付出,他不付出,那怎会进步呢?