微信公众号开发之消息加解密

专栏收录该内容

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 前文描述密文消息体

消息体验证和解密

开发者先验证消息体签名的正确性,验证通过后,再对消息体进行解密。

验证方式:

  1. 开发者计算签名,dev_msg_signature=sha1(sort(Token、timestamp、nonce, msg_encrypt))
  2. 比较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个字符,等号“=”用作后缀填充;


解密方式如下:

  1. aes_msg=Base64_Decode(msg_encrypt)

  2. rand_msg=AES_Decrypt(aes_msg)

  3. 验证尾部

  4. 去掉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

♥ 赞助 ♥

尽管去做,或许最终的结果不尽人意,但你不付出,他不付出,那怎会进步呢?