中国本土应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
11KB

  1. # -*- coding: utf-8 -*-
  2. from odoo import api, fields, models, tools, _
  3. import logging
  4. import base64
  5. import random
  6. import hashlib
  7. import time
  8. import struct
  9. from Crypto.Cipher import AES
  10. import xml.etree.cElementTree as ET
  11. import socket
  12. import hashlib
  13. import werkzeug.urls
  14. import werkzeug.utils
  15. _logger = logging.getLogger(__name__)
  16. #########################################################################
  17. # Description:定义错误码含义
  18. #########################################################################
  19. WXBizMsgCrypt_OK = 0
  20. WXBizMsgCrypt_ValidateSignature_Error = -40001
  21. WXBizMsgCrypt_ParseXml_Error = -40002
  22. WXBizMsgCrypt_ComputeSignature_Error = -40003
  23. WXBizMsgCrypt_IllegalAesKey = -40004
  24. WXBizMsgCrypt_ValidateCorpid_Error = -40005
  25. WXBizMsgCrypt_EncryptAES_Error = -40006
  26. WXBizMsgCrypt_DecryptAES_Error = -40007
  27. WXBizMsgCrypt_IllegalBuffer = -40008
  28. WXBizMsgCrypt_EncodeBase64_Error = -40009
  29. WXBizMsgCrypt_DecodeBase64_Error = -40010
  30. WXBizMsgCrypt_GenReturnXml_Error = -40011
  31. class FormatException(Exception):
  32. pass
  33. def throw_exception(message, exception_class=FormatException):
  34. """自定义引发异常函数"""
  35. raise exception_class(message)
  36. class SHA1:
  37. """计算企业微信的消息签名接口"""
  38. def getSHA1(self, token, timestamp, nonce, encrypt):
  39. """用SHA1算法生成安全签名
  40. @param token: 票据
  41. @param timestamp: 时间戳
  42. @param encrypt: 密文
  43. @param nonce: 随机字符串
  44. @return: 安全签名
  45. """
  46. try:
  47. sortlist = [token, timestamp, nonce, encrypt]
  48. sortlist.sort()
  49. sha = hashlib.sha1()
  50. sha.update("".join(sortlist).encode())
  51. return WXBizMsgCrypt_OK, sha.hexdigest()
  52. except Exception as e:
  53. logger = logging.getLogger()
  54. logger.error(e)
  55. return WXBizMsgCrypt_ComputeSignature_Error, None
  56. class XMLParse:
  57. """提供提取消息格式中的密文及生成回复消息格式的接口"""
  58. # xml消息模板
  59. AES_TEXT_RESPONSE_TEMPLATE = """<xml>
  60. <Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
  61. <MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
  62. <TimeStamp>%(timestamp)s</TimeStamp>
  63. <Nonce><![CDATA[%(nonce)s]]></Nonce>
  64. </xml>"""
  65. def extract(self, xmltext):
  66. """提取出xml数据包中的加密消息
  67. @param xmltext: 待提取的xml字符串
  68. @return: 提取出的加密消息字符串
  69. """
  70. try:
  71. xml_tree = ET.fromstring(xmltext)
  72. encrypt = xml_tree.find("Encrypt")
  73. return WXBizMsgCrypt_OK, encrypt.text
  74. except Exception as e:
  75. logger = logging.getLogger()
  76. logger.error(e)
  77. return WXBizMsgCrypt_ParseXml_Error, None, None
  78. def generate(self, encrypt, signature, timestamp, nonce):
  79. """生成xml消息
  80. @param encrypt: 加密后的消息密文
  81. @param signature: 安全签名
  82. @param timestamp: 时间戳
  83. @param nonce: 随机字符串
  84. @return: 生成的xml字符串
  85. """
  86. resp_dict = {
  87. "msg_encrypt": encrypt,
  88. "msg_signaturet": signature,
  89. "timestamp": timestamp,
  90. "nonce": nonce,
  91. }
  92. resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
  93. return resp_xml
  94. class PKCS7Encoder:
  95. """提供基于PKCS7算法的加解密接口"""
  96. block_size = 32
  97. def encode(self, text):
  98. """ 对需要加密的明文进行填充补位
  99. @param text: 需要进行填充补位操作的明文
  100. @return: 补齐明文字符串
  101. """
  102. text_length = len(text)
  103. # 计算需要填充的位数
  104. amount_to_pad = self.block_size - (text_length % self.block_size)
  105. if amount_to_pad == 0:
  106. amount_to_pad = self.block_size
  107. # 获得补位所用的字符
  108. pad = chr(amount_to_pad)
  109. return text + (pad * amount_to_pad).encode()
  110. def decode(self, decrypted):
  111. """删除解密后明文的补位字符
  112. @param decrypted: 解密后的明文
  113. @return: 删除补位字符后的明文
  114. """
  115. pad = ord(decrypted[-1])
  116. if pad < 1 or pad > 32:
  117. pad = 0
  118. return decrypted[:-pad]
  119. class Prpcrypt(object):
  120. """提供接收和推送给企业微信消息的加解密接口"""
  121. def __init__(self, key):
  122. # self.key = base64.b64decode(key+"=")
  123. self.key = key
  124. # 设置加解密模式为AES的CBC模式
  125. self.mode = AES.MODE_CBC
  126. def encrypt(self, text, receiveid):
  127. """对明文进行加密
  128. @param text: 需要加密的明文
  129. @return: 加密得到的字符串
  130. """
  131. # 16位随机字符串添加到明文开头
  132. text = text.encode()
  133. text = (
  134. self.get_random_str()
  135. + struct.pack("I", socket.htonl(len(text)))
  136. + text
  137. + receiveid.encode()
  138. )
  139. # 使用自定义的填充方式对明文进行补位填充
  140. pkcs7 = PKCS7Encoder()
  141. text = pkcs7.encode(text)
  142. # 加密
  143. cryptor = AES.new(self.key, self.mode, self.key[:16])
  144. try:
  145. ciphertext = cryptor.encrypt(text)
  146. # 使用BASE64对加密后的字符串进行编码
  147. return WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
  148. except Exception as e:
  149. logger = logging.getLogger()
  150. logger.error(e)
  151. return WXBizMsgCrypt_EncryptAES_Error, None
  152. def decrypt(self, text, receiveid):
  153. """对解密后的明文进行补位删除
  154. @param text: 密文
  155. @return: 删除填充补位后的明文
  156. """
  157. try:
  158. cryptor = AES.new(self.key, self.mode, self.key[:16])
  159. # 使用BASE64对密文进行解码,然后AES-CBC解密
  160. plain_text = cryptor.decrypt(base64.b64decode(text))
  161. except Exception as e:
  162. logger = logging.getLogger()
  163. logger.error(e)
  164. return WXBizMsgCrypt_DecryptAES_Error, None
  165. try:
  166. pad = plain_text[-1]
  167. # 去掉补位字符串
  168. # pkcs7 = PKCS7Encoder()
  169. # plain_text = pkcs7.encode(plain_text)
  170. # 去除16位随机字符串
  171. content = plain_text[16:-pad]
  172. xml_len = socket.ntohl(struct.unpack("I", content[:4])[0])
  173. xml_content = content[4 : xml_len + 4]
  174. from_receiveid = content[xml_len + 4 :]
  175. except Exception as e:
  176. logger = logging.getLogger()
  177. logger.error(e)
  178. return WXBizMsgCrypt_IllegalBuffer, None
  179. if from_receiveid.decode("utf8") != receiveid:
  180. return WXBizMsgCrypt_ValidateCorpid_Error, None
  181. return 0, xml_content
  182. def get_random_str(self):
  183. """ 随机生成16位字符串
  184. @return: 16位字符串
  185. """
  186. return str(random.randint(1000000000000000, 9999999999999999)).encode()
  187. class WecomMsgCrypt(object):
  188. # 消息加解密
  189. # _name = "wecom.msg_crypt"
  190. # _description = "Wecom Message encryption and decryption"
  191. def __init__(self, sToken, sEncodingAESKey, sReceiveId):
  192. """
  193. 初始化消息加解密对象
  194. :return:
  195. """
  196. try:
  197. self.key = base64.b64decode(sEncodingAESKey + "=")
  198. assert len(self.key) == 32
  199. except:
  200. throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
  201. # return WXBizMsgCrypt_IllegalAesKey,None
  202. self.m_sToken = sToken
  203. self.m_sReceiveId = sReceiveId
  204. def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
  205. """验证URL
  206. @param sMsgSignature: 签名串,对应URL参数的msg_signature
  207. @param sTimeStamp: 时间戳,对应URL参数的timestamp
  208. @param sNonce: 随机串,对应URL参数的nonce
  209. @param sEchoStr: 随机串,对应URL参数的echostr
  210. @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
  211. @return: 成功0,失败返回对应的错误码
  212. """
  213. sha1 = SHA1()
  214. ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
  215. if ret != 0:
  216. return ret, None
  217. if not signature == sMsgSignature:
  218. return WXBizMsgCrypt_ValidateSignature_Error, None
  219. pc = Prpcrypt(self.key)
  220. ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId)
  221. return ret, sReplyEchoStr
  222. def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
  223. """将企业回复用户的消息加密打包
  224. @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
  225. @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
  226. @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
  227. sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
  228. return:成功0,sEncryptMsg,失败返回对应的错误码None
  229. """
  230. pc = Prpcrypt(self.key)
  231. ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
  232. encrypt = encrypt.decode("utf8")
  233. if ret != 0:
  234. return ret, None
  235. if timestamp is None:
  236. timestamp = str(int(time.time()))
  237. # 生成安全签名
  238. sha1 = SHA1()
  239. ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
  240. if ret != 0:
  241. return ret, None
  242. xmlParse = XMLParse()
  243. return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce)
  244. def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
  245. """检验消息的真实性,并且获取解密后的明文
  246. # @param sMsgSignature: 签名串,对应URL参数的msg_signature
  247. # @param sTimeStamp: 时间戳,对应URL参数的timestamp
  248. # @param sNonce: 随机串,对应URL参数的nonce
  249. # @param sPostData: 密文,对应POST请求的数据
  250. # xml_content: 解密后的原文,当return返回0时有效
  251. # @return: 成功0,失败返回对应的错误码
  252. # 验证安全签名
  253. """
  254. xmlParse = XMLParse()
  255. ret, encrypt = xmlParse.extract(sPostData)
  256. if ret != 0:
  257. return ret, None
  258. sha1 = SHA1()
  259. ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
  260. if ret != 0:
  261. return ret, None
  262. if not signature == sMsgSignature:
  263. return WXBizMsgCrypt_ValidateSignature_Error, None
  264. pc = Prpcrypt(self.key)
  265. ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId)
  266. return ret, xml_content
上海开阖软件有限公司 沪ICP备12045867号-1