diff --git a/backend/status_decoder.py b/backend/status_decoder.py new file mode 100644 index 0000000..bc514d8 --- /dev/null +++ b/backend/status_decoder.py @@ -0,0 +1,21 @@ +import json +import sys +from whatsapp import HKDF,AESUnpad; +import base64; +import urllib2; +import time; +from Crypto.Cipher import AES; +query = json.loads(sys.argv[1]) +mediaK= query['mediakey'] +if query['mimetype'] == "video/mp4": + mediaKeyExpanded=HKDF(base64.b64decode(mediaK),112,"WhatsApp Video Keys") +else: + mediaKeyExpanded=HKDF(base64.b64decode(mediaK),112,"WhatsApp Image Keys") + +mediaData= urllib2.urlopen(query['url']).read() +file= mediaData[:-10] +iv=mediaKeyExpanded[:16] +cipherKey= mediaKeyExpanded[16:48] +decryptor = AES.new(cipherKey, AES.MODE_CBC, iv) +fileData=AESUnpad(decryptor.decrypt(file)) +print(base64.b64encode(fileData)) \ No newline at end of file diff --git a/backend/whatsapp.py b/backend/whatsapp.py index a7a7dfa..5edb56f 100755 --- a/backend/whatsapp.py +++ b/backend/whatsapp.py @@ -31,7 +31,7 @@ from utilities import *; from whatsapp_binary_reader import whatsappReadBinary; -WHATSAPP_WEB_VERSION="2,2121,6" +WHATSAPP_WEB_VERSION="2,2136,10" reload(sys); sys.setdefaultencoding("utf-8"); @@ -181,6 +181,22 @@ def onMessage(self, ws, message): try: processedData = whatsappReadBinary(decryptedMessage, True); messageType = "binary"; + + # sort contacts obj{jid : name} + try: + if processedData[1]['type'] is "contacts": + messageType = "jsonContacts"; + processedData.append(self.sortedContacts(processedData)) + except: + pass + # sort statuses + try: + if processedData[2][0][0] is "status": + processedData[2] = self.sortedStatuses(processedData) + messageType = "jsonStatuses"; + except: + pass + except: processedData = { "traceback": traceback.format_exc().splitlines() }; messageType = "error"; @@ -209,7 +225,7 @@ def onMessage(self, ws, message): self.loginInfo["key"]["encKey"] = keysDecrypted[:32]; self.loginInfo["key"]["macKey"] = keysDecrypted[32:64]; - self.save_session(); + self.saveSession(); # eprint("private key : ", base64.b64encode(self.loginInfo["privateKey"].serialize())); # eprint("secret : ", base64.b64encode(self.connInfo["secret"])); # eprint("shared secret : ", base64.b64encode(self.connInfo["sharedSecret"])); @@ -229,6 +245,7 @@ def onMessage(self, ws, message): eprint(json.dumps( [messageTag,["admin","challenge",challenge,self.connInfo["serverToken"],self.loginInfo["clientId"]]])) self.activeWs.send(json.dumps( [messageTag,["admin","challenge",challenge,self.connInfo["serverToken"],self.loginInfo["clientId"]]])); elif jsonObj[0] == "Stream": + self.getStatuses(); # request for contacts statuses pass; elif jsonObj[0] == "Props": pass; @@ -276,7 +293,7 @@ def restoreSession(self, callback=None): "serverToken"] + '", "' + self.loginInfo["clientId"] + '", "takeover"]' self.activeWs.send(message) - def save_session(self): + def saveSession(self): session = {"clientToken":self.connInfo["clientToken"],"serverToken":self.connInfo["serverToken"], "clientId":self.loginInfo["clientId"],"macKey": self.loginInfo["key"]["macKey"].decode("latin_1") ,"encKey": self.loginInfo["key"]["encKey"].decode("latin_1")}; @@ -284,6 +301,42 @@ def save_session(self): f.write(json.dumps(session)) f.close() + def sortedContacts(self,processedData): + contacts = {} + for contact in range(len(processedData[2])): + if 'name' in processedData[2][contact][1].keys() : + contacts[processedData[2][contact][1]['jid']] = processedData[2][contact][1]['name'] + return contacts + + def getStatuses(self): + messageId = "3EB0"+binascii.hexlify(Random.get_random_bytes(8)).upper() + encryptedMessage = WhatsAppEncrypt( + self.loginInfo["key"]["encKey"], + self.loginInfo["key"]["macKey"], + whatsappWriteBinary(["query", {"type": "status","jid":""}, None]) + ) + payload = bytearray(messageId) + bytearray(",") + bytearray( + to_bytes(WAMetrics.QUERY_MEDIA, 1) + ) + bytearray([0x80]) + encryptedMessage + self.activeWs.send(payload, websocket.ABNF.OPCODE_BINARY) + + def sortedStatuses(self,processedData): + entries = {} + bad = [] + for user in range(len(processedData[2])): + jid = processedData[2][user][1]['jid'] + for story in range(len(processedData[2][user][2])): + if processedData[2][user][2][story][0] == "picture" : + bad.append(story) + continue + decoded_msgs = WAWebMessageInfo.decode(processedData[2][user][2][story][2]) + processedData[2][user][2][story] = decoded_msgs['message'] + entries[jid] = processedData[2][user][2] + for b in bad: + del entries[jid][b] + return entries + + def getLoginInfo(self, callback): callback["func"]({ "type": "login_info", "data": self.loginInfo }, callback); diff --git a/client/css/main.css b/client/css/main.css index c9ad3da..bcbbe1d 100644 --- a/client/css/main.css +++ b/client/css/main.css @@ -110,16 +110,18 @@ body { } #console-arrow > button { font-size: 2.5vh; + background: #000; + color: #fff; } #console-arrow.extended { - right: 20vw; + right: 30vw; } #console { position: absolute; top: 0vh; left: 100vw; - width: 20vw; + width: 30vw; height: 100vh; padding: 3vh; transition: left 0.2s; @@ -128,5 +130,186 @@ body { background-color: rgba(200, 200, 200, 0.5); } #console.extended { - left: 80vw; + left: 70vw; } + +.statuses { + width: 98%; + background-color: #1e262b; + color: aliceblue; + padding: 1%; + height: 98%; + display: flow-root; + overflow: scroll; + position: relative; +} + +.user-item { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + align-content: space-between; + justify-content: flex-start; +} + +.user-item:hover { + background-color: #2c353a; +} + +.user-details { + display: flex; + justify-content: space-between; + flex-direction: column; + flex-wrap: nowrap; + align-items: stretch; + align-content: space-between; +} + +.user-details span:nth-child(2) { + font-size: small; + color: gray; + padding: 2px; +} + +.users-list { + position: absolute; +} + +.user-image { + background-repeat: no-repeat; + background-position: 50%; + background-size: cover; + border-radius: 50%; + width: 40px; + height: 40px; + overflow: hidden; + padding: 2px; + margin: 5px; + background-color: azure; +} + +.full-view { + position: sticky; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #193636; + background-position-x: center; + background-position-y: center; + background-size: contain; + background-repeat: no-repeat; + max-width: 400px; + max-height: 800px; + margin: 0 auto; +} + +.statuses .close { + padding: 8px 13px; + background: #000000b0; + border-radius: 15px; + width: fit-content; + margin: 5px; + position: absolute; + top: 3%; + color: #fff; + opacity: 1; +} + +.next { + background: #00000000; + border-radius: 5px; + position: absolute; + top: 15%; + right: 0; + height: 85%; + width: 20%; +} + +.pervious { + background: #00000000; + border-radius: 5px; + position: absolute; + top: 15%; + left: 0; + height: 85%; + width: 20%; +} + +video { + width: 100%; + height: 100%; +} + +.text-media { + display: flex; + height: 100%; + align-content: space-between; + justify-content: center; + flex-wrap: nowrap; + flex-direction: column; + text-align: center; + padding: 15px; +} + +.status-dots { + height: 10px; + background: #00000000; + width: 94%; + margin: 1% 3%; + position: absolute; + top: 0; + display: flex; + flex-wrap: nowrap; + flex-direction: row; + align-content: space-between; + justify-content: space-between; + align-items: center; +} + +.dots { + height: 50%; + background: aliceblue; + width: 10%; + margin: 0% 1%; + border-radius: 10px; +} + +.dots.active { + background: black; +} + +.loader { + border: 10px solid #f3f3f3; + border-radius: 50%; + border-top: 10px solid #298313d9; + border-right: 10px solid #b9c502d1; + border-bottom: 10px solid #4bc92b; + border-left: 10px solid #00ff4396; + width: 40px; + height: 40px; + -webkit-animation: spin 2s linear infinite; + animation: spin 2s linear infinite; + position: absolute; + right: 45%; + top: 45%; +} + +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/client/index.html b/client/index.html index 24cd268..beecf46 100644 --- a/client/index.html +++ b/client/index.html @@ -29,6 +29,10 @@ + + + +
@@ -60,10 +64,27 @@

WhatsApp Web

- +
- Hello -
+
+
+

RECENT STATUSES

+
+ +
+
{{statusInfo}}
+ + + +
+
+ diff --git a/client/js/main.js b/client/js/main.js index 05ef9ec..4959d2a 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -8,6 +8,273 @@ function sleep(ms) { } $(document).ready(function() { + + let is_full_view = false; + let selected_user ; + let page = 0 ; + let contacts = {} + let users = {} + + const axiosController = new AbortController(); + function clearStatuses() + { + users = {}; + app.$root.users = {}; + + contacts = {}; + app.$root.contacts = {}; + + } + function setStatuses(data) + { + app.$root.statusInfo = "Fetching Statuses..." + if(data.message_type == "jsonStatuses"){ + + users = data.message[2]; + app.$root.users = data.message[2]; + if(! $("#console-arrow").hasClass("extended")) + $("#console-arrow-button").click(); + } + } + function setContacts(data) + { + if(data.message_type == "jsonContacts"){ + contacts = data.message[3]; + app.$root.contacts = data.message[3]; + + } + + } + var app = new Vue({ + el: '#app', + data: { + axiosController : axiosController, + is_full_view : is_full_view, + selected_user: selected_user, + page: page, + users : users, + statusInfo: "No Statuses, Please connect to WA" + } + , + methods: { + getNameByJid: function (jid) { + return contacts[jid]; + }, + getUserDataByJid: function (jid) { + return users[jid]; + }, + getFirstMedia: function () { + if(selected_user) + return Object.values(this.getUserDataByJid(selected_user)[0])[0]; + } + + } + }); + Vue.component('users-list', { + props: ['user','num','name'], + + computed: { + getImage: function() + { + textPlaceHolder = "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAAAU5JREFUSEvtl79qAkEQhz+LNFYWElJYKHkDi6RLJb6IkJcK+B4WVjYBC18hRUIIwSKFRSCYMLIrc8vsundcFPGuuv33+2Zmd+fmWhSf36Bdd7PlBfcvrkPAYV9d8IK2hrSBTQCOReAK+FHGWsZNgKkaEK1r4FP6NPgeeFZ9HmpFpQusD4QiXC/tR+ApBL8DNw4sk16AgSEuYxos7QXwYMzdOj0xvmCI9kYPpPbaAr8BvUgEvFYSLF72nUDskFngJXCXAH8Dr8Ct30rLY1k/BFYJoZw99svHwExp7ZhVrk7ZUJv2N+CcLNWEWiKQusfN4dpF4Cyuk8654UfiX/f4ZGB/Oo9+j0/m8WWDO8CXKvY+XNmUk+tL32Md6ljJdFSwlKzicalkVGoyoKtG7Z1EYwTMc13OAYdFfaoI1Nykdi44Z55OMAe/AzmCVX/kktp/xJSEH1b13sIAAAAASUVORK5CYII="; + return Object.values(this.user[0])[0]['jpegThumbnail'] == undefined ? textPlaceHolder : Object.values(this.user[0])[0]['jpegThumbnail'] + } + }, + methods: + { + toggleFullView: function() + { + this.$root.is_full_view = this.$root.is_full_view ? false : true; + + }, + setSelectedUser: function(num) + { + this.toggleFullView() + this.$root.selected_user = num; + this.$root.page = 1 + }, + + getFirstMediaThumb: function(num) + { + this.$root.selected_user = num; + this.$root.page = 1 + } + + }, + template: ` +
+
+
+
+ {{name}} + {{moment(Object.values(user[0])[0]['mediaKeyTimestamp'], "X").fromNow()}} +
+ +
+ ` + }); + Vue.component('dots', { + props: ['count','page','index'], + methods: + { + initStyle: function() + { + return "width:" + 100 / parseInt(this.count) + "%;" + }, + initClass: function() + { + if(parseInt(this.index) == parseInt(this.page)) + return "active" + return "" + }, + + }, + template: ` +
+ +
+ ` + }); + Vue.component('full-view', { + props: ['user','num','media'], + data: function () { + return { + base64Media: 'null', + page: this.$root.page - 1, + media : this.media, + userLength : this.user.length, + isLoading : true, + } + }, + created: function () { + this.fetchFile() + }, + methods: + { + fetchFile: function() + { + + this.isLoading = true + currentFile = Object.values(this.user[this.page])[0] + this.media = currentFile + + if(this.media['text']){ + return; + } + this.base64Media = this.media['jpegThumbnail'] + + url = 'http://localhost:2018/downloadFile?mediakey='+encodeURIComponent(currentFile.mediaKey)+'&mimetype='+currentFile.mimetype + +'&url='+ encodeURIComponent(currentFile.url); + + vm = this + axios.get(url,{signal: this.$root.axiosController.signal}) + .then(function (response) { + vm.base64Media = response.data; + vm.isLoading = false + + }) + .catch(function (error) { + console.log(error) + vm.base64Media = undefined + + }) + + + }, + toggleFullView: function() + { + this.$root.is_full_view = false; + this.$root.page = 0 + + // abort media request before loading ends + this.$root.axiosController.abort() + this.$root.axiosController = new AbortController(); + + }, + nextPage: function() + { + if(this.page + 1 < this.userLength){ + this.page = this.page + 1 + this.fetchFile() + }else + this.$root.is_full_view = false; + + }, + perviousPage: function() + { + if(this.page - 1 >= 0){ + this.page = this.page - 1 + this.fetchFile() + }else + this.$root.is_full_view = false; + + }, + }, + + template: ` +
+
{{ this.media['text'] }}
+
X
+ +
+
+
+ +
+
+
X
+ +
+
+
+
+
+
+ + +
X
+ +
+
+
+
+ + + ` +}); + + + + + + + + + + $("#console-arrow-button").click(() => { if(consoleShown) { $("#console-arrow").removeClass("extended").find("i.fa").removeClass("fa-angle-right").addClass("fa-angle-left"); @@ -105,6 +372,9 @@ $(document).ready(function() { bootstrapState = 1; websocket.apiConnectedToBackend = false; websocket.backendConnectedToWhatsApp = false; + + // empty statuses + clearStatuses(); }); }, request: { @@ -132,6 +402,9 @@ $(document).ready(function() { bootstrapInfo.activateButton(bootstrapInfo.steps[2].texts.connLost, true); bootstrapState = 2; websocket.backendConnectedToWhatsApp = false; + + // empty statuses + clearStatuses(); }); }, request: { @@ -200,7 +473,12 @@ $(document).ready(function() { tree = jsonTree.create(jsonData, dialog.find(".bootbox-body").empty()[0]); }); }); - + + // set statuses + setStatuses(d); + // set contacts + setContacts(d); + let tableRow = $("").attr("data-message-index", allWhatsAppMessages.length); tableRow.append($("").attr("scope", "row").html(allWhatsAppMessages.length+1)); tableRow.append($("").html(moment.unix(d.timestamp/1000.0).format("ddd, DD.MM.YYYY, HH:mm:ss.SSS"))); @@ -273,7 +551,12 @@ $(document).ready(function() { tree = jsonTree.create(jsonData, dialog.find(".bootbox-body").empty()[0]); }); }); - + + // set statuses + setStatuses(d); + // set contacts + setContacts(d); + let tableRow = $("").attr("data-message-index", allWhatsAppMessages.length); tableRow.append($("").attr("scope", "row").html(allWhatsAppMessages.length+1)); tableRow.append($("").html(moment.unix(d.timestamp/1000.0).format("ddd, DD.MM.YYYY, HH:mm:ss.SSS"))); diff --git a/index.js b/index.js index fb1ba6e..6188f9c 100644 --- a/index.js +++ b/index.js @@ -244,6 +244,21 @@ wss.on("connection", function(clientWebsocketRaw, req) { app.use(express.static("client")); +app.get('/downloadFile', (req, res) => { + + const { spawn } = require('child_process'); + const pyProg = spawn('python', ['./backend/status_decoder.py',JSON.stringify(req.query)]); + let dataFile = ''; + pyProg.stdout.on('data', function(data) { + + dataFile += data.toString() + }); + pyProg.on('close', function(code) { + return res.end(dataFile); + }); + + +}) app.listen(2018, function() { console.log("whatsapp-web-reveng HTTP server listening on port 2018"); });