首页 > 代码库 > 微信开发 企业号(二)-- 回调模式之Tooken验证 .net/python

微信开发 企业号(二)-- 回调模式之Tooken验证 .net/python

在企业号开发者中心中,有加密解密源代码,供给开发者使用。(加解密库下载)

由于官方只提供了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&timestamp=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) 
将所有tryexcept Exception,e:    print e修改为tryexcept 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);
#Pythonwxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID)

 

2、调用URL验证接口

//c#int ret = 0;string sEchoStr = "";ret = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr, ref sEchoStr);
#Python
ret,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);
#pythonIv = 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 sysif 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.pdfMODE_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.pdfMODE_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.pdfMODE_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.pdfMODE_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.pdfMODE_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/rfc4880MODE_OPENPGP = 7def _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);
#pythonpc = 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 BaseHTTPRequestHandlerfrom http.server import HTTPServerfrom socketserver import ThreadingMixInimport urllib.parsefrom WXBizMsgCrypt import WXBizMsgCryptimport xml.etree.cElementTree as ET hostIP = ‘‘portNum = 8080serverMessage = "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()

 

微信开发 企业号(二)-- 回调模式之Tooken验证 .net/python