在企业号开发者中心中,有加密解密源代码,供给开发者使用。(加解密库下载)
由于官方只提供了python2.*的类库,使用python3.*的朋友可以再最后下载我修改后的py文件(仅修改验证Tooken代码)。
加解密库分析
一、需要用到的几个数据
在企业号中配置/获取到的数据
string sToken = "QDG6eK"; string sCorpID = "wx5823bf96d3bd56c7"; string sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
通过URL中获取到的参数
// string sVerifyMsgSig = Request("msg_signature"); string sVerifyMsgSig = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3"; // string sVerifyTimeStamp = Request("timestamp"); string sVerifyTimeStamp = "1409659589"; // string sVerifyNonce = Request("nonce"); string sVerifyNonce = "263014780"; // string sVerifyEchoStr = Request("echostr"); string sVerifyEchoStr = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
例如:
http://127.0.0.1/?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3×tamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==
二、环境搭建
1、C#
只需要在官网下载C#库后将CS文件复制到工程,或编译后引用DLL文件即可。
2、Python
需要pycrypto第三方库。
I、linux
只需要运行:pip install pycrypto安装即可
II、windows
安装比较麻烦,网上有说pip install pycrypto、easy_install pycrypto。
由于我电脑没安装VS2008,安装的是VS2013。按照网上各种方法都无法对下载的包进行编译。
最后找到了编译好的exe文件,直接安装即可http://www.voidspace.org.uk/python/modules.shtml#pycrypto
三、修改地方
1、C#
无修改
2、Python3.*
删除
reload(sys) sys.setdefaultencoding(‘utf-8‘)
将所有 try: except Exception,e: print e 修改为 try: except Exception as e: print(e)
第51行 sha.update("".join(sortlist)) 改为 sha.update("".join(sortlist).encode("ascii"))
第174行 pad = ord(plain_text[-1]) 改为 pad = plain_text[-1]
第182行 from_corpid = content[xml_len+4:] 改为 from_corpid = content[xml_len+4:].decode("utf8")
四、分析
1、实例化
//C# Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID);
#Python wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID)
2、调用URL验证接口
//c# int ret = 0; string sEchoStr = ""; ret = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr, ref sEchoStr);
#Pythonret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
3、signature验证
I、将token, timestamp, nonce, encrypt的内容按照大小字母顺序排列
II、按顺序将列表中排序号的内容拼接成一个字符串,并对其进行ASCII转码
III、对ASCII转码后的数组做SHA1加密生成 signature
IV、生成的signature和URL中获取到的sMsgSignature进行比对,如果一致则继续,否则返回错误。
//C#public static int GenarateSinature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt ,ref string sMsgSignature) { ArrayList AL = new ArrayList(); AL.Add(sToken); AL.Add(sTimeStamp); AL.Add(sNonce); AL.Add(sMsgEncrypt); AL.Sort(new DictionarySort()); string raw = ""; for (int i = 0; i < AL.Count; ++i) { raw += AL[i]; } SHA1 sha; ASCIIEncoding enc; string hash = ""; try { sha = new SHA1CryptoServiceProvider(); enc = new ASCIIEncoding(); byte[] dataToHash = enc.GetBytes(raw); byte[] dataHashed = sha.ComputeHash(dataToHash); hash = BitConverter.ToString(dataHashed).Replace("-", ""); hash = hash.ToLower(); } catch (Exception) { return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error; } sMsgSignature = hash; return 0; } public class DictionarySort : System.Collections.IComparer { public int Compare(object oLeft, object oRight) { string sLeft = oLeft as string; string sRight = oRight as string; int iLeftLength = sLeft.Length; int iRightLength = sRight.Length; int index = 0; while (index < iLeftLength && index < iRightLength) { if (sLeft[index] < sRight[index]) return -1; else if (sLeft[index] > sRight[index]) return 1; else index++; } return iLeftLength - iRightLength; } } //调用 string hash = ""; int ret = 0; ret = GenarateSinature(sToken, sTimeStamp, sNonce, sMsgEncrypt, ref hash); if (ret != 0) return ret; if (hash == sSigture) return 0;
#Python def getSHA1(self, token, timestamp, nonce, encrypt): """用SHA1算法生成安全签名 @param token: 票据 @param timestamp: 时间戳 @param encrypt: 密文 @param nonce: 随机字符串 @return: 安全签名 """ try: sortlist = [token, timestamp, nonce, encrypt] sortlist.sort() sha = hashlib.sha1() sha.update("".join(sortlist).encode("ascii")) return ierror.WXBizMsgCrypt_OK, sha.hexdigest() except Exception as e: print(e) return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
4、解密
I、在sEncodingAESKey接入加入等号(“=”)
sEncodingAESKey = sEncodingAESKey+"="
II、再用Base64对其进行编码
byte[] Key; Key = Convert.FromBase64String(EncodingAESKey + "=");
Key = base64.b64decode(sEncodingAESKey+"=")
III、根据Key生成AES加密所需要的偏移量IV
//C#byte[] Iv = new byte[16]; Array.Copy(Key, Iv, 16);
#python Iv = Key[:16]
IV、解密方法
private static byte[] AES_decrypt(String Input, byte[] Iv, byte[] Key) { RijndaelManaged aes = new RijndaelManaged(); aes.KeySize = 256; aes.BlockSize = 128; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.None; aes.Key = Key; aes.IV = Iv; var decrypt = aes.CreateDecryptor(aes.Key, aes.IV); byte[] xBuff = null; using (var ms = new MemoryStream()) { using (var cs = new CryptoStream(ms, decrypt, CryptoStreamMode.Write)) { byte[] xXml = Convert.FromBase64String(Input); byte[] msg = new byte[xXml.Length + 32 - xXml.Length % 32]; Array.Copy(xXml, msg, xXml.Length); cs.Write(xXml, 0, xXml.Length); } xBuff = decode2(ms.ToArray()); } return xBuff; }
# -*- coding: utf-8 -*- # # Cipher/blockalgo.py # # =================================================================== # The contents of this file are dedicated to the public domain. To # the extent that dedication to the public domain is not available, # everyone is granted a worldwide, perpetual, royalty-free, # non-exclusive license to exercise all rights associated with the # contents of this file for any purpose whatsoever. # No rights are reserved. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # =================================================================== """Module with definitions common to all block ciphers.""" import sys if sys.version_info[0] == 2 and sys.version_info[1] == 1: from Crypto.Util.py21compat import * from Crypto.Util.py3compat import * #: *Electronic Code Book (ECB)*. #: This is the simplest encryption mode. Each of the plaintext blocks #: is directly encrypted into a ciphertext block, independently of #: any other block. This mode exposes frequency of symbols #: in your plaintext. Other modes (e.g. *CBC*) should be used instead. #: #: See `NIST SP800-38A`_ , Section 6.1 . #: #: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf MODE_ECB = 1 #: *Cipher-Block Chaining (CBC)*. Each of the ciphertext blocks depends #: on the current and all previous plaintext blocks. An Initialization Vector #: (*IV*) is required. #: #: The *IV* is a data block to be transmitted to the receiver. #: The *IV* can be made public, but it must be authenticated by the receiver and #: it should be picked randomly. #: #: See `NIST SP800-38A`_ , Section 6.2 . #: #: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf MODE_CBC = 2 #: *Cipher FeedBack (CFB)*. This mode is similar to CBC, but it transforms #: the underlying block cipher into a stream cipher. Plaintext and ciphertext #: are processed in *segments* of **s** bits. The mode is therefore sometimes #: labelled **s**-bit CFB. An Initialization Vector (*IV*) is required. #: #: When encrypting, each ciphertext segment contributes to the encryption of #: the next plaintext segment. #: #: This *IV* is a data block to be transmitted to the receiver. #: The *IV* can be made public, but it should be picked randomly. #: Reusing the same *IV* for encryptions done with the same key lead to #: catastrophic cryptographic failures. #: #: See `NIST SP800-38A`_ , Section 6.3 . #: #: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf MODE_CFB = 3 #: This mode should not be used. MODE_PGP = 4 #: *Output FeedBack (OFB)*. This mode is very similar to CBC, but it #: transforms the underlying block cipher into a stream cipher. #: The keystream is the iterated block encryption of an Initialization Vector (*IV*). #: #: The *IV* is a data block to be transmitted to the receiver. #: The *IV* can be made public, but it should be picked randomly. #: #: Reusing the same *IV* for encryptions done with the same key lead to #: catastrophic cryptograhic failures. #: #: See `NIST SP800-38A`_ , Section 6.4 . #: #: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf MODE_OFB = 5 #: *CounTeR (CTR)*. This mode is very similar to ECB, in that #: encryption of one block is done independently of all other blocks. #: Unlike ECB, the block *position* contributes to the encryption and no #: information leaks about symbol frequency. #: #: Each message block is associated to a *counter* which must be unique #: across all messages that get encrypted with the same key (not just within #: the same message). The counter is as big as the block size. #: #: Counters can be generated in several ways. The most straightword one is #: to choose an *initial counter block* (which can be made public, similarly #: to the *IV* for the other modes) and increment its lowest **m** bits by #: one (modulo *2^m*) for each block. In most cases, **m** is chosen to be half #: the block size. #: #: Reusing the same *initial counter block* for encryptions done with the same #: key lead to catastrophic cryptograhic failures. #: #: See `NIST SP800-38A`_ , Section 6.5 (for the mode) and Appendix B (for how #: to manage the *initial counter block*). #: #: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf MODE_CTR = 6 #: OpenPGP. This mode is a variant of CFB, and it is only used in PGP and OpenPGP_ applications. #: An Initialization Vector (*IV*) is required. #: #: Unlike CFB, the IV is not transmitted to the receiver. Instead, the *encrypted* IV is. #: The IV is a random data block. Two of its bytes are duplicated to act as a checksum #: for the correctness of the key. The encrypted IV is therefore 2 bytes longer than #: the clean IV. #: #: .. _OpenPGP: http://tools.ietf.org/html/rfc4880 MODE_OPENPGP = 7 def _getParameter(name, index, args, kwargs, default=None): """Find a parameter in tuple and dictionary arguments a function receives""" param = kwargs.get(name) if len(args)>index: if param: raise ValueError("Parameter ‘%s‘ is specified twice" % name) param = args[index] return param or default class BlockAlgo: """Class modelling an abstract block cipher.""" def __init__(self, factory, key, *args, **kwargs): self.mode = _getParameter(‘mode‘, 0, args, kwargs, default=MODE_ECB) self.block_size = factory.block_size if self.mode != MODE_OPENPGP: self._cipher = factory.new(key, *args, **kwargs) self.IV = self._cipher.IV else: # OPENPGP mode. For details, see 13.9 in RCC4880. # # A few members are specifically created for this mode: # - _encrypted_iv, set in this constructor # - _done_first_block, set to True after the first encryption # - _done_last_block, set to True after a partial block is processed self._done_first_block = False self._done_last_block = False self.IV = _getParameter(‘iv‘, 1, args, kwargs) if not self.IV: raise ValueError("MODE_OPENPGP requires an IV") # Instantiate a temporary cipher to process the IV IV_cipher = factory.new(key, MODE_CFB, b(‘\x00‘)*self.block_size, # IV for CFB segment_size=self.block_size*8) # The cipher will be used for... if len(self.IV) == self.block_size: # ... encryption self._encrypted_IV = IV_cipher.encrypt( self.IV + self.IV[-2:] + # Plaintext b(‘\x00‘)*(self.block_size-2) # Padding )[:self.block_size+2] elif len(self.IV) == self.block_size+2: # ... decryption self._encrypted_IV = self.IV self.IV = IV_cipher.decrypt(self.IV + # Ciphertext b(‘\x00‘)*(self.block_size-2) # Padding )[:self.block_size+2] if self.IV[-2:] != self.IV[-4:-2]: raise ValueError("Failed integrity check for OPENPGP IV") self.IV = self.IV[:-2] else: raise ValueError("Length of IV must be %d or %d bytes for MODE_OPENPGP" % (self.block_size, self.block_size+2)) # Instantiate the cipher for the real PGP data self._cipher = factory.new(key, MODE_CFB, self._encrypted_IV[-self.block_size:], segment_size=self.block_size*8) def encrypt(self, plaintext): """Encrypt data with the key and the parameters set at initialization. The cipher object is stateful; encryption of a long block of data can be broken up in two or more calls to `encrypt()`. That is, the statement: >>> c.encrypt(a) + c.encrypt(b) is always equivalent to: >>> c.encrypt(a+b) That also means that you cannot reuse an object for encrypting or decrypting other data with the same key. This function does not perform any padding. - For `MODE_ECB`, `MODE_CBC`, and `MODE_OFB`, *plaintext* length (in bytes) must be a multiple of *block_size*. - For `MODE_CFB`, *plaintext* length (in bytes) must be a multiple of *segment_size*/8. - For `MODE_CTR`, *plaintext* can be of any length. - For `MODE_OPENPGP`, *plaintext* must be a multiple of *block_size*, unless it is the last chunk of the message. :Parameters: plaintext : byte string The piece of data to encrypt. :Return: the encrypted data, as a byte string. It is as long as *plaintext* with one exception: when encrypting the first message chunk with `MODE_OPENPGP`, the encypted IV is prepended to the returned ciphertext. """ if self.mode == MODE_OPENPGP: padding_length = (self.block_size - len(plaintext) % self.block_size) % self.block_size if padding_length>0: # CFB mode requires ciphertext to have length multiple of block size, # but PGP mode allows the last block to be shorter if self._done_last_block: raise ValueError("Only the last chunk is allowed to have length not multiple of %d bytes", self.block_size) self._done_last_block = True padded = plaintext + b(‘\x00‘)*padding_length res = self._cipher.encrypt(padded)[:len(plaintext)] else: res = self._cipher.encrypt(plaintext) if not self._done_first_block: res = self._encrypted_IV + res self._done_first_block = True return res return self._cipher.encrypt(plaintext) def decrypt(self, ciphertext): """Decrypt data with the key and the parameters set at initialization. The cipher object is stateful; decryption of a long block of data can be broken up in two or more calls to `decrypt()`. That is, the statement: >>> c.decrypt(a) + c.decrypt(b) is always equivalent to: >>> c.decrypt(a+b) That also means that you cannot reuse an object for encrypting or decrypting other data with the same key. This function does not perform any padding. - For `MODE_ECB`, `MODE_CBC`, and `MODE_OFB`, *ciphertext* length (in bytes) must be a multiple of *block_size*. - For `MODE_CFB`, *ciphertext* length (in bytes) must be a multiple of *segment_size*/8. - For `MODE_CTR`, *ciphertext* can be of any length. - For `MODE_OPENPGP`, *plaintext* must be a multiple of *block_size*, unless it is the last chunk of the message. :Parameters: ciphertext : byte string The piece of data to decrypt. :Return: the decrypted data (byte string, as long as *ciphertext*). """ if self.mode == MODE_OPENPGP: padding_length = (self.block_size - len(ciphertext) % self.block_size) % self.block_size if padding_length>0: # CFB mode requires ciphertext to have length multiple of block size, # but PGP mode allows the last block to be shorter if self._done_last_block: raise ValueError("Only the last chunk is allowed to have length not multiple of %d bytes", self.block_size) self._done_last_block = True padded = ciphertext + b(‘\x00‘)*padding_length res = self._cipher.decrypt(padded)[:len(ciphertext)] else: res = self._cipher.decrypt(ciphertext) return res return self._cipher.decrypt(ciphertext)
Python AES 解密,已存在与blockalgo.py文件中
V、AES解密,并对解密后的数据进行拆分
//c# byte[] btmpMsg = AES_decrypt(Input, Iv, Key); //调用解密方法解密 //返回由字节数组中指定位置的四个字节转换来的 32 位有符号整数 int len = BitConverter.ToInt32(btmpMsg, 16); //将数字由网络字节顺序转换为主机字节顺序。 len = IPAddress.NetworkToHostOrder(len); byte[] bMsg = new byte[len]; byte[] bCorpid = new byte[btmpMsg.Length - 20 - len]; Array.Copy(btmpMsg, 20, bMsg, 0, len); Array.Copy(btmpMsg, 20+len , bCorpid, 0, btmpMsg.Length - 20 - len); string oriMsg = Encoding.UTF8.GetString(bMsg); corpid = Encoding.UTF8.GetString(bCorpid); //用来和m_sCorpID验证,解密是否正确
try: pad = plain_text[-1] # 去除16位随机字符串 content = plain_text[16:-pad] #struct.unpack("I",content[ : 4])[0] 返回由字节数组中指定位置的四个字节转换来的 32 位有符号整数 #socket.ntohl 将数字由网络字节顺序转换为主机字节顺序。 xml_len = socket.ntohl(struct.unpack("I",content[ : 4])[0]) xml_content = content[4 : xml_len+4] from_corpid = content[xml_len+4:].decode("utf8") print(from_corpid) except Exception as e: print(e) return ierror.WXBizMsgCrypt_IllegalBuffer,None
5、调用解密方法,返回明文及cpid
//C# sReplyEchoStr = Cryptography.AES_decrypt(sEchoStr, m_sEncodingAESKey, ref cpid);
#python pc = Prpcrypt(self.key)ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sCorpid)
6、解密后需要比对m_sCorpID和cpid是否一致
五、疑问
由于技术有限,并没做过太多Socket编程,并不很了解如下内容
如有高手路过,请指点一下,谢谢。
//返回由字节数组中指定位置的四个字节转换来的 32 位有符号整数 int len = BitConverter.ToInt32(btmpMsg, 16); //将数字由网络字节顺序转换为主机字节顺序。 len = IPAddress.NetworkToHostOrder(len);
xml_len = socket.ntohl(struct.unpack("I",content[ : 4])[0])
六、Python3.*版本WXBizMsgCrypt文件下载
修改后Python3.* 对应WXBizMsgCrypt.2014.09.28.zip文件。
该版本仅使用本文中修改方法进行修改,并且仅测试过URL验证方法,其他方法暂时可能存在问题,后续慢慢完善。
如有高手愿意提供WXBizMsgCrypt.py,请直接留言,谢谢。
七、Python3.*版本 测试代码
from http.server import BaseHTTPRequestHandler from http.server import HTTPServer from socketserver import ThreadingMixIn import urllib.parse from WXBizMsgCrypt import WXBizMsgCrypt import xml.etree.cElementTree as ET hostIP = ‘‘ portNum = 8080 serverMessage = "msg_signature" sToken="QDG6eK" sEncodingAESKey="jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C" sCorpID="wx5823bf96d3bd56c7" class mySoapServer( BaseHTTPRequestHandler ): def do_head( self ): pass def do_GET( self ): try: if self.path.find(serverMessage) == -1: self.send_error( 404, message = None ) return #解析请求 query=GetQuery(self.path) sVerifyMsgSig=query["msg_signature"][0] sVerifyTimeStamp=query["timestamp"][0] sVerifyNonce=query["nonce"][0] sVerifyEchoStr=query["echostr"][0] wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID) ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr) self.send_response( 200, message = None ) self.send_header( ‘Content-type‘, ‘text/html‘ ) self.end_headers() if ret == 0: res=sEchoStr.decode("utf8") else: res = "%s" % ret self.wfile.write( res.encode( encoding = ‘utf_8‘, errors = ‘strict‘ ) ) except IOError: self.send_error( 404, message = None ) def GetQuery(str): params = str[str.index("?")+1:] parsed_result = {} list = [param for param in params.split(‘&‘)] for item in list: if item.find("=") > -1: name = item[:item.index("=")] value = urllib.parse.unquote(item[item.index("=")+1:]) else: name = item value = "" if name in parsed_result: parsed_result[name].append(value) else: parsed_result[name] = [value] return parsed_result #urllib.parse.parse_qs(temp) class ThreadingHttpServer( ThreadingMixIn, HTTPServer ): pass myServer = ThreadingHttpServer( ( hostIP, portNum ), mySoapServer ) print("Server Started ....") myServer.serve_forever() myServer.server_close()