Project Name | Stars | Downloads | Repos Using This | Packages Using This | Most Recent Commit | Total Releases | Latest Release | Open Issues | License | Language |
---|---|---|---|---|---|---|---|---|---|---|
Whatsapp Web Reveng | 5,722 | 21 days ago | 157 | mit | JavaScript | |||||
Reverse engineering WhatsApp Web. | ||||||||||
Go Whatsapp | 1,886 | 37 | 24 days ago | 12 | January 21, 2022 | 164 | mit | Go | ||
WhatsApp Web API | ||||||||||
Whatsjava | 25 | a year ago | 9 | apache-2.0 | Java | |||||
Implementation of the WhatsApp Web API in Java | ||||||||||
Whatsapppatcher | 18 | 4 days ago | 1 | Python | ||||||
A patcher that decompiles WhatsApp APK, patches the smali, recompiles and signs it. | ||||||||||
Whatsappwebrequestanalyzer | 15 | 4 months ago | Kotlin | |||||||
Browser based tool built to decrypt any request sent or received by WhatsappWeb's WebClient for WhatsappWeb4j | ||||||||||
Whatsappweb Clj | 8 | 5 years ago | Clojure | |||||||
Whatsweb Web Client written in Clojure | ||||||||||
Wadump | 7 | 3 years ago | gpl-3.0 | Rust | ||||||
CLI Tool to dump and analyze WhatsApp Web Packets | ||||||||||
Yowsup | 7 | 9 years ago | mit | |||||||
tgalal's Yowsup library | ||||||||||
Js Whatsapp | 4 | 3 years ago | 3 | mit | TypeScript | |||||
A WhatsApp bot library | ||||||||||
Whatsapp Web Reveng | 3 | 3 years ago | 1 | mit | JavaScript | |||||
Reverse engineering WhatsApp Web. Fork from sigalor/whatsapp-web-reveng |
This project intends to provide a complete description and re-implementation of the WhatsApp Web API, which will eventually lead to a custom client. WhatsApp Web internally works using WebSockets; this project does as well.
There's no need to install or manage python and node versions, the file
shell.nix
defines an environment with all the dependencies included to run
this project.
There's an .envrc
file in root folder that is called automatically when
cd
ing (changing directory) to project, if program direnv
is installed along
with nix
you should get an output like this:
>cd ~/dev/whatsapp
Installing node modules
npm WARN prepare removing existing node_modules/ before installation
> [email protected] install /home/rainy/dev/whatsapp/node_modules/fsevents
> node-gyp rebuild
make: Entering directory '/home/rainy/dev/whatsapp/node_modules/fsevents/build'
SOLINK_MODULE(target) Release/obj.target/.node
COPY Release/.node
make: Leaving directory '/home/rainy/dev/whatsapp/node_modules/fsevents/build'
> [email protected] postinstall /home/rainy/dev/whatsapp/node_modules/nodemon
> node bin/postinstall || exit 0
added 310 packages in 3.763s
Done.
$$\ $$\ $$\ $$\
$$ | $\ $$ |$$ | $$ |
$$ |$$$\ $$ |$$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\
$$ $$ $$\$$ |$$ __$$\ \____$$\\_$$ _| $$ _____| \____$$\ $$ __$$\ $$ __$$\
$$$$ _$$$$ |$$ | $$ | $$$$$$$ | $$ | \$$$$$$\ $$$$$$$ |$$ / $$ |$$ / $$ |
$$$ / \$$$ |$$ | $$ |$$ __$$ | $$ |$$\ \____$$\ $$ __$$ |$$ | $$ |$$ | $$ |
$$ / \$$ |$$ | $$ |\$$$$$$$ | \$$$$ |$$$$$$$ |\$$$$$$$ |$$$$$$$ |$$$$$$$ |
\__/ \__|\__| \__| \_______| \____/ \_______/ \_______|$$ ____/ $$ ____/
$$ | $$ |
$$ | $$ |
\__| \__|
Node v13.13.0
Python 2.7.17
Try running server with: npm start
[nix-shell:~/dev/whatsapp]$
If you don't use direnv
or just want to manually get into the build
environment do:
nix-shell
in the project root
Before you can run the application, make sure that you have the following software installed:
async
await
syntax is used)pip
packages installed:
websocket-client
and git+https://github.com/dpallot/simple-websocket-server.git
for acting as WebSocket server and client.curve25519-donna
and pycrypto
for the encryption stuff.pyqrcode
for QR code generation.protobuf
for reading and writing the binary conversation format.curve25519-donna
requires Microsoft Visual C++ 9.0 and you need to copy stdint.h
into C:\Users\YOUR USERNAME\AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0\VC\include
.Before starting the application for the first time, run npm install -f
to install all Node and pip install -r requirements.txt
for all Python dependencies.
Lastly, to finally launch it, just run npm start
on Linux based OS's and npm run win
on Windows. Using fancy concurrently
and nodemon
magic, all three local components will be started after each other and when you edit a file, the changed module will automatically restart to apply the changes.
A recent addition is a version of the decryption routine translated to in-browser JavaScript. Run node index_jsdemo.js
(just needed because browsers don't allow changing HTTP headers for WebSockets), then open client/login-via-js-demo.html
as a normal file in any browser. The console output should show decrypted binary messages after scanning the QR code.
adiwajshing created Baileys, a Node library that implements the WhatsApp Web API.
ndunks made a TypeScript reimplementation at WaJs.
p4kl0nc4t created kyros, a Python package that implements the WhatsApp Web API.
With whatsappweb-rs, wiomoc created a WhatsApp Web client in Rust.
Rhymen created go-whatsapp, a Go package that implements the WhatsApp Web API.
vzaramel created whatsappweb-clj, a Clojure library the implements the WhatsApp Web API.
The project is organized in the following way. Note the used ports and make sure that they are not in use elsewhere before starting the application.
WhatsApp Web encrypts the data using several different algorithms. These include AES 256 CBC, Curve25519 as Diffie-Hellman key agreement scheme, HKDF for generating the extended shared secret and HMAC with SHA256.
Starting the WhatsApp Web session happens by just connecting to one of its websocket servers at wss://w[1-8].web.whatsapp.com/ws
(wss://
means that the websocket connection is secure; w[1-8]
means that any number between 1 and 8 can follow the w
). Also make sure that, when establishing the connection, the HTTP header Origin: https://web.whatsapp.com
is set, otherwise the connection will be rejected.
When you send messages to a WhatsApp Web websocket, they need to be in a specific format. It is quite simple and looks like messageTag,JSON
, e.g. 1515590796,["data",123]
. Note that apparently the message tag can be anything. This application mostly uses the current timestamp as tag, just to be a bit unique. WhatsApp itself often uses message tags like s1
, 1234.--0
or something like that. Obviously the message tag may not contain a comma. Additionally, JSON objects are possible as well as payload.
To log in at an open websocket, follow these steps:
clientId
, which needs to be 16 base64-encoded bytes (i.e. 25 characters). This application just uses 16 random bytes, i.e. base64.b64encode(os.urandom(16))
in Python.messageTag,["admin","init",[0,3,2390],["Long browser description","ShortBrowserDesc"],"clientId",true]
.
messageTag
and clientId
by the values you chose before[0,3,2390]
part specifies the current WhatsApp Web version. The last value changes frequently. It should be quite backwards-compatible though."Long browser description"
is an arbitrary string that will be shown in the WhatsApp app in the list of registered WhatsApp Web clients after you scan the QR code."ShortBrowserDesc"
has not been observed anywhere yet but is arbitrary as well.status
: should be 200ref
: in the application, this is treated as the server ID; important for the QR generation, see belowttl
: is 20000, maybe the time after the QR code becomes invalidupdate
: a boolean flagcurr
: the current WhatsApp Web version, e.g. 0.2.7314
time
: the timestamp the server responded at, as floating-point milliseconds, e.g. 1515592039037.0
curve25519.Private()
.privateKey.get_public()
.ref
attribute from step 4base64.b64encode(publicKey.serialize())
pyqrcode
) and scan it using the WhatsApp app.ttl
).messageTag,["admin","Conn","reref"]
.status
: should be 200 (other ones: 304 - reuse previous ref, 429 - new ref denied)ref
: new refttl
: expiration timeConn
: array contains JSON object as second element with connection information containing the following attributes and many more:
battery
: the current battery percentage of your phonebrowserToken
: used to logout without active WebSocket connection (not implemented yet)clientToken
: used to resuming closed sessions aka "Remember me" (not implemented yet)phone
: an object with detailed information about your phone, e.g. device_manufacturer
, device_model
, os_build_number
, os_version
platform
: your phone OS, e.g. android
pushname
: the name of yours you provided WhatsAppsecret
(remember this!)serverToken
: used to resuming closed sessions aka "Remember me" (not implemented yet)wid
: your phone number in the chat identification format (see below)Stream
: array has four elements in total, so the entire payload is like ["Stream","update",false,"0.2.7314"]
Props
: array contains JSON object as second element with several properties like imageMaxKBytes
(1024), maxParticipants
(257), videoMaxEdge
(960) and otherssecret
from Conn
as base64 and storing it as secret
. This decoded secret will be 144 bytes long.sharedSecret
. The application does it using privateKey.get_shared_key(curve25519.Public(secret[:32]), lambda a:a)
.sharedSecret
to 80 bytes using HKDF. Call this value sharedSecretExpanded
.HmacSha256(sharedSecretExpanded[32:64], secret[:32] + secret[64:])
. Compare this value to secret[32:64]
. If they are not equal, abort the login.sharedSecretExpanded[64:] + secret[64:]
as keysEncrypted
.sharedSecretExpanded[:32]
as key, i.e. store AESDecrypt(sharedSecretExpanded[:32], keysEncrypted)
as keysDecrypted
.keysDecrypted
variable is 64 bytes long and contains two keys, each 32 bytes long. The encKey
is used for decrypting binary messages sent to you by the WhatsApp Web server or encrypting binary messages you send to the server. The macKey
is needed to validate the messages sent to you:
encKey
: keysDecrypted[:32]
macKey
: keysDecrypted[32:64]
init
command, check whether you have serverToken
and clientToken
.messageTag,["admin","login","clientToken","serverToken","clientId","takeover"]
{"status": 200}
. Other statuses:
tos
field in the JSON: if it equals or greater than 2, you have violated TOSserverToken
and clientToken
, you will be challenged to confirm that you still have valid encryption keys.messageTag,["Cmd",{"type":"challenge","challenge":"BASE_64_ENCODED_STRING=="}]
challenge
string from Base64, sign it with your macKey, encode it back with Base64 and send messageTag,["admin","challenge","BASE_64_ENCODED_STRING==","serverToken","clientId"]
{"status": 200}
, but it means nothing.goodbye,,["admin","Conn","disconnect"]
.encKey
with your macKey
and encode it with Base64. Let's say it is your logoutToken
.https://dyn.web.whatsapp.com/logout?t=browserToken&m=logoutToken
Now that you have the two keys, validating and decrypting messages the server sent to you is quite easy. Note that this is only needed for binary messages, all JSON you receive stays plain. The binary messages always have 32 bytes at the beginning that specify the HMAC checksum. Both JSON and binary messages have a message tag at their very start that can be discarded, i.e. only the portion after the first comma character is significant.
macKey
(here messageContent
is the entire binary message): HmacSha256(macKey, messageContent[32:])
. If this value is not equal to messageContent[:32]
, the message sent to you by the server is invalid and should be discarded.encKey
: AESDecrypt(encKey, messageContent[32:])
.The data you get in the final step has a binary format which is described in the following. Even though it's binary, you can still see several strings in it, especially the content of messages you sent is quite obvious there.
The Python script backend/decoder.py
implements the MessageParser
class. It is able to create a JSON structure out of binary data in which the data is still organized in a rather messy way. The section about Node Handling below will discuss how the nodes are reorganized afterwards.
MessageParser
initially just needs some data and then processes it byte by byte, i.e. as a stream. It has a couple of constants and a lot of methods which all build on each other.
[None,None,None,"200","400","404","500","501","502","action","add", "after","archive","author","available","battery","before","body", "broadcast","chat","clear","code","composing","contacts","count", "create","debug","delete","demote","duplicate","encoding","error", "false","filehash","from","g.us","group","groups_v2","height","id", "image","in","index","invis","item","jid","kind","last","leave", "live","log","media","message","mimetype","missing","modify","name", "notification","notify","out","owner","participant","paused", "picture","played","presence","preview","promote","query","raw", "read","receipt","received","recipient","recording","relay", "remove","response","resume","retry","s.whatsapp.net","seconds", "set","size","status","subject","subscribe","t","text","to","true", "type","unarchive","unavailable","url","user","value","web","width", "mute","read_only","admin","creator","short","update","powersave", "checksum","epoch","block","previous","409","replaced","reason", "spam","modify_tag","message_info","delivery","emoji","title", "description","canonical-url","matched-text","star","unstar", "media_key","filename","identity","unread","page","page_count", "search","media_message","security","call_log","profile","ciphertext", "invite","gif","vcard","frequent","privacy","blacklist","whitelist", "verify","location","document","elapsed","revoke_invite","expiration", "unsubscribe","disable"]
-
for 10, .
for 11 and \0
for 15.n
and does the following n&127
many times: Reads a byte l
and for each nibble, adds the result of its unpacked version to the return value (using unpacking bytes with the given tag). Most significant nibble first.n
was set, removes the last character of the return value.LIST_EMPTY
, LIST_8
or LIST_16
(i.e. 0, 248 or 249).LIST_EMPTY
, returns a read byte for LIST_8
or a read Int16 for LIST_16
.a
and b
and gets the token at index a*256+b
.Reading a string needs a tag as parameter. Depending on this tag, different data is read.
"s.whatsapp.net"
, "c.us"
is returned instead, otherwise the token is returned as is.tag-DICTIONARY_0
as first and a read byte as second parameter.None
).i
with this tag.j
with this tag.i
and j
are joined together with an @
sign and the result is returned.Reading an attribute list needs the number of attributes to read as parameter. An attribute list is always a JSON object. For each attribute read, the following steps are executed for getting key-value pairs (exactly in this order!):
A node always consists of a JSON array with exactly three entries: description, attributes and content. The following steps are needed to read a node:
a
is read by using a read byte as the tag. The list size 0 is invalid.descr
is then obtained by reading a string with this tag.attrs
is read by reading an attributes object with length (a-2 + a%2) >> 1
.a
was odd, this node does not have any content, i.e. [descr, attrs, None]
is returned.[descr, attrs, content]
is returned.Reading a list requires a list tag (i.e. LIST_EMPTY, LIST_8 or LIST_16). The length of the list is then obtained by reading a list size using this tag. For each list entry, a node is read.
After a binary message has been transformed into JSON, it is still rather hard to read. That's why, internally, WhatsApp Web completely retransforms this structure into something that can be easily processed and eventually translated into user interface content. This section will deal with this and awaits completion.
When a node has been read, the contents of messages that have been actually sent by the user (i.e. text, image, audio, video etc.) are still not directly visible or accessible via the JSON. Instead, they are kept in a protobuf message. See here for the definitions. The "wrapper" message type is WebMessageInfo
.
WhatsApp Web itself has an interesting API as well. You can even try it out directly in your browser. Just log in at the normal https://web.whatsapp.com/, then open the browser development console. Now enter something like the following (see below for details on the chat identification):
window.Store.Wap.profilePicFind("[email protected]").then(res => console.log(res));
window.Store.Wap.lastseenFind("[email protected]").then(res => console.log(res));
window.Store.Wap.statusFind("[email protected]").then(res => console.log(res));
Using the amazing Chrome developer console, you can see that window.Store.Wap
contains a lot of other very interesting functions. Many of them return JavaScript promises. When you click on the Network tab and then on WS (maybe you need to reload the site first), you can look at all the communication between WhatsApp Web and its servers.
The WhatsApp Web API uses the following formats to identify chats with individual users and groups of multiple users.
[country code][number]@c.us
, e.g. [email protected]
when you are from Germany and your phone number is 0123 456789
.[phone number of group creator]-[timestamp of group creation]@g.us
, e.g. [email protected]
for the group that [email protected]
created on November 5 2017.[timestamp of broadcast channel creation]@broadcast
, e.g. [email protected]
for an broadcast channel created on November 5 2017.There are two types of WebSocket messages that are exchanged between server and client. On the one hand, plain JSON that is rather unambiguous (especially for the API calls above), on the other hand encrypted binary messages.
Unfortunately, these binary ones cannot be looked at using the Chrome developer tools. Additionally, the Python backend, that of course also receives these messages, needs to decrypt them, as they contain encrypted data. The section about encryption details discusses how it can be decrypted.
mediaKey
, which needs to be 32 bytes.mediaKeyExpanded
.mediaKeyExpanded
into:
iv
: mediaKeyExpanded[:16]
cipherKey
: mediaKeyExpanded[16:48]
macKey
: mediaKeyExpanded[48:80]
refKey
: mediaKeyExpanded[80:]
(not used)cipherKey
and iv
, pad it and call it enc
.iv + enc
with macKey
using HMAC SHA-256 and store the first 10 bytes of the hash as mac
.fileSha256
, hash the enc + mac
with SHA-256 and store it as fileEncSha256
.fileEncSha256
with base64 and store it as fileEncSha256B64
.sidecar
. Do it by signing every [n*64K, (n+1)*64K+16]
chunk with macKey
, truncating the result to the first 10 bytes. Then combine everything in one piece.messageTag,["action", "encr_upload", filetype, fileEncSha256B64]
filetype
can be one of image
, audio
, document
or video
hash
: fileEncSha256B64
file
, filename: blob
: enc+mac
?f=j
and the correct content-type
and the multipart-form, WhatsApp will respond with the download url for the file.mediaKey
and decode it from Base64 if necessary.mediaKeyExpanded
.mediaKeyExpanded
into:
iv
: mediaKeyExpanded[:16]
cipherKey
: mediaKeyExpanded[16:48]
macKey
: mediaKeyExpanded[48:80]
refKey
: mediaKeyExpanded[80:]
(not used)url
and split it into:
file
: mediaData[:-10]
mac
: mediaData[-10:]
iv + file
with macKey
using SHA-256. Take in mind that mac
is truncated to 10 bytes, so you should compare only the first 10 bytes.file
with AES-CBC using cipherKey
and iv
, and unpad it. Note that this means that your session's keys (i.e. encKey
and macKey
from the Key generation section) are not necessary to decrypt a media file.Depending on the media type, the literal strings in the right column are the values for the appInfo
parameter from the HKDF
function.
Media Type | Application Info |
---|---|
IMAGE | WhatsApp Image Keys |
VIDEO | WhatsApp Video Keys |
AUDIO | WhatsApp Audio Keys |
DOCUMENT | WhatsApp Document Keys |
The message forwarding procedures are rather complex, as there are several layers of websockes involved in the process. For adding your own commands, follow these steps.
First, decide on what the final destination of your command shall be. To be consistent with the other, please prefix it with backend_
if it's meant to be received by the Python backend or use api_
if the command is directed to the NodeJS API.
Now, look at client/js/main.js
. In line 214, you can see an instantiation of the BootstrapStep
JavaScript class. It needs the following information:
websocket
: is probably always the samerequest.type
: should generally be call
, as this allows a response to be passed back to the command's senderrequest.callArgs
: an object which has to contain a command
attribute specifying the name of your command and as many additional key-value-pairs as you want. All of these will be passed to the receiver.request.successCondition
: on receiving a response for a call, this shall be a function returning true
when the response is valid/expected. Use the next attribute for specifying code to be executed when the response is valid.request.successActor
: when the success condition evaluated to true
, this success actor function is calledWhen the BootstrapStep
object has been constructed, call .run()
for running indefinitely or .run(timeout_ms)
for failing when no response has been received after a specific timeout. The run
function returns a Promise.
Next, edit index.js
. It contains a couple of blocks beginning with clientWebsocket.waitForMessage
. You can copy one of these blocks and edit the parameters. The waitForMessage
function needs the following attributes:
condition
: when a message is received and this condition evaluates to true
on it, the message will be processed by the following .then(...)
blockkeepWhenHit
: it is possible for a message handler to be detached immediately after it receives its first fitting message. Control this here.
The returned promise's then
block finally handles a received message. It gets a clientCallRequest
you can call .respond({...})
on to send a JSON response to the caller. If the NodeJS API is not the message's final destination, you need to instantiate a new BootstrapStep
here which will contact to the Python backend and, after it receives its response, will return it to the original caller.Thus, when you want a message for the backend, now edit backend/whatsapp-web-backend.py
. In the if-else-compound starting in line 88, add your own branch for the command name you chose. Then, edit backend/whatsapp.py
and add a function similar to generateQRCode
in line 223. Just using something like in getLoginInfo
may not be enough, as your command may require an asynchronous request to the WhatsApp Web servers. In this case, make sure to add an entry to self.messageQueue
with the message tag you chose and send an appropriate message to self.activeWs
. The servers will respond to your request with a response containing the same tag, thus this is resolved in line 134. Make sure to eventually call pend["callback"]["func"]({...})
with the JSON object containing your response data to resolve the callback.
Please note, this version is not stable enough to be deployabled in production.
docker build . -t whatsapp-web-reveng
docker run -p 2019:2019 -p 2018:2018 whatsapp-web-reveng
Front end (client) at : http://localhost:2018/
The addresses of the websockets used are "localhost" by default.
If you want to deploy this docker on your own server and share it, modify the backend websocket address on the front end.
client/js/main.js
let backendInfo = {
url: "ws://{{your-server-addr}}:2020",
timeout: 10000
};
Front end (client) at : : http://{{your-server-addr}}:2018/
gh-pages
branch.This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by WhatsApp or any of its affiliates or subsidiaries. This is an independent and unofficial software. Use at your own risk.
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. See http://www.wassenaar.org/ for more information.
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.