企业内部开发 附录 加解密方案说明
加解密方案说明
最后更新:2023/09/01
可联系渠道经理采购或代理智慧硬件接口已支持设备 联系渠道经理
加解密方案说明
最后更新:2023/09/01

目录

  • 概述
  • 使用已有库
  •       初始化加解密类
  •       验证URL函数
  •       解密函数
  •       加密函数
  • 原理详解
  •       消息体签名校验
  •       明文msg的加密过程
  •       密文解密得到msg的过程
  •       举例说明
  • 附注:ReceiveId 含义
  • 概述

    企业微信在推送消息给企业时,会对消息内容做AES加密,以XML格式POST到企业应用的URL上。
    企业在被动响应时,也需要对数据加密,以XML格式返回给企业微信。
    本章节即是对加解密方法的说明。
    阅读本章节前,需要了解以下术语:

    • msg_signature: 消息签名,用于验证请求是否来自企业微信(防止攻击者伪造)。
    • EncodingAESKey:用于消息体的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取,是AESKey的Base64编码。解码后即为32字节长的AESKey

      AESKey=Base64_Decode(EncodingAESKey + “=”)
    • AESKey:AES算法的密钥,长度为32字节。
      AES采用CBC模式,数据采用PKCS#7填充至32字节的倍数;IV初始向量大小为16字节,取AESKey前16字节,详见:http://tools.ietf.org/html/rfc2315
    • msg:为消息体明文,格式为XML
    • msg_encrypt:明文消息msg加密处理后的Base64编码。

    使用已有库

    鉴于加解密算法相对复杂,企业微信提供了算法库。
    目前已有c++/python/php/java/golang/c#等语言版本。均提供了解密、加密、验证URL三个接口,企业可根据自身需要下载,下载地址

    使用现有库,用户不必细究加解密原理。对于找不到相应语言库的用户,请阅读后文原理详解自行实现。欢迎大家分享~

    以c++为例,使用示例见下载的文件夹中的Sample.cpp, 此处做简单说明。

    初始化加解密类

    回调xml示例:

    WXBizMsgCrypt wxcpt(sToken,sEncodingAESKey,sReceiveId);

    要求传参数sToken,sEncodingAESKey,sReceiveId。
    sToken,sEncodingAESKey即设置接收消息的参数章节所述配置的Token、EncodingAESKey。
    特别注意, sReceiveId 在不同场景下有不同含义,见附注

    验证URL函数

    本函数实现:

    1. 签名校验
    2. 解密数据包,得到明文消息内容
    int VerifyURL(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sEchoStr, string &sReplyEchoStr);
    • 参数说明
    参数必须说明
    sMsgSignature从接收消息的URL中获取的msg_signature参数
    sTimeStamp从接收消息的URL中获取的timestamp参数
    sNonce从接收消息的URL中获取的nonce参数
    sEchoStr从接收消息的URL中获取的echostr参数。注意,此参数必须是urldecode后的值
    sReplyEchoStr解密后的明文消息内容,用于回包。注意,必须原样返回,不要做加引号或其它处理

     

    解密函数

    本函数实现:

    1. 签名校验
    2. 解密数据包,得到明文消息结构体
    int DecryptMsg(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sPostData, string &sMsg);
    • 参数说明
    参数必须说明
    sMsgSignature从接收消息的URL中获取的msg_signature参数
    sTimeStamp从接收消息的URL中获取的timestamp参数
    sNonce从接收消息的URL中获取的nonce参数
    sPostData从接收消息的URL中获取的整个post数据
    sMsg用于返回解密后的msg,以xml组织,参见普通消息格式事件消息格式

     

    加密函数

    本函数实现:

    1. 加密明文消息结构体
    2. 生成签名
    3. 构造被动响应包
    int EncryptMsg(const string &sReplyMsg, const string &sTimeStamp, const string &sNonce, string &sEncryptMsg);
    • 参数说明
    参数必须说明
    sReplyMsg返回的消息体原文
    sTimeStamp时间戳,调用方生成
    sNonce随机数,调用方生成
    sEncryptMsg用于返回的密文,以xml组织,参见被动回复消息格式

     

    原理详解

    目前官方已提供了php、python、c++等版本的加解密库,如果开发者需要进行别的语言的开发,需要自行根据加解密原理实现算法。

    消息体签名校验

    为了让企业确认调用来自企业微信,企业微信在回调给接收消息url时会带上消息签名,以参数msg_signature标识,企业需要验证此参数的正确性后再解密。
    验证步骤如下:

    1. 计算签名
      dev_msg_signature=sha1(sort(token、timestamp、nonce、msg_encrypt))。


      sort的含义是将参数值按照字母字典排序,然后从小到大拼接成一个字符串
      sha1处理结果要编码为可见字符,编码的方式是把每字节散列值打印为%02x(即16进制,C printf语法)格式,全部小写
    2. 比较dev_msg_signature和msg_signature是否相等,相等则表示验证通过
    3. 在被动响应消息时,企业同样需要用如上方法生成签名并传给企业微信

     

    明文msg的加密过程

    1. 拼接明文字符串
      rand_msg = random(16B) + msg_len(4B) + msg + receiveid


      明文字符串由16个字节的随机字符串、4个字节的msg长度、明文msg和receiveid拼接组成。其中msg_len为msg的字节数,网络字节序;sReceiveId 在不同场景下有不同含义,见附注
      明文字符串
    2. 对明文字符串加密并Base64编码
      msg_encrypt = Base64_Encode(AES_Encrypt(rand_msg))


      将明文字符串AESKey加密后,再进行Base64编码,即获得密文msg_encrypt。

    密文解密得到msg的过程

    1. 对密文BASE64解码

      aes_msg=Base64_Decode(msg_encrypt)

    2. 使用AESKey做AES-256-CBC解密

      rand_msg=AES_Decrypt(aes_msg)

    3. 去掉rand_msg头部的16个随机字节和4个字节的msg_len,截取msg_len长度的部分即为msg,剩下的为尾部的receiveid
    4. 验证解密后的receiveid、msg_len。注意,receiveid在不同场景含义不同。

    举例说明

    假设在服务商管理端为某个套件有如下配置参数:

    corpId = "wx5823bf96d3bd56c7"
    token = "QDG6eK"
    encodingAesKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C"

    收到来自企业微信的回调为:
    xml请求示例:

    POST /cgi-bin/wxpush?msg_signature=477715d11cdb4164915debcba66cb864d751f3e6&timestamp=1409659813&nonce=1372623149 HTTP/1.1
    Host: qy.weixin.qq.com
    Content-Length: 603
    <xml>
    <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>
    <Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt>
    <AgentID><![CDATA[218]]></AgentID>
    </xml>

    第一步:准备相关参数

    AESKey = Base64_Decode(EncodingAESKey + "=")
    signature = "477715d11cdb4164915debcba66cb864d751f3e6";
    timestamps = "1409659813";
    nonce = "1372623149";
    msg_encrypt = "RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==";

    第二步:校验签名

    1. token、timestamp、nonce、msg_encrypt 这四个参数按照字典序排序

      "1372623149"
      "1409659813"
      "QDG6eK"
      "RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q=="

    2. 拼接为一个字符串

      sort_str = "13726231491409659813QDG6eKRypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q=="

    3. 对该字符串进行sha1计算得到签名

      signature = sha1(sort_str) = "477715d11cdb4164915debcba66cb864d751f3e6"

    4. 对比从URL得到的签名,发现两者一致,签名通过,说明没被篡改,是安全的

    第三步: 解密消息

    1. 对密文base64解码

      aes_msg = base64_decode(msg_encrypt)

    2. 使用AESKey做AES解密(注意,不是EncodingAESKey

      rand_msg = aes_decrypt(aes_msg, AESKey)

    3. 去掉rand_msg头部的16个随机字节和4个字节的msg_len,截取msg_len长度的部分即为msg,剩下的为尾部的receiveid
      下面为类似python的伪代码

      content = rand_msg[16:]  # 去掉前16随机字节
      msg_len = str_to_uint(content[0:4]) # 取出4字节的msg_len
      msg = content[4:msg_len+4] # 截取msg_len 长度的msg
      receiveid = content[msg_len+4:] = "wx5823bf96d3bd56c7" # 剩余字节为receiveid


      对于回调xml解密后得到明文为:

      <xml>
          <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName>
          <FromUserName><![CDATA[mycreate]]></FromUserName>
          <CreateTime>1409659813</CreateTime>
          <MsgType><![CDATA[text]]></MsgType>
          <Content><![CDATA[hello]]></Content>
          <MsgId>4561255354251345929</MsgId>
          <AgentID>218</AgentID>
      </xml>


      根据明文中的MsgType可知,此为应用消息回调,因此receiveid应该为corpid,对比receiveid与corpid是否一致。

    附注:ReceiveId 含义

    加解密库里,ReceiveId 在各个场景的含义不同:

    1. 企业应用的回调,表示corpid
    2. 第三方事件的回调,表示suiteid
    3. 个人主体的第三方应用的回调,ReceiveId是一个空字符串

    下一篇
    访问频率限制