WXBizMsgCrypt.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #########################################################################
  4. # Author:
  5. # Created Time:
  6. # File Name: WXBizMsgCrypt.py
  7. # Description: 企业微信回调消息加解密示例代码
  8. #########################################################################
  9. import base64
  10. import string
  11. import random
  12. import hashlib
  13. import time
  14. import struct
  15. from Crypto.Cipher import AES
  16. import xml.etree.cElementTree as ET
  17. """
  18. 关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
  19. 请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
  20. 下载后,按照README中的"Installation"小节的提示进行pycrypto安装。
  21. """
  22. class FormatException(Exception):
  23. pass
  24. def throw_exception(message, exception_class=FormatException):
  25. """抛出异常信息"""
  26. raise exception_class(message)
  27. class SHA1:
  28. """计算企业微信签名的SHA1算法"""
  29. def getSHA1(self, token, timestamp, nonce, encrypt):
  30. """用SHA1算法生成安全签名
  31. @param token: 票据
  32. @param timestamp: 时间戳
  33. @param nonce: 随机字符串
  34. @param encrypt: 密文
  35. @return: 安全签名
  36. """
  37. try:
  38. sortlist = [token, timestamp, nonce, encrypt]
  39. sortlist.sort()
  40. sha = hashlib.sha1()
  41. sha.update("".join(sortlist).encode('utf-8'))
  42. return sha.hexdigest()
  43. except Exception as e:
  44. print(e)
  45. return ""
  46. class XMLParse:
  47. """提供提取消息格式中的密文及生成回复消息格式的接口"""
  48. # xml消息模板
  49. AES_TEXT_RESPONSE_TEMPLATE = """<xml>
  50. <Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
  51. <MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
  52. <TimeStamp>%(timestamp)s</TimeStamp>
  53. <Nonce><![CDATA[%(nonce)s]]></Nonce>
  54. </xml>"""
  55. def extract(self, xmltext):
  56. """提取出xml数据包中的加密消息
  57. @param xmltext: 待提取的xml字符串
  58. @return: 提取出的加密消息字符串
  59. """
  60. try:
  61. xml_tree = ET.fromstring(xmltext)
  62. encrypt = xml_tree.find("Encrypt")
  63. touser_name = xml_tree.find("ToUserName")
  64. return 0, encrypt.text, touser_name.text
  65. except Exception as e:
  66. print(e)
  67. return -40003, None, None
  68. def generate(self, encrypt, signature, timestamp, nonce):
  69. """生成xml消息
  70. @param encrypt: 加密后的消息
  71. @param signature: 安全签名
  72. @param timestamp: 时间戳
  73. @param nonce: 随机字符串
  74. @return: 生成的xml字符串
  75. """
  76. resp_dict = {
  77. 'msg_encrypt': encrypt,
  78. 'msg_signaturet': signature,
  79. 'timestamp': timestamp,
  80. 'nonce': nonce,
  81. }
  82. resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
  83. return resp_xml
  84. class PKCS7Encoder():
  85. """提供基于PKCS7算法的加解密接口"""
  86. block_size = 32
  87. def encode(self, text):
  88. """ 对需要加密的明文进行填充补位
  89. @param text: 需要进行填充补位操作的明文
  90. @return: 补齐明文字符串
  91. """
  92. text_length = len(text)
  93. # 计算需要填充的位数
  94. amount_to_pad = self.block_size - (text_length % self.block_size)
  95. if amount_to_pad == 0:
  96. amount_to_pad = self.block_size
  97. # 处理bytes和str类型的兼容性
  98. if isinstance(text, bytes):
  99. pad = bytes([amount_to_pad] * amount_to_pad)
  100. return text + pad
  101. else:
  102. # 获得补位所用的字符
  103. pad = chr(amount_to_pad)
  104. return text + pad * amount_to_pad
  105. def decode(self, decrypted):
  106. """删除解密后明文的补位字符
  107. @param decrypted: 解密后的明文
  108. @return: 删除补位字符后的明文
  109. """
  110. # 兼容Python 3.6:处理bytes和str类型
  111. if isinstance(decrypted[-1], int):
  112. pad = decrypted[-1] # 在Python 3中,bytes的索引返回int
  113. else:
  114. pad = ord(decrypted[-1]) # 在Python 2中或str类型时需要ord()
  115. if pad < 1 or pad > 32:
  116. pad = 0
  117. return decrypted[:-pad]
  118. class Prpcrypt(object):
  119. """提供接收和推送给企业微信消息的加解密接口"""
  120. def __init__(self, key):
  121. # self.key = base64.b64decode(key+"=")
  122. self.key = key
  123. # 设置加解密模式为AES的CBC模式
  124. self.mode = AES.MODE_CBC
  125. def encrypt(self, text, receiveid):
  126. """对明文进行加密
  127. @param text: 需要加密的明文
  128. @return: 加密得到的字符串
  129. """
  130. # 16位随机字符串添加到明文开头
  131. text = text.encode('utf-8')
  132. # 使用大端序(网络字节序)打包长度字段,与企业微信标准保持一致
  133. text = self.get_random_str() + struct.pack(">I", len(text)) + text + receiveid.encode('utf-8')
  134. # 使用自定义的填充方式对明文进行补位填充
  135. pkcs7 = PKCS7Encoder()
  136. text = pkcs7.encode(text)
  137. # 加密
  138. cryptor = AES.new(self.key, self.mode, self.key[:16])
  139. try:
  140. ciphertext = cryptor.encrypt(text)
  141. # 使用BASE64对加密后的字符串进行编码
  142. return base64.b64encode(ciphertext)
  143. except Exception as e:
  144. print(e)
  145. return None
  146. def decrypt(self, text, receiveid, verify_receiveid=True):
  147. """对解密后的明文进行补位删除
  148. @param text: 密文
  149. @param receiveid: 接收者ID
  150. @param verify_receiveid: 是否验证receiveid,URL验证时设为False
  151. @return: 删除填充补位后的明文
  152. """
  153. try:
  154. cryptor = AES.new(self.key, self.mode, self.key[:16])
  155. # 使用BASE64对密文进行解码,然后AES-CBC解密
  156. plain_text = cryptor.decrypt(base64.b64decode(text))
  157. except Exception as e:
  158. print(e)
  159. return None
  160. try:
  161. # 去除补位字符
  162. pkcs7 = PKCS7Encoder()
  163. plain_text = pkcs7.decode(plain_text)
  164. # 去除16位随机字符串
  165. content = plain_text[16:]
  166. # 使用大端序(网络字节序)解析长度字段,这是企业微信使用的标准
  167. xml_len = struct.unpack(">I", content[:4])[0]
  168. xml_content = content[4:xml_len + 4]
  169. from_receiveid = content[xml_len + 4:]
  170. except Exception as e:
  171. print(e)
  172. return None
  173. # 如果需要验证receiveid,则进行验证
  174. if verify_receiveid and from_receiveid.decode('utf-8') != receiveid:
  175. return None
  176. return xml_content
  177. def get_random_str(self):
  178. """ 随机生成16位字符串
  179. @return: 16位随机字符串
  180. """
  181. return "".join(random.choice(string.ascii_letters + string.digits) for _ in range(16)).encode('utf-8')
  182. class WXBizMsgCrypt(object):
  183. # 构造函数
  184. # @param sToken: 企业微信后台,开发者设置的Token
  185. # @param sEncodingAESKey: 企业微信后台,开发者设置的EncodingAESKey
  186. # @param sReceiveId: 企业微信的CorpId 或者 应用的AgentId
  187. def __init__(self, sToken, sEncodingAESKey, sReceiveId):
  188. try:
  189. self.key = base64.b64decode(sEncodingAESKey + "=")
  190. assert len(self.key) == 32
  191. except:
  192. throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
  193. # return WXBizMsgCrypt_ERROR_InvalidAesKey)
  194. self.token = sToken
  195. self.receiveid = sReceiveId
  196. # 验证URL
  197. # @param sMsgSignature: 签名串,对应URL参数的msg_signature
  198. # @param sTimeStamp: 时间戳,对应URL参数的timestamp
  199. # @param sNonce: 随机串,对应URL参数的nonce
  200. # @param sEchoStr: 随机串,对应URL参数的echostr
  201. # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
  202. # @return:成功0,失败返回对应的错误码
  203. def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
  204. sha1 = SHA1()
  205. ret = sha1.getSHA1(self.token, sTimeStamp, sNonce, sEchoStr)
  206. if ret != sMsgSignature:
  207. return -40001
  208. pc = Prpcrypt(self.key)
  209. # URL验证时不进行receiveid验证,只解密获取实际内容
  210. ret = pc.decrypt(sEchoStr, self.receiveid, verify_receiveid=False)
  211. if ret is None:
  212. return -40002
  213. sReplyEchoStr = ret.decode('utf-8')
  214. return 0, sReplyEchoStr
  215. def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
  216. # 检验消息的真实性,并且获取解密后的明文
  217. # @param sMsgSignature: 签名串,对应URL参数的msg_signature
  218. # @param sTimeStamp: 时间戳,对应URL参数的timestamp
  219. # @param sNonce: 随机串,对应URL参数的nonce
  220. # @param sPostData: 密文,对应POST请求的数据
  221. # @param sMsg: 解密后的原文,当return返回0时有效
  222. # @return: 成功0,失败返回对应的错误码
  223. xmlParse = XMLParse()
  224. ret, sEncryptMsg, sToUserName = xmlParse.extract(sPostData)
  225. if ret != 0:
  226. return ret, None
  227. sha1 = SHA1()
  228. ret = sha1.getSHA1(self.token, sTimeStamp, sNonce, sEncryptMsg)
  229. if ret != sMsgSignature:
  230. return -40001, None
  231. pc = Prpcrypt(self.key)
  232. ret = pc.decrypt(sEncryptMsg, self.receiveid)
  233. if ret is None:
  234. return -40002, None
  235. sMsg = ret.decode('utf-8')
  236. return 0, sMsg
  237. def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
  238. # 将企业回复用户的消息加密打包
  239. # @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
  240. # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
  241. # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
  242. # @param sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,当return返回0时有效
  243. # return:成功0,失败返回对应的错误码
  244. pc = Prpcrypt(self.key)
  245. ret = pc.encrypt(sReplyMsg, self.receiveid)
  246. if ret is None:
  247. return -40006, None
  248. if timestamp is None:
  249. timestamp = str(int(time.time()))
  250. # 确保ret是bytes类型,然后解码为字符串用于签名和XML生成
  251. if isinstance(ret, bytes):
  252. encrypted_str = ret.decode('utf-8')
  253. else:
  254. encrypted_str = str(ret)
  255. # 生成安全签名
  256. sha1 = SHA1()
  257. signature = sha1.getSHA1(self.token, timestamp, sNonce, encrypted_str)
  258. xmlParse = XMLParse()
  259. result_xml = xmlParse.generate(encrypted_str, signature, timestamp, sNonce)
  260. return 0, result_xml