├── libwhatsapp ├── __init__.py ├── pkcs7.py ├── whatsappencrypter.py ├── whatsappdecrypt.py ├── capreader.py ├── binaryprotocol.py ├── waserializer.py └── wadeserializer.py ├── .gitattributes ├── .gitignore ├── env.sh ├── LICENSE ├── tests └── README.txt ├── requirements.txt ├── client.py ├── server_requirements.txt ├── README.md ├── dummyserver.py ├── wawebjs └── progress_e255aa82dfc08a03f3e15a40a6bbe7de.js └── PROTOCOL.md /libwhatsapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | wawebjs/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | venv/ 3 | venvserver/ 4 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PYTHONPATH=`pwd`/:$PYTHONPATH 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Enrico204/whatsapp-decoding/HEAD/LICENSE -------------------------------------------------------------------------------- /tests/README.txt: -------------------------------------------------------------------------------- 1 | I cannot share my own traces, you should capture using wireshark or similar tools. 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | backports.ssl-match-hostname==3.5.0.1 2 | pycrypto==2.6.1 3 | scapy==2.3.3 4 | six==1.10.0 5 | websocket-client==0.40.0 6 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | from websocket import create_connection 2 | 3 | ws = create_connection("ws://localhost:5000/ws") 4 | 5 | ws.send("Hello, World") 6 | print "Sent" 7 | # print "Reeiving..." 8 | # result = ws.recv() 9 | # print "Received '%s'" % result 10 | ws.close() 11 | -------------------------------------------------------------------------------- /server_requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.6 2 | Flask==0.12 3 | Flask-Sockets==0.2.1 4 | gevent==1.2.0 5 | gevent-websocket==0.9.5 6 | greenlet==0.4.11 7 | itsdangerous==0.24 8 | Jinja2==2.8 9 | MarkupSafe==0.23 10 | Werkzeug==0.11.11 11 | backports.ssl-match-hostname==3.5.0.1 12 | pycrypto==2.6.1 13 | scapy==2.3.3 14 | six==1.10.0 15 | websocket-client==0.40.0 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This library aim to emulate the whatsapp web interface in order to create 3 | an usable gateway to whatsapp. 4 | 5 | **Please note that this work is far from complete**: it's not an usable library, it's just a research work. 6 | 7 | Please note that **bots and gateways** are not allowed by WhatsApp policy. 8 | 9 | This code is only for learning/research. 10 | 11 | Remember to set environment with `env.sh` script. 12 | 13 | See PROTOCOL.md for details 14 | -------------------------------------------------------------------------------- /libwhatsapp/pkcs7.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import StringIO 3 | 4 | class PKCS7Encoder(object): 5 | def __init__(self, k=16): 6 | self.k = k 7 | 8 | ## @param text The padded text for which the padding is to be removed. 9 | # @exception ValueError Raised when the input padding is missing or corrupt. 10 | def decode(self, text): 11 | ''' 12 | Remove the PKCS#7 padding from a text string 13 | ''' 14 | nl = len(text) 15 | val = int(binascii.hexlify(text[-1]), 16) 16 | if val > self.k: 17 | raise ValueError('Input is not padded or padding is corrupt') 18 | 19 | l = nl - val 20 | return text[:l] 21 | 22 | ## @param text The text to encode. 23 | def encode(self, text): 24 | ''' 25 | Pad an input string according to PKCS#7 26 | ''' 27 | l = len(text) 28 | output = StringIO.StringIO() 29 | val = self.k - (l % self.k) 30 | for _ in xrange(val): 31 | output.write('%02x' % val) 32 | return text + binascii.unhexlify(output.getvalue()) 33 | -------------------------------------------------------------------------------- /libwhatsapp/whatsappencrypter.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from Crypto.Cipher import AES 3 | from Crypto import Random 4 | from pkcs7 import PKCS7Encoder 5 | import hmac 6 | import hashlib 7 | import sys 8 | import json 9 | 10 | def cleanpkcs7(txt): 11 | lastbyte = ord(txt[-1]) 12 | if len(txt) <= lastbyte: 13 | return txt 14 | 15 | found = True 16 | for i in xrange(lastbyte-1): 17 | if ord(txt[-(i+1)]) != lastbyte: 18 | found = False 19 | if found: 20 | return txt[:-lastbyte] 21 | else: 22 | return txt 23 | 24 | iv = Random.new().read( AES.block_size ) 25 | encoder = PKCS7Encoder() 26 | 27 | def loadkeys(fname): 28 | j = json.load(open(fname, "r")) 29 | for i in j: 30 | j[i] = base64.b64decode(j[i]) 31 | return j 32 | 33 | def encryptmessage(tag, d, keys): 34 | decr = AES.new(keys['encKey'], AES.MODE_CBC, iv) 35 | o = iv + decr.encrypt(encoder.encode(d)) 36 | sig = hmac.new(keys['macKey'], msg=o, digestmod=hashlib.sha256).digest() 37 | return tag + "," + sig + o 38 | 39 | 40 | if __name__ == '__main__': 41 | from whatsappdecrypt import decryptmessage 42 | msg = encryptmessage("|TAG|", "{prova}", loadkeys("../tests/keyset1.json")) 43 | toskip = msg.find(",") + 1 44 | print msg[:toskip-1] 45 | print decryptmessage(msg[toskip:], loadkeys("../tests/keyset1.json")) 46 | -------------------------------------------------------------------------------- /libwhatsapp/whatsappdecrypt.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from Crypto.Cipher import AES 3 | import hmac 4 | import hashlib 5 | import sys 6 | from binaryprotocol import * 7 | from wadeserializer import * 8 | import json 9 | from pprint import pprint 10 | 11 | 12 | def cleanpkcs7(txt): 13 | lastbyte = ord(txt[-1]) 14 | if len(txt) <= lastbyte: 15 | return txt 16 | 17 | found = True 18 | for i in xrange(lastbyte-1): 19 | if ord(txt[-(i+1)]) != lastbyte: 20 | found = False 21 | if found: 22 | return txt[:-lastbyte] 23 | else: 24 | return txt 25 | 26 | def loadkeys(fname): 27 | j = json.load(open(fname, "r")) 28 | for i in j: 29 | j[i] = base64.b64decode(j[i]) 30 | return j 31 | 32 | def decryptmessage(p, keys): 33 | n = p[:32] 34 | o = p[32:] 35 | i = p[32:48] 36 | d = p[48:] 37 | 38 | decr = AES.new(keys['encKey'], AES.MODE_CBC, i) 39 | sig = hmac.new(keys['macKey'], msg=o, digestmod=hashlib.sha256).digest() 40 | 41 | if n != sig: 42 | raise Exception("Invalid signature") 43 | 44 | return cleanpkcs7(decr.decrypt(d)) 45 | 46 | def decryptfull(v, keys, skiptag=False): 47 | if not skiptag: 48 | if v is None or v.count(",") < 1: 49 | raise Exception("Message error") 50 | 51 | # trovo la ",", la salto 52 | toskip = v.find(",") + 1 53 | p = v[toskip:] 54 | print v[:toskip-1] 55 | else: 56 | p = v 57 | try: 58 | packet = decryptmessage(p, keys) 59 | except: 60 | # salto i primi due interi (interni di whatsapp) 61 | packet = decryptmessage(p[2:], keys) 62 | return packet 63 | 64 | def decryptandparse(v, keys, skiptag=False): 65 | packet = decryptfull(v, keys, skiptag) 66 | return listdecode(packet), packet 67 | 68 | 69 | if __name__ == '__main__': 70 | with open(sys.argv[1], "rb") as f: 71 | v = f.read() 72 | if len(sys.argv) == 3 and sys.argv[2] == '-b': 73 | print repr(decryptfull(v, loadkeys("../tests/keyset2.json"))) 74 | else: 75 | pprint(decryptandparse(v, loadkeys("../tests/keyset2.json"))) 76 | -------------------------------------------------------------------------------- /libwhatsapp/capreader.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | from libwhatsapp.whatsappdecrypt import decryptandparse, loadkeys 3 | from wadeserializer import listdecode 4 | import json 5 | 6 | def decodepcap(fname): 7 | p = rdpcap(fname) 8 | sessions = p.sessions() 9 | for i in sessions: 10 | for packet in sessions[i]: 11 | if not packet.haslayer(TCP): 12 | continue 13 | if (packet[TCP].dport == 5000 or packet[TCP].sport == 5000) \ 14 | and len(packet[TCP].payload) > 0 \ 15 | and str(packet[TCP].payload)[:3] != "GET" \ 16 | and str(packet[TCP].payload)[:4] != "POST" \ 17 | and str(packet[TCP].payload)[:4] != "HTTP": 18 | payload = str(packet[TCP].payload) 19 | while len(payload) > 0: 20 | # WebSocket uses 2 bytes for protocol infos, 4 for mask code if needed, and remaining for payload (max size: 2^7 - 1) 21 | fin = (ord(payload[0]) & 128) > 0 22 | opcode = ord(payload[0]) & 15 23 | # TODO manage special opcodes and fin 24 | masked = (ord(payload[1]) & 128) > 0 25 | length = ord(payload[1]) & 127 26 | if length == 126: 27 | length = ord(payload[2]) << 8 | ord(payload[3]) 28 | payload = payload[4:] 29 | elif length == 127: 30 | length = ord(payload[2]) << 56 | ord(payload[3]) << 48 | ord(payload[4]) << 40 | ord(payload[5]) << 32 \ 31 | | ord(payload[6]) << 24 | ord(payload[7]) << 16 | ord(payload[8]) << 8 | ord(payload[9]) 32 | payload = payload[10:] 33 | else: 34 | payload = payload[2:] 35 | 36 | if masked: 37 | maskingkey = payload[:4] * 32 38 | payload = payload[4:] 39 | 40 | data = payload[:length] 41 | payload = payload[length:] 42 | 43 | if masked: 44 | newdata = "" 45 | for i in xrange(len(data)): 46 | newdata += chr(ord(data[i]) ^ ord(maskingkey[i % 4])) 47 | data = newdata 48 | 49 | if data.count(",") < 1: 50 | if data[0] == '!': 51 | pass #print "Keepalive %d" % (int(data[1:])) 52 | else: 53 | print packet.show() 54 | raise Exception("Packet format error") 55 | elif data == '?,,': 56 | pass #print "Keepalive request" 57 | else: 58 | toskip = data.find(",") + 1 59 | # Sometimes there are two comma... 60 | if toskip < len(data) and data[toskip] == ',': 61 | toskip += 1 62 | 63 | if toskip >= len(data): 64 | # Empty message (ACK) 65 | continue 66 | pass 67 | 68 | messagetag = data[:toskip] 69 | # is it a plain/text JSON? 70 | try: 71 | value = json.loads(data[toskip:]) 72 | print value 73 | continue 74 | except Exception as e: 75 | pass 76 | 77 | # is it encrypted? 78 | try: 79 | print decryptandparse(data[toskip:], loadkeys("../tests/keyset2.json"), True) 80 | continue 81 | except Exception as e: 82 | #print e 83 | pass 84 | 85 | # is it plain binary protocol? 86 | try: 87 | print listdecode(data[toskip:]) 88 | continue 89 | except Exception as e: 90 | print e 91 | pass 92 | 93 | print "Decoding error...: " 94 | print packet.show() 95 | print repr(data) 96 | exit(1) 97 | 98 | if __name__ == '__main__': 99 | decodepcap("../pcapng/whatsapp3.pcap") 100 | -------------------------------------------------------------------------------- /libwhatsapp/binaryprotocol.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | messageTypes = { 4 | "DEBUG_LOG": 1, 5 | "QUERY_RESUME": 2, 6 | "QUERY_RECEIPT": 3, 7 | "QUERY_MEDIA": 4, 8 | "QUERY_CHAT": 5, 9 | "QUERY_CONTACTS": 6, 10 | "QUERY_MESSAGES": 7, 11 | "PRESENCE": 8, 12 | "PRESENCE_SUBSCRIBE": 9, 13 | "GROUP": 10, 14 | "READ": 11, 15 | "CHAT": 12, 16 | "RECEIVED": 13, 17 | "PIC": 14, 18 | "STATUS": 15, 19 | "MESSAGE": 16 20 | } 21 | 22 | # Trova il tipo di messaggio corrispondente al tag 23 | def findType(num): 24 | for i in messageTypes: 25 | if messageTypes[i] == num: 26 | return i 27 | return None 28 | 29 | # Codifica un intero di dimensione variabile 30 | def encodeVarInt(e): 31 | r = 1 if e < 128 else (2 if e < 16384 else (3 if e < 2097152 else (4 if e < 268435456 else \ 32 | (5 if e < 34359738368 else (6 if e < 4398046511104 else (7 if e < 562949953421312 else (8 if e < 72057594037927940 else 9))))))) 33 | if r is None: 34 | raise Exception("Number error: %d" % e) 35 | 36 | eneg = e < 0 37 | if eneg: 38 | e = -e 39 | 40 | n = 10 if eneg else r 41 | s = r 42 | a = e 43 | buf = "" 44 | while s > 4: 45 | c = int(math.floor(a / 128.0)) 46 | p = int(a - 128*c) 47 | buf += chr(128 | 127 & p) 48 | a = c 49 | s -= 1 50 | 51 | while s > 1: 52 | buf += chr(128 | 127 & a) 53 | a >>= 7 54 | s -= 1 55 | 56 | buf += chr(a) 57 | 58 | if eneg: 59 | raise NotImplementedError() 60 | 61 | return buf 62 | 63 | # Legge un intero di dimensione variabile 64 | def readVarInt(e): 65 | r = (ord(e[0]) < 128 and 1) or (len(e)>1 and ord(e[1]) < 128 and 2) or (len(e)>2 and ord(e[2]) < 128 and 3)\ 66 | or (len(e)>3 and ord(e[3]) < 128 and 4) or (len(e)>4 and ord(e[4]) < 128 and 5) or None 67 | if r is None: 68 | return None 69 | 70 | val = 0 71 | for i in xrange(min(r, 4)): 72 | val <<= 7 73 | val = val | 127 & ord(e[(r-1)-i]) 74 | 75 | for i in range(4, r): 76 | val *= 128 77 | val += 127 & ord(e[(r-1)-i]) 78 | 79 | return val, e[r:] 80 | 81 | # Controlla se l'intero di dimensione variabile all'inizio dell'array corrisponde al valore passato 82 | def isparamtype(d, val): 83 | found = True 84 | p = encodeVarInt(val) 85 | for i in xrange(len(p)): 86 | if d[i] != p[i]: 87 | found = False 88 | return found 89 | 90 | # Decodifica il pacchetto dati 91 | def decodepacket(d): 92 | # Struttura: 93 | # '\n5\n' (key) 94 | d = d[2:] 95 | ret = [] 96 | 97 | while len(d) > 0: 98 | if isparamtype(d, 26): # len(1) 99 | msgidlen, d = readVarInt(d[1:]) 100 | 101 | msgid = d[:msgidlen] 102 | d = d[msgidlen:] 103 | # print "Messageid: %s" % msgid 104 | ret.append({'type':'messageid', 'value': msgid}) 105 | elif isparamtype(d, 16): # len(1) 106 | # FromMe: 107 | fromme = ord(d[1]) 108 | 109 | d = d[2:] 110 | # print "FromMe: %d" % fromme 111 | ret.append({'type':'fromme', 'value': fromme}) 112 | elif isparamtype(d, 18): 113 | msglen, d = readVarInt(d[1:]) 114 | 115 | msg = d[:msglen] 116 | ret.append({'type':'message', 'value': decodemessage(msg)}) 117 | #print "Message: %s" % repr(msg) 118 | d = d[msglen:] 119 | elif isparamtype(d, 10): 120 | jidlen, d = readVarInt(d[1:]) 121 | 122 | jid = d[:jidlen] 123 | d = d[jidlen:] 124 | ret.append({'type':'jid', 'value': jid}) 125 | # print "JID %s" % jid 126 | elif isparamtype(d, 24): 127 | # print "Timestamp" 128 | ts, d = readVarInt(d[1:]) 129 | ret.append({'type':'timestamp', 'value': ts}) 130 | 131 | elif isparamtype(d, 32): 132 | status = ord(d[1]) 133 | d = d[2:] 134 | 135 | ret.append({'type':'status', 'value': status}) 136 | # print "Status: %d" % status 137 | elif isparamtype(d, 154): # len(2) 138 | msglen, d = readVarInt(d[2:]) 139 | 140 | sender = d[:msglen] 141 | # print "Sender: %s" % sender 142 | ret.append({'type':'sender', 'value': sender}) 143 | d = d[msglen:] 144 | else: 145 | paramtype = d[0] 146 | raise NotImplementedError("Param: %d %x %s" % (ord(paramtype), ord(paramtype), repr(paramtype))) 147 | return ret 148 | 149 | # Decodifica un "message" nel pacchetto dati 150 | def decodemessage(d): 151 | ret = [] 152 | while len(d) > 0: 153 | if isparamtype(d, 50): 154 | msglen, d = readVarInt(d[1:]) 155 | 156 | exmsg = d[:msglen] 157 | ret.append({'type': 'extended-message', 'value': decoderealmessage(exmsg)}) 158 | d = d[msglen:] 159 | elif isparamtype(d, 10): 160 | msglen, d = readVarInt(d[1:]) 161 | 162 | exmsg = d[:msglen] 163 | # print "Text message: %s" % exmsg 164 | ret.append({'type': 'text', 'value': exmsg}) 165 | d = d[msglen:] 166 | else: 167 | raise NotImplementedError("Param: %d" % ord(d[0])) 168 | return ret 169 | 170 | # Decodifica il secondo livello di "message" nel pacchetto dati 171 | def decoderealmessage(d): 172 | ret = [] 173 | while len(d) > 0: 174 | if isparamtype(d, 10): 175 | msglen, d = readVarInt(d[1:]) 176 | 177 | exmsg = d[:msglen] 178 | # print "Text message: %s" % exmsg 179 | d = d[msglen:] 180 | 181 | ret.append({'type': 'text', 'value': exmsg}) 182 | elif isparamtype(d, 138): 183 | ctxlen, d = readVarInt(d[1:]) 184 | d = d[ctxlen:] 185 | 186 | # TODO 187 | ret.append({'type': 'context', 'value': None}) 188 | else: 189 | raise NotImplementedError("Param: %d" % ord(d[0])) 190 | return ret 191 | 192 | def encodepacket(d): 193 | ret = bytearray("\n5\n") 194 | for item in d: 195 | if item['type'] == 'messageid': 196 | ret += encodeVarInt(26) 197 | ret += encodeVarInt(len(item['value'])) 198 | ret += item['value'] 199 | elif item['type'] == 'fromme': 200 | ret += encodeVarInt(16) 201 | ret += chr(item['value']) 202 | elif item['type'] == 'message': 203 | encmessage = encodemessage(item['value']) 204 | ret += encodeVarInt(18) 205 | ret += encodeVarInt(len(encmessage)) 206 | ret += encmessage 207 | elif item['type'] == 'jid': 208 | ret += encodeVarInt(10) 209 | ret += encodeVarInt(len(item['value'])) 210 | ret += item['value'] 211 | elif item['type'] == 'timestamp': 212 | ret += encodeVarInt(24) 213 | ret += encodeVarInt(item['value']) 214 | elif item['type'] == 'status': 215 | ret += encodeVarInt(32) 216 | ret += chr(item['value']) 217 | elif item['type'] == 'sender': 218 | ret += encodeVarInt(154) 219 | ret += encodeVarInt(len(item['value'])) 220 | ret += item['value'] 221 | else: 222 | raise NotImplementedError("Unimplemented: %s" % repr(item)) 223 | return ret 224 | 225 | # Decodifica un "message" nel pacchetto dati 226 | def encodemessage(d): 227 | ret = bytearray() 228 | for item in d: 229 | if item['type'] == 'extended-message': 230 | encmessage = encoderealmessage(item['value']) 231 | ret += encodeVarInt(50) 232 | ret += encodeVarInt(len(encmessage)) 233 | ret += encmessage 234 | elif item['type'] == 'text': 235 | ret += encodeVarInt(10) 236 | ret += encodeVarInt(len(item['value'])) 237 | ret += item['value'] 238 | else: 239 | raise NotImplementedError("?") 240 | return ret 241 | 242 | def encoderealmessage(d): 243 | ret = bytearray() 244 | for item in d: 245 | if item['type'] == 'text': 246 | ret += encodeVarInt(10) 247 | ret += encodeVarInt(len(item['value'])) 248 | ret += item['value'] 249 | elif item['type'] == 'context': 250 | raise NotImplementedError("?") 251 | else: 252 | raise NotImplementedError("?") 253 | return ret 254 | -------------------------------------------------------------------------------- /dummyserver.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Response 2 | from flask_sockets import Sockets 3 | import time 4 | import libwhatsapp.whatsappdecrypt as wd 5 | import libwhatsapp.whatsappencrypter as we 6 | import json 7 | from Queue import Queue 8 | import base64 9 | import traceback 10 | 11 | 12 | app = Flask(__name__) 13 | sockets = Sockets(app) 14 | 15 | sessions = {} 16 | keys = {"key":"0LjJkYdV78bajkjS/zQaFfXHIu8a5KvCUBqjZduvXS0=","encKey":"M2QJsKzEYfz4AjprixFCN5JRaKhYSqwZga+Lo8Y+WLw=","macKey":"0LjJkYdV78bajkjS/zQaFfXHIu8a5KvCUBqjZduvXS0="} 17 | WAToken1 = "JzZGT+hQ0V8As//PDLU7iZvdh1tseztB8IHQ9RBhwFQ=" 18 | WAToken2 = "0@2SOCx/0H8zVkUXFGChuq1bUkhmjfrTFfMSKePds9FrlrNCakHAhJpyAY/N8UjfzATx06ycrvyTKRlA==" 19 | 20 | 21 | for i in ['key', 'encKey', 'macKey']: 22 | keys[i] = base64.b64decode(keys[i]) 23 | 24 | @sockets.route('/ws') 25 | def wssocket(ws): 26 | global sessions 27 | 28 | browserid = "5vpI1TwSAZUesd0vsCNJ4Q==" 29 | sessions[browserid] = { 30 | 'browserid': browserid, 31 | 'sendq': Queue(), 32 | 'inittag': None, 33 | 'ws': ws, 34 | 'presence': None 35 | } 36 | 37 | while not ws.closed: 38 | message = ws.receive() 39 | if message is not None: 40 | # print "DEBUG: %s" % repr(message) 41 | 42 | if message == '?,,': 43 | # print "keepalive" 44 | ws.send("!%d" % int(time.time()*1000)) 45 | elif type(message) is str: 46 | try: 47 | toskip = message.find(",") + 1 48 | tag = message[:toskip-1] 49 | if message[toskip] == ",": 50 | toskip += 1 51 | data = message[toskip:] 52 | dataj = json.loads(data) 53 | if dataj[0] == 'admin' and dataj[1] == 'init': 54 | # print "Init with version: %d.%d.%d - %s - ID: %s - Remember:%s" % (dataj[2][0], \ 55 | # dataj[2][1], dataj[2][2], " on ".join(dataj[3]), dataj[4], \ 56 | # "Y" if dataj[5] else "N") 57 | sessions[browserid]['inittag'] = tag 58 | resp = { 59 | "status": 200, 60 | "ref": "0@2K00cKKVJGRyADT3cT3TTQRP+qn3axB7PLZ7TDZOJT0e1OrKBSVAHqfn", 61 | "ttl": 20000, 62 | "update": False, 63 | "curr": "%d.%d.%d" % (dataj[2][0], dataj[2][1], dataj[2][2]), 64 | "time": time.time()*1000 65 | } 66 | ws.send("%s,%s" % (tag, json.dumps(resp))) 67 | inited = True 68 | elif dataj[0] == 'admin' and dataj[1] == 'login': 69 | # print "Login with client: %s\nserver: %s\nbrowserid:%s\nMode: %s" % \ 70 | # (dataj[2], dataj[3], dataj[4], dataj[5] if len(dataj) == 6 else "-") 71 | 72 | resp, s1, s2, s3, cts, lastmsg, pkt16 = loginmessages(browserid) 73 | ws.send("%s,%s" % (tag, json.dumps(resp))) 74 | ws.send("s1,%s" % json.dumps(s1)) 75 | ws.send("s2,%s" % json.dumps(s2)) 76 | ws.send("s3,%s" % json.dumps(s3)) 77 | 78 | # contact list 79 | ws.send(we.encryptmessage("preempt-.--4b", cts, keys), True) 80 | 81 | sessions[browserid]['ws'].send(we.encryptmessage("--4c", lastmsg, keys), True) 82 | sessions[browserid]['ws'].send(we.encryptmessage("--4a", pkt16, keys), True) 83 | loggedin = True 84 | elif dataj[0] == 'admin' and dataj[1] == 'Conn' and dataj[2] == 'reref': 85 | resp = { 86 | "status": 200, 87 | "ref": "0@/ZQpqvCgwCD3d2dzgbCaU/HkgzzT+lttQgoL+l5VBpXPR9m/d1eRgibW", 88 | "ttl": 20000 89 | } 90 | ws.send("%s,%s" % (tag, json.dumps(resp))) 91 | # print "Resending data" 92 | elif dataj[0] == "query" and dataj[1] == "ProfilePicThumb": 93 | ws.send("%s,%s" % (tag, json.dumps({"status":404}))) 94 | elif dataj[0] == "query" and dataj[1] == "Presence": 95 | ws.send("%s,%s" % (tag, json.dumps({"t": time.time() }))) 96 | else: 97 | print "Text Unmanaged: %s" % message 98 | except Exception as e: 99 | print e.message 100 | tb = traceback.format_exc() 101 | print tb 102 | ws.close() 103 | else: 104 | toskip = message.find(",") + 1 105 | tag = message[:toskip-1] 106 | if message[toskip] == ",": 107 | toskip += 1 108 | data, packet = wd.decryptandparse(str(message[toskip:]), keys, True) 109 | print data 110 | if type(data) is dict: 111 | if data['name'] == 'action' and type(data['children']) is list: 112 | firstchild = data['children'][0] 113 | if data['attributes']['type'] == 'set' and firstchild['name'] == 'presence': 114 | print "Switch presence to %s" % firstchild['attributes']['type'] 115 | if firstchild['attributes']['type'] == 'available': 116 | sessions[browserid]['presence'] = None 117 | else: 118 | sessions[browserid]['presence'] = time.time() 119 | else: 120 | print repr(packet) 121 | elif data['name'] == 'query' and type(data['children']) is list and len(data['children']) > 0: 122 | firstchild = data['children'][0] 123 | print "Query for %s not implemented yet" % firstchild['name'] 124 | elif data['name'] == 'query': 125 | print "Query for %s not implemented yet" % data['attributes']['type'] 126 | else: 127 | print repr(packet) 128 | else: 129 | print repr(packet) 130 | sessions.pop(browserid) 131 | 132 | 133 | @app.route("/login") 134 | def login(): 135 | browserid = "5vpI1TwSAZUesd0vsCNJ4Q==" 136 | resp, s1, s2, s3, cts, lastmsg, pkt16 = loginmessages(browserid) 137 | sessions[browserid]['ws'].send("%s,%s" % (sessions[browserid]['inittag'], json.dumps(resp))) 138 | sessions[browserid]['ws'].send("s1,%s" % json.dumps(s1)) 139 | sessions[browserid]['ws'].send("s2,%s" % json.dumps(s2)) 140 | sessions[browserid]['ws'].send("s3,%s" % json.dumps(s3)) 141 | # contact list 142 | sessions[browserid]['ws'].send(we.encryptmessage("preempt-.--4b", cts, keys)) 143 | 144 | sessions[browserid]['ws'].send(we.encryptmessage("--4c", lastmsg, keys)) 145 | sessions[browserid]['ws'].send(we.encryptmessage("--4a", pkt16, keys)) 146 | return "Ok" 147 | 148 | @app.route("/wam", methods=['POST']) 149 | def wam(): 150 | resp = Response("") 151 | resp.headers["Access-Control-Allow-Origin"] = "*" 152 | return resp 153 | 154 | def loginmessages(browserToken): 155 | resp = { 156 | "status": 200 157 | } 158 | s1 = [ 159 | 'Conn', 160 | { 161 | "ref": "0@1K00cKKVJGRyADT3cT3TTQRP+qn2axB7PLZ7TDZOJT0e1OrKBSVAHqfn", 162 | "wid": "393319589759@c.us", 163 | "connected": True, 164 | "isResponse": "true", 165 | "serverToken": "0@16tpmIEgDKsCBE8EEfeyfYsOx6owPiDQhKUouV4lEmbBToMgHmHGmridFFusjOEumJtk4jS9rYIY7A==", 166 | "browserToken": browserToken, 167 | "clientToken": "pjZKAdSF6ZqmhByZ2dfcC0m08TKd5Wgn/imRiOZQECY=", 168 | "lc": "IT", 169 | "lg": "it", 170 | "is24h": True, 171 | "protoVersion": [0, 11], 172 | "binVersion": 7, 173 | "battery": 90, 174 | "plugged": False, 175 | "platform": "android", 176 | "features": { 177 | "DOC_TYPES": "csv,doc,docx,pdf,ppt,pptx,rtf,txt,xls,xlsx", 178 | "URL": True 179 | }, 180 | "phone": { 181 | "wa_version":"2.16.396", 182 | "mcc":"222", 183 | "mnc":"001", 184 | "os_version":"4.2.2", 185 | "device_manufacturer":"alps", 186 | "device_model":"106_v89_jbla768_fwvga", 187 | "os_build_number":"2206_jbla768_f_20140429" 188 | }, 189 | "tos": 0, 190 | "pushname":"TESTSERVER" 191 | } 192 | ] 193 | s2 = ["Stream", "update", False, "0.2.2730"] 194 | s3 = [ 195 | "Props", 196 | { 197 | "bucket":"b", 198 | "gifSearch":"tenor", 199 | "SPAM": True, 200 | "SET_BLOCK": True, 201 | "MESSAGE_INFO": True, 202 | "media":64, 203 | "maxSubject":25, 204 | "maxParticipants":257, 205 | "videoMaxEdge":960, 206 | "imageMaxKBytes":1024, 207 | "imageMaxEdge":1600 208 | } 209 | ] 210 | cl = "\xf8\x04M[\x13\xf8\x03\xf8\t\x13-\xfa\xff\x0692\x86w\x93\x01P\x18\xff\x81\x0fW\xff\x05\x14\x81\x13'\x07q \xf8\x0b\x13-\xfa\xff\x0694\x08xv\x81P\x18\xff\x81\x0fW\xff\x05\x14\x81\x89\x10r4Zq \xf8\t\x13-\xfa\xff\x0692\x93\x12bBP\x18\xff\x81\x0fW\xff\x05\x14\x81\x12D\x11q " 211 | lastmsg = "\xf8\x04\t\n/\xf8\x03\xf8\x024\xfcV\n5\n\x1b391230987654@s.whatsapp.net\x10\x01\x1a\x143EB06485910522C93941\x12\x15\n\x13Questa e' una prova\x18\xa3\x95\xa1\xc2\x05 \x03\xf8\x024\xfcK\n5\n\x1b391234567890@s.whatsapp.net\x10\x00\x1a\x143EB0BC5B9622F916D1DC\x12\x06\n\x04test\x18\x80\xba\xcf\xc2\x05 \x00\x9a\x01\x01E\xf8\x024\xfcP\n?\n\x1b391234545454@s.whatsapp.net\x10\x00\x1a\x1e8C695B1DC4DC03B70F1EFCE4228362\x12\x05\n\x03Rrr\x18\xbb\xd4\xa0\xc2\x05 \x00" 212 | pkt16 = '\xf8\x02\t\xf8\x01\xf8\x04\x17[\x8b\xf8\x03\xf8\x034-\xfa\xff\x0694\x08xv\x81P\xf8\x03(-\xfa\xff\x0694\x08xv\x81P\xf8\x03\xfc\x05video-\xfa\xff\x0694\x08xv\x81P''\xf8\x02\t\xf8\x01\xf8\x04\x17[\x8b\xf8\x03\xf8\x034-\xfa\xff\x0694\x08xv\x81P\xf8\x03(-\xfa\xff\x0694\x08xv\x81P\xf8\x03\xfc\x05video-\xfa\xff\x0694\x08xv\x81P' 213 | return resp, s1, s2, s3, cl, lastmsg, pkt16 214 | 215 | if __name__ == "__main__": 216 | from gevent import pywsgi 217 | from geventwebsocket.handler import WebSocketHandler 218 | server = pywsgi.WSGIServer(('', 5000), app, handler_class=WebSocketHandler) 219 | server.serve_forever() 220 | -------------------------------------------------------------------------------- /libwhatsapp/waserializer.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import binaryprotocol as bp 3 | import sys 4 | from wadeserializer import tokens, arraytypes, nibbleDecode, listdecode 5 | 6 | def serialize(data, attrchild=True): 7 | 8 | if not attrchild and type(data) is list and len(data) == 0: 9 | return "" 10 | 11 | ret = bytearray() 12 | ptype = arraytypes['LIST_EMPTY'] if (not attrchild and type(data) is dict and len(data['children']) == 0) \ 13 | or (not attrchild and type(data) is list and len(data) == 0) else arraytypes['LIST_8'] 14 | ret.append(chr(ptype)) 15 | 16 | if attrchild: 17 | # Get final size TODO: add children bytes 18 | size = len(data['attributes']) * 2 + (1 if len(data['children']) == 0 else 2) \ 19 | + (1 if type(data['children']) is dict and data['children'].has_key('type') and data['children']['type'] == 'string' else 0) 20 | ret.append(size) 21 | 22 | # name 23 | if not tokens.has_key(data['name']): 24 | raise Exception("string-token error") 25 | ret.append(tokens[data['name']]) 26 | 27 | # attrs 28 | for k in data['attributes']: 29 | ret.append(putstring(k)) 30 | ret = ret + putstring(data['attributes'][k]) 31 | 32 | if ptype == arraytypes['LIST_EMPTY']: 33 | return ret 34 | 35 | if not attrchild: 36 | ret.append(len(data)) 37 | for c in data: 38 | ret += serialize(c, True) 39 | return ret 40 | 41 | # Make children 42 | if type(data['children']) is list: 43 | children = serialize(data['children'], False) 44 | elif type(data['children']) is dict and data['children'].has_key('type') and data['children']['type'] == 'string': 45 | children = putstring(data['children']['packet']) 46 | elif type(data['children']) is dict and data['children'].has_key('type') and data['children']['type'] == 'binary': 47 | children = bytearray() 48 | 49 | binary = bp.encodepacket(data['children']['packet']) 50 | 51 | lentype = None 52 | binlen = len(binary) 53 | if binlen < 256: 54 | lentype = arraytypes['BINARY_8'] 55 | children += chr(binlen) 56 | elif binlen < 65536: 57 | lentype = arraytypes['BINARY_20'] 58 | children += chr(binlen) & 0x00FF 59 | children += chr(binlen) & 0xFF00 60 | elif binlen < 16777216: 61 | lentype = arraytypes['BINARY_32'] 62 | children += chr(binlen) & 0x0000FF 63 | children += (chr(binlen) & 0x00FF00) >> 8 64 | children += (chr(binlen) & 0xFF0000) >> 16 65 | else: 66 | # Nibbles 67 | raise NotImplementedError("Nibbles size not implemented") 68 | 69 | children += binary 70 | elif type(data['children']) is bytearray: 71 | children = data['children'] 72 | else: 73 | raise Exception(str(type(data['children'])) + " : " + repr(data['children'])) 74 | 75 | if ptype == arraytypes['LIST_8']: 76 | ret += children 77 | elif ptype == arraytypes['LIST_16']: 78 | raise NotImplementedError("LIST_16 not implemented") 79 | return ret 80 | 81 | 82 | def putstring(d): 83 | if tokens.has_key(d): 84 | return chr(tokens[d]) 85 | else: 86 | ret = bytearray() 87 | l = len(d) 88 | typelen = None 89 | if l < 256: 90 | typelen = 252 91 | ret.append(typelen) 92 | ret.append(chr(l)) 93 | elif l < 65536: 94 | typelen = 253 95 | ret.append(typelen) 96 | ret.append(chr(l & 255)) 97 | ret.append(chr((l >> 8) & 255)) 98 | elif l < 16777216: 99 | typelen = 254 100 | ret.append(typelen) 101 | ret.append(chr(l & 255)) 102 | ret.append(chr((l >> 8) & 255)) 103 | ret.append(chr((l >> 16) & 255)) 104 | else: 105 | raise NotImplementedError() 106 | 107 | ret += d 108 | return str(ret) 109 | 110 | # TODO: readstring3 not implemented 111 | 112 | 113 | if __name__ == '__main__': 114 | v = serialize({'attributes': {'epoch': '1', 'type': 'chat'}, 115 | 'children': [], 116 | 'name': 'query', 117 | 'rbytes': 5 118 | }) 119 | print "Chat" 120 | print repr(v) 121 | print listdecode(str(v)) 122 | print "*" * 50 123 | 124 | v = serialize({'attributes': {'epoch': '1', 'type': 'contacts'}, 125 | 'children': [], 126 | 'name': 'query', 127 | 'rbytes': 5 128 | }) 129 | print "Contacts" 130 | print repr(v) 131 | print listdecode(str(v)) 132 | print "*" * 50 133 | 134 | v = serialize({'attributes': {'epoch': '1', 'type': 'set'}, 135 | 'children': [{'attributes': {'type': 'available'}, 136 | 'children': [], 137 | 'name': 'presence', 138 | 'rbytes': 3}], 139 | 'name': 'action', 140 | 'rbytes': 6 141 | }) 142 | print "Presence" 143 | print repr(v) 144 | print listdecode(str(v)) 145 | print "*" * 50 146 | 147 | v = serialize({'attributes': {'type': 'emoji'}, 148 | 'children': [{'attributes': {'code': '\xf0\x9f\x98\x80', 'value': '1.0'}, 149 | 'children': [], 150 | 'name': 'item', 151 | 'rbytes': 5}], 152 | 'name': 'response', 153 | 'rbytes': 4 154 | }) 155 | print "Emoji" 156 | print repr(v) 157 | print listdecode(str(v)) 158 | print "*" * 50 159 | 160 | v = serialize({'attributes': {}, 161 | 'children': [{'attributes': {'type': 'frequent'}, 162 | 'children': [{'attributes': {}, 163 | 'children': {'packet': '391234567890', 164 | 'type': 'string'}, 165 | 'name': 'message', 166 | 'rbytes': 3}, 167 | {'attributes': {}, 168 | 'children': {'packet': '391234567890', 169 | 'type': 'string'}, 170 | 'name': 'message', 171 | 'rbytes': 3}, 172 | {'attributes': {}, 173 | 'children': {'packet': '391234567890', 174 | 'type': 'string'}, 175 | 'name': 'message', 176 | 'rbytes': 3}], 177 | 'name': 'contacts', 178 | 'rbytes': 4}], 179 | 'name': 'action', 180 | 'rbytes': 2}) 181 | print "Frequent" 182 | print repr(v) 183 | print listdecode(str(v)) 184 | print "*" * 50 185 | 186 | v = serialize({'attributes': {'add': 'last'}, 187 | 'children': [{'attributes': {}, 188 | 'children': {'packet': [{'type': 'jid', 189 | 'value': '391230987654@s.whatsapp.net'}, 190 | {'type': 'fromme', 'value': 1}, 191 | {'type': 'messageid', 192 | 'value': '3EB06485910522C93941'}, 193 | {'type': 'message', 194 | 'value': [{'type': 'text', 195 | 'value': "Questa e' una prova"}]}, 196 | {'type': 'timestamp', 197 | 'value': 1481132707}, 198 | {'type': 'status', 'value': 3}], 199 | 'type': 'binary'}, 200 | 'name': 'message', 201 | 'rbytes': 2}, 202 | {'attributes': {}, 203 | 'children': {'packet': [{'type': 'jid', 204 | 'value': '391230987654@s.whatsapp.net'}, 205 | {'type': 'fromme', 'value': 1}, 206 | {'type': 'messageid', 207 | 'value': '3EB06485910522C93941'}, 208 | {'type': 'message', 209 | 'value': [{'type': 'text', 210 | 'value': "Questa e' una prova"}]}, 211 | {'type': 'timestamp', 212 | 'value': 1481132707}, 213 | {'type': 'status', 'value': 3}], 214 | 'type': 'binary'}, 215 | 'name': 'message', 216 | 'rbytes': 2}, 217 | {'attributes': {}, 218 | 'children': {'packet': [{'type': 'jid', 219 | 'value': '391230987654@s.whatsapp.net'}, 220 | {'type': 'fromme', 'value': 1}, 221 | {'type': 'messageid', 222 | 'value': '3EB06485910522C93941'}, 223 | {'type': 'message', 224 | 'value': [{'type': 'text', 225 | 'value': "Questa e' una prova"}]}, 226 | {'type': 'timestamp', 227 | 'value': 1481132707}, 228 | {'type': 'status', 'value': 3}], 229 | 'type': 'binary'}, 230 | 'name': 'message', 231 | 'rbytes': 2}], 232 | 'name': 'action', 233 | 'rbytes': 4}) 234 | print "AddLast" 235 | print repr(v) 236 | print listdecode(str(v)) 237 | print "*" * 50 238 | -------------------------------------------------------------------------------- /libwhatsapp/wadeserializer.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import binaryprotocol as bp 3 | import sys 4 | 5 | # Come funziona: viene serializzato un nodo. Il nodo contiene un nome (espresso 6 | # con un intero che rappresenta un "token"), degli attributi e dei figli. 7 | # In particolare, contiene nell'ordine: #elementi, nome, attributi e figli. 8 | 9 | tokens = { 10 | "200": 3, 11 | "400": 4, 12 | "404": 5, 13 | "409": 110, 14 | "500": 6, 15 | "501": 7, 16 | "502": 8, 17 | "action": 9, 18 | "add": 10, 19 | "admin": 101, 20 | "after": 11, 21 | "archive": 12, 22 | "author": 13, 23 | "available": 14, 24 | "battery": 15, 25 | "before": 16, 26 | "block": 108, 27 | "body": 17, 28 | "broadcast": 18, 29 | "call_log": 133, 30 | "canonical-url": 120, 31 | "chat": 19, 32 | "checksum": 106, 33 | "ciphertext": 135, 34 | "clear": 20, 35 | "code": 21, 36 | "composing": 22, 37 | "contacts": 23, 38 | "count": 24, 39 | "create": 25, 40 | "creator": 102, 41 | "debug": 26, 42 | "delete": 27, 43 | "delivery": 116, 44 | "demote": 28, 45 | "description": 119, 46 | "duplicate": 29, 47 | "emoji": 117, 48 | "encoding": 30, 49 | "epoch": 107, 50 | "error": 31, 51 | "false": 32, 52 | "filehash": 33, 53 | "filename": 125, 54 | "frequent": 139, 55 | "from": 34, 56 | "g.us": 35, 57 | "gif": 137, 58 | "group": 36, 59 | "groups_v2": 37, 60 | "height": 38, 61 | "id": 39, 62 | "identity": 126, 63 | "image": 40, 64 | "in": 41, 65 | "index": 42, 66 | "invis": 43, 67 | "invite": 136, 68 | "item": 44, 69 | "jid": 45, 70 | "kind": 46, 71 | "last": 47, 72 | "leave": 48, 73 | "live": 49, 74 | "log": 50, 75 | "matched-text": 121, 76 | "media": 51, 77 | "media_key": 124, 78 | "media_message": 131, 79 | "message": 52, 80 | "message_info": 115, 81 | "mimetype": 53, 82 | "missing": 54, 83 | "modify": 55, 84 | "modify_tag": 114, 85 | "mute": 99, 86 | "name": 56, 87 | "notification": 57, 88 | "notify": 58, 89 | "out": 59, 90 | "owner": 60, 91 | "page": 128, 92 | "page_count": 129, 93 | "participant": 61, 94 | "paused": 62, 95 | "picture": 63, 96 | "played": 64, 97 | "powersave": 105, 98 | "presence": 65, 99 | "preview": 66, 100 | "previous": 109, 101 | "profile": 134, 102 | "promote": 67, 103 | "query": 68, 104 | "raw": 69, 105 | "read": 70, 106 | "read_only": 100, 107 | "reason": 112, 108 | "receipt": 71, 109 | "received": 72, 110 | "recipient": 73, 111 | "recording": 74, 112 | "relay": 75, 113 | "remove": 76, 114 | "replaced": 111, 115 | "response": 77, 116 | "resume": 78, 117 | "retry": 79, 118 | "s.whatsapp.net": 80, 119 | "search": 130, 120 | "seconds": 81, 121 | "security": 132, 122 | "set": 82, 123 | "short": 103, 124 | "size": 83, 125 | "spam": 113, 126 | "star": 122, 127 | "status": 84, 128 | "subject": 85, 129 | "subscribe": 86, 130 | "t": 87, 131 | "text": 88, 132 | "title": 118, 133 | "to": 89, 134 | "true": 90, 135 | "type": 91, 136 | "unarchive": 92, 137 | "unavailable": 93, 138 | "unread": 127, 139 | "unstar": 123, 140 | "update": 104, 141 | "url": 94, 142 | "user": 95, 143 | "value": 96, 144 | "vcard": 138, 145 | "web": 97, 146 | "width": 98 147 | } 148 | 149 | arraytypes = { 150 | "STREAM_END": 2, 151 | "LIST_EMPTY": 0, 152 | "DICTIONARY_0": 236, 153 | "DICTIONARY_1": 237, 154 | "DICTIONARY_2": 238, 155 | "DICTIONARY_3": 239, 156 | "LIST_8": 248, 157 | "LIST_16": 249, 158 | "JID_PAIR": 250, 159 | "BINARY_8": 252, 160 | "BINARY_20": 253, 161 | "BINARY_32": 254, 162 | "NIBBLE_8": 255, 163 | "SINGLE_BYTE_MAX": 256, 164 | "NIBBLE_MAX": 254 165 | } 166 | 167 | nibbleDecode = { 168 | 0: "0", 169 | 1: "1", 170 | 2: "2", 171 | 3: "3", 172 | 4: "4", 173 | 5: "5", 174 | 6: "6", 175 | 7: "7", 176 | 8: "8", 177 | 9: "9", 178 | 10: "-", 179 | 11: ".", 180 | 15: "\0" 181 | } 182 | 183 | # Trova la chiave corrispondente al token 184 | def findkey(v): 185 | for i in tokens.keys(): 186 | if tokens[i] == v: 187 | return i 188 | return None 189 | 190 | # Analisi della dimensione (in bytes) di un array di elementi serializzato 191 | def arrayrbytes(d): 192 | rbytes = 0 193 | if ord(d[0]) == 0: # Nessun elemento 194 | rbytes = 0 195 | d = d[1:] 196 | elif ord(d[0]) == 248: # Numero di elementi di 1 byte 197 | rbytes = ord(d[1]) 198 | d = d[2:] 199 | elif ord(d[0]) == 249: # Numero di elementi di 2 byte 200 | rbytes = ord(d[1])<<8 + ord(d[2]) 201 | d = d[3:] 202 | else: 203 | # ?? 204 | raise Exception("%d %x %s" % (ord(d[0]), ord(d[0]), d[0])) 205 | return rbytes, d 206 | 207 | # Decodifica lista di elementi serializzata 208 | def listdecode(d): 209 | ret = {} 210 | if ord(d[0]) == arraytypes["LIST_EMPTY"] or ord(d[0]) == arraytypes["LIST_8"] or ord(d[0]) == arraytypes["LIST_16"]: 211 | rbytes, d = arrayrbytes(d) 212 | 213 | name = readstring(d) 214 | d = d[1:] 215 | 216 | attributes, d = readattributes(d) 217 | 218 | children = readchildren(d) 219 | ret = { 220 | 'rbytes': rbytes, 221 | 'name': name, 222 | 'attributes': attributes, 223 | 'children': children 224 | } 225 | elif ord(d[0]) == arraytypes["JID_PAIR"]: 226 | len1 = ord(d[1]) 227 | d1 = readstring2(d[2:2+len1], len1) 228 | d = d[2+len1:] 229 | 230 | len2 = ord(d[0]) 231 | s = readstring2(d[1:len2], len2) 232 | ret = { 233 | 'd': d1, 234 | 's': s 235 | } 236 | # var d = this.readString(e, e.readByte()) 237 | # , s = this.readString(e, e.readByte()); 238 | else: 239 | raise NotImplementedError(ord(d[0])) 240 | return ret 241 | 242 | # Legge una stringa (come token) 243 | def readstring(d): 244 | # TODO 245 | if tokens.values().count(ord(d[0])) == 0: 246 | return None 247 | return findkey(ord(d[0])) 248 | 249 | def readstring2(n, e): 250 | t = 0 251 | r = t + e 252 | o = [] 253 | a = None 254 | i = t 255 | d = e 256 | g = 65533 257 | while i < r: 258 | if len(o) > 5e3: 259 | if a == None: 260 | a = [] 261 | a.push(fromCharArray(o)) 262 | o = [] 263 | 264 | s = 0 | ord(n[i]) 265 | if 0 == (128 & s): 266 | o.append(s) 267 | elif d >= 2 and 192 == (224 & s): 268 | u = 0 | ord(n[i + 1]) 269 | if 128 == (192 & u): 270 | p = (31 & s) << 6 | 63 & u 271 | if p >= 128: 272 | i += 1 273 | d -= 1 274 | o.append(p) 275 | else: 276 | o.append(g) 277 | else: 278 | o.append(g) 279 | elif d >= 3 and 224 == (240 & s): 280 | l = 0 | ord(n[i + 1]) 281 | f = 0 | ord(n[i + 2]) 282 | if 128 == (192 & l) and 128 == (192 & f): 283 | h = (15 & s) << 12 | (63 & l) << 6 | 63 & f 284 | if h >= 2048 and not (55296 <= h and h < 57344): 285 | i += 2 286 | d -= 2 287 | o.append(h) 288 | else: 289 | o.append(g) 290 | else: 291 | o.append(g) 292 | elif d >= 4 and 240 == (248 & s): 293 | m = 0 | ord(n[i + 1]) 294 | tmp = 0 | ord(n[i + 2]) 295 | b = 0 | ord(n[i + 3]) 296 | if 128 == (192 & m) and 128 == (192 & tmp) and 128 == (192 & b): 297 | v = (7 & s) << 18 | (63 & m) << 12 | (63 & tmp) << 6 | 63 & b 298 | if v >= 65536 and v <= 1114111: 299 | i += 3 300 | d -= 3 301 | y = v - 65536 302 | o.append(55296 | y >> 10) 303 | o.append(56320 | 1023 & y) 304 | else: 305 | o.append(g) 306 | else: 307 | o.append(g) 308 | else: 309 | o.append(g) 310 | i += 1 311 | d -= 1 312 | w = fromCharArray(o); 313 | if a != None: 314 | a.push(w) 315 | return "".join(a) 316 | else: 317 | return w 318 | 319 | def fromCharArray(s): 320 | r = "" 321 | for i in s: 322 | r += i 323 | return r 324 | 325 | def readint32(d): 326 | return ord(d[3]) << 24 | ord(d[2]) << 16 | ord(d[1]) << 8 | ord(d[0]); 327 | 328 | def readInt20(d): 329 | e = 15 & d[0] 330 | r = d[1] 331 | n = d[2] 332 | return (e << 16) + (r << 8) + n 333 | 334 | def nibblesToBytes(d): 335 | t = ord(d[0]) 336 | r = t >> 7 337 | n = 127 & t 338 | o = "" 339 | a = 0 340 | i = 1 341 | while a < n: 342 | f = unpackBytePair(ord(d[i])); 343 | o += f[0] 344 | o += f[1] 345 | a += 1 346 | i += 1 347 | if r: 348 | o = o[0: len(o) - 1] 349 | return o, d[i+1:] 350 | 351 | def unpackBytePair(e): 352 | t = (240 & e) >> 4 353 | r = 15 & e 354 | if not nibbleDecode.has_key(t): 355 | raise Exception("invalid nibble to unpack: " + t); 356 | if not nibbleDecode.has_key(r): 357 | raise Exception("invalid nibble to unpack: " + r); 358 | o = nibbleDecode[t] 359 | a = nibbleDecode[r] 360 | return [o, a] 361 | 362 | def getTokenDouble(e, t, r): 363 | n = None 364 | o = 256 * e + t; 365 | if o >= 0 and o < len(r): 366 | n = r[o] 367 | else: 368 | raise Exception("invalid double byte token " + e + " " + t); 369 | return n, r[o:] 370 | 371 | def readstring3(d): 372 | if len(d) == 0: 373 | return None, d 374 | t = ord(d[0]) 375 | d = d[1:] 376 | if t == -1: 377 | raise Exception("invalid start token readstring3") 378 | if t > 2 and t < 236: 379 | r = findkey(t) 380 | if "s.whatsapp.net" == r: 381 | r = "c.us" 382 | return r, d 383 | 384 | if t == arraytypes["DICTIONARY_0"] or t == arraytypes["DICTIONARY_1"] \ 385 | or t == arraytypes["DICTIONARY_2"] or t == arraytypes["DICTIONARY_3"]: 386 | n = ord(d[0]) 387 | f, d = getTokenDouble(t - o.DICTIONARY_0, n, d[1:]) 388 | return f, d 389 | elif t == arraytypes["LIST_EMPTY"]: 390 | return None, d 391 | elif t == arraytypes["BINARY_8"]: 392 | return readstring(d), d[1:] 393 | elif t == arraytypes["BINARY_20"]: 394 | return findkey(readInt20(d)), d[3:] 395 | elif t == arraytypes["BINARY_32"]: 396 | return findkey(readint32(d)), d[4:] 397 | elif t == arraytypes["JID_PAIR"]: 398 | a, d = readstring3(d) 399 | i, d = readstring3(d) 400 | 401 | if a is not None and i is not None: 402 | return a + "@" + i, d 403 | if i is not None: 404 | return i, d 405 | raise Exception("invalid jid " + a + "," + i) 406 | elif t == arraytypes["NIBBLE_8"]: 407 | r, d = nibblesToBytes(d) 408 | return r, d 409 | else: 410 | raise Exception("invalid string: %x" % t) 411 | 412 | 413 | # Legge tutti gli attributi al nodo attuale 414 | def readattributes(d): 415 | attributes = {} 416 | while len(d) > 0 and ord(d[0]) != 0: 417 | k = readstring(d) 418 | if k is None: 419 | break 420 | try: 421 | v = readstring(d[1:]) 422 | if v is None: 423 | v, d = readRawString(d[1:]) 424 | if v is None: 425 | v, d = readstring3(d) 426 | else: 427 | d = d[2:] 428 | except: 429 | pass 430 | if v is None: 431 | break 432 | attributes[k] = v 433 | return attributes, d 434 | 435 | # Legge tutti i figli del nodo attuale 436 | def readchildren(d): 437 | if len(d) == 0: 438 | return [] 439 | children = None 440 | if ord(d[0]) >= 252: 441 | arr, d = readarray(d) 442 | try: 443 | children = { 444 | 'type': 'binary', 445 | 'packet': bp.decodepacket(arr) 446 | } 447 | except: 448 | children = { 449 | 'type': 'binary', 450 | 'packet': str(arr) 451 | } 452 | elif ord(d[0]) == arraytypes["LIST_EMPTY"] or ord(d[0]) == arraytypes["LIST_8"] or ord(d[0]) == arraytypes["LIST_16"]: 453 | elem, d = arrayrbytes(d) 454 | children = [] 455 | # TODO: xrange ? Se elem è il numero di bytes, forse dobbiamo semplicemente leggere il blocco 456 | for i in xrange(elem): 457 | children.append(listdecode(d)) 458 | else: 459 | s = readstring(d) 460 | if s is None: 461 | stringbuf, dnew = readRawString(d) 462 | if stringbuf is None: 463 | s, d = readstring3(d[1:]) 464 | children = { 465 | 'type': 'string', 466 | 'packet': s 467 | } 468 | else: 469 | d = dnew 470 | children = { 471 | 'type': 'string', 472 | 'packet': stringbuf 473 | } 474 | else: 475 | children = { 476 | 'type': 'string', 477 | 'packet': s 478 | } 479 | d = d[1:] 480 | return children 481 | 482 | # Legge una stringa grezza del nodo 483 | def readRawString(d): 484 | if ord(d[0]) == 252: 485 | stringlen = ord(d[1]) 486 | d = d[2:] 487 | elif ord(d[0]) == 253: 488 | stringlen = ord(d[2]) << 8 + ord(d[1]) 489 | d = d[3:] 490 | elif ord(d[0]) == 254: 491 | stringlen = ord(d[3]) << 16 + ord(d[1]) << 8 + ord(d[2]) 492 | d = d[4:] 493 | else: 494 | return None, d 495 | 496 | if stringlen > len(d): 497 | return None, d 498 | 499 | stringbuf = "" 500 | for i in xrange(stringlen): 501 | stringbuf += d[i] 502 | 503 | return stringbuf, d[stringlen:] 504 | 505 | # Legge un array di oggetti 506 | def readarray(d): 507 | size = 0 508 | if ord(d[0]) == arraytypes['BINARY_8']: 509 | size = ord(d[1]) 510 | d = d[2:] 511 | elif ord(d[0]) == arraytypes['BINARY_20']: 512 | size = ord(d[2])<<8 + ord(d[1]) 513 | d = d[3:] 514 | elif ord(d[0]) == arraytypes['BINARY_32']: 515 | size = ord(d[3])<<16 + ord(d[2])<<8 + ord(d[1]) 516 | d = d[4:] 517 | elif ord(d[0]) == arraytypes["NIBBLE_8"]: 518 | r, d = nibblesToBytes(d[1:]) 519 | return r, d 520 | else: 521 | raise NotImplementedError("Not implemented size %x %d" % (ord(d[0]), ord(d[0]))) 522 | 523 | arr = d[0:size] 524 | return arr, d[size:] 525 | 526 | 527 | if __name__ == '__main__': 528 | with open(sys.argv[1], "rb") as f: 529 | v = f.read() 530 | toskip = v.find(",") + 1 531 | print listdecode(v[toskip:]) 532 | -------------------------------------------------------------------------------- /wawebjs/progress_e255aa82dfc08a03f3e15a40a6bbe7de.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2015 WhatsApp Inc. All Rights Reserved. */ 2 | !function(e) { 3 | function a(c) { 4 | if (o[c]) 5 | return o[c].exports; 6 | var l = o[c] = { 7 | exports: {}, 8 | id: c, 9 | loaded: !1 10 | }; 11 | return e[c].call(l.exports, l, l.exports, a), 12 | l.loaded = !0, 13 | l.exports 14 | } 15 | var c = window.webpackJsonp; 16 | window.webpackJsonp = function(o, t, r) { 17 | for (var n, s, f = 0, d = []; f < o.length; f++) 18 | s = o[f], 19 | l[s] && d.push(l[s][0]), 20 | l[s] = 0; 21 | for (n in t) 22 | e[n] = t[n]; 23 | for (c && c(o, t); d.length; ) 24 | d.shift()(); 25 | if (r + 1) 26 | return a(r) 27 | } 28 | ; 29 | var o = {} 30 | , l = { 31 | 3: 0 32 | }; 33 | return a.e = function(e) { 34 | function c() { 35 | t.onerror = t.onload = null , 36 | clearTimeout(r); 37 | var a = l[e]; 38 | 0 !== a && (a && a[1](new Error("Loading chunk " + e + " failed.")), 39 | l[e] = void 0) 40 | } 41 | if (0 === l[e]) 42 | return Promise.resolve(); 43 | if (l[e]) 44 | return l[e][2]; 45 | var o = document.getElementsByTagName("head")[0] 46 | , t = document.createElement("script"); 47 | t.type = "text/javascript", 48 | t.charset = "utf-8", 49 | t.async = !0, 50 | t.timeout = 12e4, 51 | t.src = a.p + "" + ({ 52 | 0: "main", 53 | 1: "style_rtl.useable", 54 | 2: "style.useable", 55 | 5: "unorm", 56 | 6: "locales/zu", 57 | 7: "locales/zh-TW", 58 | 8: "locales/zh-CN", 59 | 9: "locales/vi", 60 | 10: "locales/uz", 61 | 11: "locales/ur", 62 | 12: "locales/uk", 63 | 13: "locales/tr", 64 | 14: "locales/th", 65 | 15: "locales/te", 66 | 16: "locales/ta", 67 | 17: "locales/sw", 68 | 18: "locales/sv", 69 | 19: "locales/sr", 70 | 20: "locales/sq", 71 | 21: "locales/sn", 72 | 22: "locales/sl", 73 | 23: "locales/sk", 74 | 24: "locales/si", 75 | 25: "locales/ru", 76 | 26: "locales/ro", 77 | 27: "locales/pt", 78 | 28: "locales/pt-ZZ", 79 | 29: "locales/pt-BR", 80 | 30: "locales/ps", 81 | 31: "locales/pl", 82 | 32: "locales/pa", 83 | 33: "locales/or", 84 | 34: "locales/nn", 85 | 35: "locales/nl", 86 | 36: "locales/ne", 87 | 37: "locales/nb", 88 | 38: "locales/my", 89 | 39: "locales/ms", 90 | 40: "locales/mr", 91 | 41: "locales/ml", 92 | 42: "locales/mk", 93 | 43: "locales/lv", 94 | 44: "locales/lt", 95 | 45: "locales/lo", 96 | 46: "locales/ky", 97 | 47: "locales/ko", 98 | 48: "locales/kn", 99 | 49: "locales/km", 100 | 50: "locales/kk", 101 | 51: "locales/ka", 102 | 52: "locales/ja", 103 | 53: "locales/it", 104 | 54: "locales/is", 105 | 55: "locales/id", 106 | 56: "locales/hy", 107 | 57: "locales/hu", 108 | 58: "locales/hr", 109 | 59: "locales/hi", 110 | 60: "locales/he", 111 | 61: "locales/ha", 112 | 62: "locales/gu", 113 | 63: "locales/gl", 114 | 64: "locales/fr", 115 | 65: "locales/fil", 116 | 66: "locales/fi", 117 | 67: "locales/fa", 118 | 68: "locales/eu", 119 | 69: "locales/et", 120 | 70: "locales/es", 121 | 71: "locales/el", 122 | 72: "locales/de", 123 | 73: "locales/da", 124 | 74: "locales/cs", 125 | 75: "locales/ca", 126 | 76: "locales/bs", 127 | 77: "locales/bn", 128 | 78: "locales/bg", 129 | 79: "locales/be", 130 | 80: "locales/az", 131 | 81: "locales/ar", 132 | 82: "locales/ak", 133 | 83: "locales/af", 134 | 84: "locales/aa", 135 | 87: "vendor1", 136 | 88: "app", 137 | 89: "vendor2", 138 | 91: "pdf" 139 | }[e] || e) + "_" + { 140 | 0: "90901331917d9c9fd828", 141 | 1: "628ab2f88a55c861db47", 142 | 2: "f84ac3cfe41f736574ad", 143 | 5: "0496e96bc8a37a76385a", 144 | 6: "f329e772ecff38480dcc", 145 | 7: "5ad0576aa8a74745f666", 146 | 8: "4be66c4183df3fac7b83", 147 | 9: "3e35f074eca834671ba8", 148 | 10: "dcea9855866199a42e5d", 149 | 11: "99a4967537ac4ac5ba5d", 150 | 12: "9fc22e0a6d48171d4a82", 151 | 13: "a099a16cbe5cb5e1d986", 152 | 14: "bd86dd43502861c735bb", 153 | 15: "ab90e1b606378333e57c", 154 | 16: "583596c426613c6a1a91", 155 | 17: "fbf71ec085041264918f", 156 | 18: "e9c72ce35392e567464f", 157 | 19: "5f6c358f07ae5d71aa31", 158 | 20: "db4afdef94e42301e240", 159 | 21: "89ee897ac7e1b65918cd", 160 | 22: "05ad7bc6e4a0b61076e4", 161 | 23: "c73929aa5f293c4ae8fa", 162 | 24: "2cf09a81e0456ea31c2d", 163 | 25: "7d60bc8e4081f65b1257", 164 | 26: "1c4bcd961d10110d47c8", 165 | 27: "3025511e6644f57566af", 166 | 28: "395d44106b5beb312406", 167 | 29: "efc83d91f604a53d6639", 168 | 30: "4d49e314fb4b55e8c5f3", 169 | 31: "bdefba205c810a3194cc", 170 | 32: "fe959145be94d00158f7", 171 | 33: "e535cff3f3fb3bdcbde4", 172 | 34: "b14c5f05aa28eeefb2e1", 173 | 35: "34afbac19f29fd405aab", 174 | 36: "269b20f0219da66f389f", 175 | 37: "2efb58b86e57143f7526", 176 | 38: "cc6d8790d5be55b0c111", 177 | 39: "b0a31b503733c9d8e72d", 178 | 40: "da77c31f7b2790806277", 179 | 41: "931f0bb4b17788380abd", 180 | 42: "912149d22c64be26f4cd", 181 | 43: "ae6495abf2efdb36cb01", 182 | 44: "ac8b71d87616e75c3151", 183 | 45: "fc0f6598e168c0590cb1", 184 | 46: "7c342eae6c002a0097f7", 185 | 47: "0a6813918cd83359d2f3", 186 | 48: "6bffdb87854266c54bf3", 187 | 49: "10044e79d0e22ff438e2", 188 | 50: "5351ffc5ff2593d39f03", 189 | 51: "f1d329d4c1f09a3f5fe8", 190 | 52: "60ee7a03bb15ce8b153c", 191 | 53: "4872312d54abc0584ebe", 192 | 54: "4f638331241e181ca189", 193 | 55: "22d84f3c433bb6c08669", 194 | 56: "db1134af73e20ec94fde", 195 | 57: "c069cb38b8ffad55e636", 196 | 58: "0c5f7e6311c682f7af45", 197 | 59: "af34bfec399ba60fd008", 198 | 60: "6a38ade2881ed69a8b70", 199 | 61: "78bc8fe2a3884a2df7b6", 200 | 62: "cbe3e129c7424f39644c", 201 | 63: "0927b8224a8a01f1cac3", 202 | 64: "796857ce951c3e47ad16", 203 | 65: "50fe45d3ef85b607d091", 204 | 66: "2784df7564484dce5242", 205 | 67: "548919c9ab11adda88c8", 206 | 68: "0ae6096da1fab65b70c9", 207 | 69: "1fe14536a47bf9fd4904", 208 | 70: "faf6e99221ecb2f22f6d", 209 | 71: "af452dde0dc4bfc00dca", 210 | 72: "a00f06b958a6a12c011c", 211 | 73: "619167235f545fa068fb", 212 | 74: "61200336358216c534c8", 213 | 75: "b4740162261a6d1d4abb", 214 | 76: "559dca0040f689177bde", 215 | 77: "32d1e2dd18b4aa11ab29", 216 | 78: "f10d42adcb020bb3506a", 217 | 79: "1f5217669e494a7d9633", 218 | 80: "9801f24977cbe9205c3d", 219 | 81: "d82332becd5f09d3e287", 220 | 82: "23699a5b01e5841f2254", 221 | 83: "db38548223e674b217b6", 222 | 84: "70225125ea983da7a260", 223 | 87: "a1ca37909dfb1eb4d886", 224 | 88: "ac8346fe9c3ec36d4136", 225 | 89: "28ef5c0c89de59b40187", 226 | 90: "24138d4ed6bab42136a6", 227 | 91: "34e66f09f93564de48e2" 228 | }[e] + ".js"; 229 | var r = setTimeout(c, 12e4); 230 | t.onerror = t.onload = c, 231 | o.appendChild(t); 232 | var n = new Promise(function(a, c) { 233 | l[e] = [a, c] 234 | } 235 | ); 236 | return l[e][2] = n 237 | } 238 | , 239 | a.m = e, 240 | a.c = o, 241 | a.oe = function(e) { 242 | throw e 243 | } 244 | , 245 | a.p = "/", 246 | a(a.s = 485) 247 | }({ 248 | 8: function(e, a) { 249 | var c = e.exports = { 250 | version: "2.4.0" 251 | }; 252 | "number" == typeof __e && (__e = c) 253 | }, 254 | 21: function(e, a, c) { 255 | e.exports = { 256 | "default": c(518), 257 | __esModule: !0 258 | } 259 | }, 260 | 86: function(e, a) { 261 | "use strict"; 262 | e.exports = function() { 263 | try { 264 | if (!navigator.serviceWorker) 265 | return !1; 266 | var e = navigator.userAgent; 267 | if (/\bEdge\//.test(e)) 268 | return !1; 269 | var a = /\bChrom(?:e|ium)\/(\d+)/.exec(e); 270 | return a ? parseInt(a[1], 10) >= 47 : (a = /\brv:(\d+)/.exec(e), 271 | !(!a || !/\bGecko\/\d+/.test(e)) && parseInt(a[1], 10) >= 47) 272 | } catch (c) { 273 | return !1 274 | } 275 | }() 276 | }, 277 | 132: function(e, a, c) { 278 | "use strict"; 279 | function o(e) { 280 | return e && e.__esModule ? e : { 281 | "default": e 282 | } 283 | } 284 | function l(e) { 285 | var a = "progress_script_" + e 286 | , c = document.getElementById(a); 287 | c && document.head.removeChild(c); 288 | var o = document.createElement("script"); 289 | o.id = a, 290 | o.type = "text/javascript", 291 | o.charset = "utf-8", 292 | o.async = !0, 293 | o.onload = function() { 294 | o.onload = void 0, 295 | o.onerror = void 0, 296 | clearTimeout(_[e]), 297 | t(e) 298 | } 299 | , 300 | o.onerror = function() { 301 | o.onload = void 0, 302 | o.onerror = void 0, 303 | clearTimeout(_[e]), 304 | r(e, !1) 305 | } 306 | , 307 | _[e] = window.setTimeout(function() { 308 | o.onload = void 0, 309 | o.onerror = void 0, 310 | r(e, !0) 311 | }, 12e4), 312 | o.src = m[e].src, 313 | document.head.appendChild(o) 314 | } 315 | function t(e) { 316 | y++, 317 | w += m[e].size, 318 | h && h.setAttribute("value", w), 319 | y === m.length ? (n(), 320 | window.Exe()) : N && l(e + 1) 321 | } 322 | function r(e, a) { 323 | b(e, a), 324 | k = Math.min(k + 1, x); 325 | var c = 1e3 * Math.pow(2, k) * (1 + .1 * Math.random()); 326 | window.setTimeout(function() { 327 | l(e) 328 | }, c) 329 | } 330 | function n() { 331 | var e = document.createElement("script"); 332 | e.type = "text/javascript", 333 | e.charset = "utf-8", 334 | e.async = !0, 335 | e.src = g, 336 | document.head.appendChild(e) 337 | } 338 | function s() { 339 | var e = "WhatsApp/" + S.VERSION_STR 340 | , a = "Web/Unparsed-0.0.0" 341 | , c = "Device/Unparsed"; 342 | return e + " " + a + " " + c 343 | } 344 | function f() { 345 | var e = new Date 346 | , a = d(e.getMonth(), 2) 347 | , c = d(e.getHours(), 2) 348 | , o = d(e.getMinutes(), 2) 349 | , l = d(e.getSeconds(), 2); 350 | return e.getFullYear() + "-" + a + "-" + e.getDate() + " " + c + ":" + o + ":" + l + ": " 351 | } 352 | function d(e, a) { 353 | var c = e.toString() 354 | , o = a - c.length; 355 | return o > 0 ? Array(o + 1).join("0") + c : c 356 | } 357 | function i() { 358 | if (u) 359 | return u; 360 | try { 361 | u = JSON.parse(window.localStorage.getItem(S.KEY_UNKNOWN_ID)), 362 | u && (u = u.replace("-", "")) 363 | } catch (e) {} 364 | if (!u) { 365 | u = "unknown" + Math.floor(1e10 * Math.random()); 366 | try { 367 | window.localStorage.setItem(S.KEY_UNKNOWN_ID, (0, 368 | v["default"])(u)) 369 | } catch (e) {} 370 | } 371 | return u 372 | } 373 | function b(e, a) { 374 | var c = f() 375 | , o = a ? "Timeout" : "OnError" 376 | , l = [c + "==================================================", c + ("error: Missing Dependencies (" + o + ") generation"), c + ("reason for logs: Missing Lib: " + m[e].src + " (" + o + ")"), c + ("userAgent: " + window.navigator.userAgent), c + "==================================================", c + ("Generation: " + k)].join("\n") 377 | , t = new FormData 378 | , r = new Blob([l],{ 379 | type: "text/plain" 380 | }); 381 | t.append("from", i()), 382 | t.append("agent", s()), 383 | t.append("file", r, "logs.txt"); 384 | var n = new XMLHttpRequest; 385 | n.open("POST", S.CLB_URL, !0), 386 | n.send(t) 387 | } 388 | var u, p = c(21), v = o(p), m = [{ 389 | "src": "/vendor1_a1ca37909dfb1eb4d886.js", 390 | "size": 549 391 | }, { 392 | "src": "/vendor2_97fbe2b3c08f4fc42b7a.js", 393 | "size": 320 394 | }, { 395 | "src": "/app_ac8346fe9c3ec36d4136.js", 396 | "size": 757 397 | }], g = "/main_90901331917d9c9fd828.js", h = document.getElementById("progressbar"), w = 0, _ = {}, y = 0, k = 0, N = !0, S = { 398 | VERSION_STR: "0.2.2730", 399 | CLB_URL: "https://web-crashlog.whatsapp.net/upload.php", 400 | KEY_UNKNOWN_ID: "WAUnknownID" 401 | }, x = 10, E = c(86); 402 | if (E && !self.navigator.serviceWorker.controller) 403 | try { 404 | self.navigator.serviceWorker.register("/serviceworker.js", { 405 | scope: "/" 406 | }) 407 | } catch (I) {} 408 | if (N) 409 | l(0); 410 | else { 411 | for (var M = 0; M < m.length; M++) 412 | l(M); 413 | n() 414 | } 415 | }, 416 | 485: function(e, a, c) { 417 | "use strict"; 418 | c(132) 419 | }, 420 | 518: function(e, a, c) { 421 | var o = c(8) 422 | , l = o.JSON || (o.JSON = { 423 | stringify: JSON.stringify 424 | }); 425 | e.exports = function(e) { 426 | return l.stringify.apply(l, arguments) 427 | } 428 | } 429 | }); 430 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | 2 | Version 0.2.2730 3 | 4 | # Local storage 5 | 6 | On web browser, whatsapp sets these strings: 7 | 8 | * `localStorage.WASecretBundle`: a JSON with keys for encryption and signature 9 | * `localStorage.WAToken1`: key client token 10 | * `localStorage.WAToken2`: key server token 11 | * `localStorage.WALangPref`: two-char language code 12 | * `localStorage.WABrowserId`: a browser ID self-generated 13 | 14 | # Main WebSocket channel protocol 15 | 16 | The socket used for C&C is bound to `w[1-8].web.whatsapp.com`. By now these domains 17 | are CNAME of `dyn.web.whatsapp.com` which is a round-robin `A` service. 18 | 19 | The socket is protected with SSL/TLS. No WebSocket extensions are used. The endpoint is `/ws`. 20 | 21 | ## Encryption 22 | 23 | Binary messages (message text and others kind of communications) are encrypted with 24 | AES-CBC (PKCS7 padded) and signed with HMAC-SHA256. 25 | 26 | The format of binary websocket is: 27 | 28 | , 29 | 30 | * `` is the plaintext ID/code that the webapp use to link this message to others (see above). 31 | * `` is the 32 byte HMAC-SHA256 signature of the remaining (IV and ciphertext) 32 | * `` is the IV for AES-CBC 33 | * `` is the binary protocol / serialized data (see below) encrypted 34 | 35 | Keys are stored into local storage, into the Web Browser. By now you should estract them manually. 36 | 37 | Please note that sometimes (most of the time on sending packets) two bytes are added 38 | before ``. Meanings is unknown. 39 | 40 | ## Protocol 41 | 42 | Packets are in-order. 43 | 44 | ### pre-login 45 | 46 | #### qrcode regeneration request 47 | 48 | 1482519306.--1,["admin","Conn","reref"] 49 | 50 | #### qrcode regeneration response 51 | 52 | 1482519306.--1,{"status":200,"ref":"0@/ZQpqvCgwCD3d3dzgbCaU/HkgzzT+lttQgoL+l5VBpXPR9m/d0eRgibW","ttl":20000} 53 | 54 | ### Init 55 | 56 | Packets 33->56 (wireshark1.pcapng.gz) 57 | 58 | #### Login Init (c->s) 59 | 60 | 1481798371.--0,["admin","init",[0,2,2730],["Linux x86_64","Firefox"],"5vpI1TwSAZUesd0vsCNJ4Q==",false] 61 | 62 | This packet contains some info about version and OS, and a base64-encoded string called "browser ID" (found on localStorage.WABrowserId) 63 | 64 | Last parameters (boolean) is: !this.isIncognito && !!b.getRememberMe() 65 | 66 | ##### Browser ID 67 | 68 | id: function i() { 69 | var e = r(15), i = e.getBrowserId(); 70 | if (i) 71 | return i; 72 | var t = new Uint32Array(4); 73 | window.crypto.getRandomValues(t); 74 | var n = CryptoJS.lib.WordArray.create(t); 75 | i = CryptoJS.enc.Base64.stringify(n); 76 | e.setBrowserId(i); 77 | return i 78 | } 79 | 80 | #### Login Init response (s->c) 81 | 82 | 1481798371.--0,{"status":200,"ref":"0@2K01cKKVJGRyADT3cT3TTQRP+qn2axB7PLZ7TDZOJT1e1OrKBSVAHqfn","ttl":20000,"update":false,"curr":"0.2.2730","time":1481798371929.0} 83 | 84 | #### Login auth (c->s) 85 | 86 | 1481798371.--1,["admin","login","tBBNlZlb/6nlX6GtM3Fw66AgnXElWwyYOYVmjKD7MZo=","0@3PAE9OnHOQeL29k/X+zXfp6SYZpuHLhazQRIIeotMgtut9iwSsVZuUTTzjducvyCZp1/AIACwb7eYg==","5vpI1TwSAZUesd0vsCNJ4Q==","takeover"] 87 | 88 | Here we have: 89 | 90 | * "admin" and "login" literals 91 | * "client" client token (see at top) 92 | * "server" server token (see at top) 93 | * "browser id" (see above) 94 | * "takeover" or "reconnect" literals 95 | 96 | #### Login auth response (s->c) 97 | 98 | 4 Packets (3 text, 1 binary): 99 | 100 | s1,["Conn",{"ref":"0@G8kirzpZCKC3mJpk7uHH6aKCAYU/009+bnujFPMRHiijAkVx4pRGpbyI","wid":"391234567890@c.us","connected":true,"isResponse":"true","serverToken":"0@16tpmIEgDKsCBE8EEfEYfYsOx6owPiDQhKUouV4lEmbBToMgHmHGmridFFusjOEumJtk4jS9rYIY7A==","browserToken":"0@hJe75y+lm4BukKVTCOI4SZCX+1sEi2zyLxsqWw8020jHk70ViGwk/OaiVdgQfK/puPL4zYhQTH6HfNfTXommBT3ls5+YbL8oHRX0nP3xFMUK15iDNQi2CY7+pwx0UAvt+ZMhPy2pdWgZsLv0QaOCpQ==","clientToken":"pjZKAdSF7ZqmhByZ2dfcC0m08TKd5Wgn/imRiOZQECY=","lc":"IT","lg":"it","is24h":true,"protoVersion":[0,11],"binVersion":7,"battery":90,"plugged":false,"platform":"android","features":{"DOC_TYPES":"csv,doc,docx,pdf,ppt,pptx,rtf,txt,xls,xlsx","URL":true},"phone":{"wa_version":"2.16.396","mcc":"222","mnc":"001","os_version":"4.2.2","device_manufacturer":"alps","device_model":"106_v89_jbla768_fwvga","os_build_number":"2206_jbla768_f_20140429"},"tos":0,"pushname":"A"}] 101 | 102 | Here the server updates serverToken and clientToken, as well as provide us "ref" token and "browserToken" (seems used only for logout). 103 | 104 | 1481798371.--1,{"status":200} 105 | 106 | Maybe this is a reply. 107 | 108 | s2,["Stream","update",false,"0.2.2730"] 109 | 110 | If requested, we need to "update" our version. TODO: library should monitoring it 111 | 112 | s3,["Props",{"bucket":"b","gifSearch":"tenor","SPAM":true,"SET_BLOCK":true,"MESSAGE_INFO":true,"media":64,"maxSubject":25,"maxParticipants":257,"videoMaxEdge":960,"imageMaxKBytes":1024,"imageMaxEdge":1600}] 113 | 114 | The 4th one is encrypted (binary). You can find the packet into `tests/pkt_3_14-3`. 115 | It contains the contact list: 116 | 117 | {'attributes': {'type': 'chat'}, 118 | 'children': [{'attributes': {'jid': '391234567890@count'}, 119 | 'children': {'packet': [], 'type': 'binary'}, 120 | 'name': 'chat', 121 | 'rbytes': 9}, 122 | {'attributes': {'jid': '391231212123@count'}, 123 | 'children': {'packet': [], 'type': 'binary'}, 124 | 'name': 'chat', 125 | 'rbytes': 9}, 126 | {'attributes': {'jid': '4412345412343@count'}, 127 | 'children': {'packet': [], 'type': 'binary'}, 128 | 'name': 'chat', 129 | 'rbytes': 9}], 130 | 'name': 'response', 131 | 'rbytes': 4} 132 | 133 | #### Login auth response 2 (s->c) 134 | 135 | Last message for each chat, binary encoded and encrypted. `tests/pkt_3_14-5` 136 | 137 | {'attributes': {'add': 'last'}, 138 | 'children': [{'attributes': {}, 139 | 'children': {'packet': [{'type': 'jid', 140 | 'value': '391230987654@s.whatsapp.net'}, 141 | {'type': 'fromme', 'value': 1}, 142 | {'type': 'messageid', 143 | 'value': '3EB06485910522C93941'}, 144 | {'type': 'message', 145 | 'value': [{'type': 'text', 146 | 'value': "Questa e' una prova"}]}, 147 | {'type': 'timestamp', 148 | 'value': 1481132707}, 149 | {'type': 'status', 'value': 3}], 150 | 'type': 'binary'}, 151 | 'name': 'message', 152 | 'rbytes': 2}, 153 | {'attributes': {}, 154 | 'children': {'packet': [{'type': 'jid', 155 | 'value': '391230987654@s.whatsapp.net'}, 156 | {'type': 'fromme', 'value': 1}, 157 | {'type': 'messageid', 158 | 'value': '3EB06485910522C93941'}, 159 | {'type': 'message', 160 | 'value': [{'type': 'text', 161 | 'value': "Questa e' una prova"}]}, 162 | {'type': 'timestamp', 163 | 'value': 1481132707}, 164 | {'type': 'status', 'value': 3}], 165 | 'type': 'binary'}, 166 | 'name': 'message', 167 | 'rbytes': 2}, 168 | {'attributes': {}, 169 | 'children': {'packet': [{'type': 'jid', 170 | 'value': '391230987654@s.whatsapp.net'}, 171 | {'type': 'fromme', 'value': 1}, 172 | {'type': 'messageid', 173 | 'value': '3EB06485910522C93941'}, 174 | {'type': 'message', 175 | 'value': [{'type': 'text', 176 | 'value': "Questa e' una prova"}]}, 177 | {'type': 'timestamp', 178 | 'value': 1481132707}, 179 | {'type': 'status', 'value': 3}], 180 | 'type': 'binary'}, 181 | 'name': 'message', 182 | 'rbytes': 2}], 183 | 'name': 'action', 184 | 'rbytes': 4} 185 | 186 | #### Login auth response 3 (s->c) 187 | 188 | The packet is encrypted (binary). You can find the packet into `tests/pkt_3_16` 189 | It seems to be the same as the 4th of login auth response 1. 190 | 191 | {'attributes': {}, 192 | 'children': [{'attributes': {'type': 'frequent'}, 193 | 'children': [{'attributes': {}, 194 | 'children': {'packet': '391234567890', 195 | 'type': 'string'}, 196 | 'name': 'message', 197 | 'rbytes': 3}, 198 | {'attributes': {}, 199 | 'children': {'packet': '391234567890', 200 | 'type': 'string'}, 201 | 'name': 'message', 202 | 'rbytes': 3}, 203 | {'attributes': {}, 204 | 'children': {'packet': '391234567890', 205 | 'type': 'string'}, 206 | 'name': 'message', 207 | 'rbytes': 3}], 208 | 'name': 'contacts', 209 | 'rbytes': 4}], 210 | 'name': 'action', 211 | 'rbytes': 2} 212 | 213 | #### Login post-auth request 1 (c->s) 214 | 215 | The packet is encrypted (binary-masked). You can find the packet into `tests/pkt_3_18` 216 | Maybe this packets is used to query active chats 217 | 218 | {'attributes': {'epoch': '1', 'type': 'chat'}, 219 | 'children': [], 220 | 'name': 'query', 221 | 'rbytes': 5} 222 | 223 | #### Login post-auth request 2 (c->s) 224 | 225 | The packet is encrypted (binary-masked). You can find the packet into `tests/pkt_3_20` 226 | Maybe this packets is used to query contact list 227 | 228 | {'attributes': {'epoch': '1', 'type': 'contacts'}, 229 | 'children': [], 230 | 'name': 'query', 231 | 'rbytes': 5} 232 | 233 | #### Login post-auth request 3 (c->s) 234 | 235 | The packet is encrypted (binary-masked). You can find the packet into `tests/pkt_3_22` 236 | This packets is used set the presence (available|unavailable) 237 | 238 | {'attributes': {'epoch': '1', 'type': 'set'}, 239 | 'children': [{'attributes': {'type': 'available'}, 240 | 'children': [], 241 | 'name': 'presence', 242 | 'rbytes': 3}], 243 | 'name': 'action', 244 | 'rbytes': 6} 245 | 246 | #### Login post-auth request 4 (c->s) 247 | 248 | The packet is encrypted (binary-masked). You can find the packet into `tests/pkt_3_24` 249 | Maybe this packets is used to request emoji? 250 | 251 | {'attributes': {'epoch': '1', 'type': 'emoji'}, 252 | 'children': [], 253 | 'name': 'query', 254 | 'rbytes': 5} 255 | 256 | #### Login query-picture request 1 (c->s) 257 | 258 | 371.--6,,["query","ProfilePicThumb","391234567890@c.us"] 259 | 260 | #### Login post-auth response 1 (s->c) 261 | 262 | 2 packets. 263 | 264 | The packet is encrypted (binary-masked). You can find the packet into `tests/pkt_3_44-1` 265 | Apparently it's a dump of last messages (reply for chats? - see below) 266 | 267 | {'attributes': {'add': 'before', 'last': 'true'}, 268 | 'children': [{'attributes': {}, 269 | 'children': {'packet': [{'type': 'jid', 270 | 'value': '391234567890@s.whatsapp.net'}, 271 | {'type': 'fromme', 'value': 1}, 272 | {'type': 'messageid', 273 | 'value': '3EB09F24FC9C2C2D8683'}, 274 | {'type': 'message', 275 | 'value': [{'type': 'text', 276 | 'value': 'test'}]}, 277 | {'type': 'timestamp', 278 | 'value': 1481879920}, 279 | {'type': 'status', 'value': 3}], 280 | 'type': 'binary'}, 281 | 'name': 'message', 282 | 'rbytes': 2}, 283 | {'attributes': {}, 284 | 'children': {'packet': [{'type': 'jid', 285 | 'value': '391234567890@s.whatsapp.net'}, 286 | {'type': 'fromme', 'value': 1}, 287 | {'type': 'messageid', 288 | 'value': '3EB09F24FC9C2C2D8683'}, 289 | {'type': 'message', 290 | 'value': [{'type': 'text', 291 | 'value': 'test'}]}, 292 | {'type': 'timestamp', 293 | 'value': 1481879920}, 294 | {'type': 'status', 'value': 3}], 295 | 'type': 'binary'}, 296 | 'name': 'message', 297 | 'rbytes': 2}, 298 | {'attributes': {}, 299 | 'children': {'packet': [{'type': 'jid', 300 | 'value': '391234567890@s.whatsapp.net'}, 301 | {'type': 'fromme', 'value': 1}, 302 | {'type': 'messageid', 303 | 'value': '3EB09F24FC9C2C2D8683'}, 304 | {'type': 'message', 305 | 'value': [{'type': 'text', 306 | 'value': 'test'}]}, 307 | {'type': 'timestamp', 308 | 'value': 1481879920}, 309 | {'type': 'status', 'value': 3}], 310 | 'type': 'binary'}, 311 | 'name': 'message', 312 | 'rbytes': 2}, 313 | ... 314 | {'attributes': {}, 315 | 'children': {'packet': [{'type': 'jid', 316 | 'value': '391234567890@s.whatsapp.net'}, 317 | {'type': 'fromme', 'value': 1}, 318 | {'type': 'messageid', 319 | 'value': '3EB09F24FC9C2C2D8683'}, 320 | {'type': 'message', 321 | 'value': [{'type': 'text', 322 | 'value': 'test'}]}, 323 | {'type': 'timestamp', 324 | 'value': 1481879920}, 325 | {'type': 'status', 'value': 3}], 326 | 'type': 'binary'}, 327 | 'name': 'message', 328 | 'rbytes': 2}], 329 | 'name': 'action', 330 | 'rbytes': 6} 331 | 332 | The packet is encrypted (binary-masked). You can find the packet into `tests/pkt_3_44-2` 333 | It contains a checksum for contact list 334 | 335 | {'attributes': {'checksum': '5UBQhqH4qM1TC0nvNBHtFag==', 'type': 'contacts'}, 336 | 'children': [], 337 | 'name': 'response', 338 | 'rbytes': 5} 339 | 340 | #### Login post-auth response 2 (s->c) 341 | 342 | 2 packets: `tests/pkt_3_51-1` and `tests/pkt_3_51-2`. 343 | 344 | The former contains a reponse with tag "duplicate" for chat, the latter contains the same for contacts. 345 | 346 | Pkt_3_51-1: 347 | 348 | {'attributes': {'duplicate': 'true', 'type': 'chat'}, 349 | 'children': [], 350 | 'name': 'response', 351 | 'rbytes': 5} 352 | 353 | Pkt_3_51-2: 354 | 355 | {'attributes': {'duplicate': 'true', 'type': 'contacts'}, 356 | 'children': [], 357 | 'name': 'response', 358 | 'rbytes': 5} 359 | 360 | #### Login post-auth response 3 (s->c) 361 | 362 | `tests/pkt_3_53` contains the emoji response 363 | 364 | {'attributes': {'type': 'emoji'}, 365 | 'children': [{'attributes': {'code': '\xf0\x9f\x98\x80', 'value': '1.0'}, 366 | 'children': [], 367 | 'name': 'item', 368 | 'rbytes': 5}], 369 | 'name': 'response', 370 | 'rbytes': 4} 371 | 372 | ### JID picture query 373 | 374 | Packets 60, 75, 375 | 376 | #### JID picture request (c->s) 377 | 378 | 371.--7,,["query","ProfilePicThumb","391234567890@c.us"] 379 | 380 | #### JID picture ACK (s->c) 381 | 382 | 371.--7, 383 | 384 | 385 | #### JID picture response (s->c) 386 | 387 | 371.--7,{"tag":"1430075072"} 388 | 389 | Then this URL is downloaded for the Picture: https://dyn.web.whatsapp.com/pp?t=s&u=391234567890%40c.us&i=1430075072&ref=&tok= 390 | 391 | Query String parameters: 392 | 393 | * `t` unknown (maybe command?) 394 | * `u` the user for the picture 395 | * `i` the tag that we received 396 | * `ref` is the same "ref" that we found into INIT packets (Login auth response) 397 | * `tok` is the same "serverToken" we found into INIT packets 398 | 399 | Note that the response can be: 400 | 401 | 371.--9,{"status":404} 402 | 403 | ### JID last-seen 404 | 405 | #### Last-seen request (c->s) 406 | 407 | 371.--10,,["query","Presence","lastseen","391234567890@c.us"] 408 | 409 | #### Last-seen ACK (s->c) 410 | 411 | 371.--10, 412 | 413 | #### Last-seen response (s->c) 414 | 415 | 371.--10,{"t":1481789838} 416 | 417 | I think that, as for pictures, the response JSON can contains only "status" with value "404" for full-privacy users. 418 | 419 | ### Keep-alive 420 | 421 | #### Keep-alive request (c->s) 422 | 423 | ?,, 424 | 425 | #### Keep-alive response (s->c) 426 | 427 | !1481799299025 428 | 429 | It's unknown if the time is only for web-client checking, or also for phone-checking. 430 | 431 | ### Send messages 432 | 433 | #### Send message request (c->s) 434 | 435 | Binary-encrypted packet (see Binary protocol chapter below). Decoded with this library: 436 | 437 | {'attributes': {'epoch': '103', 'type': 'relay'}, 'elements': 6, 'name': 'action', 'children': [{'attributes': {}, 'elements': 2, 'name': 'message', 'children': [[{'type': 'jid', 'value': '391234567890@s.whatsapp.net'}, {'type': 'fromme', 'value': 1}, {'type': 'messageid', 'value': '3EB06792C2106FE24217'}, {'type': 'message', 'value': [{'type': 'extended-message', 'value': [{'type': 'text', 'value': 'prova1234567890'}, {'type': 'context', 'value': None}]}]}, {'type': 'timestamp', 'value': 1481879713}, {'type': 'status', 'value': 1}]]}]} 438 | 439 | #### Send message ACK (s->c) 440 | 441 | 3EB06792C2106FE24217, 442 | 443 | #### Send message response (s->c) 444 | 445 | 3EB06792C2106FE24217,{"status":200,"t":1481879714} 446 | 447 | This packet marks the packet as "sent", not "delivered". TODO: check this statement 448 | 449 | #### Reconnection (s->c) 450 | 451 | Phone reconnection infos 452 | 453 | 1 text packet 454 | 455 | s22,["Conn",{"protoVersion":[0,11],"binVersion":7,"phone":{"wa_version":"2.16.396","mcc":"222","mnc":"088","os_version":"6.0.1","device_manufacturer":"LGE","device_model":"hammerhead","os_build_number":"MMB29Q"},"features":{"DOC_TYPES":"csv,doc,docx,pdf,ppt,pptx,rtf,txt,xls,xlsx","URL":true},"pushname":"E"}] 456 | 457 | ### TODO 458 | 459 | Pkts 347, 197 460 | 461 | ## Binary protocol 462 | 463 | Some WebSocket packets are stored/serialized in binary TLV format. Some points: 464 | 465 | * Strings are tokenized, eg. an integer is written in place of a string 466 | * Numbers are stored as fixed type (int8, int16, int32) or in a variable-size type 467 | * Text is always UTF 8 468 | 469 | TLV structure: 470 | 471 | 472 | 473 | `` and `` are VarInts. Length includes only the `` part. 474 | 475 | TLV records can be nested of course. 476 | 477 | ## Serialized data 478 | 479 | Some packets are serialized data. A "serialized-packet" contains nodes with different types: 480 | 481 | * Lists 482 | * JID (pair) 483 | * Dictionaries 484 | * Binary data 485 | * Nibble 486 | * String-token 487 | * Raw strings 488 | 489 | At first, you can find: 490 | 491 | * Lists 492 | * JID 493 | 494 | ### Lists 495 | 496 | 497 | 498 | List type can be LIST_EMPTY, LIST_8 or LIST_16 499 | 500 | * If LIST_EMPTY, there are no elements/attributes 501 | * If LIST_8, the next byte (8-bit unsigned int) is the list length 502 | * If LIST_8, next 2 bytes (16-bit unsigned int) is the list length 503 | 504 | Then you find a "string-token" (1 byte - see source for mapping) for list name 505 | 506 | Then, you can find the attribute list. An attribute is a key-value pair, where "key" 507 | is a string-token, and "value" can be any type. 508 | 509 | Last item is child/children: it can be a bytearray (binary-protocol), a list, a string-token or raw string. 510 | 511 | ### JID pair 512 | 513 | Is a pair of string-token items 514 | 515 | ### De-serialized messages 516 | 517 | * name=query 518 | * type=emoji 519 | * type=contacts 520 | * type=chat 521 | * type=receipt 522 | * name=last (attributes: jid, children: msgid) 523 | * name=action 524 | * type=relay 525 | * name=messages (for N messages) 526 | * type=set 527 | * name=presence 528 | * type=available 529 | * type=unavailable 530 | * add=last 531 | * name=message (for N messages) 532 | * add=before and last=true 533 | * type=message (for N messages) 534 | * (no type) 535 | * name=contacts 536 | * type=frequent 537 | * name=response 538 | * type=chat and duplicate=true 539 | * type=contacts and duplicate=true 540 | * type=emoji 541 | * name=item (see above) 542 | 543 | ## VarInt 544 | 545 | A VarInt is a variable-size type (grows as needed). To do so, first 4 bytes are 546 | limited to 7 bits, then starting from the 4th byte is stored the remainder of 547 | integer division (128, as we store only 7 bits). 548 | 549 | The first bit of each byte is then flipped to 1, except for the last one (it's 550 | used to indicate the last byte) 551 | 552 | The current implementation into this library doesn't support negative values, 553 | nor integer that doesn't fit into 5 bytes (7-bit encoded, so >= 34359738368). 554 | --------------------------------------------------------------------------------