Skip to content

Commit fa7b564

Browse files
authored
Merge pull request #113 from alfred82santa/master
Fixes & first steps in order to download encrypted files
2 parents 8c967b5 + 45372b3 commit fa7b564

File tree

10 files changed

+165
-34
lines changed

10 files changed

+165
-34
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ chrome_cache/
1010
**/*.pyc
1111
profiles
1212
.idea
13-
docs/_build
13+
docs/_build
14+
.coverage

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,4 @@
173173
# -- Options for todo extension ----------------------------------------------
174174

175175
# If true, `todo` and `todoList` produce output, else they produce nothing.
176-
todo_include_todos = True
176+
todo_include_todos = True

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
python-dateutil>=2.6.0
22
selenium>=3.4.3
33
six>=1.10.0
4+
python-axolotl
5+
pycrypto

setup.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99

1010
# To use a consistent encoding
1111
from codecs import open
12-
from os import path
12+
import os
1313
# Always prefer setuptools over distutils
1414
from setuptools import setup
1515

1616
PACKAGE_NAME = 'webwhatsapi'
1717

18-
path = path.join(path.dirname(__file__), PACKAGE_NAME, '__init__.py')
18+
path = os.path.join(os.path.dirname(__file__), PACKAGE_NAME, '__init__.py')
1919

2020
with open(path, 'r') as file:
2121
t = compile(file.read(), path, 'exec', ast.PyCF_ONLY_AST)
@@ -43,7 +43,7 @@
4343
break
4444

4545
# Get the long description from the README file
46-
with open(path.join(path.dirname(__file__), 'README.rst'), encoding='utf-8') as f:
46+
with open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='utf-8') as f:
4747
long_description = f.read()
4848

4949
setup(
@@ -95,11 +95,13 @@
9595

9696
# You can just specify the packages manually here if your project is
9797
# simple. Or you can use find_packages().
98-
packages=PACKAGE_NAME,
98+
packages=[PACKAGE_NAME, ],
9999
install_requires=[
100100
'python-dateutil>=2.6.0',
101101
'selenium>=3.4.3',
102102
'six>=1.10.0',
103+
'python-axolotl',
104+
'pycrypto'
103105
],
104106
extras_require={
105107
},

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ commands =
3131
[flake8]
3232
exclude = .tox,*.egg,build,data
3333
select = E,W,F
34+
max-line-length = 120

webwhatsapi/__init__.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
66
"""
77

8+
import binascii
89
import logging
910
from json import dumps, loads
1011

1112
import os
1213
import shutil
1314
import tempfile
15+
from Crypto.Cipher import AES
16+
from axolotl.kdf.hkdfv3 import HKDFv3
17+
from axolotl.util.byteutil import ByteUtil
18+
from base64 import b64decode
19+
from io import BytesIO
1420
from selenium import webdriver
1521
from selenium.common.exceptions import NoSuchElementException
1622
from selenium.webdriver.common.by import By
@@ -19,10 +25,9 @@
1925
from selenium.webdriver.support import expected_conditions as EC
2026
from selenium.webdriver.support.ui import WebDriverWait
2127

22-
from webwhatsapi.objects.chat import factory_chat, UserChat, Chat
23-
from webwhatsapi.objects.message import factory_message, MessageGroup
24-
from .consts import Selectors, URL
28+
from .objects.chat import UserChat, factory_chat
2529
from .objects.contact import Contact
30+
from .objects.message import MessageGroup, factory_message
2631
from .wapi_js_wrapper import WapiJsWrapper
2732

2833
__version__ = '2.0.2'
@@ -141,10 +146,11 @@ def set_proxy(self, proxy):
141146
self._profile.set_preference("network.proxy.ssl_port", int(proxy_port))
142147

143148
def __init__(self, client="firefox", username="API", proxy=None, command_executor=None, loadstyles=False,
144-
profile=None, headless=False, autoconnect=True, logger=None):
149+
profile=None, headless=False, autoconnect=True, logger=None, extra_params=None):
145150
"Initialises the webdriver"
146151

147152
self.logger = logger or self.logger
153+
extra_params = extra_params or {}
148154

149155
if profile is not None:
150156
self._profile_path = profile
@@ -161,7 +167,7 @@ def __init__(self, client="firefox", username="API", proxy=None, command_executo
161167
self._profile = webdriver.FirefoxProfile(self._profile_path)
162168
else:
163169
self._profile = webdriver.FirefoxProfile()
164-
if loadstyles == False:
170+
if not loadstyles:
165171
# Disable CSS
166172
self._profile.set_preference('permissions.default.stylesheet', 2)
167173
# Disable images
@@ -183,21 +189,22 @@ def __init__(self, client="firefox", username="API", proxy=None, command_executo
183189
capabilities['webStorageEnabled'] = True
184190

185191
self.logger.info("Starting webdriver")
186-
self.driver = webdriver.Firefox(capabilities=capabilities, options=options)
192+
self.driver = webdriver.Firefox(capabilities=capabilities, options=options, **extra_params)
187193

188194
elif self.client == "chrome":
189195
self._profile = webdriver.chrome.options.Options()
190196
if self._profile_path is not None:
191197
self._profile.add_argument("user-data-dir=%s" % self._profile_path)
192198
if proxy is not None:
193199
profile.add_argument('--proxy-server=%s' % proxy)
194-
self.driver = webdriver.Chrome(chrome_options=self._profile)
200+
self.driver = webdriver.Chrome(chrome_options=self._profile, **extra_params)
195201

196202
elif client == 'remote':
197203
capabilities = DesiredCapabilities.FIREFOX.copy()
198204
self.driver = webdriver.Remote(
199205
command_executor=command_executor,
200-
desired_capabilities=capabilities
206+
desired_capabilities=capabilities,
207+
**extra_params
201208
)
202209

203210
else:
@@ -395,5 +402,32 @@ def group_get_admins(self, group_id):
395402
for admin_id in admin_ids:
396403
yield self.get_contact_from_id(admin_id)
397404

405+
def download_file(self, url):
406+
return b64decode(self.wapi_functions.downloadFile(url))
407+
408+
def download_media(self, media_msg):
409+
try:
410+
if media_msg.content:
411+
return BytesIO(b64decode(self.content))
412+
except AttributeError:
413+
pass
414+
415+
file_data = self.download_file(media_msg.client_url)
416+
417+
media_key = b64decode(media_msg.media_key)
418+
derivative = HKDFv3().deriveSecrets(media_key,
419+
binascii.unhexlify(media_msg.crypt_keys[media_msg.type]),
420+
112)
421+
422+
parts = ByteUtil.split(derivative, 16, 32)
423+
iv = parts[0]
424+
cipher_key = parts[1]
425+
e_file = file_data[:-10]
426+
427+
AES.key_size = 128
428+
cr_obj = AES.new(key=cipher_key, mode=AES.MODE_CBC, IV=iv)
429+
430+
return BytesIO(cr_obj.decrypt(e_file))
431+
398432
def quit(self):
399433
self.driver.quit()

webwhatsapi/async_driver.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from asyncio import get_event_loop
2+
3+
import binascii
4+
5+
from Crypto.Cipher import AES
6+
from axolotl.kdf.hkdfv3 import HKDFv3
7+
from axolotl.util.byteutil import ByteUtil
8+
from base64 import b64decode
29
from concurrent.futures import ThreadPoolExecutor
310

411
from functools import partial
12+
from io import BytesIO
513

614
from webwhatsapi import factory_message
715
from . import WhatsAPIDriver
@@ -10,14 +18,14 @@
1018
class WhatsAPIDriverAsync:
1119

1220
def __init__(self, client="firefox", username="API", proxy=None, command_executor=None, loadstyles=False,
13-
profile=None, headless=False, logger=None, loop=None):
21+
profile=None, headless=False, logger=None, extra_params=None, loop=None):
1422

1523
self._driver = WhatsAPIDriver(client=client, username=username, proxy=proxy, command_executor=command_executor,
1624
loadstyles=loadstyles, profile=profile, headless=headless, logger=logger,
17-
autoconnect=False)
25+
autoconnect=False, extra_params=extra_params)
1826

1927
self.loop = loop or get_event_loop()
20-
self._pool_executor = ThreadPoolExecutor(max_workers=4)
28+
self._pool_executor = ThreadPoolExecutor(max_workers=1)
2129

2230
async def get_local_storage(self):
2331
return await self.loop.run_in_executor(self._pool_executor, self._driver.get_local_storage)
@@ -130,5 +138,35 @@ async def group_get_admins(self, group_id):
130138
for admin_id in admin_ids:
131139
yield await self.get_contact_from_id(admin_id)
132140

141+
async def download_file(self, url):
142+
return await self.loop.run_in_executor(self._pool_executor,
143+
self._driver.download_file,
144+
url)
145+
146+
async def download_media(self, media_msg):
147+
try:
148+
if media_msg.content:
149+
return BytesIO(b64decode(self.content))
150+
except AttributeError:
151+
pass
152+
153+
file_data = await self.download_file(media_msg.client_url)
154+
155+
media_key = b64decode(media_msg.media_key)
156+
derivative = HKDFv3().deriveSecrets(media_key,
157+
binascii.unhexlify(media_msg.crypt_keys[media_msg.type]),
158+
112)
159+
160+
parts = ByteUtil.split(derivative, 16, 32)
161+
iv = parts[0]
162+
cipher_key = parts[1]
163+
e_file = file_data[:-10]
164+
165+
AES.key_size = 128
166+
cr_obj = AES.new(key=cipher_key, mode=AES.MODE_CBC, IV=iv)
167+
168+
return BytesIO(cr_obj.decrypt(e_file))
169+
133170
async def quit(self):
134171
return await self.loop.run_in_executor(self._pool_executor, self._driver.quit)
172+

webwhatsapi/js/wapi.js

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,41 @@ window.WAPI._serializeNotificationObj = (obj) => ({
6060
});
6161

6262
//TODO: Add chat ref
63-
window.WAPI._serializeMessageObj = (obj) => ({
64-
sender: WAPI._serializeContactObj(obj["senderObj"]),
65-
timestamp: obj["t"],
66-
content: obj["body"],
67-
isGroupMsg: obj.__x_isGroupMsg,
68-
isLink: obj.__x_isLink,
69-
isMMS: obj.__x_isMMS,
70-
isMedia: obj.__x_isMedia,
71-
isNotification: obj.__x_isNotification,
72-
isPSA: obj.__x_isPSA,
73-
type: obj.__x_type,
74-
size: obj.__x_size,
75-
mime: obj.__x_mimetype,
76-
});
63+
window.WAPI._serializeMessageObj = function(obj) {
64+
65+
let data = {
66+
sender: WAPI._serializeContactObj(obj["senderObj"]),
67+
id: obj.id._serialized,
68+
timestamp: obj["t"],
69+
content: obj["body"],
70+
isGroupMsg: obj.__x_isGroupMsg,
71+
isLink: obj.__x_isLink,
72+
isMMS: obj.__x_isMMS,
73+
isMedia: obj.__x_isMedia,
74+
isNotification: obj.__x_isNotification,
75+
isPSA: obj.__x_isPSA,
76+
type: obj.__x_type,
77+
size: obj.__x_size,
78+
mime: obj.__x_mimetype,
79+
chatId: obj.__x_id.remote
80+
}
7781

82+
if (data.isMedia || data.isMMS) {
83+
data['clientUrl'] = obj['__x_clientUrl'];
84+
data['mediaKey'] = obj['__x_mediaKey'];
85+
data['mediaData'] = {
86+
duration: obj['__x_mediaData']['__x_duration'],
87+
filehash: obj['__x_mediaData']['__x_filehash'],
88+
mimetype: obj['__x_mediaData']['__x_mimetype'],
89+
encriptationKey: obj['__x_mediaData']['__x_encryptionKey'],
90+
fullHeight: obj['__x_mediaData']['__x_fullHeight'],
91+
fullWidth: obj['__x_mediaData']['__x_fullWidth'],
92+
size: obj['__x_mediaData']['__x_size'],
93+
}
94+
}
95+
96+
return data
97+
}
7898
/**
7999
* Fetches all contact objects from store
80100
*
@@ -444,4 +464,24 @@ window.WAPI.getCommonGroups = async function (id, done) {
444464
return output;
445465
};
446466

467+
window.WAPI.downloadFile = function (url, done) {
468+
let xhr = new XMLHttpRequest();
469+
470+
xhr.onload = function() {
471+
if (xhr.readyState == 4) {
472+
if (xhr.status == 200) {
473+
let reader = new FileReader();
474+
reader.readAsDataURL(xhr.response);
475+
reader.onload = function(e){
476+
done(reader.result.substr(reader.result.indexOf(',')+1))
477+
};
478+
} else {
479+
console.error(xhr.statusText);
480+
}
481+
}
482+
};
483+
xhr.open("GET", url, true);
484+
xhr.responseType = 'blob';
485+
xhr.send(null);
486+
}
447487

webwhatsapi/objects/contact.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class Contact(WhatsappObjectWithId):
88
"""
99
Class which represents a Contact on user's phone
1010
"""
11+
1112
def __init__(self, js_obj, driver=None):
1213
"""
1314

webwhatsapi/objects/message.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@ def __init__(self, js_obj, driver=None):
3535
:type js_obj: dict
3636
"""
3737
super(Message, self).__init__(js_obj, driver)
38-
self.sender = False if js_obj["sender"] == False else Contact(js_obj["sender"], driver)
38+
39+
self.id = js_obj["id"]
40+
self.sender = False if js_obj["sender"] is False else Contact(js_obj["sender"], driver)
3941
self.timestamp = datetime.fromtimestamp(js_obj["timestamp"])
42+
self.chat_id = js_obj['chatId']
43+
4044
if js_obj["content"]:
4145
self.content = js_obj["content"]
4246
self.safe_content = safe_str(self.content[0:25]) + '...'
@@ -50,18 +54,26 @@ def __repr__(self):
5054

5155

5256
class MediaMessage(Message):
57+
crypt_keys = {'document': '576861747341707020446f63756d656e74204b657973',
58+
'image': '576861747341707020496d616765204b657973',
59+
'video': '576861747341707020566964656f204b657973',
60+
'ptt': '576861747341707020417564696f204b657973'}
61+
5362
def __init__(self, js_obj, driver=None):
5463
super(MediaMessage, self).__init__(js_obj, driver)
5564

5665
self.type = self.js_obj["type"]
5766
self.size = self.js_obj["size"]
5867
self.mime = self.js_obj["mime"]
5968

69+
self.media_key = self.js_obj.get('mediaKey')
70+
self.client_url = self.js_obj.get('clientUrl')
71+
6072
extension = mimetypes.guess_extension(self.mime)
6173
try:
62-
self.filename = ''.join([self.js_obj["__x_filehash"], extension])
74+
self.filename = ''.join([self.js_obj["filehash"], extension])
6375
except KeyError:
64-
self.filename = ''.join([str(id(self)), extension])
76+
self.filename = ''.join([str(id(self)), extension or ''])
6577

6678
def save_media(self, path):
6779
with open(os.path.join(path, self.filename), "wb") as output:

0 commit comments

Comments
 (0)