├── .vscode └── settings.json ├── README.md ├── __init__.py ├── setup.py └── src ├── __init__.py ├── api.py └── tea.py /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": false 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QQLib for Python 2 | --- 3 | 模拟登录`QQ`。 4 | 5 | 安装 6 | --- 7 | 8 | ``` 9 | pip install rsa requests 10 | pip install git+https://github.com/JetLua/qqlib 11 | ``` 12 | 13 | 14 | 使用 15 | --- 16 | ```py 17 | import qq 18 | 19 | results = qq.QQ('qq', 'password') 20 | print(results) 21 | ``` 22 | 23 | 更新说明 24 | --- 25 | 26 | * 2017.05.08 27 | * 更新了验证码获取方式 28 | * 支持扫码登录 29 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import re 4 | import rsa 5 | import base64 6 | import hashlib 7 | import binascii 8 | 9 | if __name__ == '__main__': 10 | from src import api, tea 11 | else: 12 | from .src import api, tea 13 | 14 | class QQ: 15 | def __init__(self, qq, password): 16 | self.qq = qq 17 | self.password = password 18 | 19 | # Get login sign 20 | results = api.get_login_sign() 21 | assert not results['err'], results['msg'] 22 | 23 | self.login_sign = results['login_sign'] 24 | 25 | # Check 26 | results = api.check(self.qq, self.login_sign) 27 | assert not results['err'], results['msg'] 28 | 29 | matches = re.findall('\'(.*?)\'', results['data']) 30 | 31 | self.salt = matches[2].replace(r'\x', '') 32 | 33 | if matches[0] == '1': 34 | # The captcha is required. 35 | self.mode = 1 36 | self.get_capture(matches[1]) 37 | else: 38 | self.mode = 0 39 | self.code = matches[1] 40 | self.session = matches[3] 41 | pass 42 | 43 | self.login() 44 | 45 | def get_capture(self, code): 46 | results = api.get_capture(self.qq, code) 47 | assert not results['err'], results['msg'] 48 | capture = input('Please enter the verification code: ') 49 | 50 | results = api.verify_capture(self.qq, 51 | results['sess'], code, results['sign'], capture) 52 | 53 | assert not results['err'], results['msg'] 54 | 55 | self.code = results['code'] 56 | self.session = results['session'] 57 | 58 | def login(self): 59 | results = api.login( 60 | self.qq, 61 | self._encrpyt(), 62 | self.code, 63 | self.login_sign, 64 | self.session, 65 | self.mode 66 | ) 67 | 68 | matches = re.findall('\'(.*?)\'', results) 69 | if matches[0] == '22009': 70 | return api.get_qr() 71 | else: 72 | return matches 73 | 74 | def _encrpyt(self): 75 | # RSA: public key 76 | puk = rsa.PublicKey(int( 77 | 'e9a815ab9d6e86abbf33a4ac64e9196d5be44a09bd0ed6ae052914e1a865ac83' 78 | '31fed863de8ea697e9a7f63329e5e23cda09c72570f46775b7e39ea9670086f8' 79 | '47d3c9c51963b131409b1e04265d9747419c635404ca651bbcbc87f99b8008f7' 80 | 'f5824653e3658be4ba73e4480156b390bb73bc1f8b33578e7a4e12440e9396f2' 81 | '552c1aff1c92e797ebacdc37c109ab7bce2367a19c56a033ee04534723cc2558' 82 | 'cb27368f5b9d32c04d12dbd86bbd68b1d99b7c349a8453ea75d1b2e94491ab30' 83 | 'acf6c46a36a75b721b312bedf4e7aad21e54e9bcbcf8144c79b6e3c05eb4a154' 84 | '7750d224c0085d80e6da3907c3d945051c13c7c1dcefd6520ee8379c4f5231ed', 16 85 | ), 65537) 86 | 87 | a = bytes.fromhex(self.salt) 88 | b = hashlib.md5(self.password.encode()) 89 | c = bytes.fromhex(b.hexdigest()) 90 | d = hashlib.md5(c + a).hexdigest() 91 | e = binascii.b2a_hex(rsa.encrypt(c, puk)).decode() 92 | f = hex(len(e) // 2)[2:] 93 | g = binascii.hexlify(self.code.upper().encode()).decode() 94 | h = hex(len(g) // 2)[2:] 95 | i = '0' * (4 - len(f)) + f 96 | j = '0' * (4 - len(h)) + h 97 | k = '{}{}{}{}{}'.format(i, e, 98 | binascii.hexlify(a).decode(), j, g) 99 | 100 | return base64.b64encode(tea.encrypt( 101 | bytes.fromhex(k), bytes.fromhex(d) 102 | )).decode().replace('/', '-').replace('+', '*').replace('=', '_') -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from distutils.core import setup 4 | 5 | setup( 6 | name='qq', 7 | version='1.0', 8 | packages=['qq', 'qq.src'], 9 | package_dir={'qq': '.'}, 10 | author='JetLu', 11 | author_email='i@lufei.so', 12 | description='QQ lib for Python.' 13 | ) 14 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # from . import api 2 | # from . import tea -------------------------------------------------------------------------------- /src/api.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import re 5 | import json 6 | import time 7 | import tempfile 8 | import requests 9 | 10 | 11 | 12 | 13 | APPID = '549000912' 14 | 15 | req = requests.session() 16 | 17 | uri = { 18 | 'login_sign' : 'https://xui.ptlogin2.qq.com/cgi-bin/xlogin', 19 | 'check' : 'https://ssl.ptlogin2.qq.com/check', 20 | 'cap_sess' : 'https://ssl.captcha.qq.com/cap_union_new_gettype', 21 | 'cap_sign' : 'https://ssl.captcha.qq.com/cap_union_new_getsig', 22 | 'cap_image' : 'https://ssl.captcha.qq.com/cap_union_new_getcapbysig', 23 | 'cap_verify' : 'https://ssl.captcha.qq.com/cap_union_new_verify', 24 | 'login' : 'https://ssl.ptlogin2.qq.com/login', 25 | 'qr' : 'https://ssl.ptlogin2.qq.com/ptqrshow', 26 | 'check_qr' : 'https://ssl.ptlogin2.qq.com/ptqrlogin' 27 | 28 | } 29 | 30 | def get_login_sign(): 31 | res = req.get(uri['login_sign'], params={ 32 | 'appid' : APPID, 33 | 's_url' : 'http://qzs.qq.com/qzone/v5/loginsucc.html?para=izone' 34 | }) 35 | 36 | if res.status_code == 200: 37 | sign = req.cookies.get('pt_login_sig') 38 | if sign: 39 | return {'err': 0, 'msg': 'ok', 'login_sign': sign} 40 | else: 41 | return {'err': 1, 'msg': 'Failed to get login sign.', 'login_sign': None} 42 | else: 43 | return {'err': 1, 'msg': res.text, 'login_sign': None} 44 | 45 | 46 | def check(qq, sign): 47 | res = req.get(uri['check'], params={ 48 | 'pt_tea' : 1, 49 | 'pt_vcode' : 1, 50 | 'uin' : qq, 51 | 'appid' : APPID, 52 | 'js_type' : 1, 53 | 'login_sig' : sign, 54 | 'u1' : 'http://qzs.qq.com/qzone/v5/loginsucc.html?para=izone' 55 | }) 56 | 57 | if res.status_code == 200: 58 | return {'err': 0, 'msg': 'ok', 'data': res.text} 59 | else: 60 | return {'err': 1, 'msg': res.text, 'data': None} 61 | 62 | def get_capture(qq, code): 63 | # Get sess 64 | res = req.get(uri['cap_sess'], params={ 65 | 'uin' : qq, 66 | 'aid' : APPID, 67 | 'cap_cd' : code 68 | }) 69 | 70 | if res.status_code != 200: 71 | return {'err': 1, 'msg': res.text} 72 | 73 | sess = json.loads(res.text[1:-1])['sess'] 74 | 75 | # Get sign 76 | res = req.get(uri['cap_sign'], params={ 77 | 'sess' : sess, 78 | 'aid' : APPID, 79 | 'uid' : qq, 80 | 'cap_cd' : code 81 | }) 82 | 83 | if res.status_code != 200: 84 | return {'err': 1, 'msg': res.text} 85 | 86 | sign = json.loads(res.text)['vsig'] 87 | 88 | # Get capture 89 | res = req.get(uri['cap_image'], params={ 90 | 'sess' : sess, 91 | 'vsig' : sign, 92 | 'uid' : qq, 93 | 'cap_cd' : code, 94 | 'aid' : APPID, 95 | 'ischartype' : 1 96 | }) 97 | 98 | if res.status_code != 200: 99 | return {'err': 1, 'msg': res.text} 100 | 101 | tmp = tempfile.mkstemp(suffix = '.jpg') 102 | os.write(tmp[0], res.content) 103 | os.close(tmp[0]) 104 | os.startfile(tmp[1]) 105 | return {'err': 0, 'msg': 'ok', 106 | 'sess' : sess, 107 | 'sign' : sign 108 | } 109 | 110 | 111 | def verify_capture(qq, sess, code, sign, capture): 112 | res = req.post(uri['cap_verify'], data={ 113 | 'aid' : APPID, 114 | 'sess' : sess, 115 | 'uid' : qq, 116 | 'cap_cd' : code, 117 | 'vsig' : sign, 118 | 'ans' : capture 119 | }) 120 | 121 | if res.status_code != 200: 122 | return {'err': 1, 'msg': res.text} 123 | 124 | res = json.loads(res.text) 125 | if res['errorCode'] != '0': 126 | return { 127 | 'err': 1, 128 | 'msg': bytes.fromhex(ascii(res['errMessage']) 129 | .replace(r'\x', '').replace('\'', '')).decode('utf-8') 130 | } 131 | else: 132 | return {'err': 0, 'msg': 'ok', 133 | 'code': res['randstr'], 134 | 'session': res['ticket'] 135 | } 136 | 137 | 138 | 139 | def login(qq, password, code, login_sign, session, mode): 140 | 141 | res = req.get(uri['login'], params={ 142 | 'action' : '5-3-1495966725481', 143 | 'aid' : APPID, 144 | 'daid' : 5, 145 | 'from_ui' : 1, 146 | 'g' : 1, 147 | 'h' : 1, 148 | 'js_type' : 1, 149 | 'js_ver' : 10220, 150 | 'login_sig' : login_sign, 151 | 'p' : password, 152 | 'pt_jstoken' : 1419592444, 153 | 'pt_randsalt' : 2, 154 | 'pt_uistyle' : 40, 155 | 'pt_vcode_v1' : mode, 156 | 'pt_verifysession_v1' : session, 157 | 'ptlang' : 2052, 158 | 'ptredirect' : 0, 159 | 't' : 1, 160 | 'u' : qq, 161 | 'u1' : 'http://qzs.qq.com/qzone/v5/loginsucc.html?para=izone', 162 | 'verifycode' : code 163 | }) 164 | return res.text 165 | 166 | def get_qr(): 167 | res = req.get(uri['qr'], params={ 168 | 'aid' : APPID 169 | }) 170 | 171 | tmp = tempfile.mkstemp(suffix = '.jpg') 172 | os.write(tmp[0], res.content) 173 | os.close(tmp[0]) 174 | os.startfile(tmp[1]) 175 | return check_qr() 176 | 177 | def _hash(str): 178 | i = 0 179 | for x in str: 180 | i += (i << 5) + ord(x) 181 | return 2147483647 & i 182 | 183 | 184 | def check_qr(): 185 | params={ 186 | 'login_sig' : req.cookies.get('pt_login_sig'), 187 | 'aid' : APPID, 188 | 'ptqrtoken' : _hash(req.cookies.get('qrsig')), 189 | 'action' : '0-0-1495974089776', 190 | 'ptredirect': 0, 191 | 'js_ver' : 10220, 192 | 'js_type' : 1, 193 | 'g' : 1, 194 | 'h' : 1, 195 | 'from_ui' : 1, 196 | 't' : 1, 197 | 'pt_uistyle': 40, 198 | 'daid' : 5, 199 | 'u1' : 'https://qzs.qq.com/qzone/v5/loginsucc.html?para=izone' 200 | } 201 | while True: 202 | res = req.get(uri['check_qr'], params=params) 203 | matches = re.findall('\'(.*?)\'', res.text) 204 | if matches[0] == '66' or matches[0] == '67': 205 | pass 206 | else: 207 | return matches 208 | 209 | time.sleep(1) 210 | -------------------------------------------------------------------------------- /src/tea.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | """ 4 | The MIT License 5 | Copyright (c) 2005 hoxide 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 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. 9 | QQ Crypt module. 10 | Maintainer: Gerald 11 | Last change: 2015 Apr 19 12 | """ 13 | 14 | import struct, ctypes 15 | from binascii import b2a_hex, a2b_hex 16 | from random import randint 17 | 18 | __all__ = ['encrypt', 'decrypt'] 19 | 20 | def xor(a, b): 21 | a1,a2 = struct.unpack('!LL', a[0:8]) 22 | b1,b2 = struct.unpack('!LL', b[0:8]) 23 | r = struct.pack('!LL', a1 ^ b1, a2 ^ b2) 24 | return r 25 | 26 | def encipher(v, k): 27 | """ 28 | TEA coder encrypt 64 bits value, by 128 bits key, 29 | QQ uses 16 round TEA. 30 | http://www.ftp.cl.cam.ac.uk/ftp/papers/djw-rmn/djw-rmn-tea.html . 31 | >>> c = encipher('abcdefgh', 'aaaabbbbccccdddd') 32 | >>> b2a_hex(c) 33 | 'a557272c538d3e96' 34 | """ 35 | n=16 #qq use 16 36 | delta = 0x9e3779b9 37 | k = struct.unpack('!LLLL', k[0:16]) 38 | y, z = map(ctypes.c_uint32, struct.unpack('!LL', v[0:8])) 39 | s = ctypes.c_uint32(0) 40 | for i in range(n): 41 | s.value += delta 42 | y.value += (z.value << 4) + k[0] ^ z.value+ s.value ^ (z.value >> 5) + k[1] 43 | z.value += (y.value << 4) + k[2] ^ y.value+ s.value ^ (y.value >> 5) + k[3] 44 | r = struct.pack('!LL', y.value, z.value) 45 | return r 46 | 47 | def encrypt(v, k): 48 | """ 49 | Encrypt function for QQ. 50 | v is the message to encrypt, k is the key 51 | fill char is randomized (which is 0xAD in old version) 52 | the length of the final data is filln + 8 + len(v) 53 | The message is encrypted 8 bytes at at time, 54 | the result is: 55 | r = encipher( v ^ tr, key) ^ to (*) 56 | `encipher` is the QQ's TEA function. 57 | v is 8 bytes data to be encrypted. 58 | tr is the result in preceding round. 59 | to is the data coded in perceding round (v_pre ^ r_pre_pre) 60 | For the first 8 bytes 'tr' and 'to' is filled by zero. 61 | >>> en = encrypt('', b2a_hex('b537a06cf3bcb33206237d7149c27bc3')) 62 | >>> decrypt(en, b2a_hex('b537a06cf3bcb33206237d7149c27bc3')) 63 | '' 64 | """ 65 | vl = len(v) 66 | #filln = (8 - (vl + 2)) % 8 67 | filln = (6 - vl) % 8 68 | v_arr = [ 69 | bytes(bytearray([filln | 0xf8])), 70 | b'\xad' * (filln + 2), # random char * (filln + 2) 71 | v, 72 | b'\0' * 7, 73 | ] 74 | v = b''.join(v_arr) 75 | tr = b'\0'*8 76 | to = b'\0'*8 77 | r = [] 78 | o = b'\0' * 8 79 | for i in range(0, len(v), 8): 80 | o = xor(v[i:i+8], tr) 81 | tr = xor(encipher(o, k), to) 82 | to = o 83 | r.append(tr) 84 | r = b''.join(r) 85 | return r 86 | 87 | def decrypt(v, k): 88 | """ 89 | Decrypt function for QQ. 90 | according to (*) we can get: 91 | x = decipher(v[i:i+8] ^ prePlain, key) ^ preCyrpt 92 | prePlain is the previously encrypted 8 bytes: 93 | per 8 byte from v XOR previous preCyrpt 94 | preCrypt is previous 8 bytes of encrypted data. 95 | After decrypting, we must truncate the padding bytes. 96 | The number of padding bytes in the front of message is 97 | pos + 1. 98 | pos is the first byte of deCrypted: r[0] & 0x07 + 2 99 | The number of padding bytes in the end is 7 (b'\0' * 7). 100 | The returned value is r[pos+1:-7]. 101 | >>> r = encrypt('', b2a_hex('b537a06cf3bcb33206237d7149c27bc3')) 102 | >>> decrypt(r, b2a_hex('b537a06cf3bcb33206237d7149c27bc3')) 103 | '' 104 | >>> r = encrypt('abcdefghijklimabcdefghijklmn', b2a_hex('b537a06cf3bcb33206237d7149c27bc3')) 105 | >>> decrypt(r, b2a_hex('b537a06cf3bcb33206237d7149c27bc3')) 106 | 'abcdefghijklimabcdefghijklmn' 107 | >>> import md5 108 | >>> key = md5.new(md5.new('python').digest()).digest() 109 | >>> data='8CE160B9F312AEC9AC8D8AEAB41A319EDF51FB4BB5E33820C77C48DFC53E2A48CD1C24B29490329D2285897A32E7B32E9830DC2D0695802EB1D9890A0223D0E36C35B24732CE12D06403975B0BC1280EA32B3EE98EAB858C40670C9E1A376AE6C7DCFADD4D45C1081571D2AF3D0F41B73BDC915C3AE542AF2C8B1364614861FC7272E33D90FA012620C18ABF76BE0B9EC0D24017C0C073C469B4376C7C08AA30' 110 | >>> data = a2b_hex(data) 111 | >>> b2a_hex(decrypt(data, key)) 112 | '00553361637347436654695a354d7a51531c69f1f5dde81c4332097f0000011f4042c89732030aa4d290f9f941891ae3670bb9c21053397d05f35425c7bf80000000001f40da558a481f40000100004dc573dd2af3b28b6a13e8fa72ea138cd13aa145b0e62554fe8df4b11662a794000000000000000000000000dde81c4342c8966642c4df9142c3a4a9000a000a' 113 | """ 114 | l = len(v) 115 | #if l%8 !=0 or l<16: 116 | # return '' 117 | prePlain = decipher(v, k) 118 | pos = ord(prePlain[0]) & 0x07 + 2 119 | r = prePlain 120 | preCrypt = v[0:8] 121 | for i in range(8, l, 8): 122 | x = xor(decipher(xor(v[i:i+8], prePlain), k), preCrypt) 123 | prePlain = xor(x, preCrypt) 124 | preCrypt = v[i:i+8] 125 | r += x 126 | if r[-7:] == '\0'*7: 127 | return r[pos+1:-7] 128 | 129 | def decipher(v, k): 130 | """ 131 | TEA decipher, decrypt 64bits value with 128 bits key. 132 | it's the inverse function of TEA encrypt. 133 | >>> c = encipher('abcdefgh', 'aaaabbbbccccdddd') 134 | >>> decipher( c, 'aaaabbbbccccdddd') 135 | 'abcdefgh' 136 | """ 137 | 138 | n = 16 139 | y, z = map(ctypes.c_uint32, struct.unpack('!LL', v[0:8])) 140 | a, b, c, d = map(ctypes.c_uint32, struct.unpack('!LLLL', k[0:16])) 141 | delta = 0x9E3779B9 142 | s = ctypes.c_uint32(delta << 4) 143 | for i in range(n): 144 | z.value -= ((y.value << 4) + c.value) ^ (y.value + s.value) ^ ((y.value >> 5) + d.value) 145 | y.value -= ((z.value << 4) + a.value) ^ (z.value + s.value) ^ ((z.value >> 5) + b.value) 146 | s.value -= delta 147 | return struct.pack('!LL', y.value, z.value) 148 | --------------------------------------------------------------------------------