From ab81180a87cf4712f1fddd56388fc604973d2760 Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 4 May 2018 08:21:12 -0300 Subject: [PATCH 01/45] Update README New fork with the script working again. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f9c7d4..13b6953 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ Allows WhatsApp users on Android to extract their backed up WhatsApp data from G ###### BRANCH UPDATES: v1.0 - Initial release. v1.1 - Added Python 3 support. +v2.0 - Fixed gDriveFileMap after Whatsapp q requirements update + Fixed downloadurl (the scrip is working again!) +v2.5 - Added multithreading support ###### PREREQUISITES: 1. O/S: Windows Vista, Windows 7, Windows 8, Windows 10, Mac OS X or Linux - 2. Python 2.x or 3.x - If not installed: https://www.python.org/downloads/ + 2. Python 3.x - If not installed: https://www.python.org/downloads/ 3. Android device with WhatsApp installed and the Google Drive backup feature enabled 4. Google services device id (if you want to reduce the risk of being logged out of Google) Search Google Play for "device id" for plenty of apps that can reveal this information 5. Google account login credentials (username and password) + 6. Whatsapp cellphone number as shown in backup tab on google drive website. ###### INSTRUCTIONS: @@ -30,3 +34,6 @@ v1.1 - Added Python 3 support. ###### CREDITS: AUTHOR: TripCode + +###### CREDITS: + CONTRIBUTORS: DrDeath1122 from XDA for the multithreading backbone part From 6c6296246cc25f2e8977cfd71f4cf54856203800 Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 4 May 2018 08:42:15 -0300 Subject: [PATCH 02/45] Update WhatsAppGDExtract.py Fixed gDriveFileMap Fixed downloadLink added multithread (DrDeath1122 ) --- WhatsAppGDExtract.py | 132 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 22 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 808572e..879ce30 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -1,21 +1,37 @@ #!/usr/bin/env python - + from configparser import ConfigParser import json import os import re import requests import sys - +import queue +import threading +import time +from pyportify.gpsoauth import google +exitFlag = False + + def getGoogleAccountTokenFromAuth(): - payload = {'Email':gmail, 'Passwd':passw, 'app':client_pkg, 'client_sig':client_sig, 'parentAndroidId':devid} + + b64_key_7_3_29 = (b"AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3" + b"iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pK" + b"RI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/" + b"6rmf5AAAAAwEAAQ==") + + android_key_7_3_29 = google.key_from_b64(b64_key_7_3_29) + encpass = google.signature(gmail, passw, android_key_7_3_29) + payload = {'Email':gmail, 'EncryptedPasswd':encpass, 'app':client_pkg, 'client_sig':client_sig, 'parentAndroidId':devid} request = requests.post('https://android.clients.google.com/auth', data=payload) token = re.search('Token=(.*?)\n', request.text) + if token: return token.group(1) else: quit(request.text) - + + def getGoogleDriveToken(token): payload = {'Token':token, 'app':pkg, 'client_sig':sig, 'device':devid, 'google_play_services_version':client_ver, 'service':'oauth2:https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file', 'has_permission':'1'} request = requests.post('https://android.clients.google.com/auth', data=payload) @@ -24,12 +40,18 @@ def getGoogleDriveToken(token): return token.group(1) else: quit(request.text) - + def rawGoogleDriveRequest(bearer, url): headers = {'Authorization': 'Bearer '+bearer} request = requests.get(url, headers=headers) return request.text - + +def gDriveFileMapRequest(bearer): + header = {'Authorization': 'Bearer '+bearer} + url = "https://www.googleapis.com/drive/v2/files?mode=restore&spaces=appDataFolder&maxResults=1000&fields=items(description%2Cid%2CfileSize%2Ctitle%2Cmd5Checksum%2CmimeType%2CmodifiedDate%2Cparents(id)%2Cproperties(key%2Cvalue))%2CnextPageToken&q=title%20%3D%20'"+celnumbr+"-invisible'%20or%20title%20%3D%20'gdrive_file_map'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt12'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt11'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt10'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt9'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt8'" + request = requests.get(url, headers=header) + return request.text + def downloadFileGoogleDrive(bearer, url, local): if not os.path.exists(os.path.dirname(local)): os.makedirs(os.path.dirname(local)) @@ -43,30 +65,31 @@ def downloadFileGoogleDrive(bearer, url, local): for chunk in request.iter_content(1024): asset.write(chunk) print('Downloaded: "'+local+'".') - + def gDriveFileMap(): global bearer - data = rawGoogleDriveRequest(bearer, 'https://www.googleapis.com/drive/v2/files') + data = gDriveFileMapRequest(bearer) jres = json.loads(data) backups = [] for result in jres['items']: try: if result['title'] == 'gdrive_file_map': - backups.append((result['description'], rawGoogleDriveRequest(bearer, result['downloadUrl']))) + backups.append((result['description'], rawGoogleDriveRequest(bearer, 'https://www.googleapis.com/drive/v2/files/'+result['id']+'?alt=media'))) except: pass if len(backups) == 0: quit('Unable to locate google drive file map for: '+pkg) return backups - + def getConfigs(): - global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver + global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver, celnumbr config = ConfigParser() try: config.read('settings.cfg') gmail = config.get('auth', 'gmail') passw = config.get('auth', 'passw') devid = config.get('auth', 'devid') + celnumbr = config.get('auth', 'celnumbr') pkg = config.get('app', 'pkg') sig = config.get('app', 'sig') client_pkg = config.get('client', 'pkg') @@ -74,17 +97,17 @@ def getConfigs(): client_ver = config.get('client', 'ver') except(ConfigParser.NoSectionError, ConfigParser.NoOptionError): quit('The "settings.cfg" file is missing or corrupt!') - + def jsonPrint(data): print(json.dumps(json.loads(data), indent=4, sort_keys=True)) - + def localFileLog(md5): logfile = 'logs'+os.path.sep+'files.log' if not os.path.exists(os.path.dirname(logfile)): os.makedirs(os.path.dirname(logfile)) with open(logfile, 'a') as log: log.write(md5+'\n') - + def localFileList(): logfile = 'logs'+os.path.sep+'files.log' if os.path.isfile(logfile): @@ -93,29 +116,93 @@ def localFileList(): else: open(logfile, 'w') return localFileList() - + def createSettingsFile(): with open('settings.cfg', 'w') as cfg: - cfg.write('[auth]\ngmail = alias@gmail.com\npassw = yourpassword\ndevid = 0000000000000000\n\n[app]\npkg = com.whatsapp\nsig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799\n\n[client]\npkg = com.google.android.gms\nsig = 38918a453d07199354f8b19af05ec6562ced5788\nver = 9877000') - + cfg.write('[auth]\ngmail = alias@gmail.com\npassw = yourpassword\ndevid = 0000000000000000\ncelnumbr = BACKUPPHONENUMBER\n\n[app]\npkg = com.whatsapp\nsig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799\n\n[client]\npkg = com.google.android.gms\nsig = 38918a453d07199354f8b19af05ec6562ced5788\nver = 9877000') + def getSingleFile(data, asset): data = json.loads(data) for entries in data: if entries['f'] == asset: return entries['f'], entries['m'], entries['r'], entries['s'] +class myThread (threading.Thread): + def __init__(self, threadID, name, q): + threading.Thread.__init__(self) + self.threadID = threadID + self.name = name + self.q = q + def run(self): + print ('Initiated: ' + self.name) + process_data(self.name, self.q) + print ('Terminated: ' + self.name) + +def process_data(threadName, q): + while not exitFlag: + queueLock.acquire() + if not workQueue.empty(): + data = q.get() + queueLock.release() + getMultipleFilesThread(data['bearer'], data['entries_r'], data['local'], data['entries_m'], threadName) + else: + queueLock.release() + time.sleep(1) + +def getMultipleFilesThread(bearer, entries_r, local, entries_m, threadName): + url = 'https://www.googleapis.com/drive/v2/files/'+entries_r+'?alt=media' + if not os.path.exists(os.path.dirname(local)): + try: + os.makedirs(os.path.dirname(local)) + except (FileExistsError): + pass + if os.path.isfile(local): + os.remove(local) + headers = {'Authorization': 'Bearer '+bearer} + request = requests.get(url, headers=headers, stream=True) + request.raw.decode_content = True + if request.status_code == 200: + with open(local, 'wb') as asset: + for chunk in request.iter_content(1024): + asset.write(chunk) + print(threadName + '=> Downloaded: "'+local+'".') + logfile = 'logs'+os.path.sep+'files.log' + if not os.path.exists(os.path.dirname(logfile)): + os.makedirs(os.path.dirname(logfile)) + with open(logfile, 'a') as log: + log.write(entries_m+'\n') + +queueLock = threading.Lock() +workQueue = queue.Queue(9999999) + def getMultipleFiles(data, folder): + threadList = ["Thread-1", "Thread-2", "Thread-3", "Thread-4", "Thread-5", "Thread-6", "Thread-7", "Thread-8", "Thread-9", "Thread-10", "Thread-11", "Thread-12", "Thread-13", "Thread-14", "Thread-15", "Thread-16", "Thread-17", "Thread-18", "Thread-19", "Thread-20"] + threads = [] + threadID = 1 + global exitFlag + for tName in threadList: + thread = myThread(threadID, tName, workQueue) + thread.start() + threads.append(thread) + threadID += 1 files = localFileList() data = json.loads(data) + queueLock.acquire() for entries in data: if any(entries['m'] in lists for lists in files) == False or 'database' in entries['f'].lower(): local = folder+os.path.sep+entries['f'].replace("/", os.path.sep) if os.path.isfile(local) and 'database' not in local.lower(): quit('Skipped: "'+local+'".') else: - downloadFileGoogleDrive(bearer, 'https://www.googleapis.com/drive/v2/files/'+entries['r']+'?alt=media', local) - localFileLog(entries['m']) - + workQueue.put({'bearer':bearer, 'entries_r':entries['r'], 'local':local, 'entries_m':entries['m']}) + queueLock.release() + while not workQueue.empty(): + pass + exitFlag = True + for t in threads: + t.join() + print ("File List Downloaded") + def runMain(mode, asset, bID): global bearer if os.path.isfile('settings.cfg') == False: @@ -159,7 +246,7 @@ def runMain(mode, asset, bID): print('Backup: '+str(i)) folder = 'WhatsApp-' + str(i) getMultipleFiles(drive[1], folder) - + def main(): args = len(sys.argv) if args < 2 or str(sys.argv[1]) == '-help' or str(sys.argv[1]) == 'help': @@ -188,6 +275,7 @@ def main(): runMain('pull', str(sys.argv[2]), bID) else: quit('\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync|-pull file [backupID]\n') - + if __name__ == "__main__": main() + From b91c16a1c72fcf5a3fa4d105a99666c72e61098c Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 4 May 2018 08:45:08 -0300 Subject: [PATCH 03/45] Fixed exitFlag For downloading multiple drives. --- WhatsAppGDExtract.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 879ce30..1ff6bf5 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -205,6 +205,8 @@ def getMultipleFiles(data, folder): def runMain(mode, asset, bID): global bearer + golbal exitFlag + if os.path.isfile('settings.cfg') == False: createSettingsFile() getConfigs() @@ -241,6 +243,7 @@ def runMain(mode, asset, bID): localFileLog(m) elif mode == 'sync': for i, drive in enumerate(drives): + exitFlag = False folder = 'WhatsApp' if len(drives) > 1: print('Backup: '+str(i)) From 422e107cb5d28255dc66fc67338b339038db42c8 Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 4 May 2018 08:46:03 -0300 Subject: [PATCH 04/45] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13b6953..eeeb07b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Allows WhatsApp users on Android to extract their backed up WhatsApp data from G v1.0 - Initial release. v1.1 - Added Python 3 support. v2.0 - Fixed gDriveFileMap after Whatsapp q requirements update - Fixed downloadurl (the scrip is working again!) + Fixed downloadurl (the scrip is working again!) v2.5 - Added multithreading support From 0a24b917531b55bb91dbdbcaa57d1daec3d9767b Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 4 May 2018 08:46:39 -0300 Subject: [PATCH 05/45] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eeeb07b..743d48a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Allows WhatsApp users on Android to extract their backed up WhatsApp data from G v1.0 - Initial release. v1.1 - Added Python 3 support. v2.0 - Fixed gDriveFileMap after Whatsapp q requirements update - Fixed downloadurl (the scrip is working again!) + Fixed downloadurl (the script is working again!) v2.5 - Added multithreading support From e8d6cd766108c975d928a20e92aa9b3534e25e78 Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 4 May 2018 08:53:04 -0300 Subject: [PATCH 06/45] Update WhatsAppGDExtract.py --- WhatsAppGDExtract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 1ff6bf5..c5c0f84 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -9,7 +9,7 @@ import queue import threading import time -from pyportify.gpsoauth import google +from pkgxtra.gpsoauth import google exitFlag = False From 78272b10dce76c6820e4e39c8649d354f9dfcb02 Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 4 May 2018 08:56:30 -0300 Subject: [PATCH 07/45] Added files for google auth functions Won't need pyportify anymore --- pkgxtra/__init__.py | 1 + pkgxtra/__init__.pyc | Bin 0 -> 154 bytes pkgxtra/__pycache__/__init__.cpython-35.pyc | Bin 0 -> 203 bytes pkgxtra/gpsoauth/__init__.py | 103 +++++++++++ pkgxtra/gpsoauth/__init__.pyc | Bin 0 -> 2985 bytes .../__pycache__/__init__.cpython-35.pyc | Bin 0 -> 2755 bytes .../__pycache__/google.cpython-35.pyc | Bin 0 -> 1617 bytes .../gpsoauth/__pycache__/util.cpython-35.pyc | Bin 0 -> 1278 bytes pkgxtra/gpsoauth/google.py | 54 ++++++ pkgxtra/gpsoauth/google.pyc | Bin 0 -> 1843 bytes pkgxtra/gpsoauth/util.py | 35 ++++ pkgxtra/gpsoauth/util.pyc | Bin 0 -> 1377 bytes pkgxtra/pkcs1/__init__.py | 5 + pkgxtra/pkcs1/__init__.pyc | Bin 0 -> 317 bytes .../pkcs1/__pycache__/__init__.cpython-35.pyc | Bin 0 -> 345 bytes .../pkcs1/__pycache__/defaults.cpython-35.pyc | Bin 0 -> 303 bytes .../__pycache__/exceptions.cpython-35.pyc | Bin 0 -> 2055 bytes pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc | Bin 0 -> 6319 bytes pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc | Bin 0 -> 958 bytes .../pkcs1/__pycache__/primes.cpython-35.pyc | Bin 0 -> 4212 bytes .../__pycache__/primitives.cpython-35.pyc | Bin 0 -> 4743 bytes .../__pycache__/rsaes_oaep.cpython-35.pyc | Bin 0 -> 3341 bytes pkgxtra/pkcs1/defaults.py | 4 + pkgxtra/pkcs1/defaults.pyc | Bin 0 -> 263 bytes pkgxtra/pkcs1/exceptions.py | 35 ++++ pkgxtra/pkcs1/exceptions.pyc | Bin 0 -> 2336 bytes pkgxtra/pkcs1/keys.py | 165 ++++++++++++++++++ pkgxtra/pkcs1/keys.pyc | Bin 0 -> 6859 bytes pkgxtra/pkcs1/mgf.py | 24 +++ pkgxtra/pkcs1/mgf.pyc | Bin 0 -> 988 bytes pkgxtra/pkcs1/primes.py | 159 +++++++++++++++++ pkgxtra/pkcs1/primes.pyc | Bin 0 -> 4605 bytes pkgxtra/pkcs1/primitives.py | 140 +++++++++++++++ pkgxtra/pkcs1/primitives.pyc | Bin 0 -> 5362 bytes pkgxtra/pkcs1/rsaes_oaep.py | 97 ++++++++++ pkgxtra/pkcs1/rsaes_oaep.pyc | Bin 0 -> 3521 bytes 36 files changed, 822 insertions(+) create mode 100644 pkgxtra/__init__.py create mode 100644 pkgxtra/__init__.pyc create mode 100644 pkgxtra/__pycache__/__init__.cpython-35.pyc create mode 100644 pkgxtra/gpsoauth/__init__.py create mode 100644 pkgxtra/gpsoauth/__init__.pyc create mode 100644 pkgxtra/gpsoauth/__pycache__/__init__.cpython-35.pyc create mode 100644 pkgxtra/gpsoauth/__pycache__/google.cpython-35.pyc create mode 100644 pkgxtra/gpsoauth/__pycache__/util.cpython-35.pyc create mode 100644 pkgxtra/gpsoauth/google.py create mode 100644 pkgxtra/gpsoauth/google.pyc create mode 100644 pkgxtra/gpsoauth/util.py create mode 100644 pkgxtra/gpsoauth/util.pyc create mode 100644 pkgxtra/pkcs1/__init__.py create mode 100644 pkgxtra/pkcs1/__init__.pyc create mode 100644 pkgxtra/pkcs1/__pycache__/__init__.cpython-35.pyc create mode 100644 pkgxtra/pkcs1/__pycache__/defaults.cpython-35.pyc create mode 100644 pkgxtra/pkcs1/__pycache__/exceptions.cpython-35.pyc create mode 100644 pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc create mode 100644 pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc create mode 100644 pkgxtra/pkcs1/__pycache__/primes.cpython-35.pyc create mode 100644 pkgxtra/pkcs1/__pycache__/primitives.cpython-35.pyc create mode 100644 pkgxtra/pkcs1/__pycache__/rsaes_oaep.cpython-35.pyc create mode 100644 pkgxtra/pkcs1/defaults.py create mode 100644 pkgxtra/pkcs1/defaults.pyc create mode 100644 pkgxtra/pkcs1/exceptions.py create mode 100644 pkgxtra/pkcs1/exceptions.pyc create mode 100644 pkgxtra/pkcs1/keys.py create mode 100644 pkgxtra/pkcs1/keys.pyc create mode 100644 pkgxtra/pkcs1/mgf.py create mode 100644 pkgxtra/pkcs1/mgf.pyc create mode 100644 pkgxtra/pkcs1/primes.py create mode 100644 pkgxtra/pkcs1/primes.pyc create mode 100644 pkgxtra/pkcs1/primitives.py create mode 100644 pkgxtra/pkcs1/primitives.pyc create mode 100644 pkgxtra/pkcs1/rsaes_oaep.py create mode 100644 pkgxtra/pkcs1/rsaes_oaep.pyc diff --git a/pkgxtra/__init__.py b/pkgxtra/__init__.py new file mode 100644 index 0000000..6c291bb --- /dev/null +++ b/pkgxtra/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.01' diff --git a/pkgxtra/__init__.pyc b/pkgxtra/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c4e232d9bfe7c899b9f7ae66ea1d79cb56c5045 GIT binary patch literal 154 zcmZSn%*%CTdr@RE0~9a0Er`iY%uLSDiz&!XuP7->jERrW%*!l^kJl@x WEa3nuv&qd*Da}c>0~uQk#0&sI9Uo!< literal 0 HcmV?d00001 diff --git a/pkgxtra/__pycache__/__init__.cpython-35.pyc b/pkgxtra/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9eea19b198c3223539766f40095d9eee1d11abaa GIT binary patch literal 203 zcmWgR<>flEy(m(gfq~&M5W@i@kmUfx#auulg@GXoNHQ`6Ycf@_8R!9_p`Rw>E$;aE zvecsD%>2Cg_>~MrOhBbz;#Y#RRZK`~Zb3|VMq){^V?lwgyNj-CMM+U&a!G!XZf;_6 zNorAyUw&DlLIIGSh+kbnb~;FFOniK1US>&ryk0@&Ee@O9{FKt1R6CGEi-DK{08{=r A>i_@% literal 0 HcmV?d00001 diff --git a/pkgxtra/gpsoauth/__init__.py b/pkgxtra/gpsoauth/__init__.py new file mode 100644 index 0000000..5eb014d --- /dev/null +++ b/pkgxtra/gpsoauth/__init__.py @@ -0,0 +1,103 @@ +import requests + +from . import google + + +# The key is distirbuted with Google Play Services. +# This one is from version 7.3.29. +b64_key_7_3_29 = (b"AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3" + b"iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pK" + b"RI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/" + b"6rmf5AAAAAwEAAQ==") + +android_key_7_3_29 = google.key_from_b64(b64_key_7_3_29) + +auth_url = 'https://android.clients.google.com/auth' +useragent = 'gpsoauth-portify/1.0' + + +def _perform_auth_request(data): + res = requests.post(auth_url, data, + headers={'User-Agent': useragent}) + + return google.parse_auth_response(res.text) + + +def perform_master_login(email, password, android_id, + service='ac2dm', device_country='us', operatorCountry='us', + lang='en', sdk_version=17): + """ + Perform a master login, which is what Android does when you first add a Google account. + + Return a dict, eg:: + + { + 'Auth': '...', + 'Email': 'email@gmail.com', + 'GooglePlusUpgrade': '1', + 'LSID': '...', + 'PicasaUser': 'My Name', + 'RopRevision': '1', + 'RopText': ' ', + 'SID': '...', + 'Token': 'oauth2rt_1/...', + 'firstName': 'My', + 'lastName': 'Name', + 'services': 'hist,mail,googleme,...' + } + """ + + data = { + 'accountType': 'HOSTED_OR_GOOGLE', + 'Email': email, + 'has_permission': 1, + 'add_account': 1, + 'EncryptedPasswd': google.signature(email, password, android_key_7_3_29), + 'service': service, + 'source': 'android', + 'androidId': android_id, + 'device_country': device_country, + 'operatorCountry': device_country, + 'lang': lang, + 'sdk_version': sdk_version + } + + return _perform_auth_request(data) + + +def perform_oauth(email, master_token, android_id, service, app, client_sig, + device_country='us', operatorCountry='us', lang='en', sdk_version=17): + """ + Use a master token from master_login to perform OAuth to a specific Google service. + + Return a dict, eg:: + + { + 'Auth': '...', + 'LSID': '...', + 'SID': '..', + 'issueAdvice': 'auto', + 'services': 'hist,mail,googleme,...' + } + + To authenticate requests to this service, include a header + ``Authorization: GoogleLogin auth=res['Auth']``. + """ + + data = { + 'accountType': 'HOSTED_OR_GOOGLE', + 'Email': email, + 'has_permission': 1, + 'EncryptedPasswd': master_token, + 'service': service, + 'source': 'android', + 'androidId': android_id, + 'app': app, + 'client_sig': client_sig, + 'device_country': device_country, + 'operatorCountry': device_country, + 'lang': lang, + 'sdk_version': sdk_version + } + + return _perform_auth_request(data) diff --git a/pkgxtra/gpsoauth/__init__.pyc b/pkgxtra/gpsoauth/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26dbad08e15181aa0f7dc9f71f29669a13a69ae2 GIT binary patch literal 2985 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 literal 0 HcmV?d00001 diff --git a/pkgxtra/gpsoauth/__pycache__/util.cpython-35.pyc b/pkgxtra/gpsoauth/__pycache__/util.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5e896963a813abde80235336fe0f5cf80d8a910 GIT binary patch literal 1278 zcmaJ>!EW0|5S=AuN>-#MXbd+7dRU-9TLP9~G`FG%Qn*2jqA&v3Xd6L*K#?nn7P(98 zuIwmCE=~F|{fi#@6}`H+u2f+a&eIj>^P&Wv6H6Dv=fBm{2`mz6Wg#;>LupBLk5TMwH!X5P#= zk}jtkVImQUN5Xyt>4v#T1Kz|yP5vY1FDzOzK(swz7TyXvIKSNkWyee$(<10M!Sq4J zL^Xx@J)om^FbSH9MXbP*Ic?eO(sR3}tjFt5Sks})y;~*Q0jEprknEq0EN%P<+ng{p1H^`7%k@PWqLSh2E6%%j;F1u4K7!Ue1+FjlNRWC-Xu{ zZ)75wuvns^SoCE(m`&-t&Gk~ApfFjcvi5YjDSTeR)+~7cZrSh;6zTFeYAjE7NTo`& zliD;O40p}mH@a?5kbpU+BP}>_Z5(IEkaW|8r7o)C+5kwS4XB$Z{@K9TrO0y?L$^ zm*#oBztQNkB42Fd4jKxOlLz-0whv%C87}goP}$6x*&?w`i?!2`k%t^NH$hVk-g2K} zqWyvBi2;6x;vxE>kNF9{1M#u2N6^O+{uE#zx~;3bD2)RvmK&&Po#xCvU)Fo9WF?b}}PeGBwpzHPu~DJ=M{l2ctiJZ@yp1{%7(20*`+Uk>PKo5Sjj? z8<`rlTPdupBbi2G7#B&=k#$$5U93Bjb*1RZx-ZkdtOqh3z}k~vk!mPeUzL$$163YL zHdJNI?Ow$rY@Ppw-$iVohA~9q({DDHMZWmjZCfj&8lO5puTod%jEs5uZ~$?+4RC~N zUR32$EmUYw&w|l?JY765@%Y~$g2>9qPUMY|J0ro$4z(J2%C)^;!?d$9d7tKJZ-xQp z$^J!E-JO5A@d@30b@uMAgh+Pycjb+hyPoWNs6WD4F7gg!K7fd+g&xFZ>fQ76Fr@nY zJadaGa|vrs@JP&p0tKA&8@Fu(^D=F=KrOK59Q%o^t4vp`!2IsEu1Z%1b>XuZHc{a} zg!b~&*|%<8&(>+asLEM=v%C#WI$PGhN;lzpwydgU;m+!<_6gt5ubOH-#~Iimf5>d+ z$R3-CJvFEH$SU|e22ZZwpFsimKpyz-!EcvVmx?WVF;VaU;2xtuWbpgttr97A36pDq zrApUYl}5xac?9v`DoHO^2`>52Y!=}O44&;lnJ84t?_dlr#jcOALwk|6*9RC$58@1X7>@a*bH%lxdRKPm0&A`jaB zI&DH8@~Vt^B*iJX+n^)SIFHVIH_y_Ls;kILmk=jSI9EozPKyU)^)6E31lWR9nZMoe zlt*ld(K9EeXL|ODIkXG*jr`?Xk^316h=Fk;D==$1dDJ_^VuhiEgx+Dm3R8q&s{?Dm zCR|Dr(q`3@Od|j!24poL%=a^=i7-7S zYR3#0PTDl-HhD}F!3NB(%yb~HQ-59Lmx{5!PM@hi%a_iFHnh}HW_FEt<)YcvA<@Zy z2i8qf`1Hzgs^G)ex5LhZI1FZ?gIuTH|5PmwZVLA~H}%I5V>8C(b!<TyyFNY>S9G`ju3N?Is9Gie;}0gqM5}i7kVjZ^>BKI zuDs*kaOHx23o;>1Xt^CVdVK}^O1P3aK7xpOmBbob*E_Q!~zF^L(h| zq0Gk4%bt?x?2yl_t)^IJ{t~_!Mbs{+T zq;)?y4grI#yqb z!WQjd$%=xD%frm1NQ!T=r%mKyt^Fc|vB8j~x<3iyR}K;4`6tjlxP&%4hH2>QG*wC0 z*=|27!axP`=&9m9@KA-@_D1mb?i4Q_p%*VnzFZX4NQO&t+uG)T{WI+qyo8`)Kz^vuK)M5% z-BzD921Q^hPtNz9Mol*VYD`L6vuxR?Aw KyapZ=rv3#5S2bGz literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/__pycache__/__init__.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94518d66b1e4f4160c2d66839d5d76e5ad1b034c GIT binary patch literal 345 zcmaKnO-sZu5QZmB*Zq(MPoBN@P(hE2h@y*lso*Ytlwt_msG(aMCUJ3pl7FdJPyGv? zoT=c^B=csT;hiwqpH3&shvUa50QjQ#TavOf3O^$$=?GW@)BrYHjXH+%+_<&p{;GO8{<3jK{sZJNwP%ZfNtDj8ImM} z7BFJs;gC)Fu2OgN+r{-wrqm}OclYxBd@(DQrgp{ss}BC$x%BEXoqq(>tI%LtYaa|2 uS+mgxj&l9K&s8tnTDWTEj|!#in+-}Gx@|i;shjeBZBF@{o@dd;AFp5lNU|@I*#Bjg_WH|tFu?CPx0U}0*90rCc1_q$YEJlWAAe#xyW(Koa zKnj92nO*`_Gx%w;-eN0C%uC77y~Pt;SzMBu8wBBrrlh7NmgbbiCl^%~l;p=l6|q62 zqu|m7#i^w!FzJ;HMQlJ*!NjjLXRDZy)ZBuY@QlQgV#k64U3V8<*NT#&#N?9vBHi3X zplwAle)(mI3I#xRB7SuR+36syF$LMl#fC9Z#}(@pRNmsS$<0qG%}KRm1X^9p0wj2t Gco+eL;aEKY literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/__pycache__/exceptions.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/exceptions.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b6ce570218c2c0333cfca79f7bc687c608bad12 GIT binary patch literal 2055 zcmbuA%}(1u5P&CfLVisGlu)EfwA^w?C3>u?sx(9hm7j_r(V|G!D!Vul#CFFU1n2f$ zs=iXLeT$wtv+05lsLF~&v35Q?{$|Is>z40V&%V6*`ZEvk8#12)jX%&Yt>*ytC4kJq z4|7xi-#xg<5zT}24D~?e49ycQfGiqXwA3TI1hQmk$RVbOx(u>mXv5Mn(I&_hLsu-V5M2eiX6TxwRie*8t{b{;X^m(LiV3l-~vDwCsY^Rv~@pSt~>k0MpQ+o4MIIF63n-mDx1k(j6; zm{o&d63gi|kLyA3Z7QzkXMO<|>7M5BXViT=I8&2ka6S?`-A$6t{$8hdt5FPfjGc){ zwZg%1d?Q{aG#3BAF1ZT%uY=@jnC=XeO)Pz#+#yehe<^hY)N>|wnsH-K4e`#*w1*fY zO&9rI=<$L3>S12<(Gs%~9r&d6a_ryfAph{Jw zg&yCilc_$rJQdNxc`TE6Gm{6$PI+v1$H_>cR=4_zsWeFaB9jWoPN}@Q@4F{v!o9ea z$%JF4O#EInjO92AU3Rz?vTSFp93Qv+S>^uCU^ZK`6C&?J$QuLlDn(u~$i0i)NXSnS dxqis0Bj4${EgE!7k9g^`%e!&J4MvZf=S+MOWJ4T+f_y_FTT1 z(%4vj<%?@ie>+R`XBv5OsDF$%_yYeHmG9gc8VHF zYNn`?((N=g($vgQB}2_DRk9==IvH}Z^b+*4(c0(uMN6jg6= z&|4&DjGS?DQ{-luIp-8P6XZ;iGes}46GjCsm#1HYeOi1iP?klM_odhLLT|?nxaIEF zTtD>MEpE*^?z-J+gjP-N`C;3VcFSou%ZVsgcSDO8y3ypl`w#AX{@IH4$;$nuJ1d_& zUaT?w!IMPgW4yrw6fV>SzEDC#WT zRA5lh;wH3+Qr8o-Zcu5toLKCB@X;tavvtdS-!WZP4%Ebr+ht>@2&K2Okf%UhsYDl( zXfor{^`&J^p%w7wq@yhB88tPE3H-}nNU?kq6=qpb(%2HzKHKLW4m*!_D?tenT&7Ua zW`cgEq~a4?nWsY z*sfnrD7hjL92PB&@g4svLJkFK6eKR-i*%<)6}X`191t!38uzj0KM^f7XfxUOf=w8? z`iclShr~>ehSvHE6-E=K0%3D^m?Q|MkFIv`i$8PjTa+Vb|+F=0uVbhV*T+DI7oQ!bCJ}BAN zB)bK_0qR$JUqe^?@E7h`e zIioJ9qD`z(4mXZ}AliJ(fmF^j=NE>YOAZo;dFN0W_*cJsi20d==A|da5yZso6C#K_ z&6~W##ThQnaxn}O%#M7Ii=mYdP5m4O_#K9cvHvAuh}W~x+#_H#58LY!i28sjk=NXxC%P_nz6*OR1@OMGndkH2)$&YX`{wn&f2GxMye~cvCYf5VK>G&C ze~8^LX+KGur_=-p!$J${f$aiqPSAdFpvTF9axercP^H$*81=@fhxM=*LC}qSD)wPV z&`OfzAGzLwN+GaXHHRNyDbVHi6wT=Fl3pA9tuN1$?Wbb3HO1hU7r_2>MfE7Wz!6hn zqw=)8$aMonl<_6faV;Dg+i}@MmU(Wv>UP_mFdDxT1c-4sf83X{Eu*yU`)&&j0B>*`1u5{H6SMfsi7{~wP`W6_#SF^4m=mYb z&Z9Le(!vlWw9aDWyf8&J0gz{&;J)8DMP4~JMe3MTU52OZoLdLua1%Qp!J|9`mfesu#O}pRgv%hF6ug+>3eNIqqKJ*K9G@__@S>o(sz28=n2VnR$LCPXPwnk$t`KPrlU0ee~Wy#N3J literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..190fbe379f36edd02ee092be303e213977696344 GIT binary patch literal 958 zcmaJfgc5S{f$QkhnK99nUKmEb_6rjVRag%DL*3PMzfR1~71Xd7?rZEUaET}N$| zTxj_*`~v=qubg`3#)(-cEj=LCyW{8GH#2X>ySlOx?0;;3`r!clgloe^`~sJLiYCNK zAOYwha3FLcA&@%Ib0Br0=K|pXM;?SYeK;r3^Q}X-QM*8I*t;NT8&0__GQ%UT2172A z(id&1^P$aTM;(`stAk6QqnSEzhG79_E=-9HIQBq21cMll2z*80!pwu?*I-bV2c}|9 zT{uI61KXb;!^{Wc!4w7gc57CHDSkRI#RN52!!Sp90I-H}zI5OKPebGo_~yXfZllf~ zq49zn-$r@b;{`BiQ&jrB8#q=m~4>nY{5hnnV8B^<(FC|M$%|5hF?0xOvi&^!n9tv zg(BkGP?khe2@|74Nej)_X~;*cNDLL4c3*9^t^~g4rcfE3u%zIds4UmK2xU`YAy}Zz zi+c6HB?|xorz;C_k|r{Xe&MeKWpBWRhi0!)D{st24YWArW$?jf+EGfX(#I@#KV0i! z$z^TMY}9*8X*8-DhiK85GWc6<>BqKLCD|{DC`so#s9PA|H|uQn_j#K44`OEYR-QL^ zx0^f1MzNuhs)?ZtSN%OX!9o$U|MT*3WL^9Dc&J-_RJ4(wlpelY_U3Av5rSo6pBlMO zZjyUM-N8Mqxo)*4;;;;IB~oF;gzIaCdOBvUid*TvW^SW@x%4cRVUh6XwvXDjX4zSF F{s3Df_PziB literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/__pycache__/primes.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/primes.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8f210de015069ddde3c4d687dbf62d8a043b978 GIT binary patch literal 4212 zcmb7HO>Y~?5v`sfDT<=LWvya3M&|>GX^8gP$RTUQSu3`@P+)5fQHIxw4PwY?QDc!a zbobB_1sfa0K4gDL4!H$Et~mur_P)2dgK>K>A^cC&FP&9J9GYpULRRa~8& zt?Yhw{V^>O{ew<@=Fq-{%lsX;5dVU@M0;p>6#CQ^)bnW1Bf-xF>iX0x&|ZOhMcON( z7hfe_v^Pb=)zJo1{1}#a=ux~x1y!cQ3h4t9LOhNIeHLQIG=*i_tB@*?LR(OXdxhTk zv^PWH45{L%ze?dOy}_|)%uzT`Z(!jpg$ooe+V!!$pqn=PMh(KGse?F7dXw(99`@5w_er%E zb-PN}bb zT6hh&wRK84_$&Oet4u1ReHqKh$Uw4Zl#BCL{b6vh%c2ul%kS2D)z$m%TP3+UxNh|0^CXL$mK(y20Xt$u-NpzGR^vwFG>oDKm znI)5b8Kt~)l*ERO`P$yC=>OP#d|-YGyTDo`CnN5Vb(uqjeRbdAV25#VMeZJ`k+lp7 z8TI;I)l+e5t>pPIGGs@)VFPEHGys&sD`P+JsC1xX`6B2J)D5|-2XMJTWQLOX6<*kqhUai>9K2nPkIqO{|;~8c(SW{{U?tOg4AsE`}I%m)Hjb|W*Z2q_X2~EKiP)y zvX6G~?eqGF9bWpRf7mwHo>+6umHtpyaTJu0%qng)R~03}S4k|0s<({myl3aU{WXTi zBrf8ijLVYPpmE$~);$9?t#Gcr7W4|TBkZ$BYs)X~)5-@B&kryHDsvCm47CGe!ACIE zr`N*QJ3M6BTe9r+nf^@t0*0ya+^0t%Z=+TY{tUW);y9(wP?b5s%<4eXv@{3;@CLs- z+U5bfRFem)+sB0cLEL8cOrU3%)ZNM`maQl?qdl$nCeQ~Ju3i7>2<(pgfy|kImhz)% z-@@ScB=Cfc6I~dCXrvDTv@7?-d$Jf#8`)@8X{;Ql={-4n;;?tQ`mS)U%Ye1Cn;-zW zX4*qEgGe*oCRET1c>D(TZi6y29R(1_AxFc)Qdcl}uM(?cWGhzcS}`kd=+5k?=lKlg zmAf(IvuW(+R(*&^!vz4%_+nX{7Z*hVJkU&vHk-#EKff&K|Aq&L^}_-go4Jso7|qCK z==mY%GVZZ_;9T|-n|Xo%R~{X|$ccfe`j2`0mi}$tTH}4dniUY_V>E8*lqoWUl^DU6 ztGK*ZNS1=0-?V*#R(J)tZZh%WtBAP{MI=J*p1e zR}lvmFx!QAf6ODktz0nfDw2;lfDOd2Q7fE0blfm`su~v;vD_S>bds*ls#^fdBcyuS z3Xsx{t>>&;Z0b9c;;fhM?$$hOedhO*BhD(v7VDXKmWlndewlNAY2u7n8S^W=n_&h~ zaoJn7{?XsVQ=7fcAJ%!ktS9cEaeUJ{1NE1nJ5R6LcZTj;*fX{OLHLLT9-goz86Gt% zR5;@KEQA}%4!nJJ3U7cCr*2nowUeI3kCK;;4x;vft--oS!O+NdqP1$LHDJZM#CdBn zEsFCQ&V!0`vn#Av!qTCJD8A z;wlb3MlNFFXGQ2#M`cqiSCBmG*ILo(&?N1ProITbo{5uYDeB;I!C6`st74wYp6wq8 zhxsuM&LU0+6uEkKVc-}LM+VQFjYnCObFNRzr-C(Jg8uW9Wt@y{K(X}r4~%X7z{ZL4w+T zRK>Y+G|!R$E62roULDo_Ib!ni|9#5y^+~;-!xBfMyii=5I7mYtP9@<OPDpi`PTo-O2W(nLmWa^B3Q_qo929}=10X@~j;n5MtWb4$k- z(zUV!F_*12%Vxr4BGljEX9ja7^jSR5qWg1P<`Ql+T@n><-a8NRT^7@j<5jUFE(jm8 zyo9VA|aI(23F;+@UA8xQa8 zHn(;+cQ$snKHc71c-Yu%-ru@+Z*!-)v+;|qZC68ZmbV&eNoq-E&p!1QwsNpVL-}XD oB!p`Joat`9k6TqNKs>o-_F;WR%)3gfh7qW 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 sP7-yWfMYtitvKax-C1{BcYlG?e3>nKl^5b z=x18H$|%2$M}G?w;U{QJ)JDOhs6b;u6OY;+39kFDUZ8d%s~26}r?#KfOEfOhq(to! z>SdBdLs*}N6~>h%icqN1S4C=9>Cu21MRocL;3x>Xv)?#HMs({`!0roMOI19HO?*#k zuF3mdnVLA6a;-4w_wct9$zC`cn@(4qq$cU8a2h3(-x;RMc=YGMo)_pL z(V|TAG7V9v&>U$CT2yI{Bn8bY)Y`-U-l9gvqq(4maMoz#lX{O#frdVP2@jeVY3}h` z!IhH3t@A`0pszLFLr81U;Q7VO%C?`CO}=I33tm8aU`bU3>mn(tr{E8&kG(T%BFksu6?PmByEQtPlU zgPD$}eKT*xIw4~o$ff)+BLd;Vg|`H$Q->y0VnS;iu+QVU^NyQD5+D~HEX>GZVni893Kbq z6yxiKU72@=9e?P$nS^>2^yO5lkUxi@H=6<{ndX67%E$$}f=Mvz5^&ncFbb01swq|? zHmfDcKArq!qH;8awc#0g^nEtW%WLCs=|38l!>ooA^i(og51k~@Y%DJaaW8NQ$2z!` zOxa-ql#x6(N0Q?#$BhA*E35tRku)2L zeFJxs?8%5`SqHHnI zSL3u@R63OD{)Kk!>&4){Bm9IXdp}7PM8iGU~urW!TJHUwYYNJvp%gzpgDMGn3BDccd;M zk3R<2-#EG}C+X2AgV5+}X?pPPI|n!JW2jw|r~_bN_aEI#?%@+eIsEsybkygjN9m}m z507|8bSJ^ITuD!CdASF8<@75sv{My)Z{x0t4bc!!il;SaWC zdEWT?n~wZ;rXwKIqSKL5t|R3&)hN@bK>8Ys1uIL5TKL-?VLfiLl7MupdbZAxwKG6l37aL;yf)NC@q#hXciCN$qJ4TBahTVm;(PeCA1`fZ zI`F@y{fDOIy#H8=UlRMVgy60SpJnmoC|fBR?K=>~4DeuU>E#HiRH77fyrcA68Qo`0 z?@0gjPh9(3s?H$|HaJX!`afo&x#3zfSF0Tu$HY;pw%M`CrpcxSWBpz{jWQW%F82@m zcL27?14wNVhux_GkJnM)-NQ#2#hjyd*{mpBJ;Bu{**wMOMK%}NyujvZ7%NWH^IUn3 zO~7W4&9iKtfoX4@@p^|(+0yzrj~FXrN6(pg36Ws?Y!+WdMZXS1O-G;JCDHI4O?phB zuYoihjx={coX?}w0J$=yRz=H|)gjvM*KK)uz_H|X&do}uO_k+gUaJy9AytF$ukK_Nj z3!js5c32!a^c7&=;I|2iz(=42H~~n2D}e`c&~eytDP7L%);OK_R-gJ~GFCR?Yw~BoPr5U=OPWb#sA2`z6*afZ-PH^` bw{zOBvNO}V5oT9Pi>8!of@~v{IEcv`O#?v5 literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/exceptions.py b/pkgxtra/pkcs1/exceptions.py new file mode 100644 index 0000000..7720863 --- /dev/null +++ b/pkgxtra/pkcs1/exceptions.py @@ -0,0 +1,35 @@ +class PKCS1BaseException(Exception): + pass + +class DecryptionError(PKCS1BaseException): + pass + +class MessageTooLong(PKCS1BaseException): + pass + +class WrongLength(PKCS1BaseException): + pass + +class MessageTooShort(PKCS1BaseException): + pass + +class InvalidSignature(PKCS1BaseException): + pass + +class RSAModulusTooShort(PKCS1BaseException): + pass + +class IntegerTooLarge(PKCS1BaseException): + pass + +class MessageRepresentativeOutOfRange(PKCS1BaseException): + pass + +class CiphertextRepresentativeOutOfRange(PKCS1BaseException): + pass + +class SignatureRepresentativeOutOfRange(PKCS1BaseException): + pass + +class EncodingError(PKCS1BaseException): + pass diff --git a/pkgxtra/pkcs1/exceptions.pyc b/pkgxtra/pkcs1/exceptions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93174007d2d2afdec63744d6e2fcede17a895d63 GIT binary patch literal 2336 zcmd6oTW`}a6vt1R-Y?tOpn!yg;HlaJ(s)D&!Ac({Hin!^FjT3sVlRzYoyc~l?is!@ zUx0J6+T?);q)F*o#XkP2vHv|chrb8i>9^MxF>F2|&hPOt{Qy7!NPx=#L;$}4ega&4 z$3+cfP|(oR8i=~14NpT5O-Eav)(SfHe5Ic_UdfEoD=ja1Z zJ0SKQJ@B*(;-RCDJnexPI{MhtK8Pm}jh>=c>^=@(K8>TJV{YVR9n0LNneyI4pTRMC zuZ;t`4!y;q5!x`S;a~~R=ZY`od~UnNVws866)taog$N4E9HKRTGn>j~K3noM&eSab zk*uxeILGE_Cf(`gReraA6&3VBNfyQLL|K_gO)1WG zQoXA>s9{*r^c?L?PBEElX{55;rq}X(WzR1eS6k3XIZqAMh4Ura<1}ALZROhD8=2G| zo|R<2K-<^Ay;0dmRTO0BOe)@qiZhX_WILUo6i`Sviaw7BSV~NPXBIaW3lTAD%8WW8 sqt?Ty)EN~PqZb&Z3Zph($ literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/keys.py b/pkgxtra/pkcs1/keys.py new file mode 100644 index 0000000..2093365 --- /dev/null +++ b/pkgxtra/pkcs1/keys.py @@ -0,0 +1,165 @@ +import fractions +from . import primitives +from . import exceptions + +from .defaults import default_crypto_random +from .primes import get_prime, DEFAULT_ITERATION + +class RsaPublicKey(object): + __slots__ = ('n', 'e', 'bit_size', 'byte_size') + + def __init__(self, n, e): + self.n = n + self.e = e + self.bit_size = primitives.integer_bit_size(n) + self.byte_size = primitives.integer_byte_size(n) + + + def __repr__(self): + return '' % (self.n, self.e, self.bit_size) + + def rsavp1(self, s): + if not (0 <= s <= self.n-1): + raise exceptions.SignatureRepresentativeOutOfRange + return self.rsaep(s) + + def rsaep(self, m): + if not (0 <= m <= self.n-1): + raise exceptions.MessageRepresentativeOutOfRange + return primitives._pow(m, self.e, self.n) + +class RsaPrivateKey(object): + __slots__ = ('n', 'd', 'bit_size', 'byte_size') + + def __init__(self, n, d): + self.n = n + self.d = d + self.bit_size = primitives.integer_bit_size(n) + self.byte_size = primitives.integer_byte_size(n) + + def __repr__(self): + return '' % (self.n, self.d, self.bit_size) + + def rsadp(self, c): + if not (0 <= c <= self.n-1): + raise exceptions.CiphertextRepresentativeOutOfRange + return primitives._pow(c, self.d, self.n) + + def rsasp1(self, m): + if not (0 <= m <= self.n-1): + raise exceptions.MessageRepresentativeOutOfRange + return self.rsadp(m) + +class MultiPrimeRsaPrivateKey(object): + __slots__ = ('primes', 'blind', 'blind_inv', 'n', 'e', 'exponents', 'crts', 'bit_size', 'byte_size') + + def __init__(self, primes, e, blind=True, rnd=default_crypto_random): + self.primes = primes + self.n = primitives.product(*primes) + self.e = e + self.bit_size = primitives.integer_bit_size(self.n) + self.byte_size = primitives.integer_byte_size(self.n) + self.exponents = [] + for prime in primes: + exponent, a, b = primitives.bezout(e, prime-1) + assert b == 1 + if exponent < 0: + exponent += prime-1 + self.exponents.append(exponent) + self.crts = [1] + R = primes[0] + for prime in primes[1:]: + crt, a, b = primitives.bezout(R, prime) + assert b == 1 + R *= prime + self.crts.append(crt) + public = RsaPublicKey(self.n, self.e) + if blind: + while True: + blind_factor = rnd.getrandbits(self.bit_size-1) + self.blind = public.rsaep(blind_factor) + u, v, gcd = primitives.bezout(blind_factor, self.n) + if gcd == 1: + self.blind_inv = u if u > 0 else u + self.n + assert (blind_factor * self.blind_inv) % self.n == 1 + break + else: + self.blind = None + self.blind_inv = None + + + def __repr__(self): + return '' % (self.n, self.primes, self.bit_size) + + + def rsadp(self, c): + if not (0 <= c <= self.n-1): + raise exceptions.CiphertextRepresentativeOutOfRange + R = 1 + m = 0 + if self.blind: + c = (c * self.blind) % self.n + for prime, exponent, crt in zip(self.primes, self.exponents, self.crts): + m_i = primitives._pow(c, exponent, prime) + h = ((m_i - m) * crt) % prime + m += R * h + R *= prime + if self.blind_inv: + m = (m * self.blind_inv) % self.n + return m + + def rsasp1(self, m): + if not (0 <= m <= self.n-1): + raise exceptions.MessageRepresentativeOutOfRange + return self.rsadp(m) + +def generate_key_pair(size=512, number=2, rnd=default_crypto_random, k=DEFAULT_ITERATION, + primality_algorithm=None, strict_size=True, e=0x10001): + '''Generates an RSA key pair. + + size: + the bit size of the modulus, default to 512. + number: + the number of primes to use, default to 2. + rnd: + the random number generator to use, default to SystemRandom from the + random library. + k: + the number of iteration to use for the probabilistic primality + tests. + primality_algorithm: + the primality algorithm to use. + strict_size: + whether to use size as a lower bound or a strict goal. + e: + the public key exponent. + + Returns the pair (public_key, private_key). + ''' + primes = [] + lbda = 1 + bits = size // number + 1 + n = 1 + while len(primes) < number: + if number - len(primes) == 1: + bits = size - primitives.integer_bit_size(n) + 1 + prime = get_prime(bits, rnd, k, algorithm=primality_algorithm) + if prime in primes: + continue + if e is not None and fractions.gcd(e, lbda) != 1: + continue + if strict_size and number - len(primes) == 1 and primitives.integer_bit_size(n*prime) != size: + continue + primes.append(prime) + n *= prime + lbda *= prime - 1 + if e is None: + e = 0x10001 + while e < lbda: + if fractions.gcd(e, lbda) == 1: + break + e += 2 + assert 3 <= e <= n-1 + public = RsaPublicKey(n, e) + private = MultiPrimeRsaPrivateKey(primes, e, blind=True, rnd=rnd) + return public, private diff --git a/pkgxtra/pkcs1/keys.pyc b/pkgxtra/pkcs1/keys.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0af1015c2580624826ae41b673e34b1deee81da3 GIT binary patch literal 6859 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 OrZ97CW@hI5W9Hxan+K%; literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/mgf.py b/pkgxtra/pkcs1/mgf.py new file mode 100644 index 0000000..6f6ee7b --- /dev/null +++ b/pkgxtra/pkcs1/mgf.py @@ -0,0 +1,24 @@ +import hashlib + +from .primitives import integer_ceil, i2osp + +def mgf1(mgf_seed, mask_len, hash_class=hashlib.sha1): + ''' + Mask Generation Function v1 from the PKCS#1 v2.0 standard. + + mgs_seed - the seed, a byte string + mask_len - the length of the mask to generate + hash_class - the digest algorithm to use, default is SHA1 + + Return value: a pseudo-random mask, as a byte string + ''' + h_len = hash_class().digest_size + if mask_len > 0x10000: + raise ValueError('mask too long') + T = b'' + for i in range(0, integer_ceil(mask_len, h_len)): + C = i2osp(i, 4) + T = T + hash_class(mgf_seed + C).digest() + return T[:mask_len] + + diff --git a/pkgxtra/pkcs1/mgf.pyc b/pkgxtra/pkcs1/mgf.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af106a7c90f55017e5c91a52a2d355b50049f876 GIT binary patch literal 988 zcmb7CO>Yx15FPJsLKRZrL~mfJ2Xbf&snkn_5GYVU2!xV~k0>bG?mD|}cGto7MlDJX zQ2sGL0G^$+<-i3iemkDeHaeFA)+;+g|MiXWqbXb2&oG^B!28PG7G zGNfThOc0$$lt%QE(XipX2b~5Yk)l)Rz`f#X*wz$bkO3f^ENh9{!&yz$5yI} zAJtXjg?X1xv?{sH1%LmhH`wm-`TpJ`Zfsnou}=3|mnq83jExW}-}TN;+~F}lIky7d zT2|SWCyI>=QLQ~8Wj5z(;^Ruk#44Vx$cW3JJU00_DPm*RVX4f7u{VmKNH)To0$ZTS4YkrCs z3bdgh*Z>j0&(+?9v?4b)@|&dIpv)$I zo~y@tWIoyTD15Tq%@VsqbnH=j>~)|Z4`v%u?{IW1%Gs!lWumIlY?_@}9pjsr?g+K^ zX6M)?U$*PE)!YG4tIgW%Hfyr`zu^@ Y{}I|i?Bi0Ubs?U(293+w4( 0 + assert b > 0 + + if a == 0: return 0 + result = 1 + while a > 1: + if a & 1: + if ((a-1)*(b-1) >> 2) & 1: + result = -result + a, b = b % a, a + else: + if (((b * b) - 1) >> 3) & 1: + result = -result + a >>= 1 + if a == 0: return 0 + return result + +def jacobi_witness(x, n): + '''Returns False if n is an Euler pseudo-prime with base x, and + True otherwise. + ''' + + j = jacobi(x, n) % n + + f = pow(x, n >> 1, n) + + if j == f: return False + return True + +def randomized_primality_testing(n, rnd=default_crypto_random, k=DEFAULT_ITERATION): + '''Calculates whether n is composite (which is always correct) or + prime (which is incorrect with error probability 2**-k) + + Returns False if the number is composite, and True if it's + probably prime. + ''' + + # 50% of Jacobi-witnesses can report compositness of non-prime numbers + + # The implemented algorithm using the Jacobi witness function has error + # probability q <= 0.5, according to Goodrich et. al + # + # q = 0.5 + # t = int(math.ceil(k / log(1 / q, 2))) + # So t = k / log(2, 2) = k / 1 = k + # this means we can use range(k) rather than range(t) + + for _ in range(k): + x = rnd.randint(0, n-1) + if jacobi_witness(x, n): return False + + return True + +def miller_rabin(n, k, rnd=default_pseudo_random): + ''' + Pure python implementation of the Miller-Rabin algorithm. + + n - the integer number to test, + k - the number of iteration, the probability of n being prime if the + algorithm returns True is 1/2**k, + rnd - a random generator + ''' + s = 0 + d = n-1 + # Find nearest power of 2 + s = primitives.integer_bit_size(n) + # Find greatest factor which is a power of 2 + s = fractions.gcd(2**s, n-1) + d = (n-1) // s + s = primitives.integer_bit_size(s) - 1 + while k: + k = k - 1 + a = rnd.randint(2, n-2) + x = pow(a,d,n) + if x == 1 or x == n - 1: + continue + for r in xrange(1,s-1): + x = pow(x,2,n) + if x == 1: + return False + if x == n - 1: + break + else: + return False + return True + diff --git a/pkgxtra/pkcs1/primes.pyc b/pkgxtra/pkcs1/primes.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b97543c95ebc5e2d0b4351de311cfc155fe5c57 GIT binary patch literal 4605 zcmbVP-)|gA5w4zHJNEi#Y{xOyakLPE&B;#UhzE!+TukCS89Baa){%orw3^*%+Y|53 ztfyz=U1SSH%mdW2tEtM) z^LsyUQvNrB--jsnAE-k7SJWcfL&Kx6L@h;akM=xL{9UG2iP~k_D^q)f_D0Z)pHW`4 zH%9%%!3Jae8IJPMZhW1}W}J>Hlpc^$DpvG7yheK!G842nNv2Gu!aIaxbY7ypDGJBQ zj0`#}bdK|;DV(5l$TdUZq%>v+bEfDV0$%5R!f85(By+r5tvZ7XMgPX%cBIg(a+Ot3 zb<(IEWzlP6s~-2=Koy$(pxeso9c#K_Qcr_8OxkaaG}C@38;saVtZ7HBmPuFBpb^FP zIyw&8o&M^@-soQ?qGlOGT&|%!La~29by^~5;b@G~8koDdS_erQWruCMGUz(Ww|8Rc zWM4-a?;ItuWntdgyAgeZ-G>L}r?3h{B3+EQLsoR|H>|5C&K-PqhueDR&#)3(kuEBe!* z1yv@{V|o%AMM*}P21HV2nA;YOM!X~y0a*}6DWR$vD>gR-&@FzP%d`lShF zmgu|AP8G!mRX$gwiG3E>m+x`3h6btPgQT+GN9)o37dxiidC?A{W)i>X93AvE!+fLZ(;;M<{m*R2$j(YOqS@4Dy7ePNU%F9*ex;s8TMri zQ|-7!y9HSP3iEvC5M?d_l@YQ+XvZbX?K>?ztS93MbfyZ57kt2^WeI>-HGzz?SV5B$C-du3QuE ziD4*hAfs^7kT))ZyEwa0*xN{bn>JT;z)adoV1S%4bnr?MD< zuT1G*^7g~@_jzlH_W|Y>0IyHbc$l6rz$UPgqpM(Y5vO}+L}!YQA4s2~1zrR&x;1~i zxCUZ)w1xSYyqgOSq5dvHVegu#*Zz+07qpt)RshlxoC#|W{O@N$GigNH_S=o5rK`c6 z#w~pU#x}YE=7x8`in^hL)cId0v67(-{KdkDWCG|v2}2hk?xzwS?0vnuwxaK91k*L= zSP+K9v7C#s@?HAF7Fq|lrwn(Y<|+>gw+2!-kS&1Jj72tdHNW8XujIO7aE(v8HmByi zNh*U}XH*65QBe!tBI;?yz2YWJ>Re>i12o_oP&llHd9aY4bQa%xq%4(M9^EUBEgXM59O_6+M1=M-(T4tB6%l^e{p}&-hHI_-?i1DcS||)WlotKY822 z+2O~We7KCUnY6`ujjo;?M$JP`Sc29`(6_poq^W6Ux8SIvSEuq~S`_CqoEZ#f4lXrh z8qWIq-bWv;9^G=z|Bh8T-sSlqjvR{Nfa6jpP7{A%^Rtn0Hd=j`M7f+P!nGW19A&ch zz^>ppW>w}{u($thwEb^!B^KsMV%vgP#lCfa86#IRvMG2nY|GPYf-FK00)&zRigM!4~c(*+5 zf^Ctb9wvC&3=&kAjps?i{|oYQo>&Z?J-NH(ub=mMovakko!qB!H1aFM$(h3g2 zQsgu_*$-aZlMfWb8PD-k4DKa&f?~8H|z5|$LCDN=d1YwX-T9Q zh{}6Ki%ARDH%U7 zBuF^w;5~i5NTC90eS>$4Jf}WxCm{mZPgr=%x0_V6;QvwFzl7UY)RZcFqxd)X;f?w6 KrSZv`rT+knnl`)u literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/primitives.py b/pkgxtra/pkcs1/primitives.py new file mode 100644 index 0000000..e418544 --- /dev/null +++ b/pkgxtra/pkcs1/primitives.py @@ -0,0 +1,140 @@ +import binascii + +import operator + +import math + +import sys + +from functools import reduce + +from .defaults import default_crypto_random + +try: + import gmpy +except ImportError: + gmpy = None + +from . import exceptions + + +'''Primitive functions extracted from the PKCS1 RFC''' + +def _pow(a, b, mod): + '''Exponentiation function using acceleration from gmpy if possible''' + if gmpy: + return long(pow(gmpy.mpz(a), gmpy.mpz(b), gmpy.mpz(mod))) + else: + return pow(a, b, mod) + +def integer_ceil(a, b): + '''Return the ceil integer of a div b.''' + quanta, mod = divmod(a, b) + if mod: + quanta += 1 + return quanta + +def integer_byte_size(n): + '''Returns the number of bytes necessary to store the integer n.''' + quanta, mod = divmod(integer_bit_size(n), 8) + if mod or n == 0: + quanta += 1 + return quanta + +def integer_bit_size(n): + '''Returns the number of bits necessary to store the integer n.''' + if n == 0: + return 1 + s = 0 + while n: + s += 1 + n >>= 1 + return s + +def bezout(a, b): + '''Compute the bezout algorithm of a and b, i.e. it returns u, v, p such as: + + p = GCD(a,b) + a * u + b * v = p + + Copied from http://www.labri.fr/perso/betrema/deug/poly/euclide.html. + ''' + u = 1 + v = 0 + s = 0 + t = 1 + while b > 0: + q = a // b + r = a % b + a = b + b = r + tmp = s + s = u - q * s + u = tmp + tmp = t + t = v - q * t + v = tmp + return u, v, a + +def i2osp(x, x_len): + '''Converts the integer x to its big-endian representation of length + x_len. + ''' + if x > 256**x_len: + raise exceptions.IntegerTooLarge + h = hex(x)[2:] + if h[-1] == 'L': + h = h[:-1] + if len(h) & 1 == 1: + h = '0%s' % h + x = binascii.unhexlify(h) + return b'\x00' * int(x_len-len(x)) + x + +def os2ip(x): + '''Converts the byte string x representing an integer reprented using the + big-endian convient to an integer. + ''' + h = binascii.hexlify(x) + return int(h, 16) + +def string_xor(a, b): + '''Computes the XOR operator between two byte strings. If the strings are + of different lengths, the result string is as long as the shorter. + ''' + if sys.version_info[0] < 3: + return ''.join((chr(ord(x) ^ ord(y)) for (x,y) in zip(a,b))) + else: + return bytes(x ^ y for (x, y) in zip(a, b)) + +def product(*args): + '''Computes the product of its arguments.''' + return reduce(operator.__mul__, args) + +def get_nonzero_random_bytes(length, rnd=default_crypto_random): + ''' + Accumulate random bit string and remove \0 bytes until the needed length + is obtained. + ''' + result = [] + i = 0 + while i < length: + l = rnd.getrandbits(12*length) + s = i2osp(l, 3*length) + s = s.replace('\x00', '') + result.append(s) + i += len(s) + return (''.join(result))[:length] + +def constant_time_cmp(a, b): + '''Compare two strings using constant time.''' + result = True + for x, y in zip(a,b): + result &= (x == y) + return result + +import textwrap + +def dump_hex(data): + if isinstance(data, basestring): + print('length', len(data)) + print(textwrap.fill(''.join(['%s ' % x.encode('hex') for x in data]), 72)) diff --git a/pkgxtra/pkcs1/primitives.pyc b/pkgxtra/pkcs1/primitives.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55158d6df32b68fb83eddf37f4a8e54a9338a6e9 GIT binary patch literal 5362 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 Mv(5RX+39)zKXd$UUjP6A literal 0 HcmV?d00001 diff --git a/pkgxtra/pkcs1/rsaes_oaep.py b/pkgxtra/pkcs1/rsaes_oaep.py new file mode 100644 index 0000000..5910a16 --- /dev/null +++ b/pkgxtra/pkcs1/rsaes_oaep.py @@ -0,0 +1,97 @@ +import hashlib + +from . import primitives +from . import exceptions +from . import mgf +from .defaults import default_crypto_random + +def encrypt(public_key, message, label=b'', hash_class=hashlib.sha1, + mgf=mgf.mgf1, seed=None, rnd=default_crypto_random): + '''Encrypt a byte message using a RSA public key and the OAEP wrapping + algorithm, + + Parameters: + public_key - an RSA public key + message - a byte string + label - a label a per-se PKCS#1 standard + hash_class - a Python class for a message digest algorithme respecting + the hashlib interface + mgf1 - a mask generation function + seed - a seed to use instead of generating it using a random generator + rnd - a random generator class, respecting the random generator + interface from the random module, if seed is None, it is used to + generate it. + + Return value: + the encrypted string of the same length as the public key + ''' + + hash = hash_class() + h_len = hash.digest_size + k = public_key.byte_size + max_message_length = k - 2 * h_len - 2 + if len(message) > max_message_length: + raise exceptions.MessageTooLong + hash.update(label) + label_hash = hash.digest() + ps = b'\0' * int(max_message_length - len(message)) + db = b''.join((label_hash, ps, b'\x01', message)) + if not seed: + seed = primitives.i2osp(rnd.getrandbits(h_len*8), h_len) + db_mask = mgf(seed, k - h_len - 1, hash_class=hash_class) + masked_db = primitives.string_xor(db, db_mask) + seed_mask = mgf(masked_db, h_len, hash_class=hash_class) + masked_seed = primitives.string_xor(seed, seed_mask) + em = b''.join((b'\x00', masked_seed, masked_db)) + m = primitives.os2ip(em) + c = public_key.rsaep(m) + output = primitives.i2osp(c, k) + return output + +def decrypt(private_key, message, label=b'', hash_class=hashlib.sha1, + mgf=mgf.mgf1): + '''Decrypt a byte message using a RSA private key and the OAEP wrapping algorithm, + + Parameters: + public_key - an RSA public key + message - a byte string + label - a label a per-se PKCS#1 standard + hash_class - a Python class for a message digest algorithme respecting + the hashlib interface + mgf1 - a mask generation function + + Return value: + the string before encryption (decrypted) + ''' + hash = hash_class() + h_len = hash.digest_size + k = private_key.byte_size + # 1. check length + if len(message) != k or k < 2 * h_len + 2: + raise ValueError('decryption error') + # 2. RSA decryption + c = primitives.os2ip(message) + m = private_key.rsadp(c) + em = primitives.i2osp(m, k) + # 4. EME-OAEP decoding + hash.update(label) + label_hash = hash.digest() + y, masked_seed, masked_db = em[0], em[1:h_len+1], em[1+h_len:] + if y != b'\x00' and y != 0: + raise ValueError('decryption error') + seed_mask = mgf(masked_db, h_len) + seed = primitives.string_xor(masked_seed, seed_mask) + db_mask = mgf(seed, k - h_len - 1) + db = primitives.string_xor(masked_db, db_mask) + label_hash_prime, rest = db[:h_len], db[h_len:] + i = rest.find(b'\x01') + if i == -1: + raise exceptions.DecryptionError + if rest[:i].strip(b'\x00') != b'': + print(rest[:i].strip(b'\x00')) + raise exceptions.DecryptionError + m = rest[i+1:] + if label_hash_prime != label_hash: + raise exceptions.DecryptionError + return m + diff --git a/pkgxtra/pkcs1/rsaes_oaep.pyc b/pkgxtra/pkcs1/rsaes_oaep.pyc new file mode 100644 index 0000000000000000000000000000000000000000..676fe3bc12dd1c2aa281673ccb811849f1393129 GIT binary patch literal 3521 zcmeHK-EQ1e5FUGflHF{6n~=0s)zcQLP})ig60s#G8qO5|1uqDU+6`s}Xv$2LA^ zOM<*NwBlKK1upvnT<{uP@&NG7*xpTpstR#SwjQ6KIWu$SjKA-+^5<&v*|+z;>r(Mo z$NN(}=4Xfi{~Q_<9iib;P@%Cy6PJ!$a=2fWeua)IdA}z8Djikxex1fOn$+p2j{Xv9 zq9J6JhRbYM*C|-0mo++S()rm2y@c5nR(rbNL@423_7XtXQWE>X0xsSKBDnU}i(qHjBUz zc(%#JQNHS&TCFD9 z7{9O8)PzwVgWl5zDw%f1VONdxslw4IJJ9Ou2agWb52>FdP;09E&mZ^WG_-@s?PfW2 z=%@ZfTb-KwAYRv8CW^1zHu&$t@<9X;mtDsTBJ(diwd)#2lZPj4J3 zV-cyJ2Bo=yZwBq|*f&NN9iG}j9I1Su7pJgOIt;?THdf8|qE)I*qPzBtcO^Mso8z#f z!U+58`CVN)L-8Mon~85ms;?uR`uu&U-ZX-pI4Tv5)`2KW!^X(EhB;%kAE>xDUy73m z?fgjc@28xM)6!asv*C=KpU<~>`^-Kh{PSw1nfZ29FO4T>RVQ&U9qZdF>?sL2H0ntl z@qkrmBX|@?T-qu2jSbnm^Lp^Kou-j`?vJN>UJ+bSIxj<5$Pb#g&*KK=qsBVw+kx_p z3@yr0T!koNvW!sVRk^CU>4aS10$ze!i}T-K$An#h$WOX$!XGt0X(F!wyjMXFK0SfG zHFUnpzlvvZ{AG+#7_z5H;9D)t+z?;~9wTcxh+!N?9^Pab*}{9VNyKMgTh4nYv_|5} zH`_jmQ&Eh~y)cnhYJ8pSGc@M7E1w{LG(auDYaWki$_LJ z2W@OjvNf_>Y{@@q7uI+NX%>HCv@NUHXkq}f$XG|b2c0%owj_-kI%s2tQ^;kJ&vAl_ zIUbbddLoW<*_G7AQ=3ffK9os>xrNrl`^V4pBsre=VKdtlN3PyJD>^bZBZ8=>&oY&~*_XYg`d22#H z^^$&;&XCO#jVfdwFu^b()oB~A-4RiG$OHtG1~}&&gacp>5|S#o3?@Mr!e~4T*aufE z2|j~a;1&c_a27fMZm=N82CQA7i~Is(fXl12du`s^y;^ZWU!Wa z3yqlv${y zNDv@zRfq=+H*zWc-mxtju9hSX%^gz1HI5moxTA37zc^U`+QjEy{Bs#6;VRz@zU@Xwc5p##Btk3rL8gz&f&1meD74tm5E1gJem z!xDOf#ABN97{$C6i%p(oXzhhjkP`!I6!nB)#FDJ%Jr&T+>v3h~dSdMM_%`e@;4JXk zyU5*3EH1OS$>Kd0@3Oc8A*gxk-QfN;7S~zqvQR9pLRe1C0=3(GQP&bMt}j+Bg#Qg@ zi@bjbKjtF{S{ImgZ#wI)z^Thn`@tMscLi+MG1hV}qTO=Vq`lAi6doxqP;yN988G4I zEXHS5{J+6?%&f8(Gw=_j!B;G4@CC%i{Vu#ZDw`{^rKhAGCpKvHrZ!LGjk)PXe Jc5XGCzX4HUEJ^?X literal 0 HcmV?d00001 From 473bbd593acdd67f8b6a3dc848bde60776b85ce0 Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 4 May 2018 09:00:38 -0300 Subject: [PATCH 08/45] Update config parser RawConfigParser is more suited since certain caracteres in password will generate errors if ConfigParser is used. --- WhatsAppGDExtract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index c5c0f84..ae0fdf8 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from configparser import ConfigParser +from configparser import RawConfigParser import json import os import re @@ -83,7 +83,7 @@ def gDriveFileMap(): def getConfigs(): global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver, celnumbr - config = ConfigParser() + config = RawConfigParser() try: config.read('settings.cfg') gmail = config.get('auth', 'gmail') From 0970cad0b3719bc2df099e37500c9e99ca969d07 Mon Sep 17 00:00:00 2001 From: YuriCosta Date: Fri, 4 May 2018 09:21:48 -0300 Subject: [PATCH 09/45] Commenting/testing git HTPPS commits from desktop --- WhatsAppGDExtract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index ae0fdf8..1f9f03d 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -154,8 +154,10 @@ def getMultipleFilesThread(bearer, entries_r, local, entries_m, threadName): if not os.path.exists(os.path.dirname(local)): try: os.makedirs(os.path.dirname(local)) + #Other thead was trying to create the same 'local' except (FileExistsError): pass + if os.path.isfile(local): os.remove(local) headers = {'Authorization': 'Bearer '+bearer} From a93158e38a17105f12e54a8348ab20ec9cc17acc Mon Sep 17 00:00:00 2001 From: YuriCosta Date: Wed, 23 May 2018 17:20:57 -0300 Subject: [PATCH 10/45] Mispelling, thank you Dardgaih --- WhatsAppGDExtract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 1f9f03d..445e526 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -207,7 +207,7 @@ def getMultipleFiles(data, folder): def runMain(mode, asset, bID): global bearer - golbal exitFlag + global exitFlag if os.path.isfile('settings.cfg') == False: createSettingsFile() From 0aadb22173df762c9790e4053167616c366f49e7 Mon Sep 17 00:00:00 2001 From: longstone Date: Sun, 27 May 2018 18:28:28 +0200 Subject: [PATCH 11/45] add missing property --- settings.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.cfg b/settings.cfg index 400d974..037015e 100644 --- a/settings.cfg +++ b/settings.cfg @@ -2,6 +2,7 @@ gmail = alias@gmail.com passw = yourpassword devid = 0000000000000000 +celnumbr = 41790000000 [app] pkg = com.whatsapp From 1632226abec8961f1918c9e45d0caa909823d3e0 Mon Sep 17 00:00:00 2001 From: longstone Date: Sun, 27 May 2018 18:31:03 +0200 Subject: [PATCH 12/45] add gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..542ecdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +pkgxtra/pkcs1/__pycache__/ +pkgxtra/__pycache__/ +pkgxtra/gpsoauth/__pycache__/ From 8589a0c0c626f381ba24b5063d272753d41e094f Mon Sep 17 00:00:00 2001 From: longstone Date: Sun, 27 May 2018 18:32:46 +0200 Subject: [PATCH 13/45] add description to cellnumbr --- settings.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/settings.cfg b/settings.cfg index 037015e..cfdd848 100644 --- a/settings.cfg +++ b/settings.cfg @@ -2,6 +2,7 @@ gmail = alias@gmail.com passw = yourpassword devid = 0000000000000000 +#cell number, without leading + or zeros celnumbr = 41790000000 [app] @@ -11,4 +12,4 @@ sig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799 [client] pkg = com.google.android.gms sig = 38918a453d07199354f8b19af05ec6562ced5788 -ver = 9877000 \ No newline at end of file +ver = 9877000 From 6bb330281a752d41b57940d48ef71ddd99e44b17 Mon Sep 17 00:00:00 2001 From: YuriCosta Date: Thu, 14 Jun 2018 03:15:30 -0300 Subject: [PATCH 14/45] ConfigParser exceptions error fixed --- WhatsAppGDExtract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 445e526..4354391 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from configparser import RawConfigParser +import ConfigParser import json import os import re @@ -83,7 +83,7 @@ def gDriveFileMap(): def getConfigs(): global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver, celnumbr - config = RawConfigParser() + config = ConfigParser.RawConfigParser() try: config.read('settings.cfg') gmail = config.get('auth', 'gmail') From 3dc08f46374b38c686d35804355e3fa0f82fbb20 Mon Sep 17 00:00:00 2001 From: YuriCosta Date: Thu, 14 Jun 2018 13:57:06 -0300 Subject: [PATCH 15/45] fixed config parser again, tested still working --- WhatsAppGDExtract.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 4354391..581e64e 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import ConfigParser +import configparser import json import os import re @@ -83,7 +83,7 @@ def gDriveFileMap(): def getConfigs(): global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver, celnumbr - config = ConfigParser.RawConfigParser() + config = configparser.RawConfigParser() try: config.read('settings.cfg') gmail = config.get('auth', 'gmail') @@ -95,7 +95,7 @@ def getConfigs(): client_pkg = config.get('client', 'pkg') client_sig = config.get('client', 'sig') client_ver = config.get('client', 'ver') - except(ConfigParser.NoSectionError, ConfigParser.NoOptionError): + except(configparser.NoSectionError, configparser.NoOptionError): quit('The "settings.cfg" file is missing or corrupt!') def jsonPrint(data): From 545b703088d5e82e3d9bd23d8ea4d0cd35814186 Mon Sep 17 00:00:00 2001 From: YuriCosta Date: Sun, 1 Jul 2018 23:36:35 -0300 Subject: [PATCH 16/45] Log system deprecated, it was checking the filelist on realtime anyway, with exception of database it's almost impossible to exist files with same name on updated backups Better error information --- .gitignore | 1 + README.md | 11 ++-- WhatsAppGDExtract.py | 59 +++++++----------- logs/files.log | 1 - pkgxtra/__pycache__/__init__.cpython-35.pyc | Bin 203 -> 168 bytes .../__pycache__/__init__.cpython-35.pyc | Bin 2755 -> 2720 bytes .../__pycache__/google.cpython-35.pyc | Bin 1617 -> 1582 bytes .../gpsoauth/__pycache__/util.cpython-35.pyc | Bin 1278 -> 1243 bytes .../pkcs1/__pycache__/__init__.cpython-35.pyc | Bin 345 -> 310 bytes .../pkcs1/__pycache__/defaults.cpython-35.pyc | Bin 303 -> 268 bytes .../__pycache__/exceptions.cpython-35.pyc | Bin 2055 -> 2020 bytes pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc | Bin 6319 -> 6284 bytes pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc | Bin 958 -> 923 bytes .../pkcs1/__pycache__/primes.cpython-35.pyc | Bin 4212 -> 4177 bytes .../__pycache__/primitives.cpython-35.pyc | Bin 4743 -> 4708 bytes .../__pycache__/rsaes_oaep.cpython-35.pyc | Bin 3341 -> 3306 bytes 16 files changed, 31 insertions(+), 41 deletions(-) delete mode 100644 logs/files.log diff --git a/.gitignore b/.gitignore index 542ecdf..1c8a609 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ pkgxtra/pkcs1/__pycache__/ pkgxtra/__pycache__/ pkgxtra/gpsoauth/__pycache__/ +.vscode/ diff --git a/README.md b/README.md index 743d48a..1d52d0f 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Allows WhatsApp users on Android to extract their backed up WhatsApp data from G ###### BRANCH UPDATES: v1.0 - Initial release. v1.1 - Added Python 3 support. -v2.0 - Fixed gDriveFileMap after Whatsapp q requirements update - Fixed downloadurl (the script is working again!) -v2.5 - Added multithreading support - +v2.0 - Fixed gDriveFileMap after Whatsapp q requirements update. + Fixed downloadurl (the script is working again!). +v2.5 - Added multi-threading support. +v2.6 - Better errors description, logging system deprecated. ###### PREREQUISITES: 1. O/S: Windows Vista, Windows 7, Windows 8, Windows 10, Mac OS X or Linux @@ -36,4 +36,5 @@ v2.5 - Added multithreading support AUTHOR: TripCode ###### CREDITS: - CONTRIBUTORS: DrDeath1122 from XDA for the multithreading backbone part + CONTRIBUTORS: DrDeath1122 from XDA for the multi-threading backbone part, + YuriCosta for reverse engineering the new restore system diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 581e64e..5d738ba 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -71,14 +71,25 @@ def gDriveFileMap(): data = gDriveFileMapRequest(bearer) jres = json.loads(data) backups = [] + incomplete_backup_marker = False + if not('items' in jres): + quit('Unable to locate google drive file map for: '+pkg) for result in jres['items']: try: if result['title'] == 'gdrive_file_map': backups.append((result['description'], rawGoogleDriveRequest(bearer, 'https://www.googleapis.com/drive/v2/files/'+result['id']+'?alt=media'))) + elif 'invisible' in result['title']: + for p in result['properties']: + if (p['key'] == 'incomplete_backup_marker') and (p['value'] == 'true'): + incomplete_backup_marker = True + except: pass if len(backups) == 0: - quit('Unable to locate google drive file map for: '+pkg) + if incomplete_backup_marker: + quit(pkg + ' has an incomplete backup, it may be corrupted!\nMake sure the backup is ok and try again') + else: + quit(pkg + ' has no backup filemap, make sure the backup is ok') return backups def getConfigs(): @@ -101,22 +112,6 @@ def getConfigs(): def jsonPrint(data): print(json.dumps(json.loads(data), indent=4, sort_keys=True)) -def localFileLog(md5): - logfile = 'logs'+os.path.sep+'files.log' - if not os.path.exists(os.path.dirname(logfile)): - os.makedirs(os.path.dirname(logfile)) - with open(logfile, 'a') as log: - log.write(md5+'\n') - -def localFileList(): - logfile = 'logs'+os.path.sep+'files.log' - if os.path.isfile(logfile): - flist = open(logfile, 'r') - return [line.split('\n') for line in flist.readlines()] - else: - open(logfile, 'w') - return localFileList() - def createSettingsFile(): with open('settings.cfg', 'w') as cfg: cfg.write('[auth]\ngmail = alias@gmail.com\npassw = yourpassword\ndevid = 0000000000000000\ncelnumbr = BACKUPPHONENUMBER\n\n[app]\npkg = com.whatsapp\nsig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799\n\n[client]\npkg = com.google.android.gms\nsig = 38918a453d07199354f8b19af05ec6562ced5788\nver = 9877000') @@ -144,17 +139,18 @@ def process_data(threadName, q): if not workQueue.empty(): data = q.get() queueLock.release() - getMultipleFilesThread(data['bearer'], data['entries_r'], data['local'], data['entries_m'], threadName) + getMultipleFilesThread(data['bearer'], data['entries_r'], data['local'], threadName) else: queueLock.release() time.sleep(1) -def getMultipleFilesThread(bearer, entries_r, local, entries_m, threadName): +def getMultipleFilesThread(bearer, entries_r, local, threadName): url = 'https://www.googleapis.com/drive/v2/files/'+entries_r+'?alt=media' - if not os.path.exists(os.path.dirname(local)): + folder_t = os.path.dirname(local) + if not os.path.exists(folder_t): try: - os.makedirs(os.path.dirname(local)) - #Other thead was trying to create the same 'local' + os.makedirs(folder_t) + #Other thead was trying to create the same 'folder' except (FileExistsError): pass @@ -168,11 +164,7 @@ def getMultipleFilesThread(bearer, entries_r, local, entries_m, threadName): for chunk in request.iter_content(1024): asset.write(chunk) print(threadName + '=> Downloaded: "'+local+'".') - logfile = 'logs'+os.path.sep+'files.log' - if not os.path.exists(os.path.dirname(logfile)): - os.makedirs(os.path.dirname(logfile)) - with open(logfile, 'a') as log: - log.write(entries_m+'\n') + queueLock = threading.Lock() workQueue = queue.Queue(9999999) @@ -187,16 +179,14 @@ def getMultipleFiles(data, folder): thread.start() threads.append(thread) threadID += 1 - files = localFileList() data = json.loads(data) queueLock.acquire() for entries in data: - if any(entries['m'] in lists for lists in files) == False or 'database' in entries['f'].lower(): - local = folder+os.path.sep+entries['f'].replace("/", os.path.sep) - if os.path.isfile(local) and 'database' not in local.lower(): - quit('Skipped: "'+local+'".') - else: - workQueue.put({'bearer':bearer, 'entries_r':entries['r'], 'local':local, 'entries_m':entries['m']}) + local = folder+os.path.sep+entries['f'].replace("/", os.path.sep) + if os.path.isfile(local) and 'database' not in local.lower(): + print('Skipped: "'+local+'".') + else: + workQueue.put({'bearer':bearer, 'entries_r':entries['r'], 'local':local}) queueLock.release() while not workQueue.empty(): pass @@ -242,7 +232,6 @@ def runMain(mode, asset, bID): quit('Skipped: "'+local+'".') else: downloadFileGoogleDrive(bearer, 'https://www.googleapis.com/drive/v2/files/'+r+'?alt=media', local) - localFileLog(m) elif mode == 'sync': for i, drive in enumerate(drives): exitFlag = False diff --git a/logs/files.log b/logs/files.log deleted file mode 100644 index 8b13789..0000000 --- a/logs/files.log +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pkgxtra/__pycache__/__init__.cpython-35.pyc b/pkgxtra/__pycache__/__init__.cpython-35.pyc index 9eea19b198c3223539766f40095d9eee1d11abaa..9deb302103628bba818ac63aba917b74e96664ee 100644 GIT binary patch delta 37 tcmX@jxPp;WjF*>-#rI8=#6(U%M%#%Ia@@Y9IVG7T8AYjyDHE5d0|38(3x5Cr delta 53 zcmZ3%c$$$@jF*?|$o8U0@rj&%j0qDXsx-#rI8A$wtlsCPuf(RZMc+zNI-OnI#!Tsfj6@k1_pa2LSs~4T%5% delta 58 zcmZ1=dRUZGjF*=yB*G|?cOz#36JzG&DkeF>+{EIN)S?)_{IW!a0w6naawC&C8$@*T IVy3_B0P9~8xc~qF diff --git a/pkgxtra/gpsoauth/__pycache__/google.cpython-35.pyc b/pkgxtra/gpsoauth/__pycache__/google.cpython-35.pyc index 1bb9127bbbed9ab7088f3ff353a771fa34623a9b..f682674b384db8e10c5a2eaf39e60c483d0f6e3b 100644 GIT binary patch delta 41 xcmcb}vyO*TjF*>-#rI9rzm1#UqAb1=zq`-#rI8=+eFU4j82o;8RfWrOLIyxOEQX56H_(^Gp=R^0R6=b;s5{u delta 57 zcmcc3`Hz!RjF*=yB*G|Cc_Qau#?;B|jB-#rI8A@>ma;`=5lb0X&o4rgbpn2^-mf{72sxqVA>N-|3_ic%9(CjK)704Z1x ARsaA1 delta 98 zcmeBSTF=BO#>>kU5@8gXI+1gQZ<@1JOh{^OK}>i?Vo9-ML4mHji>_-$Nl{{QNq&)T ZZenpsYEg_|ep#YI0g#=DSKY+hh5$#xBLx5e diff --git a/pkgxtra/pkcs1/__pycache__/exceptions.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/exceptions.cpython-35.pyc index 7b6ce570218c2c0333cfca79f7bc687c608bad12..7d8109821df45379d83e362ef8576a208658962e 100644 GIT binary patch delta 41 xcmZn{c*4&q#>>ma;`=7*)JD$vjEt_6S2D_R`muC1^^1c4S4_n delta 58 zcmaFD-!8x@#>>kU5@8g%d?V+4M#hZED;ebka}$e8Qj22z^2-tx3V`gy$r~8O*&w2u JFEI+S0RR>O6B_^k diff --git a/pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc index 54322d28e4efd1bad96ba29014de5abf3e5d8913..a3726564039cdb222e6f8db5aa633e4972f6272a 100644 GIT binary patch delta 42 ycmZ2)*ki~g#>>ma;`=5lKx`vdF%zTx>kU5@8g%T67~=(qHgnYrl+C+wi+6= diff --git a/pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc index 190fbe379f36edd02ee092be303e213977696344..16efa41ce989abe25935c44f7e189b17376ca79e 100644 GIT binary patch delta 47 zcmdnTKAW9WjF*>-#rI9r`Hh@6m^kd5tztq_a|8n6(vQ9$tC$k cy19wPC8-#rI9r{EeIoSQs5AuVRto_ASjR$t=kzN=;1J{EelK8vq*O4nhC` delta 58 zcmcbp@I`@BjF*=yB*G|?VI$`P7RHpxt61a&a}$e8Qj22z^2-tx3V`gy$s1Y3*&w2u JFSFEf0{|5!6Kntg diff --git a/pkgxtra/pkcs1/__pycache__/primitives.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/primitives.cpython-35.pyc index 955ffe9529896ea7cbbcf65f67d2cfa971a6e9e4..20897a4986577ee4b01f710da7218ca2a4c35cb8 100644 GIT binary patch delta 42 ycmZoyeWJo8#>>ma;`=75op&QwJQJhq>kU5@8hin`a|eJQHKa>ma;`=7*cjx=d;Oi`dS delta 76 zcmaDQ*(=2<#>>kU5@8hCv5|8s8)L@g`D}7}xrxOksYNk<`DKX;1weLUOn63ONwH%= afv&rYu4_d}QDSmQevvLj-R2W)N^AhRsv0W* From 82025c6418ccc72038a0a1726b4f9670e6dfe6bf Mon Sep 17 00:00:00 2001 From: YuriCosta Date: Tue, 22 Oct 2019 00:35:03 -0300 Subject: [PATCH 17/45] Fixed request urls, adapted the code to deal with the new filemap format, -pull option removed, -sync and -info working --- WhatsAppGDExtract.py | 108 +++++++++++++++++-------------------------- settings.cfg | 10 ++-- 2 files changed, 48 insertions(+), 70 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 5d738ba..af1bdfc 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -10,6 +10,7 @@ import threading import time from pkgxtra.gpsoauth import google + exitFlag = False @@ -42,13 +43,13 @@ def getGoogleDriveToken(token): quit(request.text) def rawGoogleDriveRequest(bearer, url): - headers = {'Authorization': 'Bearer '+bearer} + headers = {'Authorization': 'Bearer '+bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} request = requests.get(url, headers=headers) return request.text def gDriveFileMapRequest(bearer): - header = {'Authorization': 'Bearer '+bearer} - url = "https://www.googleapis.com/drive/v2/files?mode=restore&spaces=appDataFolder&maxResults=1000&fields=items(description%2Cid%2CfileSize%2Ctitle%2Cmd5Checksum%2CmimeType%2CmodifiedDate%2Cparents(id)%2Cproperties(key%2Cvalue))%2CnextPageToken&q=title%20%3D%20'"+celnumbr+"-invisible'%20or%20title%20%3D%20'gdrive_file_map'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt12'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt11'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt10'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt9'%20or%20title%20%3D%20'Databases%2Fmsgstore.db.crypt8'" + header = {'Authorization': 'Bearer ' + bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} + url = "https://backup.googleapis.com/v1/clients/wa/backups/{}/files?pageSize=5000".format(celnumbr) request = requests.get(url, headers=header) return request.text @@ -70,27 +71,29 @@ def gDriveFileMap(): global bearer data = gDriveFileMapRequest(bearer) jres = json.loads(data) - backups = [] + incomplete_backup_marker = False - if not('items' in jres): + description_url = 'https://backup.googleapis.com/v1/clients/wa/backups/'+celnumbr + + description = rawGoogleDriveRequest(bearer, description_url) + if not('files' in jres): quit('Unable to locate google drive file map for: '+pkg) - for result in jres['items']: - try: - if result['title'] == 'gdrive_file_map': - backups.append((result['description'], rawGoogleDriveRequest(bearer, 'https://www.googleapis.com/drive/v2/files/'+result['id']+'?alt=media'))) - elif 'invisible' in result['title']: - for p in result['properties']: - if (p['key'] == 'incomplete_backup_marker') and (p['value'] == 'true'): - incomplete_backup_marker = True - - except: - pass - if len(backups) == 0: + + try: + if 'invisible' in description['title']: + for p in result['properties']: + if (p['key'] == 'incomplete_backup_marker') and (p['value'] == 'true'): + incomplete_backup_marker = True + except: + pass + if len(jres) == 0: if incomplete_backup_marker: quit(pkg + ' has an incomplete backup, it may be corrupted!\nMake sure the backup is ok and try again') else: quit(pkg + ' has no backup filemap, make sure the backup is ok') - return backups + + + return description, jres['files'] def getConfigs(): global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver, celnumbr @@ -139,13 +142,13 @@ def process_data(threadName, q): if not workQueue.empty(): data = q.get() queueLock.release() - getMultipleFilesThread(data['bearer'], data['entries_r'], data['local'], threadName) + getMultipleFilesThread(data['bearer'], data['entries_r'], data['local'], threadName, data['progress'], data['max']) else: queueLock.release() time.sleep(1) -def getMultipleFilesThread(bearer, entries_r, local, threadName): - url = 'https://www.googleapis.com/drive/v2/files/'+entries_r+'?alt=media' +def getMultipleFilesThread(bearer, entries_r, local, threadName, progress, max): + url = entries_r folder_t = os.path.dirname(local) if not os.path.exists(folder_t): try: @@ -156,14 +159,14 @@ def getMultipleFilesThread(bearer, entries_r, local, threadName): if os.path.isfile(local): os.remove(local) - headers = {'Authorization': 'Bearer '+bearer} + headers = {'Authorization': 'Bearer '+bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} request = requests.get(url, headers=headers, stream=True) request.raw.decode_content = True if request.status_code == 200: with open(local, 'wb') as asset: for chunk in request.iter_content(1024): asset.write(chunk) - print(threadName + '=> Downloaded: "'+local+'".') + print(threadName + '=> Downloaded: "'+local+'".\nPogress: {:3.5f}%'.format(progress*100/max)) queueLock = threading.Lock() @@ -179,14 +182,20 @@ def getMultipleFiles(data, folder): thread.start() threads.append(thread) threadID += 1 - data = json.loads(data) + + progress = 1 + max = len(data) + url_file = 'https://backup.googleapis.com/v1/' queueLock.acquire() + for entries in data: - local = folder+os.path.sep+entries['f'].replace("/", os.path.sep) + name = entries['name'] + local = folder+os.path.sep+name.split('files/')[1].replace("/", os.path.sep) if os.path.isfile(local) and 'database' not in local.lower(): print('Skipped: "'+local+'".') else: - workQueue.put({'bearer':bearer, 'entries_r':entries['r'], 'local':local}) + workQueue.put({'bearer':bearer, 'entries_r':url_file+name+'?alt=media', 'local':local, 'progress':progress, 'max':max}) + progress += 1 queueLock.release() while not workQueue.empty(): pass @@ -203,43 +212,19 @@ def runMain(mode, asset, bID): createSettingsFile() getConfigs() bearer = getGoogleDriveToken(getGoogleAccountTokenFromAuth()) - drives = gDriveFileMap() + description, files = gDriveFileMap() if mode == 'info': - for i, drive in enumerate(drives): - if len(drives) > 1: - print("Backup: "+str(i)) - jsonPrint(drive[0]) + print(description) elif mode == 'list': - for i, drive in enumerate(drives): - if len(drives) > 1: + for i, drive in enumerate(files): + if len(files) > 1: print("Backup: "+str(i)) jsonPrint(drive[1]) - elif mode == 'pull': - try: - drive = drives[bID] - except IndexError: - quit("Invalid backup ID: " + str(bID)) - target = getSingleFile(drive[1], asset) - try: - f = target[0] - m = target[1] - r = target[2] - s = target[3] - except TypeError: - quit('Unable to locate: "'+asset+'".') - local = 'WhatsApp'+os.path.sep+f.replace("/", os.path.sep) - if os.path.isfile(local) and 'database' not in local.lower(): - quit('Skipped: "'+local+'".') - else: - downloadFileGoogleDrive(bearer, 'https://www.googleapis.com/drive/v2/files/'+r+'?alt=media', local) + elif mode == 'sync': - for i, drive in enumerate(drives): - exitFlag = False - folder = 'WhatsApp' - if len(drives) > 1: - print('Backup: '+str(i)) - folder = 'WhatsApp-' + str(i) - getMultipleFiles(drive[1], folder) + exitFlag = False + folder = 'WhatsApp' + getMultipleFiles(files, folder) def main(): args = len(sys.argv) @@ -250,7 +235,6 @@ def main(): print('python '+str(sys.argv[0])+' -info (google drive app settings)') print('python '+str(sys.argv[0])+' -list (list all availabe files)') print('python '+str(sys.argv[0])+' -sync (sync all files locally)') - print('python '+str(sys.argv[0])+' -pull "Databases/msgstore.db.crypt12" [backupID] (download)\n') elif str(sys.argv[1]) == '-info' or str(sys.argv[1]) == 'info': runMain('info', 'settings', 0) elif str(sys.argv[1]) == '-list' or str(sys.argv[1]) == 'list': @@ -261,12 +245,6 @@ def main(): print('\nWhatsAppGDExtract Version 1.1 Copyright (C) 2016 by TripCode\n') elif args < 3: quit('\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync|-pull file [backupID]\n') - elif str(sys.argv[1]) == '-pull' or str(sys.argv[1]) == 'pull': - try: - bID = int(sys.argv[3]) - except (IndexError, ValueError): - bID = 0 - runMain('pull', str(sys.argv[2]), bID) else: quit('\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync|-pull file [backupID]\n') diff --git a/settings.cfg b/settings.cfg index cfdd848..d22bd56 100644 --- a/settings.cfg +++ b/settings.cfg @@ -1,9 +1,9 @@ [auth] -gmail = alias@gmail.com -passw = yourpassword -devid = 0000000000000000 -#cell number, without leading + or zeros -celnumbr = 41790000000 +gmail = username@gmail.com +passw = password +devid = 1234567887654321 +#cell phone number, exactly as portrait on Google Drive Backups +celnumbr = 000000000000 [app] pkg = com.whatsapp From c04815884df63e573c04789aef59b54515bfcc57 Mon Sep 17 00:00:00 2001 From: Joachim Werner Date: Sat, 7 Dec 2019 02:58:06 +0100 Subject: [PATCH 18/45] added nextPageToken functionality for large archive --- WhatsAppGDExtract.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index af1bdfc..8e64a4c 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -47,9 +47,9 @@ def rawGoogleDriveRequest(bearer, url): request = requests.get(url, headers=headers) return request.text -def gDriveFileMapRequest(bearer): +def gDriveFileMapRequest(bearer, nextPageToken): header = {'Authorization': 'Bearer ' + bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} - url = "https://backup.googleapis.com/v1/clients/wa/backups/{}/files?pageSize=5000".format(celnumbr) + url = "https://backup.googleapis.com/v1/clients/wa/backups/{}/files?pageToken={}&pageSize=5000".format(celnumbr, nextPageToken) request = requests.get(url, headers=header) return request.text @@ -67,14 +67,14 @@ def downloadFileGoogleDrive(bearer, url, local): asset.write(chunk) print('Downloaded: "'+local+'".') -def gDriveFileMap(): +def gDriveFileMap(nextPageToken): global bearer - data = gDriveFileMapRequest(bearer) + data = gDriveFileMapRequest(bearer, nextPageToken) jres = json.loads(data) incomplete_backup_marker = False description_url = 'https://backup.googleapis.com/v1/clients/wa/backups/'+celnumbr - + description = rawGoogleDriveRequest(bearer, description_url) if not('files' in jres): quit('Unable to locate google drive file map for: '+pkg) @@ -91,9 +91,12 @@ def gDriveFileMap(): quit(pkg + ' has an incomplete backup, it may be corrupted!\nMake sure the backup is ok and try again') else: quit(pkg + ' has no backup filemap, make sure the backup is ok') - - - return description, jres['files'] + files = jres['files'] + if 'nextPageToken' in jres.keys(): + descriptionOnThisPage, filesOnThisPage = gDriveFileMap(jres['nextPageToken']) + description += descriptionOnThisPage + files += filesOnThisPage + return description, files def getConfigs(): global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver, celnumbr @@ -212,14 +215,14 @@ def runMain(mode, asset, bID): createSettingsFile() getConfigs() bearer = getGoogleDriveToken(getGoogleAccountTokenFromAuth()) - description, files = gDriveFileMap() + description, files = gDriveFileMap("") if mode == 'info': print(description) elif mode == 'list': for i, drive in enumerate(files): if len(files) > 1: print("Backup: "+str(i)) - jsonPrint(drive[1]) + print('/'.join(drive['name'].split('/')[5:])) elif mode == 'sync': exitFlag = False From f701a361df094dc57fdbaf5a67f28fdf6b09da23 Mon Sep 17 00:00:00 2001 From: Joachim Werner Date: Tue, 31 Mar 2020 10:58:59 +0200 Subject: [PATCH 19/45] Google apparently stopped sending a newline character at the end of the auth token reply. This patch adds the newline character if necessary. --- WhatsAppGDExtract.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 8e64a4c..0a3d46e 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -36,11 +36,12 @@ def getGoogleAccountTokenFromAuth(): def getGoogleDriveToken(token): payload = {'Token':token, 'app':pkg, 'client_sig':sig, 'device':devid, 'google_play_services_version':client_ver, 'service':'oauth2:https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file', 'has_permission':'1'} request = requests.post('https://android.clients.google.com/auth', data=payload) - token = re.search('Auth=(.*?)\n', request.text) + answer = request.text if request.text[-1] == '\n' else "%s\n" % request.text + token = re.search('Auth=(.*?)\n', answer) if token: return token.group(1) else: - quit(request.text) + quit(answer) def rawGoogleDriveRequest(bearer, url): headers = {'Authorization': 'Bearer '+bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} From e9609413003e276d5c5d8c451211d7619d25dbc4 Mon Sep 17 00:00:00 2001 From: Kartik Date: Thu, 28 May 2020 20:30:45 +0530 Subject: [PATCH 20/45] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1d52d0f..4026eb6 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ v2.6 - Better errors description, logging system deprecated. ###### TROUBLESHOOTING: 1. Check you have the required imports installed (configparser and requests). I.E.: pip install configparser requests + 2. If you have `Error:Need Browser`, go to this url to solve the issue: + [https://accounts.google.com/b/0/DisplayUnlockCaptcha] ###### CREDITS: From d0f45b746cc72ee63f92d914bd7e78201f0aa254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C4=81vis?= Date: Sun, 19 Jul 2020 03:11:14 +0300 Subject: [PATCH 21/45] Remove compiled Python files --- .gitignore | 2 ++ pkgxtra/__init__.pyc | Bin 154 -> 0 bytes pkgxtra/__pycache__/__init__.cpython-35.pyc | Bin 168 -> 0 bytes pkgxtra/gpsoauth/__init__.pyc | Bin 2985 -> 0 bytes .../gpsoauth/__pycache__/__init__.cpython-35.pyc | Bin 2720 -> 0 bytes .../gpsoauth/__pycache__/google.cpython-35.pyc | Bin 1582 -> 0 bytes pkgxtra/gpsoauth/__pycache__/util.cpython-35.pyc | Bin 1243 -> 0 bytes pkgxtra/gpsoauth/google.pyc | Bin 1843 -> 0 bytes pkgxtra/gpsoauth/util.pyc | Bin 1377 -> 0 bytes pkgxtra/pkcs1/__init__.pyc | Bin 317 -> 0 bytes .../pkcs1/__pycache__/__init__.cpython-35.pyc | Bin 310 -> 0 bytes .../pkcs1/__pycache__/defaults.cpython-35.pyc | Bin 268 -> 0 bytes .../pkcs1/__pycache__/exceptions.cpython-35.pyc | Bin 2020 -> 0 bytes pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc | Bin 6284 -> 0 bytes pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc | Bin 923 -> 0 bytes pkgxtra/pkcs1/__pycache__/primes.cpython-35.pyc | Bin 4177 -> 0 bytes .../pkcs1/__pycache__/primitives.cpython-35.pyc | Bin 4708 -> 0 bytes .../pkcs1/__pycache__/rsaes_oaep.cpython-35.pyc | Bin 3306 -> 0 bytes pkgxtra/pkcs1/defaults.pyc | Bin 263 -> 0 bytes pkgxtra/pkcs1/exceptions.pyc | Bin 2336 -> 0 bytes pkgxtra/pkcs1/keys.pyc | Bin 6859 -> 0 bytes pkgxtra/pkcs1/mgf.pyc | Bin 988 -> 0 bytes pkgxtra/pkcs1/primes.pyc | Bin 4605 -> 0 bytes pkgxtra/pkcs1/primitives.pyc | Bin 5362 -> 0 bytes pkgxtra/pkcs1/rsaes_oaep.pyc | Bin 3521 -> 0 bytes 25 files changed, 2 insertions(+) delete mode 100644 pkgxtra/__init__.pyc delete mode 100644 pkgxtra/__pycache__/__init__.cpython-35.pyc delete mode 100644 pkgxtra/gpsoauth/__init__.pyc delete mode 100644 pkgxtra/gpsoauth/__pycache__/__init__.cpython-35.pyc delete mode 100644 pkgxtra/gpsoauth/__pycache__/google.cpython-35.pyc delete mode 100644 pkgxtra/gpsoauth/__pycache__/util.cpython-35.pyc delete mode 100644 pkgxtra/gpsoauth/google.pyc delete mode 100644 pkgxtra/gpsoauth/util.pyc delete mode 100644 pkgxtra/pkcs1/__init__.pyc delete mode 100644 pkgxtra/pkcs1/__pycache__/__init__.cpython-35.pyc delete mode 100644 pkgxtra/pkcs1/__pycache__/defaults.cpython-35.pyc delete mode 100644 pkgxtra/pkcs1/__pycache__/exceptions.cpython-35.pyc delete mode 100644 pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc delete mode 100644 pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc delete mode 100644 pkgxtra/pkcs1/__pycache__/primes.cpython-35.pyc delete mode 100644 pkgxtra/pkcs1/__pycache__/primitives.cpython-35.pyc delete mode 100644 pkgxtra/pkcs1/__pycache__/rsaes_oaep.cpython-35.pyc delete mode 100644 pkgxtra/pkcs1/defaults.pyc delete mode 100644 pkgxtra/pkcs1/exceptions.pyc delete mode 100644 pkgxtra/pkcs1/keys.pyc delete mode 100644 pkgxtra/pkcs1/mgf.pyc delete mode 100644 pkgxtra/pkcs1/primes.pyc delete mode 100644 pkgxtra/pkcs1/primitives.pyc delete mode 100644 pkgxtra/pkcs1/rsaes_oaep.pyc diff --git a/.gitignore b/.gitignore index 1c8a609..8046f12 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ pkgxtra/pkcs1/__pycache__/ pkgxtra/__pycache__/ pkgxtra/gpsoauth/__pycache__/ .vscode/ +.pyc + diff --git a/pkgxtra/__init__.pyc b/pkgxtra/__init__.pyc deleted file mode 100644 index 1c4e232d9bfe7c899b9f7ae66ea1d79cb56c5045..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 154 zcmZSn%*%CTdr@RE0~9a0Er`iY%uLSDiz&!XuP7->jERrW%*!l^kJl@x WEa3nuv&qd*Da}c>0~uQk#0&sI9Uo!< diff --git a/pkgxtra/__pycache__/__init__.cpython-35.pyc b/pkgxtra/__pycache__/__init__.cpython-35.pyc deleted file mode 100644 index 9deb302103628bba818ac63aba917b74e96664ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168 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 diff --git a/pkgxtra/gpsoauth/__pycache__/util.cpython-35.pyc b/pkgxtra/gpsoauth/__pycache__/util.cpython-35.pyc deleted file mode 100644 index e906952d207aaeb47e0f1518ee3bc8a3994c0c58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1243 zcmYjQ!EW0|5S=AuN>-#MXbm?8a@bQ_0+wJPx1uQ0x@m%<2?981Q9*z}kt>N7xl8P> z;wVTiP5LqUiyZP5yyj$}U(i$Dke2HuxtyKZd9(9o=Wsaecb@z^`L#pziEfM#@&kPC zH%u8`K?Tv|S{5`Bqy(J@Y(2^XS_d=%>tvM#;DZpebp;@&0j=xHtP zr-CI6{&TofeC`;NC$fj+8}Qdn=(ho#`H-+U2W&uZ1qWhn+<97u^L$x{EA{g74!q;N zI(+um{W{FGuloyY%GrGDm8(02(aSngIyIS!MJ?QWvIBVh_w(uZ)90$Jrhl#y?|!PP z(W75R4_|tlq~6%klTG1szp^UHrqz1M`suQACfWGabmQ}4Ty5)N6J+MijKg#}mk1My zNZb+j3rLroXd18*1BCyd^+zn)Ou?c(;1pjAIyk%1fsbQmg>x44w_t8yVip>f{|V^m z4P1hz91$C^WWrjiy7a)VDeLk2YqoUgeDBK0e!%I{Iwbp{k;RpK4S)@YZ-9o{?riM| zp}Bi-1&ZHUyq`Qqmd}!O?WA8RS?EnEzq?rF=}MLx=jB|<)aVz=`ea@x>5WVz6BbL9 z3yZ#N2eT=ix4B-*6Imr$rm}W-$Fs^Na-vGg1<1| zx*bAlN*ugY+QYvB1f<00MsqGqSCu%2JQ18Z8C$+FjwgEc%_-1~F19=Vkdhv0wFJNm zCBgGtSvS7q`-S%U-QI>!i<3C2`+p?GMm@CFSj*S0jx6U=)nTD@-J9n+acQ2{`x}iW zD)PlP?x0%$Ik|O%VS5X{li?yS3YE>AnJp6A^iMkt8F|R(<|b&W!76tT6YUQ~PYm!J zireUiKIXgl55$+k9>E?*=y>>tZtLnUO5?za}0``6=O|~i2j}28l M=n*CODjYFo9WF?b}}PeGBwpzHPu~DJ=M{l2ctiJZ@yp1{%7(20*`+Uk>PKo5Sjj? z8<`rlTPdupBbi2G7#B&=k#$$5U93Bjb*1RZx-ZkdtOqh3z}k~vk!mPeUzL$$163YL zHdJNI?Ow$rY@Ppw-$iVohA~9q({DDHMZWmjZCfj&8lO5puTod%jEs5uZ~$?+4RC~N zUR32$EmUYw&w|l?JY765@%Y~$g2>9qPUMY|J0ro$4z(J2%C)^;!?d$9d7tKJZ-xQp z$^J!E-JO5A@d@30b@uMAgh+Pycjb+hyPoWNs6WD4F7gg!K7fd+g&xFZ>fQ76Fr@nY zJadaGa|vrs@JP&p0tKA&8@Fu(^D=F=KrOK59Q%o^t4vp`!2IsEu1Z%1b>XuZHc{a} zg!b~&*|%<8&(>+asLEM=v%C#WI$PGhN;lzpwydgU;m+!<_6gt5ubOH-#~Iimf5>d+ z$R3-CJvFEH$SU|e22ZZwpFsimKpyz-!EcvVmx?WVF;VaU;2xtuWbpgttr97A36pDq zrApUYl}5xac?9v`DoHO^2`>52Y!=}O44&;lnJ84t?_dlr#jcOALwk|6*9RC$58@1X7>@a*bH%lxdRKPm0&A`jaB zI&DH8@~Vt^B*iJX+n^)SIFHVIH_y_Ls;kILmk=jSI9EozPKyU)^)6E31lWR9nZMoe zlt*ld(K9EeXL|ODIkXG*jr`?Xk^316h=Fk;D==$1dDJ_^VuhiEgx+Dm3R8q&s{?Dm zCR|Dr(q`3@Od|j!24poL%=a^=i7-7S zYR3#0PTDl-HhD}F!3NB(%yb~HQ-59Lmx{5!PM@hi%a_iFHnh}HW_FEt<)YcvA<@Zy z2i8qf`1Hzgs^G)ex5LhZI1FZ?gIuTH|5PmwZVLA~H}%I5V>8C(b!<TyyFNY>S9G`ju3N?Is9Gie;}0gqM5}i7kVjZ^>BKI zuDs*kaOHx23o;>1Xt^CVdVK}^O1P3aK7xpOmBbob*E_Q!~zF^L(h| zq0Gk4%bt?x?2yl_t)^IJ{t~_!Mbs{+T zq;)?y4grI#yqb z!WQjd$%=xD%frm1NQ!T=r%mKyt^Fc|vB8j~x<3iyR}K;4`6tjlxP&%4hH2>QG*wC0 z*=|27!axP`=&9m9@KA-@_D1mb?i4Q_p%*VnzFZX4NQO&t+uG)T{WI+qyo8`)Kz^vuK)M5% z-BzD921Q^hPtNz9Mol*VYD`L6vuxR?Aw KyapZ=rv3#5S2bGz diff --git a/pkgxtra/pkcs1/__pycache__/__init__.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/__init__.cpython-35.pyc deleted file mode 100644 index acecd7acda9e4ff0df3195952d5bec84538933b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 310 zcmX|+%}N6?5P&Di*8WJrlkc#H6}%P^r55qf7Ob=o7DCt!HgwkwNh2Q7F` zMvwEy=WU;OTUapZR>2znI$PySg}WeG`?*z`*brh~a<<$Z`PUVhtdX0z`}qISdR@3=F9Z3|Wi}%|JF2n9U4k zvw#!?YcjnAs%G%hWWB{!l$e*2pL>fZxU#q;H8%*t6HQ4?ODxSPiBB%7EGWs3hbm%& zNJqh?3yM=qQ()378H(6|rhf1-kAox~>%^MTyBJ`9-?E zK*KXjGKx|YQ(_9T(?Q}uI=R>|25NJ$UP0w84x8Nkl+v73J4T?n#VkOAhlz&~05||k AZU6uP diff --git a/pkgxtra/pkcs1/__pycache__/exceptions.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/exceptions.cpython-35.pyc deleted file mode 100644 index 7d8109821df45379d83e362ef8576a208658962e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2020 zcmbuA%}(1u5P-*VLViskDWOP}P`PoS64$D#N<)ND2~-3L2}QE3?AEawYv*S(A^BU#hpPOGLfZtGfm1+KgK3U5FI1vEC zfggI50N-7>afp^cxSF~k9ZgF_%OEP6Rt$BCu7Rj(S~avxv<9NCY2DBY(FTa7rcFcF zh_*m@ntFy-iLQfaYuYxnMsx$jrly;Q)`@mNbT#c7+93J@#FnO8hBk@zKx}KeZ7BI) z!f1bIQYaHpb(VG7pb3?R>O#f3p~^V)+tQ-*`BQ&%_%X=jU_Ozlijw$z-(A#vKMroC z?=Kp@f18MW#`Bi%f6If}^2$%Z3f)r=e+T`y<12Zaj<2VI%8t_X;Nx}5(~OgOg6#2dtu zL_~3DbHZ(sWv^i6_xY~=@|~VK VR2Q{(%Ri3y`b6h`?72_9{{g!)I{^Ry diff --git a/pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/keys.cpython-35.pyc deleted file mode 100644 index a3726564039cdb222e6f8db5aa633e4972f6272a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6284 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 diff --git a/pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/mgf.cpython-35.pyc deleted file mode 100644 index 16efa41ce989abe25935c44f7e189b17376ca79e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 923 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 diff --git a/pkgxtra/pkcs1/__pycache__/primitives.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/primitives.cpython-35.pyc deleted file mode 100644 index 20897a4986577ee4b01f710da7218ca2a4c35cb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4708 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 diff --git a/pkgxtra/pkcs1/__pycache__/rsaes_oaep.cpython-35.pyc b/pkgxtra/pkcs1/__pycache__/rsaes_oaep.cpython-35.pyc deleted file mode 100644 index 998c6577a2c26488b0c8d276fc0e242cf765b2bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3306 zcmeHJ&5j#I5Uw7N|Gn$Y#+%(F{7eW6*(e)n0|$fuK|+)ukgOnU*3-7fYkNlB z?U0S^3xOLi!XqF~@ECpNv;x6_8yEPh$Ky2-C@;X+Q`1#bU0v1P^L>48ZLQ|t{k46v zM)WhSTxGQ1$F0AGi0~IQB5I@IQB*VVk?jr8nxb+u^JuA>N zqIsESWg4PUp&8N^G_TSONeY@(sI`OV&b&s(qnV&*FxKeEC-otj0u6oo3Kn!P(#+$x zf|ZhsTjz;1h`!Q!2QIC7gXb4BGpo}KZJ#6eeC|G9qgj#k4`iw|tkJAQL-=6oiqzt< zEA-5xIR--?NF-XJov4Hx)9Ea zHK~VyNNLk~BHE@?&$?M!r`cJjw(?Y4oDOHVMYAm|VkP|1BRZJ46$!%N@Yu*;EVT~% zGMMUk(ueWE!&^Z*Jsic|;7A?^Shc_mWbnzYgZsgkDoj(j)q?B_NBu;_W-z{9%T4z~ z6^^BmO23nvvS=NSwugAnjgWigdB7!0OdGW_)F?cZBkmt$tuRQX+S4+)|IzJ-m-hp0 zkZGu*Jlr7EgHCr8YVA7RKQ@D85@d#6q7Ww^FpB$9n_yXs43yNV?3$I)12+d8a}*y2 z@dWkt!mi8*!;ar~!;C|H6!hgps*pd2pf{Z$PBO_IwUm)_bPbbW)+NGeBf}_2ddsd@ ziP$WcB>Qyomx;=wDXb08$n)>BGOw@H;nIKJEf2FSC+MkUyy`kmqUlIp58__n5{`9n zFPX5x1ZX39Y>p(4v#1*dnH$Uf@IacWngox-(Nr#X1aByrZ9_!J7MjavyTZ zX5YZ4N%CD$sYjY1=BDeGwqm?7&AlX#*E9KRUfOmu4NeQ9`34`ZWkEvusQBvH1Q z=$mocE-D?$boX4lcKQX_@?zWBqT9!+##u84TA!!5UBIGRp9`?XK?f;Xan#-%hfg|- zj%>r+*qZC#Vb8XgYU@RZwj3RHcnjJpOBr=wt8&=PT3=Y%Vm&#w0{^ZpB~z15&3B}( zAdf!}Zol*BQ#no_eLe_{zLlnXAKckHc!E;9CQ*BLf$8|<6?W{S^r+8vJiB`T5tpkw z32MHP9^3L_fq7Z8L>Z7Bd0UMu}Q@Y#(8=?=npQQ30f23Ijq!B*!BQxfUqDcPy(2;K_u%Y0AZNMs^ z1*V!3C#rsP?ZTqHnjrHEJV9Up@}|odSpfT+&a&Z{1QKa7>G-t6j%RX5bUou7H<>Mv z0=!&W(2-=8k~cp~bFf8=xebKG#XHWx`QXSSH5aBq!!^gOEf-|V1sSe${_w_8f~)vR z7;xIZdcf%)K|n}6LyH8M-@#&a7v+}~$0}&67JiRazaw8vTPl8x@AkiW>lEbwC#)9` zRu1a_iQfynxfqS`gOn#giz*_~v5&V0;(A7P)}b z7IBiD7=Uyg4c8Beb+W4jaab*kNxb-hd~dJe$Qgq3E|EsOjL-yCNE%gGrAe^eup9!-3{D zfb$Ys4S*{{YE`scTkWIoZrzp_rx#0(#N4c8#8X+E)U_(X6+AQu_uD?7X&ex{04wP+ eNhp_N<=yOn@E$Lv-hgO$8^Zm(s@SkK_Nj z3!js5c32!a^c7&=;I|2iz(=42H~~n2D}e`c&~eytDP7L%);OK_R-gJ~GFCR?Yw~BoPr5U=OPWb#sA2`z6*afZ-PH^` bw{zOBvNO}V5oT9Pi>8!of@~v{IEcv`O#?v5 diff --git a/pkgxtra/pkcs1/exceptions.pyc b/pkgxtra/pkcs1/exceptions.pyc deleted file mode 100644 index 93174007d2d2afdec63744d6e2fcede17a895d63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2336 zcmd6oTW`}a6vt1R-Y?tOpn!yg;HlaJ(s)D&!Ac({Hin!^FjT3sVlRzYoyc~l?is!@ zUx0J6+T?);q)F*o#XkP2vHv|chrb8i>9^MxF>F2|&hPOt{Qy7!NPx=#L;$}4ega&4 z$3+cfP|(oR8i=~14NpT5O-Eav)(SfHe5Ic_UdfEoD=ja1Z zJ0SKQJ@B*(;-RCDJnexPI{MhtK8Pm}jh>=c>^=@(K8>TJV{YVR9n0LNneyI4pTRMC zuZ;t`4!y;q5!x`S;a~~R=ZY`od~UnNVws866)taog$N4E9HKRTGn>j~K3noM&eSab zk*uxeILGE_Cf(`gReraA6&3VBNfyQLL|K_gO)1WG zQoXA>s9{*r^c?L?PBEElX{55;rq}X(WzR1eS6k3XIZqAMh4Ura<1}ALZROhD8=2G| zo|R<2K-<^Ay;0dmRTO0BOe)@qiZhX_WILUo6i`Sviaw7BSV~NPXBIaW3lTAD%8WW8 sqt?Ty)EN~PqZb&Z3Zph($ diff --git a/pkgxtra/pkcs1/keys.pyc b/pkgxtra/pkcs1/keys.pyc deleted file mode 100644 index 0af1015c2580624826ae41b673e34b1deee81da3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6859 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 OrZ97CW@hI5W9Hxan+K%; diff --git a/pkgxtra/pkcs1/mgf.pyc b/pkgxtra/pkcs1/mgf.pyc deleted file mode 100644 index af106a7c90f55017e5c91a52a2d355b50049f876..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 988 zcmb7CO>Yx15FPJsLKRZrL~mfJ2Xbf&snkn_5GYVU2!xV~k0>bG?mD|}cGto7MlDJX zQ2sGL0G^$+<-i3iemkDeHaeFA)+;+g|MiXWqbXb2&oG^B!28PG7G zGNfThOc0$$lt%QE(XipX2b~5Yk)l)Rz`f#X*wz$bkO3f^ENh9{!&yz$5yI} zAJtXjg?X1xv?{sH1%LmhH`wm-`TpJ`Zfsnou}=3|mnq83jExW}-}TN;+~F}lIky7d zT2|SWCyI>=QLQ~8Wj5z(;^Ruk#44Vx$cW3JJU00_DPm*RVX4f7u{VmKNH)To0$ZTS4YkrCs z3bdgh*Z>j0&(+?9v?4b)@|&dIpv)$I zo~y@tWIoyTD15Tq%@VsqbnH=j>~)|Z4`v%u?{IW1%Gs!lWumIlY?_@}9pjsr?g+K^ zX6M)?U$*PE)!YG4tIgW%Hfyr`zu^@ Y{}I|i?Bi0Ubs?U(293+w4(W2tEtM) z^LsyUQvNrB--jsnAE-k7SJWcfL&Kx6L@h;akM=xL{9UG2iP~k_D^q)f_D0Z)pHW`4 zH%9%%!3Jae8IJPMZhW1}W}J>Hlpc^$DpvG7yheK!G842nNv2Gu!aIaxbY7ypDGJBQ zj0`#}bdK|;DV(5l$TdUZq%>v+bEfDV0$%5R!f85(By+r5tvZ7XMgPX%cBIg(a+Ot3 zb<(IEWzlP6s~-2=Koy$(pxeso9c#K_Qcr_8OxkaaG}C@38;saVtZ7HBmPuFBpb^FP zIyw&8o&M^@-soQ?qGlOGT&|%!La~29by^~5;b@G~8koDdS_erQWruCMGUz(Ww|8Rc zWM4-a?;ItuWntdgyAgeZ-G>L}r?3h{B3+EQLsoR|H>|5C&K-PqhueDR&#)3(kuEBe!* z1yv@{V|o%AMM*}P21HV2nA;YOM!X~y0a*}6DWR$vD>gR-&@FzP%d`lShF zmgu|AP8G!mRX$gwiG3E>m+x`3h6btPgQT+GN9)o37dxiidC?A{W)i>X93AvE!+fLZ(;;M<{m*R2$j(YOqS@4Dy7ePNU%F9*ex;s8TMri zQ|-7!y9HSP3iEvC5M?d_l@YQ+XvZbX?K>?ztS93MbfyZ57kt2^WeI>-HGzz?SV5B$C-du3QuE ziD4*hAfs^7kT))ZyEwa0*xN{bn>JT;z)adoV1S%4bnr?MD< zuT1G*^7g~@_jzlH_W|Y>0IyHbc$l6rz$UPgqpM(Y5vO}+L}!YQA4s2~1zrR&x;1~i zxCUZ)w1xSYyqgOSq5dvHVegu#*Zz+07qpt)RshlxoC#|W{O@N$GigNH_S=o5rK`c6 z#w~pU#x}YE=7x8`in^hL)cId0v67(-{KdkDWCG|v2}2hk?xzwS?0vnuwxaK91k*L= zSP+K9v7C#s@?HAF7Fq|lrwn(Y<|+>gw+2!-kS&1Jj72tdHNW8XujIO7aE(v8HmByi zNh*U}XH*65QBe!tBI;?yz2YWJ>Re>i12o_oP&llHd9aY4bQa%xq%4(M9^EUBEgXM59O_6+M1=M-(T4tB6%l^e{p}&-hHI_-?i1DcS||)WlotKY822 z+2O~We7KCUnY6`ujjo;?M$JP`Sc29`(6_poq^W6Ux8SIvSEuq~S`_CqoEZ#f4lXrh z8qWIq-bWv;9^G=z|Bh8T-sSlqjvR{Nfa6jpP7{A%^Rtn0Hd=j`M7f+P!nGW19A&ch zz^>ppW>w}{u($thwEb^!B^KsMV%vgP#lCfa86#IRvMG2nY|GPYf-FK00)&zRigM!4~c(*+5 zf^Ctb9wvC&3=&kAjps?i{|oYQo>&Z?J-NH(ub=mMovakko!qB!H1aFM$(h3g2 zQsgu_*$-aZlMfWb8PD-k4DKa&f?~8H|z5|$LCDN=d1YwX-T9Q zh{}6Ki%ARDH%U7 zBuF^w;5~i5NTC90eS>$4Jf}WxCm{mZPgr=%x0_V6;QvwFzl7UY)RZcFqxd)X;f?w6 KrSZv`rT+knnl`)u diff --git a/pkgxtra/pkcs1/primitives.pyc b/pkgxtra/pkcs1/primitives.pyc deleted file mode 100644 index 55158d6df32b68fb83eddf37f4a8e54a9338a6e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5362 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 Mv(5RX+39)zKXd$UUjP6A diff --git a/pkgxtra/pkcs1/rsaes_oaep.pyc b/pkgxtra/pkcs1/rsaes_oaep.pyc deleted file mode 100644 index 676fe3bc12dd1c2aa281673ccb811849f1393129..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3521 zcmeHK-EQ1e5FUGflHF{6n~=0s)zcQLP})ig60s#G8qO5|1uqDU+6`s}Xv$2LA^ zOM<*NwBlKK1upvnT<{uP@&NG7*xpTpstR#SwjQ6KIWu$SjKA-+^5<&v*|+z;>r(Mo z$NN(}=4Xfi{~Q_<9iib;P@%Cy6PJ!$a=2fWeua)IdA}z8Djikxex1fOn$+p2j{Xv9 zq9J6JhRbYM*C|-0mo++S()rm2y@c5nR(rbNL@423_7XtXQWE>X0xsSKBDnU}i(qHjBUz zc(%#JQNHS&TCFD9 z7{9O8)PzwVgWl5zDw%f1VONdxslw4IJJ9Ou2agWb52>FdP;09E&mZ^WG_-@s?PfW2 z=%@ZfTb-KwAYRv8CW^1zHu&$t@<9X;mtDsTBJ(diwd)#2lZPj4J3 zV-cyJ2Bo=yZwBq|*f&NN9iG}j9I1Su7pJgOIt;?THdf8|qE)I*qPzBtcO^Mso8z#f z!U+58`CVN)L-8Mon~85ms;?uR`uu&U-ZX-pI4Tv5)`2KW!^X(EhB;%kAE>xDUy73m z?fgjc@28xM)6!asv*C=KpU<~>`^-Kh{PSw1nfZ29FO4T>RVQ&U9qZdF>?sL2H0ntl z@qkrmBX|@?T-qu2jSbnm^Lp^Kou-j`?vJN>UJ+bSIxj<5$Pb#g&*KK=qsBVw+kx_p z3@yr0T!koNvW!sVRk^CU>4aS10$ze!i}T-K$An#h$WOX$!XGt0X(F!wyjMXFK0SfG zHFUnpzlvvZ{AG+#7_z5H;9D)t+z?;~9wTcxh+!N?9^Pab*}{9VNyKMgTh4nYv_|5} zH`_jmQ&Eh~y)cnhYJ8pSGc@M7E1w{LG(auDYaWki$_LJ z2W@OjvNf_>Y{@@q7uI+NX%>HCv@NUHXkq}f$XG|b2c0%owj_-kI%s2tQ^;kJ&vAl_ zIUbbddLoW<*_G7AQ=3ffK9os>xrNrl`^V4pBsre=VKdtlN3PyJD>^bZBZ8=>&oY&~*_XYg`d22#H z^^$&;&XCO#jVfdwFu^b()oB~A-4RiG$OHtG1~}&&gacp>5|S#o3?@Mr!e~4T*aufE z2|j~a;1&c_a27fMZm=N82CQA7i~Is(fXl12du`s^y;^ZWU!Wa z3yqlv${y zNDv@zRfq=+H*zWc-mxtju9hSX%^gz1HI5moxTA37zc^U`+QjEy{Bs#6;VRz@zU@Xwc5p##Btk3rL8gz&f&1meD74tm5E1gJem z!xDOf#ABN97{$C6i%p(oXzhhjkP`!I6!nB)#FDJ%Jr&T+>v3h~dSdMM_%`e@;4JXk zyU5*3EH1OS$>Kd0@3Oc8A*gxk-QfN;7S~zqvQR9pLRe1C0=3(GQP&bMt}j+Bg#Qg@ zi@bjbKjtF{S{ImgZ#wI)z^Thn`@tMscLi+MG1hV}qTO=Vq`lAi6doxqP;yN988G4I zEXHS5{J+6?%&f8(Gw=_j!B;G4@CC%i{Vu#ZDw`{^rKhAGCpKvHrZ!LGjk)PXe Jc5XGCzX4HUEJ^?X From 88835a3d9971859796d52b01a74c127a4c3b7a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C4=81vis?= Date: Sun, 19 Jul 2020 03:44:02 +0300 Subject: [PATCH 22/45] Prefer systems gpsoauth --- README.md | 13 +++++++------ WhatsAppGDExtract.py | 6 +++++- requirements.txt | 3 +++ 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 requirements.txt diff --git a/README.md b/README.md index 4026eb6..6cb55de 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,13 @@ v2.6 - Better errors description, logging system deprecated. ###### INSTRUCTIONS: - 1. Extract "WhatsApp-GD-Extractor-master.zip". - 2. Edit the [auth] section in "settings.cfg". - 3. Run python WhatsAppGDExtract.py from your command console. - 4. Read the usage examples that are displayed. - 5. Run any of the examples. - + 1. Extract "WhatsApp-GD-Extractor-master.zip". + 2. Install dependencies `pip install -r requirements.txt` (or using your distribution package manager) + 3. Edit the [auth] section in "settings.cfg". + 4. Run python WhatsAppGDExtract.py from your command console. + 5. Read the usage examples that are displayed. + 6. Run any of the examples. + ###### TROUBLESHOOTING: 1. Check you have the required imports installed (configparser and requests). diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 0a3d46e..c32e522 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -9,7 +9,11 @@ import queue import threading import time -from pkgxtra.gpsoauth import google + +try: + from gpsoauth import google +except ImportError: + from pkgxtra.gpsoauth import google exitFlag = False diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5e78cc2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +configparser +gpsoauth +requests From 0550a87b3216247c298eb7c0c9ed41277ba7a9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C4=81vis?= Date: Sun, 19 Jul 2020 04:18:55 +0300 Subject: [PATCH 23/45] Add flag to ask for password instead of using from settings.cfg --- WhatsAppGDExtract.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 0a3d46e..d73aae4 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -9,6 +9,7 @@ import queue import threading import time +from getpass import getpass from pkgxtra.gpsoauth import google exitFlag = False @@ -99,13 +100,19 @@ def gDriveFileMap(nextPageToken): files += filesOnThisPage return description, files -def getConfigs(): +def getConfigs(askpassword=False): global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver, celnumbr config = configparser.RawConfigParser() try: config.read('settings.cfg') gmail = config.get('auth', 'gmail') - passw = config.get('auth', 'passw') + if askpassword: + try: + passw = getpass("Enter your password for {}: ".format(gmail)) + except KeyboardInterrupt: + quit('\nCancelled!') + else: + passw = config.get('auth', 'passw') devid = config.get('auth', 'devid') celnumbr = config.get('auth', 'celnumbr') pkg = config.get('app', 'pkg') @@ -208,13 +215,13 @@ def getMultipleFiles(data, folder): t.join() print ("File List Downloaded") -def runMain(mode, asset, bID): +def runMain(mode, asset, args): global bearer global exitFlag if os.path.isfile('settings.cfg') == False: createSettingsFile() - getConfigs() + getConfigs('-p' in args) bearer = getGoogleDriveToken(getGoogleAccountTokenFromAuth()) description, files = gDriveFileMap("") if mode == 'info': @@ -232,25 +239,25 @@ def runMain(mode, asset, bID): def main(): args = len(sys.argv) + usage = '\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync [-p]\n' if args < 2 or str(sys.argv[1]) == '-help' or str(sys.argv[1]) == 'help': - print('\nUsage: '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync|-pull file [backupID]\n\nExamples:\n') + print(usage + '\nExamples:\n') print('python '+str(sys.argv[0])+' -help (this help screen)') print('python '+str(sys.argv[0])+' -vers (version information)') print('python '+str(sys.argv[0])+' -info (google drive app settings)') print('python '+str(sys.argv[0])+' -list (list all availabe files)') print('python '+str(sys.argv[0])+' -sync (sync all files locally)') + print('\nYou can add -p flag to ask for password instead of using from settings.cfg') elif str(sys.argv[1]) == '-info' or str(sys.argv[1]) == 'info': - runMain('info', 'settings', 0) + runMain('info', 'settings', sys.argv) elif str(sys.argv[1]) == '-list' or str(sys.argv[1]) == 'list': - runMain('list', 'all', 0) + runMain('list', 'all', sys.argv) elif str(sys.argv[1]) == '-sync' or str(sys.argv[1]) == 'sync': - runMain('sync', 'all', 0) + runMain('sync', 'all', sys.argv) elif str(sys.argv[1]) == '-vers' or str(sys.argv[1]) == 'vers': - print('\nWhatsAppGDExtract Version 1.1 Copyright (C) 2016 by TripCode\n') - elif args < 3: - quit('\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync|-pull file [backupID]\n') + print('\nWhatsAppGDExtract Version 1.2 Copyright (C) 2016 by TripCode\n') else: - quit('\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync|-pull file [backupID]\n') + quit(usage) if __name__ == "__main__": main() From eb5572345d60fe8fff997ade2576b2859c073f35 Mon Sep 17 00:00:00 2001 From: Nicolas Lazo Date: Tue, 4 Aug 2020 21:12:09 -0300 Subject: [PATCH 24/45] Code cleanup and typo fix --- WhatsAppGDExtract.py | 196 +++++++++++++++++++++++++++---------------- 1 file changed, 123 insertions(+), 73 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index d2dee67..48b995f 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -1,14 +1,15 @@ + #!/usr/bin/env python - + import configparser import json import os import re -import requests import sys import queue import threading import time +import requests from getpass import getpass try: from gpsoauth import google @@ -16,48 +17,75 @@ from pkgxtra.gpsoauth import google exitFlag = False - - + + def getGoogleAccountTokenFromAuth(): - + b64_key_7_3_29 = (b"AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3" b"iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pK" b"RI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/" b"6rmf5AAAAAwEAAQ==") - + android_key_7_3_29 = google.key_from_b64(b64_key_7_3_29) encpass = google.signature(gmail, passw, android_key_7_3_29) - payload = {'Email':gmail, 'EncryptedPasswd':encpass, 'app':client_pkg, 'client_sig':client_sig, 'parentAndroidId':devid} + + payload = { + 'Email':gmail, + 'EncryptedPasswd':encpass, + 'app':client_pkg, + 'client_sig':client_sig, + 'parentAndroidId':devid + } + request = requests.post('https://android.clients.google.com/auth', data=payload) token = re.search('Token=(.*?)\n', request.text) - + if token: - return token.group(1) - else: - quit(request.text) - - + return token.group(1) + + quit(request.text) + + def getGoogleDriveToken(token): - payload = {'Token':token, 'app':pkg, 'client_sig':sig, 'device':devid, 'google_play_services_version':client_ver, 'service':'oauth2:https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file', 'has_permission':'1'} + payload = { + 'Token': token, + 'app': pkg, + 'client_sig': sig, + 'device': devid, + 'google_play_services_version': client_ver, + 'service': 'oauth2:https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file', + 'has_permission': '1' + } request = requests.post('https://android.clients.google.com/auth', data=payload) answer = request.text if request.text[-1] == '\n' else "%s\n" % request.text token = re.search('Auth=(.*?)\n', answer) if token: - return token.group(1) - else: - quit(answer) - + return token.group(1) + + quit(answer) + def rawGoogleDriveRequest(bearer, url): - headers = {'Authorization': 'Bearer '+bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} + headers = { + 'Authorization': 'Bearer ' + bearer, + 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', + 'Content-Type': 'application/json; charset=UTF-8', + 'Connection': 'Keep-Alive', + 'Accept-Encoding': 'gzip' + } request = requests.get(url, headers=headers) return request.text - + def gDriveFileMapRequest(bearer, nextPageToken): - header = {'Authorization': 'Bearer ' + bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} + header = { + 'Authorization': 'Bearer ' + bearer, + 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', + 'Content-Type': 'application/json; charset=UTF-8', + 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip' + } url = "https://backup.googleapis.com/v1/clients/wa/backups/{}/files?pageToken={}&pageSize=5000".format(celnumbr, nextPageToken) request = requests.get(url, headers=header) return request.text - + def downloadFileGoogleDrive(bearer, url, local): if not os.path.exists(os.path.dirname(local)): os.makedirs(os.path.dirname(local)) @@ -71,24 +99,24 @@ def downloadFileGoogleDrive(bearer, url, local): for chunk in request.iter_content(1024): asset.write(chunk) print('Downloaded: "'+local+'".') - + def gDriveFileMap(nextPageToken): global bearer data = gDriveFileMapRequest(bearer, nextPageToken) jres = json.loads(data) - + incomplete_backup_marker = False description_url = 'https://backup.googleapis.com/v1/clients/wa/backups/'+celnumbr description = rawGoogleDriveRequest(bearer, description_url) - if not('files' in jres): + if not 'files' in jres: quit('Unable to locate google drive file map for: '+pkg) - + try: if 'invisible' in description['title']: for p in result['properties']: if (p['key'] == 'incomplete_backup_marker') and (p['value'] == 'true'): - incomplete_backup_marker = True + incomplete_backup_marker = True except: pass if len(jres) == 0: @@ -98,9 +126,9 @@ def gDriveFileMap(nextPageToken): quit(pkg + ' has no backup filemap, make sure the backup is ok') files = jres['files'] if 'nextPageToken' in jres.keys(): - descriptionOnThisPage, filesOnThisPage = gDriveFileMap(jres['nextPageToken']) - description += descriptionOnThisPage - files += filesOnThisPage + descriptionOnThisPage, filesOnThisPage = gDriveFileMap(jres['nextPageToken']) + description += descriptionOnThisPage + files += filesOnThisPage return description, files def getConfigs(askpassword=False): @@ -125,30 +153,41 @@ def getConfigs(askpassword=False): client_ver = config.get('client', 'ver') except(configparser.NoSectionError, configparser.NoOptionError): quit('The "settings.cfg" file is missing or corrupt!') - + def jsonPrint(data): print(json.dumps(json.loads(data), indent=4, sort_keys=True)) - + def createSettingsFile(): with open('settings.cfg', 'w') as cfg: - cfg.write('[auth]\ngmail = alias@gmail.com\npassw = yourpassword\ndevid = 0000000000000000\ncelnumbr = BACKUPPHONENUMBER\n\n[app]\npkg = com.whatsapp\nsig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799\n\n[client]\npkg = com.google.android.gms\nsig = 38918a453d07199354f8b19af05ec6562ced5788\nver = 9877000') - + cfg.write('[auth]\n' + 'gmail = alias@gmail.com\n' + 'passw = yourpassword\n' + 'devid = 0000000000000000\n' + 'celnumbr = BACKUPPHONENUMBER\n\n' + '[app]\n' + 'pkg = com.whatsapp\n' + 'sig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799\n\n' + '[client]\n' + 'pkg = com.google.android.gms\n' + 'sig = 38918a453d07199354f8b19af05ec6562ced5788\n' + 'ver = 9877000') + def getSingleFile(data, asset): data = json.loads(data) for entries in data: if entries['f'] == asset: return entries['f'], entries['m'], entries['r'], entries['s'] -class myThread (threading.Thread): +class myThread(threading.Thread): def __init__(self, threadID, name, q): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.q = q def run(self): - print ('Initiated: ' + self.name) + print('Initiated: ' + self.name) process_data(self.name, self.q) - print ('Terminated: ' + self.name) + print('Terminated: ' + self.name) def process_data(threadName, q): while not exitFlag: @@ -161,33 +200,39 @@ def process_data(threadName, q): queueLock.release() time.sleep(1) -def getMultipleFilesThread(bearer, entries_r, local, threadName, progress, max): - url = entries_r - folder_t = os.path.dirname(local) - if not os.path.exists(folder_t): - try: - os.makedirs(folder_t) - #Other thead was trying to create the same 'folder' - except (FileExistsError): - pass - - if os.path.isfile(local): - os.remove(local) - headers = {'Authorization': 'Bearer '+bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} - request = requests.get(url, headers=headers, stream=True) - request.raw.decode_content = True - if request.status_code == 200: - with open(local, 'wb') as asset: - for chunk in request.iter_content(1024): - asset.write(chunk) - print(threadName + '=> Downloaded: "'+local+'".\nPogress: {:3.5f}%'.format(progress*100/max)) +def getMultipleFilesThread(bearer, entries_r, local, threadName, progress, max_len): + url = entries_r + folder_t = os.path.dirname(local) + if not os.path.exists(folder_t): + try: + os.makedirs(folder_t) + #Other thead was trying to create the same 'folder' + except FileExistsError: + pass + + if os.path.isfile(local): + os.remove(local) + headers = { + 'Authorization': 'Bearer ' + bearer, + 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', + 'Content-Type': 'application/json; charset=UTF-8', + 'Connection': 'Keep-Alive', + 'Accept-Encoding': 'gzip' + } + request = requests.get(url, headers=headers, stream=True) + request.raw.decode_content = True + if request.status_code == 200: + with open(local, 'wb') as asset: + for chunk in request.iter_content(1024): + asset.write(chunk) + print(threadName + '=> Downloaded: "' + local + '".\nProgress: {:3.5f}%'.format(progress*100/max_len)) queueLock = threading.Lock() workQueue = queue.Queue(9999999) - + def getMultipleFiles(data, folder): - threadList = ["Thread-1", "Thread-2", "Thread-3", "Thread-4", "Thread-5", "Thread-6", "Thread-7", "Thread-8", "Thread-9", "Thread-10", "Thread-11", "Thread-12", "Thread-13", "Thread-14", "Thread-15", "Thread-16", "Thread-17", "Thread-18", "Thread-19", "Thread-20"] + threadList = ["Thread-" + str(thread_num) for thread_num in range(1, 21)] threads = [] threadID = 1 global exitFlag @@ -196,19 +241,25 @@ def getMultipleFiles(data, folder): thread.start() threads.append(thread) threadID += 1 - + progress = 1 - max = len(data) + max_len = len(data) url_file = 'https://backup.googleapis.com/v1/' queueLock.acquire() - + for entries in data: name = entries['name'] local = folder+os.path.sep+name.split('files/')[1].replace("/", os.path.sep) if os.path.isfile(local) and 'database' not in local.lower(): print('Skipped: "'+local+'".') else: - workQueue.put({'bearer':bearer, 'entries_r':url_file+name+'?alt=media', 'local':local, 'progress':progress, 'max':max}) + workQueue.put({ + 'bearer': bearer, + 'entries_r': url_file + name + '?alt=media', + 'local': local, + 'progress': progress, + 'max': max_len + }) progress += 1 queueLock.release() while not workQueue.empty(): @@ -218,11 +269,11 @@ def getMultipleFiles(data, folder): t.join() print ("File List Downloaded") -def runMain(mode, asset, args): +def runMain(mode, args): global bearer global exitFlag - - if os.path.isfile('settings.cfg') == False: + + if not os.path.isfile('settings.cfg'): createSettingsFile() getConfigs('-p' in args) bearer = getGoogleDriveToken(getGoogleAccountTokenFromAuth()) @@ -234,12 +285,12 @@ def runMain(mode, asset, args): if len(files) > 1: print("Backup: "+str(i)) print('/'.join(drive['name'].split('/')[5:])) - + elif mode == 'sync': exitFlag = False folder = 'WhatsApp' getMultipleFiles(files, folder) - + def main(): args = len(sys.argv) usage = '\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync [-p]\n' @@ -252,16 +303,15 @@ def main(): print('python '+str(sys.argv[0])+' -sync (sync all files locally)') print('\nYou can add -p flag to ask for password instead of using from settings.cfg') elif str(sys.argv[1]) == '-info' or str(sys.argv[1]) == 'info': - runMain('info', 'settings', sys.argv) + runMain('info', sys.argv) elif str(sys.argv[1]) == '-list' or str(sys.argv[1]) == 'list': - runMain('list', 'all', sys.argv) + runMain('list', sys.argv) elif str(sys.argv[1]) == '-sync' or str(sys.argv[1]) == 'sync': - runMain('sync', 'all', sys.argv) + runMain('sync', sys.argv) elif str(sys.argv[1]) == '-vers' or str(sys.argv[1]) == 'vers': print('\nWhatsAppGDExtract Version 1.2 Copyright (C) 2016 by TripCode\n') else: quit(usage) - + if __name__ == "__main__": main() - From 3498a567067e6fac9cc3e0bb8816578b00767660 Mon Sep 17 00:00:00 2001 From: Nicolas Lazo Date: Sat, 8 Aug 2020 12:30:33 -0300 Subject: [PATCH 25/45] Remove deprecated incomplete backup marker --- .gitignore | 2 +- WhatsAppGDExtract.py | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 8046f12..46b047f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ pkgxtra/__pycache__/ pkgxtra/gpsoauth/__pycache__/ .vscode/ .pyc - +WhatsApp/ diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 48b995f..c58af2c 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -105,25 +105,14 @@ def gDriveFileMap(nextPageToken): data = gDriveFileMapRequest(bearer, nextPageToken) jres = json.loads(data) - incomplete_backup_marker = False description_url = 'https://backup.googleapis.com/v1/clients/wa/backups/'+celnumbr description = rawGoogleDriveRequest(bearer, description_url) if not 'files' in jres: quit('Unable to locate google drive file map for: '+pkg) - try: - if 'invisible' in description['title']: - for p in result['properties']: - if (p['key'] == 'incomplete_backup_marker') and (p['value'] == 'true'): - incomplete_backup_marker = True - except: - pass if len(jres) == 0: - if incomplete_backup_marker: - quit(pkg + ' has an incomplete backup, it may be corrupted!\nMake sure the backup is ok and try again') - else: - quit(pkg + ' has no backup filemap, make sure the backup is ok') + quit(pkg + ' has no backup filemap, make sure the backup is ok') files = jres['files'] if 'nextPageToken' in jres.keys(): descriptionOnThisPage, filesOnThisPage = gDriveFileMap(jres['nextPageToken']) @@ -267,7 +256,7 @@ def getMultipleFiles(data, folder): exitFlag = True for t in threads: t.join() - print ("File List Downloaded") + print("File List Downloaded") def runMain(mode, args): global bearer From 819a92db522be2bc9cb2b29e004358e18ec5601c Mon Sep 17 00:00:00 2001 From: Nicolas Lazo Date: Tue, 4 Aug 2020 21:12:09 -0300 Subject: [PATCH 26/45] Code cleanup and typo fix --- WhatsAppGDExtract.py | 199 +++++++++++++++++++++++++++---------------- 1 file changed, 125 insertions(+), 74 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index d2dee67..9b91b45 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -1,63 +1,92 @@ #!/usr/bin/env python - + import configparser import json import os import re -import requests import sys import queue import threading import time from getpass import getpass +import requests + try: from gpsoauth import google except ImportError: from pkgxtra.gpsoauth import google exitFlag = False - - + + def getGoogleAccountTokenFromAuth(): - + b64_key_7_3_29 = (b"AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3" b"iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pK" b"RI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/" b"6rmf5AAAAAwEAAQ==") - + android_key_7_3_29 = google.key_from_b64(b64_key_7_3_29) encpass = google.signature(gmail, passw, android_key_7_3_29) - payload = {'Email':gmail, 'EncryptedPasswd':encpass, 'app':client_pkg, 'client_sig':client_sig, 'parentAndroidId':devid} + + payload = { + 'Email':gmail, + 'EncryptedPasswd':encpass, + 'app':client_pkg, + 'client_sig':client_sig, + 'parentAndroidId':devid + } + request = requests.post('https://android.clients.google.com/auth', data=payload) token = re.search('Token=(.*?)\n', request.text) - + if token: - return token.group(1) - else: - quit(request.text) - - + return token.group(1) + + quit(request.text) + + def getGoogleDriveToken(token): - payload = {'Token':token, 'app':pkg, 'client_sig':sig, 'device':devid, 'google_play_services_version':client_ver, 'service':'oauth2:https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file', 'has_permission':'1'} + payload = { + 'Token': token, + 'app': pkg, + 'client_sig': sig, + 'device': devid, + 'google_play_services_version': client_ver, + 'service': 'oauth2:https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file', + 'has_permission': '1' + } request = requests.post('https://android.clients.google.com/auth', data=payload) answer = request.text if request.text[-1] == '\n' else "%s\n" % request.text token = re.search('Auth=(.*?)\n', answer) if token: - return token.group(1) - else: - quit(answer) - + return token.group(1) + + quit(answer) + def rawGoogleDriveRequest(bearer, url): - headers = {'Authorization': 'Bearer '+bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} + headers = { + 'Authorization': 'Bearer ' + bearer, + 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', + 'Content-Type': 'application/json; charset=UTF-8', + 'Connection': 'Keep-Alive', + 'Accept-Encoding': 'gzip' + } request = requests.get(url, headers=headers) return request.text - + def gDriveFileMapRequest(bearer, nextPageToken): - header = {'Authorization': 'Bearer ' + bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} + header = { + 'Authorization': 'Bearer ' + bearer, + 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', + 'Content-Type': 'application/json; charset=UTF-8', + 'Connection': 'Keep-Alive', + 'Accept-Encoding': 'gzip' + } url = "https://backup.googleapis.com/v1/clients/wa/backups/{}/files?pageToken={}&pageSize=5000".format(celnumbr, nextPageToken) request = requests.get(url, headers=header) return request.text - + def downloadFileGoogleDrive(bearer, url, local): if not os.path.exists(os.path.dirname(local)): os.makedirs(os.path.dirname(local)) @@ -71,24 +100,24 @@ def downloadFileGoogleDrive(bearer, url, local): for chunk in request.iter_content(1024): asset.write(chunk) print('Downloaded: "'+local+'".') - + def gDriveFileMap(nextPageToken): global bearer data = gDriveFileMapRequest(bearer, nextPageToken) jres = json.loads(data) - + incomplete_backup_marker = False description_url = 'https://backup.googleapis.com/v1/clients/wa/backups/'+celnumbr description = rawGoogleDriveRequest(bearer, description_url) - if not('files' in jres): + if not 'files' in jres: quit('Unable to locate google drive file map for: '+pkg) - + try: if 'invisible' in description['title']: for p in result['properties']: if (p['key'] == 'incomplete_backup_marker') and (p['value'] == 'true'): - incomplete_backup_marker = True + incomplete_backup_marker = True except: pass if len(jres) == 0: @@ -98,9 +127,9 @@ def gDriveFileMap(nextPageToken): quit(pkg + ' has no backup filemap, make sure the backup is ok') files = jres['files'] if 'nextPageToken' in jres.keys(): - descriptionOnThisPage, filesOnThisPage = gDriveFileMap(jres['nextPageToken']) - description += descriptionOnThisPage - files += filesOnThisPage + descriptionOnThisPage, filesOnThisPage = gDriveFileMap(jres['nextPageToken']) + description += descriptionOnThisPage + files += filesOnThisPage return description, files def getConfigs(askpassword=False): @@ -125,30 +154,41 @@ def getConfigs(askpassword=False): client_ver = config.get('client', 'ver') except(configparser.NoSectionError, configparser.NoOptionError): quit('The "settings.cfg" file is missing or corrupt!') - + def jsonPrint(data): print(json.dumps(json.loads(data), indent=4, sort_keys=True)) - + def createSettingsFile(): with open('settings.cfg', 'w') as cfg: - cfg.write('[auth]\ngmail = alias@gmail.com\npassw = yourpassword\ndevid = 0000000000000000\ncelnumbr = BACKUPPHONENUMBER\n\n[app]\npkg = com.whatsapp\nsig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799\n\n[client]\npkg = com.google.android.gms\nsig = 38918a453d07199354f8b19af05ec6562ced5788\nver = 9877000') - + cfg.write('[auth]\n' + 'gmail = alias@gmail.com\n' + 'passw = yourpassword\n' + 'devid = 0000000000000000\n' + 'celnumbr = BACKUPPHONENUMBER\n\n' + '[app]\n' + 'pkg = com.whatsapp\n' + 'sig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799\n\n' + '[client]\n' + 'pkg = com.google.android.gms\n' + 'sig = 38918a453d07199354f8b19af05ec6562ced5788\n' + 'ver = 9877000') + def getSingleFile(data, asset): data = json.loads(data) for entries in data: if entries['f'] == asset: return entries['f'], entries['m'], entries['r'], entries['s'] -class myThread (threading.Thread): +class myThread(threading.Thread): def __init__(self, threadID, name, q): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.q = q def run(self): - print ('Initiated: ' + self.name) + print('Initiated: ' + self.name) process_data(self.name, self.q) - print ('Terminated: ' + self.name) + print('Terminated: ' + self.name) def process_data(threadName, q): while not exitFlag: @@ -161,33 +201,39 @@ def process_data(threadName, q): queueLock.release() time.sleep(1) -def getMultipleFilesThread(bearer, entries_r, local, threadName, progress, max): - url = entries_r - folder_t = os.path.dirname(local) - if not os.path.exists(folder_t): - try: - os.makedirs(folder_t) - #Other thead was trying to create the same 'folder' - except (FileExistsError): - pass - - if os.path.isfile(local): - os.remove(local) - headers = {'Authorization': 'Bearer '+bearer, 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', 'Content-Type': 'application/json; charset=UTF-8', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'} - request = requests.get(url, headers=headers, stream=True) - request.raw.decode_content = True - if request.status_code == 200: - with open(local, 'wb') as asset: - for chunk in request.iter_content(1024): - asset.write(chunk) - print(threadName + '=> Downloaded: "'+local+'".\nPogress: {:3.5f}%'.format(progress*100/max)) +def getMultipleFilesThread(bearer, entries_r, local, threadName, progress, max_len): + url = entries_r + folder_t = os.path.dirname(local) + if not os.path.exists(folder_t): + try: + os.makedirs(folder_t) + #Other thead was trying to create the same 'folder' + except FileExistsError: + pass + + if os.path.isfile(local): + os.remove(local) + headers = { + 'Authorization': 'Bearer ' + bearer, + 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', + 'Content-Type': 'application/json; charset=UTF-8', + 'Connection': 'Keep-Alive', + 'Accept-Encoding': 'gzip' + } + request = requests.get(url, headers=headers, stream=True) + request.raw.decode_content = True + if request.status_code == 200: + with open(local, 'wb') as asset: + for chunk in request.iter_content(1024): + asset.write(chunk) + print(threadName + '=> Downloaded: "' + local + '".\nProgress: {:3.5f}%'.format(progress*100/max_len)) queueLock = threading.Lock() workQueue = queue.Queue(9999999) - + def getMultipleFiles(data, folder): - threadList = ["Thread-1", "Thread-2", "Thread-3", "Thread-4", "Thread-5", "Thread-6", "Thread-7", "Thread-8", "Thread-9", "Thread-10", "Thread-11", "Thread-12", "Thread-13", "Thread-14", "Thread-15", "Thread-16", "Thread-17", "Thread-18", "Thread-19", "Thread-20"] + threadList = ["Thread-" + str(thread_num) for thread_num in range(1, 21)] threads = [] threadID = 1 global exitFlag @@ -196,19 +242,25 @@ def getMultipleFiles(data, folder): thread.start() threads.append(thread) threadID += 1 - + progress = 1 - max = len(data) + max_len = len(data) url_file = 'https://backup.googleapis.com/v1/' queueLock.acquire() - + for entries in data: name = entries['name'] local = folder+os.path.sep+name.split('files/')[1].replace("/", os.path.sep) if os.path.isfile(local) and 'database' not in local.lower(): print('Skipped: "'+local+'".') else: - workQueue.put({'bearer':bearer, 'entries_r':url_file+name+'?alt=media', 'local':local, 'progress':progress, 'max':max}) + workQueue.put({ + 'bearer': bearer, + 'entries_r': url_file + name + '?alt=media', + 'local': local, + 'progress': progress, + 'max': max_len + }) progress += 1 queueLock.release() while not workQueue.empty(): @@ -216,13 +268,13 @@ def getMultipleFiles(data, folder): exitFlag = True for t in threads: t.join() - print ("File List Downloaded") + print("File List Downloaded") -def runMain(mode, asset, args): +def runMain(mode, args): global bearer global exitFlag - - if os.path.isfile('settings.cfg') == False: + + if not os.path.isfile('settings.cfg'): createSettingsFile() getConfigs('-p' in args) bearer = getGoogleDriveToken(getGoogleAccountTokenFromAuth()) @@ -234,12 +286,12 @@ def runMain(mode, asset, args): if len(files) > 1: print("Backup: "+str(i)) print('/'.join(drive['name'].split('/')[5:])) - + elif mode == 'sync': exitFlag = False folder = 'WhatsApp' getMultipleFiles(files, folder) - + def main(): args = len(sys.argv) usage = '\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync [-p]\n' @@ -252,16 +304,15 @@ def main(): print('python '+str(sys.argv[0])+' -sync (sync all files locally)') print('\nYou can add -p flag to ask for password instead of using from settings.cfg') elif str(sys.argv[1]) == '-info' or str(sys.argv[1]) == 'info': - runMain('info', 'settings', sys.argv) + runMain('info', sys.argv) elif str(sys.argv[1]) == '-list' or str(sys.argv[1]) == 'list': - runMain('list', 'all', sys.argv) + runMain('list', sys.argv) elif str(sys.argv[1]) == '-sync' or str(sys.argv[1]) == 'sync': - runMain('sync', 'all', sys.argv) + runMain('sync', sys.argv) elif str(sys.argv[1]) == '-vers' or str(sys.argv[1]) == 'vers': print('\nWhatsAppGDExtract Version 1.2 Copyright (C) 2016 by TripCode\n') else: quit(usage) - + if __name__ == "__main__": main() - From e59d8551831a3d82b6b79761114e9e1e56907dab Mon Sep 17 00:00:00 2001 From: Veit Date: Sun, 8 Nov 2020 14:54:21 +0100 Subject: [PATCH 27/45] Update settings.cfg added description for cellnumber --- settings.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.cfg b/settings.cfg index d22bd56..fda0fbe 100644 --- a/settings.cfg +++ b/settings.cfg @@ -3,6 +3,7 @@ gmail = username@gmail.com passw = password devid = 1234567887654321 #cell phone number, exactly as portrait on Google Drive Backups +#(eg. 49123456789, no + or 00) celnumbr = 000000000000 [app] From 989646b2f2a67bad737c96390030401c59e1aa1d Mon Sep 17 00:00:00 2001 From: Jocki84 Date: Thu, 11 Feb 2021 11:36:45 +0100 Subject: [PATCH 28/45] Fix "Bad Authentication". - Fix recent "Bad Authentication" error. - Don't re-download a file if checksum matches the local file. - Make "info" output prettier. - Remove useless settings from settings.cfg. - Create `md5sum.txt` for verifying downloaded files. - Clean up the code. --- .gitignore | 6 - README.md | 84 +++--- WhatsAppGDExtract.py | 498 +++++++++++++++-------------------- pkgxtra/__init__.py | 1 - pkgxtra/gpsoauth/__init__.py | 103 -------- pkgxtra/gpsoauth/google.py | 54 ---- pkgxtra/gpsoauth/util.py | 35 --- pkgxtra/pkcs1/__init__.py | 5 - pkgxtra/pkcs1/defaults.py | 4 - pkgxtra/pkcs1/exceptions.py | 35 --- pkgxtra/pkcs1/keys.py | 165 ------------ pkgxtra/pkcs1/mgf.py | 24 -- pkgxtra/pkcs1/primes.py | 159 ----------- pkgxtra/pkcs1/primitives.py | 140 ---------- pkgxtra/pkcs1/rsaes_oaep.py | 97 ------- requirements.txt | 2 - settings.cfg | 21 +- 17 files changed, 265 insertions(+), 1168 deletions(-) delete mode 100644 .gitignore delete mode 100644 pkgxtra/__init__.py delete mode 100644 pkgxtra/gpsoauth/__init__.py delete mode 100644 pkgxtra/gpsoauth/google.py delete mode 100644 pkgxtra/gpsoauth/util.py delete mode 100644 pkgxtra/pkcs1/__init__.py delete mode 100644 pkgxtra/pkcs1/defaults.py delete mode 100644 pkgxtra/pkcs1/exceptions.py delete mode 100644 pkgxtra/pkcs1/keys.py delete mode 100644 pkgxtra/pkcs1/mgf.py delete mode 100644 pkgxtra/pkcs1/primes.py delete mode 100644 pkgxtra/pkcs1/primitives.py delete mode 100644 pkgxtra/pkcs1/rsaes_oaep.py diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 46b047f..0000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -pkgxtra/pkcs1/__pycache__/ -pkgxtra/__pycache__/ -pkgxtra/gpsoauth/__pycache__/ -.vscode/ -.pyc -WhatsApp/ diff --git a/README.md b/README.md index 6cb55de..ceebd2f 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,57 @@ -# WhatsApp Google Drive Extractor -Allows WhatsApp users on Android to extract their backed up WhatsApp data from Google Drive. - -###### BRANCH UPDATES: -v1.0 - Initial release. -v1.1 - Added Python 3 support. -v2.0 - Fixed gDriveFileMap after Whatsapp q requirements update. - Fixed downloadurl (the script is working again!). -v2.5 - Added multi-threading support. -v2.6 - Better errors description, logging system deprecated. - -###### PREREQUISITES: - 1. O/S: Windows Vista, Windows 7, Windows 8, Windows 10, Mac OS X or Linux - 2. Python 3.x - If not installed: https://www.python.org/downloads/ - 3. Android device with WhatsApp installed and the Google Drive backup feature enabled - 4. Google services device id (if you want to reduce the risk of being logged out of Google) - Search Google Play for "device id" for plenty of apps that can reveal this information - 5. Google account login credentials (username and password) - 6. Whatsapp cellphone number as shown in backup tab on google drive website. - - -###### INSTRUCTIONS: - 1. Extract "WhatsApp-GD-Extractor-master.zip". - 2. Install dependencies `pip install -r requirements.txt` (or using your distribution package manager) - 3. Edit the [auth] section in "settings.cfg". - 4. Run python WhatsAppGDExtract.py from your command console. +WhatsApp Google Drive Extractor +=============================== + +Allows WhatsApp users on Android to extract their backed up WhatsApp data +from Google Drive. + + +Prerequisites +------------- + + 1. [Python 3][PYTHON] + 2. Android device with WhatsApp installed and the Google Drive backup + feature enabled. + 3. The device's Android ID (if you want to reduce the risk of being logged + out of Google). Search Google Play for "device id" for plenty of apps + that can reveal this information. + 4. Google account login credentials (username and password). App password + when using 2-factor authentication. + + +Instructions +------------ + + 1. Extract `WhatsApp-GD-Extractor-master.zip`. + 2. Install dependencies: Run `python3 -m pip install -r requirements.txt` + from your command console. + 3. Edit the `[auth]` section in `settings.cfg`. + 4. Run `python3 WhatsAppGDExtract.py` from your command console. 5. Read the usage examples that are displayed. 6. Run any of the examples. +If downloading is interrupted, the files that were received successfully +won't be re-downloaded when running the tool one more time. After +downloading, you may verify the integrity of the downloaded files using +`md5sum --check md5sum.txt` on Linux or [md5summer][MD5SUMMER] on Windows. + + +Troubleshooting +--------------- -###### TROUBLESHOOTING: - 1. Check you have the required imports installed (configparser and requests). - I.E.: pip install configparser requests + 1. Check that you have the required imports installed: `python3 -m pip + install gpsoauth` 2. If you have `Error:Need Browser`, go to this url to solve the issue: - [https://accounts.google.com/b/0/DisplayUnlockCaptcha] + https://accounts.google.com/b/0/DisplayUnlockCaptcha + + +Credits +------- + +Author: TripCode +Contributors: DrDeath1122 from XDA for the multi-threading backbone part, +YuriCosta for reverse engineering the new restore system -###### CREDITS: - AUTHOR: TripCode -###### CREDITS: - CONTRIBUTORS: DrDeath1122 from XDA for the multi-threading backbone part, - YuriCosta for reverse engineering the new restore system +[MD5SUMMER]: http://md5summer.org/ +[PYTHON]: https://www.python.org/downloads/ diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 41048aa..2eeb154 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -1,308 +1,230 @@ - -#!/usr/bin/env python +#!/usr/bin/env python3 +""" +usage: python3 {} help|info|list|sync + + help Show this help. + info Show WhatsApp backups. + list Show WhatsApp backup files. + sync Download all WhatsApp backups. +""" + +from base64 import b64decode +from getpass import getpass +from multiprocessing.pool import ThreadPool +from textwrap import dedent import configparser +import gpsoauth +import hashlib import json import os -import re -import sys -import queue -import threading -import time -import requests -from getpass import getpass import requests +import sys -try: - from gpsoauth import google -except ImportError: - from pkgxtra.gpsoauth import google - -exitFlag = False - - -def getGoogleAccountTokenFromAuth(): - - b64_key_7_3_29 = (b"AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3" - b"iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pK" - b"RI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/" - b"6rmf5AAAAAwEAAQ==") - - android_key_7_3_29 = google.key_from_b64(b64_key_7_3_29) - encpass = google.signature(gmail, passw, android_key_7_3_29) - - payload = { - 'Email':gmail, - 'EncryptedPasswd':encpass, - 'app':client_pkg, - 'client_sig':client_sig, - 'parentAndroidId':devid - } - - request = requests.post('https://android.clients.google.com/auth', data=payload) - token = re.search('Token=(.*?)\n', request.text) - - if token: - return token.group(1) - - quit(request.text) - - -def getGoogleDriveToken(token): - payload = { - 'Token': token, - 'app': pkg, - 'client_sig': sig, - 'device': devid, - 'google_play_services_version': client_ver, - 'service': 'oauth2:https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file', - 'has_permission': '1' - } - request = requests.post('https://android.clients.google.com/auth', data=payload) - answer = request.text if request.text[-1] == '\n' else "%s\n" % request.text - token = re.search('Auth=(.*?)\n', answer) - if token: - return token.group(1) - - quit(answer) - -def rawGoogleDriveRequest(bearer, url): - headers = { - 'Authorization': 'Bearer ' + bearer, - 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', - 'Content-Type': 'application/json; charset=UTF-8', - 'Connection': 'Keep-Alive', - 'Accept-Encoding': 'gzip' - } - request = requests.get(url, headers=headers) - return request.text - -def gDriveFileMapRequest(bearer, nextPageToken): - header = { - 'Authorization': 'Bearer ' + bearer, - 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', - 'Content-Type': 'application/json; charset=UTF-8', - - } - url = "https://backup.googleapis.com/v1/clients/wa/backups/{}/files?pageToken={}&pageSize=5000".format(celnumbr, nextPageToken) - request = requests.get(url, headers=header) - return request.text - -def downloadFileGoogleDrive(bearer, url, local): - if not os.path.exists(os.path.dirname(local)): - os.makedirs(os.path.dirname(local)) - if os.path.isfile(local): - os.remove(local) - headers = {'Authorization': 'Bearer '+bearer} - request = requests.get(url, headers=headers, stream=True) - request.raw.decode_content = True - if request.status_code == 200: - with open(local, 'wb') as asset: - for chunk in request.iter_content(1024): - asset.write(chunk) - print('Downloaded: "'+local+'".') - -def gDriveFileMap(nextPageToken): - global bearer - data = gDriveFileMapRequest(bearer, nextPageToken) - jres = json.loads(data) - - description_url = 'https://backup.googleapis.com/v1/clients/wa/backups/'+celnumbr - - description = rawGoogleDriveRequest(bearer, description_url) - if not 'files' in jres: - quit('Unable to locate google drive file map for: '+pkg) - - if len(jres) == 0: - quit(pkg + ' has no backup filemap, make sure the backup is ok') - files = jres['files'] - if 'nextPageToken' in jres.keys(): - descriptionOnThisPage, filesOnThisPage = gDriveFileMap(jres['nextPageToken']) - description += descriptionOnThisPage - files += filesOnThisPage - return description, files - -def getConfigs(askpassword=False): - global gmail, passw, devid, pkg, sig, client_pkg, client_sig, client_ver, celnumbr - config = configparser.RawConfigParser() +def human_size(size): + for s in ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]: + if abs(size) < 1024: + break + size = int(size / 1024) + return "{}{}".format(size, s) + +def have_file(file, size, md5): + """ + Determine whether the named file's contents have the given size and hash. + """ + if not os.path.exists(file) or size != os.path.getsize(file): + return False + + digest = hashlib.md5() + with open(file, "br") as input: + while True: + b = input.read(8 * 1024) + if not b: + break + digest.update(b) + + return md5 == digest.digest() + +def download_file(file, stream): + """ + Download a file from the given stream. + """ + os.makedirs(os.path.dirname(file), exist_ok=True) + with open(file, "bw") as dest: + for chunk in stream.iter_content(chunk_size=None): + dest.write(chunk) + +class WaBackup: + """ + Provide access to WhatsApp backups stored in Google drive. + """ + def __init__(self, gmail, password, android_id): + token = gpsoauth.perform_master_login(gmail, password, android_id) + if "Token" not in token: + quit(token) + self.auth = gpsoauth.perform_oauth( + gmail, + token["Token"], + android_id, + "oauth2:https://www.googleapis.com/auth/drive.appdata", + "com.whatsapp", + "38a0f7d505fe18fec64fbf343ecaaaf310dbd799", + ) + + def get(self, path, params=None, **kwargs): + response = requests.get( + "https://backup.googleapis.com/v1/{}".format(path), + headers={"Authorization": "Bearer {}".format(self.auth["Auth"])}, + params=params, + **kwargs, + ) + response.raise_for_status() + return response + + def get_page(self, path, page_token=None): + return self.get( + path, + None if page_token is None else {"pageToken": page_token}, + ).json() + + def list_path(self, path): + last_component = path.split("/")[-1] + page_token = None + while True: + page = self.get_page(path, page_token) + for item in page[last_component]: + yield item + if "nextPageToken" not in page: + break + page_token = page["nextPageToken"] + + def backups(self): + return self.list_path("clients/wa/backups") + + def backup_files(self, backup): + return self.list_path("{}/files".format(backup["name"])) + + def fetch(self, file): + name = os.path.sep.join(file["name"].split("/")[3:]) + md5Hash = b64decode(file["md5Hash"], validate=True) + if not have_file(name, int(file["sizeBytes"]), md5Hash): + download_file( + name, + self.get(file["name"], {"alt": "media"}, stream=True) + ) + + return name, int(file["sizeBytes"]), md5Hash + + def fetch_all(self, backup, cksums): + num_files = 0 + total_size = 0 + with ThreadPool(10) as pool: + downloads = pool.imap_unordered( + lambda file: self.fetch(file), + self.backup_files(backup) + ) + for name, size, md5Hash in downloads: + num_files += 1 + total_size += size + print( + "\rProgress: {:7.3f}% {:60}".format( + 100 * total_size / int(backup["sizeBytes"]), + os.path.basename(name)[-60:] + ), + end="", + flush=True, + ) + + cksums.write("{md5Hash} *{name}\n".format( + name=name, + md5Hash=md5Hash.hex(), + )) + + print("\n{} files ({})".format(num_files, human_size(total_size))) + + +def getConfigs(): + config = configparser.ConfigParser() try: config.read('settings.cfg') gmail = config.get('auth', 'gmail') - if askpassword: + password = config.get("auth", "password", fallback="") + if not password: try: - passw = getpass("Enter your password for {}: ".format(gmail)) + password = getpass("Enter your password for {}: ".format(gmail)) except KeyboardInterrupt: - quit('\nCancelled!') - else: - passw = config.get('auth', 'passw') - devid = config.get('auth', 'devid') - celnumbr = config.get('auth', 'celnumbr') - pkg = config.get('app', 'pkg') - sig = config.get('app', 'sig') - client_pkg = config.get('client', 'pkg') - client_sig = config.get('client', 'sig') - client_ver = config.get('client', 'ver') - except(configparser.NoSectionError, configparser.NoOptionError): + quit('\nCancelled!') + android_id = config.get("auth", "android_id") + return { + "android_id": android_id, + "gmail": gmail, + "password": password, + } + except (configparser.NoSectionError, configparser.NoOptionError): quit('The "settings.cfg" file is missing or corrupt!') -def jsonPrint(data): - print(json.dumps(json.loads(data), indent=4, sort_keys=True)) - def createSettingsFile(): with open('settings.cfg', 'w') as cfg: - cfg.write('[auth]\n' - 'gmail = alias@gmail.com\n' - 'passw = yourpassword\n' - 'devid = 0000000000000000\n' - 'celnumbr = BACKUPPHONENUMBER\n\n' - '[app]\n' - 'pkg = com.whatsapp\n' - 'sig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799\n\n' - '[client]\n' - 'pkg = com.google.android.gms\n' - 'sig = 38918a453d07199354f8b19af05ec6562ced5788\n' - 'ver = 9877000') - -def getSingleFile(data, asset): - data = json.loads(data) - for entries in data: - if entries['f'] == asset: - return entries['f'], entries['m'], entries['r'], entries['s'] - -class myThread(threading.Thread): - def __init__(self, threadID, name, q): - threading.Thread.__init__(self) - self.threadID = threadID - self.name = name - self.q = q - def run(self): - print('Initiated: ' + self.name) - process_data(self.name, self.q) - print('Terminated: ' + self.name) - -def process_data(threadName, q): - while not exitFlag: - queueLock.acquire() - if not workQueue.empty(): - data = q.get() - queueLock.release() - getMultipleFilesThread(data['bearer'], data['entries_r'], data['local'], threadName, data['progress'], data['max']) - else: - queueLock.release() - time.sleep(1) - -def getMultipleFilesThread(bearer, entries_r, local, threadName, progress, max_len): - url = entries_r - folder_t = os.path.dirname(local) - if not os.path.exists(folder_t): - try: - os.makedirs(folder_t) - #Other thead was trying to create the same 'folder' - except FileExistsError: - pass - - if os.path.isfile(local): - os.remove(local) - headers = { - 'Authorization': 'Bearer ' + bearer, - 'User-Agent': 'WhatsApp/2.19.291 Android/5.1.1 Device/samsung-SM-N950W', - 'Content-Type': 'application/json; charset=UTF-8', - 'Connection': 'Keep-Alive', - 'Accept-Encoding': 'gzip' - } - request = requests.get(url, headers=headers, stream=True) - request.raw.decode_content = True - if request.status_code == 200: - with open(local, 'wb') as asset: - for chunk in request.iter_content(1024): - asset.write(chunk) - print(threadName + '=> Downloaded: "' + local + '".\nProgress: {:3.5f}%'.format(progress*100/max_len)) - - -queueLock = threading.Lock() -workQueue = queue.Queue(9999999) - -def getMultipleFiles(data, folder): - threadList = ["Thread-" + str(thread_num) for thread_num in range(1, 21)] - threads = [] - threadID = 1 - global exitFlag - for tName in threadList: - thread = myThread(threadID, tName, workQueue) - thread.start() - threads.append(thread) - threadID += 1 - - progress = 1 - max_len = len(data) - url_file = 'https://backup.googleapis.com/v1/' - queueLock.acquire() - - for entries in data: - name = entries['name'] - local = folder+os.path.sep+name.split('files/')[1].replace("/", os.path.sep) - if os.path.isfile(local) and 'database' not in local.lower(): - print('Skipped: "'+local+'".') - else: - workQueue.put({ - 'bearer': bearer, - 'entries_r': url_file + name + '?alt=media', - 'local': local, - 'progress': progress, - 'max': max_len - }) - progress += 1 - queueLock.release() - while not workQueue.empty(): - pass - exitFlag = True - for t in threads: - t.join() - print("File List Downloaded") - -def runMain(mode, args): - global bearer - global exitFlag + cfg.write(dedent(""" + [auth] + gmail = alias@gmail.com + # Optional. The account password or app password when using 2FA. + # You will be prompted if omitted. + password = yourpassword + # The result of "adb shell settings get secure android_id". + android_id = 0000000000000000 + """).lstrip()) + +def backup_info(backup): + metadata = json.loads(backup["metadata"]) + for size in "backupSize", "chatdbSize", "mediaSize", "videoSize": + metadata[size] = human_size(int(metadata[size])) + return dedent(""" + Backup {name} ({backupSize}): + WhatsApp version: {versionOfAppWhenBackup} + Password protected: {passwordProtectedBackupEnabled} + Messages: {numOfMessages} ({chatdbSize}) + Media files: {numOfMediaFiles} ({mediaSize}) + Photos: {numOfPhotos} + Videos: included={includeVideosInBackup} ({videoSize}) + """.format( + name=backup["name"].split("/")[-1], + **metadata + ) + ) + +def main(args): + if len(args) != 2 or args[1] not in ("info", "list", "sync"): + quit(__doc__.format(args[0])) if not os.path.isfile('settings.cfg'): createSettingsFile() - getConfigs('-p' in args) - bearer = getGoogleDriveToken(getGoogleAccountTokenFromAuth()) - description, files = gDriveFileMap("") - if mode == 'info': - print(description) - elif mode == 'list': - for i, drive in enumerate(files): - if len(files) > 1: - print("Backup: "+str(i)) - print('/'.join(drive['name'].split('/')[5:])) - - elif mode == 'sync': - exitFlag = False - folder = 'WhatsApp' - getMultipleFiles(files, folder) - -def main(): - args = len(sys.argv) - usage = '\nUsage: python '+str(sys.argv[0])+' -help|-vers|-info|-list|-sync [-p]\n' - if args < 2 or str(sys.argv[1]) == '-help' or str(sys.argv[1]) == 'help': - print(usage + '\nExamples:\n') - print('python '+str(sys.argv[0])+' -help (this help screen)') - print('python '+str(sys.argv[0])+' -vers (version information)') - print('python '+str(sys.argv[0])+' -info (google drive app settings)') - print('python '+str(sys.argv[0])+' -list (list all availabe files)') - print('python '+str(sys.argv[0])+' -sync (sync all files locally)') - print('\nYou can add -p flag to ask for password instead of using from settings.cfg') - elif str(sys.argv[1]) == '-info' or str(sys.argv[1]) == 'info': - runMain('info', sys.argv) - elif str(sys.argv[1]) == '-list' or str(sys.argv[1]) == 'list': - runMain('list', sys.argv) - elif str(sys.argv[1]) == '-sync' or str(sys.argv[1]) == 'sync': - runMain('sync', sys.argv) - elif str(sys.argv[1]) == '-vers' or str(sys.argv[1]) == 'vers': - print('\nWhatsAppGDExtract Version 1.2 Copyright (C) 2016 by TripCode\n') - else: - quit(usage) + wa_backup = WaBackup(**getConfigs()) + backups = wa_backup.backups() + + if args[1] == "info": + for backup in backups: + print(backup_info(backup)) + + elif args[1] == "list": + num_files = 0 + total_size = 0 + for backup in backups: + for file in wa_backup.backup_files(backup): + num_files += 1 + total_size += int(file["sizeBytes"]) + print(os.path.sep.join(file["name"].split("/")[3:])) + print("{} files ({})".format(num_files, human_size(total_size))) + + elif args[1] == "sync": + with open("md5sum.txt", "w", buffering=1) as cksums: + for backup in backups: + print("Backup {} ({}):".format( + backup["name"], + human_size(int(backup["sizeBytes"])), + )) + + wa_backup.fetch_all(backup, cksums) if __name__ == "__main__": - main() + main(sys.argv) diff --git a/pkgxtra/__init__.py b/pkgxtra/__init__.py deleted file mode 100644 index 6c291bb..0000000 --- a/pkgxtra/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.0.01' diff --git a/pkgxtra/gpsoauth/__init__.py b/pkgxtra/gpsoauth/__init__.py deleted file mode 100644 index 5eb014d..0000000 --- a/pkgxtra/gpsoauth/__init__.py +++ /dev/null @@ -1,103 +0,0 @@ -import requests - -from . import google - - -# The key is distirbuted with Google Play Services. -# This one is from version 7.3.29. -b64_key_7_3_29 = (b"AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3" - b"iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pK" - b"RI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/" - b"6rmf5AAAAAwEAAQ==") - -android_key_7_3_29 = google.key_from_b64(b64_key_7_3_29) - -auth_url = 'https://android.clients.google.com/auth' -useragent = 'gpsoauth-portify/1.0' - - -def _perform_auth_request(data): - res = requests.post(auth_url, data, - headers={'User-Agent': useragent}) - - return google.parse_auth_response(res.text) - - -def perform_master_login(email, password, android_id, - service='ac2dm', device_country='us', operatorCountry='us', - lang='en', sdk_version=17): - """ - Perform a master login, which is what Android does when you first add a Google account. - - Return a dict, eg:: - - { - 'Auth': '...', - 'Email': 'email@gmail.com', - 'GooglePlusUpgrade': '1', - 'LSID': '...', - 'PicasaUser': 'My Name', - 'RopRevision': '1', - 'RopText': ' ', - 'SID': '...', - 'Token': 'oauth2rt_1/...', - 'firstName': 'My', - 'lastName': 'Name', - 'services': 'hist,mail,googleme,...' - } - """ - - data = { - 'accountType': 'HOSTED_OR_GOOGLE', - 'Email': email, - 'has_permission': 1, - 'add_account': 1, - 'EncryptedPasswd': google.signature(email, password, android_key_7_3_29), - 'service': service, - 'source': 'android', - 'androidId': android_id, - 'device_country': device_country, - 'operatorCountry': device_country, - 'lang': lang, - 'sdk_version': sdk_version - } - - return _perform_auth_request(data) - - -def perform_oauth(email, master_token, android_id, service, app, client_sig, - device_country='us', operatorCountry='us', lang='en', sdk_version=17): - """ - Use a master token from master_login to perform OAuth to a specific Google service. - - Return a dict, eg:: - - { - 'Auth': '...', - 'LSID': '...', - 'SID': '..', - 'issueAdvice': 'auto', - 'services': 'hist,mail,googleme,...' - } - - To authenticate requests to this service, include a header - ``Authorization: GoogleLogin auth=res['Auth']``. - """ - - data = { - 'accountType': 'HOSTED_OR_GOOGLE', - 'Email': email, - 'has_permission': 1, - 'EncryptedPasswd': master_token, - 'service': service, - 'source': 'android', - 'androidId': android_id, - 'app': app, - 'client_sig': client_sig, - 'device_country': device_country, - 'operatorCountry': device_country, - 'lang': lang, - 'sdk_version': sdk_version - } - - return _perform_auth_request(data) diff --git a/pkgxtra/gpsoauth/google.py b/pkgxtra/gpsoauth/google.py deleted file mode 100644 index ea342fc..0000000 --- a/pkgxtra/gpsoauth/google.py +++ /dev/null @@ -1,54 +0,0 @@ -import base64 -import hashlib - -from pkgxtra.pkcs1.keys import RsaPublicKey -from pkgxtra.pkcs1 import rsaes_oaep - -from .util import bytes_to_long, long_to_bytes - - -def key_from_b64(b64_key): - binaryKey = base64.b64decode(b64_key) - - i = bytes_to_long(binaryKey[:4]) - mod = bytes_to_long(binaryKey[4:4+i]) - - j = bytes_to_long(binaryKey[i+4:i+4+4]) - exponent = bytes_to_long(binaryKey[i+8:i+8+j]) - - key = RsaPublicKey(mod, exponent) - - return key - - -def key_to_struct(key): - mod = long_to_bytes(key.n) - exponent = long_to_bytes(key.e) - - return b'\x00\x00\x00\x80' + mod + b'\x00\x00\x00\x03' + exponent - - -def parse_auth_response(text): - response_data = {} - for line in text.split('\n'): - if not line: - continue - - key, _, val = line.partition('=') - response_data[key] = val - - return response_data - - -def signature(email, password, key): - signature = bytearray(b'\x00') - - struct = key_to_struct(key) - signature.extend(hashlib.sha1(struct).digest()[:4]) - - message = (email + u'\x00' + password).encode('utf-8') - encrypted_login = rsaes_oaep.encrypt(key, message) - - signature.extend(encrypted_login) - - return base64.urlsafe_b64encode(signature) diff --git a/pkgxtra/gpsoauth/util.py b/pkgxtra/gpsoauth/util.py deleted file mode 100644 index 376b48f..0000000 --- a/pkgxtra/gpsoauth/util.py +++ /dev/null @@ -1,35 +0,0 @@ -import binascii -import sys - -PY3 = sys.version[0] == '3' - - -def bytes_to_long(s): - if PY3: - return int.from_bytes(s, "big") - return long(s.encode('hex'), 16) - - -def long_to_bytes(lnum, padmultiple=1): - """Packs the lnum (which must be convertable to a long) into a - byte string 0 padded to a multiple of padmultiple bytes in size. 0 - means no padding whatsoever, so that packing 0 result in an empty - string. The resulting byte string is the big-endian two's - complement representation of the passed in long.""" - - # source: http://stackoverflow.com/a/14527004/1231454 - - if lnum == 0: - return b'\0' * padmultiple - elif lnum < 0: - raise ValueError("Can only convert non-negative numbers.") - s = hex(lnum)[2:] - s = s.rstrip('L') - if len(s) & 1: - s = '0' + s - s = binascii.unhexlify(s) - if (padmultiple != 1) and (padmultiple != 0): - filled_so_far = len(s) % padmultiple - if filled_so_far != 0: - s = b'\0' * (padmultiple - filled_so_far) + s - return s diff --git a/pkgxtra/pkcs1/__init__.py b/pkgxtra/pkcs1/__init__.py deleted file mode 100644 index f8d7001..0000000 --- a/pkgxtra/pkcs1/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import rsaes_oaep -from . import keys -from . import primitives - -__VERSION__ = (0, 9, 4) diff --git a/pkgxtra/pkcs1/defaults.py b/pkgxtra/pkcs1/defaults.py deleted file mode 100644 index f1370fb..0000000 --- a/pkgxtra/pkcs1/defaults.py +++ /dev/null @@ -1,4 +0,0 @@ -import random - -default_crypto_random = random.SystemRandom() -default_pseudo_random = random.Random() diff --git a/pkgxtra/pkcs1/exceptions.py b/pkgxtra/pkcs1/exceptions.py deleted file mode 100644 index 7720863..0000000 --- a/pkgxtra/pkcs1/exceptions.py +++ /dev/null @@ -1,35 +0,0 @@ -class PKCS1BaseException(Exception): - pass - -class DecryptionError(PKCS1BaseException): - pass - -class MessageTooLong(PKCS1BaseException): - pass - -class WrongLength(PKCS1BaseException): - pass - -class MessageTooShort(PKCS1BaseException): - pass - -class InvalidSignature(PKCS1BaseException): - pass - -class RSAModulusTooShort(PKCS1BaseException): - pass - -class IntegerTooLarge(PKCS1BaseException): - pass - -class MessageRepresentativeOutOfRange(PKCS1BaseException): - pass - -class CiphertextRepresentativeOutOfRange(PKCS1BaseException): - pass - -class SignatureRepresentativeOutOfRange(PKCS1BaseException): - pass - -class EncodingError(PKCS1BaseException): - pass diff --git a/pkgxtra/pkcs1/keys.py b/pkgxtra/pkcs1/keys.py deleted file mode 100644 index 2093365..0000000 --- a/pkgxtra/pkcs1/keys.py +++ /dev/null @@ -1,165 +0,0 @@ -import fractions -from . import primitives -from . import exceptions - -from .defaults import default_crypto_random -from .primes import get_prime, DEFAULT_ITERATION - -class RsaPublicKey(object): - __slots__ = ('n', 'e', 'bit_size', 'byte_size') - - def __init__(self, n, e): - self.n = n - self.e = e - self.bit_size = primitives.integer_bit_size(n) - self.byte_size = primitives.integer_byte_size(n) - - - def __repr__(self): - return '' % (self.n, self.e, self.bit_size) - - def rsavp1(self, s): - if not (0 <= s <= self.n-1): - raise exceptions.SignatureRepresentativeOutOfRange - return self.rsaep(s) - - def rsaep(self, m): - if not (0 <= m <= self.n-1): - raise exceptions.MessageRepresentativeOutOfRange - return primitives._pow(m, self.e, self.n) - -class RsaPrivateKey(object): - __slots__ = ('n', 'd', 'bit_size', 'byte_size') - - def __init__(self, n, d): - self.n = n - self.d = d - self.bit_size = primitives.integer_bit_size(n) - self.byte_size = primitives.integer_byte_size(n) - - def __repr__(self): - return '' % (self.n, self.d, self.bit_size) - - def rsadp(self, c): - if not (0 <= c <= self.n-1): - raise exceptions.CiphertextRepresentativeOutOfRange - return primitives._pow(c, self.d, self.n) - - def rsasp1(self, m): - if not (0 <= m <= self.n-1): - raise exceptions.MessageRepresentativeOutOfRange - return self.rsadp(m) - -class MultiPrimeRsaPrivateKey(object): - __slots__ = ('primes', 'blind', 'blind_inv', 'n', 'e', 'exponents', 'crts', 'bit_size', 'byte_size') - - def __init__(self, primes, e, blind=True, rnd=default_crypto_random): - self.primes = primes - self.n = primitives.product(*primes) - self.e = e - self.bit_size = primitives.integer_bit_size(self.n) - self.byte_size = primitives.integer_byte_size(self.n) - self.exponents = [] - for prime in primes: - exponent, a, b = primitives.bezout(e, prime-1) - assert b == 1 - if exponent < 0: - exponent += prime-1 - self.exponents.append(exponent) - self.crts = [1] - R = primes[0] - for prime in primes[1:]: - crt, a, b = primitives.bezout(R, prime) - assert b == 1 - R *= prime - self.crts.append(crt) - public = RsaPublicKey(self.n, self.e) - if blind: - while True: - blind_factor = rnd.getrandbits(self.bit_size-1) - self.blind = public.rsaep(blind_factor) - u, v, gcd = primitives.bezout(blind_factor, self.n) - if gcd == 1: - self.blind_inv = u if u > 0 else u + self.n - assert (blind_factor * self.blind_inv) % self.n == 1 - break - else: - self.blind = None - self.blind_inv = None - - - def __repr__(self): - return '' % (self.n, self.primes, self.bit_size) - - - def rsadp(self, c): - if not (0 <= c <= self.n-1): - raise exceptions.CiphertextRepresentativeOutOfRange - R = 1 - m = 0 - if self.blind: - c = (c * self.blind) % self.n - for prime, exponent, crt in zip(self.primes, self.exponents, self.crts): - m_i = primitives._pow(c, exponent, prime) - h = ((m_i - m) * crt) % prime - m += R * h - R *= prime - if self.blind_inv: - m = (m * self.blind_inv) % self.n - return m - - def rsasp1(self, m): - if not (0 <= m <= self.n-1): - raise exceptions.MessageRepresentativeOutOfRange - return self.rsadp(m) - -def generate_key_pair(size=512, number=2, rnd=default_crypto_random, k=DEFAULT_ITERATION, - primality_algorithm=None, strict_size=True, e=0x10001): - '''Generates an RSA key pair. - - size: - the bit size of the modulus, default to 512. - number: - the number of primes to use, default to 2. - rnd: - the random number generator to use, default to SystemRandom from the - random library. - k: - the number of iteration to use for the probabilistic primality - tests. - primality_algorithm: - the primality algorithm to use. - strict_size: - whether to use size as a lower bound or a strict goal. - e: - the public key exponent. - - Returns the pair (public_key, private_key). - ''' - primes = [] - lbda = 1 - bits = size // number + 1 - n = 1 - while len(primes) < number: - if number - len(primes) == 1: - bits = size - primitives.integer_bit_size(n) + 1 - prime = get_prime(bits, rnd, k, algorithm=primality_algorithm) - if prime in primes: - continue - if e is not None and fractions.gcd(e, lbda) != 1: - continue - if strict_size and number - len(primes) == 1 and primitives.integer_bit_size(n*prime) != size: - continue - primes.append(prime) - n *= prime - lbda *= prime - 1 - if e is None: - e = 0x10001 - while e < lbda: - if fractions.gcd(e, lbda) == 1: - break - e += 2 - assert 3 <= e <= n-1 - public = RsaPublicKey(n, e) - private = MultiPrimeRsaPrivateKey(primes, e, blind=True, rnd=rnd) - return public, private diff --git a/pkgxtra/pkcs1/mgf.py b/pkgxtra/pkcs1/mgf.py deleted file mode 100644 index 6f6ee7b..0000000 --- a/pkgxtra/pkcs1/mgf.py +++ /dev/null @@ -1,24 +0,0 @@ -import hashlib - -from .primitives import integer_ceil, i2osp - -def mgf1(mgf_seed, mask_len, hash_class=hashlib.sha1): - ''' - Mask Generation Function v1 from the PKCS#1 v2.0 standard. - - mgs_seed - the seed, a byte string - mask_len - the length of the mask to generate - hash_class - the digest algorithm to use, default is SHA1 - - Return value: a pseudo-random mask, as a byte string - ''' - h_len = hash_class().digest_size - if mask_len > 0x10000: - raise ValueError('mask too long') - T = b'' - for i in range(0, integer_ceil(mask_len, h_len)): - C = i2osp(i, 4) - T = T + hash_class(mgf_seed + C).digest() - return T[:mask_len] - - diff --git a/pkgxtra/pkcs1/primes.py b/pkgxtra/pkcs1/primes.py deleted file mode 100644 index 4ae0576..0000000 --- a/pkgxtra/pkcs1/primes.py +++ /dev/null @@ -1,159 +0,0 @@ -import fractions -from . import primitives - -from .defaults import default_pseudo_random, default_crypto_random - -PRIME_ALGO = 'miller-rabin' -gmpy = None -try: - import gmpy - PRIME_ALGO = 'gmpy-miller-rabin' -except ImportError: - pass - -DEFAULT_ITERATION = 1000 - -USE_MILLER_RABIN = True - -def is_prime(n, rnd=default_pseudo_random, k=DEFAULT_ITERATION, algorithm=None): - '''Test if n is a prime number - - m - the integer to test - rnd - the random number generator to use for the probalistic primality - algorithms, - k - the number of iterations to use for the probabilistic primality - algorithms, - algorithm - the primality algorithm to use, default is Miller-Rabin. The - gmpy implementation is used if gmpy is installed. - - Return value: True is n seems prime, False otherwise. - ''' - - if algorithm is None: - algorithm = PRIME_ALGO - if algorithm == 'gmpy-miller-rabin': - if not gmpy: - raise NotImplementedError - return gmpy.is_prime(n, k) - elif algorithm == 'miller-rabin': - # miller rabin probability of primality is 1/4**k - return miller_rabin(n, k, rnd=rnd) - elif algorithm == 'solovay-strassen': - # for jacobi it's 1/2**k - return randomized_primality_testing(n, rnd=rnd, k=k*2) - else: - raise NotImplementedError - - -def get_prime(size=128, rnd=default_crypto_random, k=DEFAULT_ITERATION, algorithm=None): - '''Generate a prime number of the giver size using the is_prime() helper function. - - size - size in bits of the prime, default to 128 - rnd - a random generator to use - k - the number of iteration to use for the probabilistic primality algorithms, - algorithm - the name of the primality algorithm to use, default is the - probabilistic Miller-Rabin algorithm. - - Return value: a prime number, as a long integer - ''' - while True: - n = rnd.getrandbits(size-2) - n = 2 ** (size-1) + n * 2 + 1 - if is_prime(n, rnd=rnd, k=k, algorithm=algorithm): - return n - if algorithm == 'gmpy-miller-rabin': - return gmpy.next_prime(n) - -def jacobi(a, b): - '''Calculates the value of the Jacobi symbol (a/b) where both a and b are - positive integers, and b is odd - - :returns: -1, 0 or 1 - ''' - - assert a > 0 - assert b > 0 - - if a == 0: return 0 - result = 1 - while a > 1: - if a & 1: - if ((a-1)*(b-1) >> 2) & 1: - result = -result - a, b = b % a, a - else: - if (((b * b) - 1) >> 3) & 1: - result = -result - a >>= 1 - if a == 0: return 0 - return result - -def jacobi_witness(x, n): - '''Returns False if n is an Euler pseudo-prime with base x, and - True otherwise. - ''' - - j = jacobi(x, n) % n - - f = pow(x, n >> 1, n) - - if j == f: return False - return True - -def randomized_primality_testing(n, rnd=default_crypto_random, k=DEFAULT_ITERATION): - '''Calculates whether n is composite (which is always correct) or - prime (which is incorrect with error probability 2**-k) - - Returns False if the number is composite, and True if it's - probably prime. - ''' - - # 50% of Jacobi-witnesses can report compositness of non-prime numbers - - # The implemented algorithm using the Jacobi witness function has error - # probability q <= 0.5, according to Goodrich et. al - # - # q = 0.5 - # t = int(math.ceil(k / log(1 / q, 2))) - # So t = k / log(2, 2) = k / 1 = k - # this means we can use range(k) rather than range(t) - - for _ in range(k): - x = rnd.randint(0, n-1) - if jacobi_witness(x, n): return False - - return True - -def miller_rabin(n, k, rnd=default_pseudo_random): - ''' - Pure python implementation of the Miller-Rabin algorithm. - - n - the integer number to test, - k - the number of iteration, the probability of n being prime if the - algorithm returns True is 1/2**k, - rnd - a random generator - ''' - s = 0 - d = n-1 - # Find nearest power of 2 - s = primitives.integer_bit_size(n) - # Find greatest factor which is a power of 2 - s = fractions.gcd(2**s, n-1) - d = (n-1) // s - s = primitives.integer_bit_size(s) - 1 - while k: - k = k - 1 - a = rnd.randint(2, n-2) - x = pow(a,d,n) - if x == 1 or x == n - 1: - continue - for r in xrange(1,s-1): - x = pow(x,2,n) - if x == 1: - return False - if x == n - 1: - break - else: - return False - return True - diff --git a/pkgxtra/pkcs1/primitives.py b/pkgxtra/pkcs1/primitives.py deleted file mode 100644 index e418544..0000000 --- a/pkgxtra/pkcs1/primitives.py +++ /dev/null @@ -1,140 +0,0 @@ -import binascii - -import operator - -import math - -import sys - -from functools import reduce - -from .defaults import default_crypto_random - -try: - import gmpy -except ImportError: - gmpy = None - -from . import exceptions - - -'''Primitive functions extracted from the PKCS1 RFC''' - -def _pow(a, b, mod): - '''Exponentiation function using acceleration from gmpy if possible''' - if gmpy: - return long(pow(gmpy.mpz(a), gmpy.mpz(b), gmpy.mpz(mod))) - else: - return pow(a, b, mod) - -def integer_ceil(a, b): - '''Return the ceil integer of a div b.''' - quanta, mod = divmod(a, b) - if mod: - quanta += 1 - return quanta - -def integer_byte_size(n): - '''Returns the number of bytes necessary to store the integer n.''' - quanta, mod = divmod(integer_bit_size(n), 8) - if mod or n == 0: - quanta += 1 - return quanta - -def integer_bit_size(n): - '''Returns the number of bits necessary to store the integer n.''' - if n == 0: - return 1 - s = 0 - while n: - s += 1 - n >>= 1 - return s - -def bezout(a, b): - '''Compute the bezout algorithm of a and b, i.e. it returns u, v, p such as: - - p = GCD(a,b) - a * u + b * v = p - - Copied from http://www.labri.fr/perso/betrema/deug/poly/euclide.html. - ''' - u = 1 - v = 0 - s = 0 - t = 1 - while b > 0: - q = a // b - r = a % b - a = b - b = r - tmp = s - s = u - q * s - u = tmp - tmp = t - t = v - q * t - v = tmp - return u, v, a - -def i2osp(x, x_len): - '''Converts the integer x to its big-endian representation of length - x_len. - ''' - if x > 256**x_len: - raise exceptions.IntegerTooLarge - h = hex(x)[2:] - if h[-1] == 'L': - h = h[:-1] - if len(h) & 1 == 1: - h = '0%s' % h - x = binascii.unhexlify(h) - return b'\x00' * int(x_len-len(x)) + x - -def os2ip(x): - '''Converts the byte string x representing an integer reprented using the - big-endian convient to an integer. - ''' - h = binascii.hexlify(x) - return int(h, 16) - -def string_xor(a, b): - '''Computes the XOR operator between two byte strings. If the strings are - of different lengths, the result string is as long as the shorter. - ''' - if sys.version_info[0] < 3: - return ''.join((chr(ord(x) ^ ord(y)) for (x,y) in zip(a,b))) - else: - return bytes(x ^ y for (x, y) in zip(a, b)) - -def product(*args): - '''Computes the product of its arguments.''' - return reduce(operator.__mul__, args) - -def get_nonzero_random_bytes(length, rnd=default_crypto_random): - ''' - Accumulate random bit string and remove \0 bytes until the needed length - is obtained. - ''' - result = [] - i = 0 - while i < length: - l = rnd.getrandbits(12*length) - s = i2osp(l, 3*length) - s = s.replace('\x00', '') - result.append(s) - i += len(s) - return (''.join(result))[:length] - -def constant_time_cmp(a, b): - '''Compare two strings using constant time.''' - result = True - for x, y in zip(a,b): - result &= (x == y) - return result - -import textwrap - -def dump_hex(data): - if isinstance(data, basestring): - print('length', len(data)) - print(textwrap.fill(''.join(['%s ' % x.encode('hex') for x in data]), 72)) diff --git a/pkgxtra/pkcs1/rsaes_oaep.py b/pkgxtra/pkcs1/rsaes_oaep.py deleted file mode 100644 index 5910a16..0000000 --- a/pkgxtra/pkcs1/rsaes_oaep.py +++ /dev/null @@ -1,97 +0,0 @@ -import hashlib - -from . import primitives -from . import exceptions -from . import mgf -from .defaults import default_crypto_random - -def encrypt(public_key, message, label=b'', hash_class=hashlib.sha1, - mgf=mgf.mgf1, seed=None, rnd=default_crypto_random): - '''Encrypt a byte message using a RSA public key and the OAEP wrapping - algorithm, - - Parameters: - public_key - an RSA public key - message - a byte string - label - a label a per-se PKCS#1 standard - hash_class - a Python class for a message digest algorithme respecting - the hashlib interface - mgf1 - a mask generation function - seed - a seed to use instead of generating it using a random generator - rnd - a random generator class, respecting the random generator - interface from the random module, if seed is None, it is used to - generate it. - - Return value: - the encrypted string of the same length as the public key - ''' - - hash = hash_class() - h_len = hash.digest_size - k = public_key.byte_size - max_message_length = k - 2 * h_len - 2 - if len(message) > max_message_length: - raise exceptions.MessageTooLong - hash.update(label) - label_hash = hash.digest() - ps = b'\0' * int(max_message_length - len(message)) - db = b''.join((label_hash, ps, b'\x01', message)) - if not seed: - seed = primitives.i2osp(rnd.getrandbits(h_len*8), h_len) - db_mask = mgf(seed, k - h_len - 1, hash_class=hash_class) - masked_db = primitives.string_xor(db, db_mask) - seed_mask = mgf(masked_db, h_len, hash_class=hash_class) - masked_seed = primitives.string_xor(seed, seed_mask) - em = b''.join((b'\x00', masked_seed, masked_db)) - m = primitives.os2ip(em) - c = public_key.rsaep(m) - output = primitives.i2osp(c, k) - return output - -def decrypt(private_key, message, label=b'', hash_class=hashlib.sha1, - mgf=mgf.mgf1): - '''Decrypt a byte message using a RSA private key and the OAEP wrapping algorithm, - - Parameters: - public_key - an RSA public key - message - a byte string - label - a label a per-se PKCS#1 standard - hash_class - a Python class for a message digest algorithme respecting - the hashlib interface - mgf1 - a mask generation function - - Return value: - the string before encryption (decrypted) - ''' - hash = hash_class() - h_len = hash.digest_size - k = private_key.byte_size - # 1. check length - if len(message) != k or k < 2 * h_len + 2: - raise ValueError('decryption error') - # 2. RSA decryption - c = primitives.os2ip(message) - m = private_key.rsadp(c) - em = primitives.i2osp(m, k) - # 4. EME-OAEP decoding - hash.update(label) - label_hash = hash.digest() - y, masked_seed, masked_db = em[0], em[1:h_len+1], em[1+h_len:] - if y != b'\x00' and y != 0: - raise ValueError('decryption error') - seed_mask = mgf(masked_db, h_len) - seed = primitives.string_xor(masked_seed, seed_mask) - db_mask = mgf(seed, k - h_len - 1) - db = primitives.string_xor(masked_db, db_mask) - label_hash_prime, rest = db[:h_len], db[h_len:] - i = rest.find(b'\x01') - if i == -1: - raise exceptions.DecryptionError - if rest[:i].strip(b'\x00') != b'': - print(rest[:i].strip(b'\x00')) - raise exceptions.DecryptionError - m = rest[i+1:] - if label_hash_prime != label_hash: - raise exceptions.DecryptionError - return m - diff --git a/requirements.txt b/requirements.txt index 5e78cc2..0c690b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1 @@ -configparser gpsoauth -requests diff --git a/settings.cfg b/settings.cfg index fda0fbe..8870104 100644 --- a/settings.cfg +++ b/settings.cfg @@ -1,16 +1,7 @@ [auth] -gmail = username@gmail.com -passw = password -devid = 1234567887654321 -#cell phone number, exactly as portrait on Google Drive Backups -#(eg. 49123456789, no + or 00) -celnumbr = 000000000000 - -[app] -pkg = com.whatsapp -sig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799 - -[client] -pkg = com.google.android.gms -sig = 38918a453d07199354f8b19af05ec6562ced5788 -ver = 9877000 +gmail = alias@gmail.com +# Optional. The account password or app password when using 2FA. +# You will be prompted if omitted. +password = yourpassword +# The result of "adb shell settings get secure android_id". +android_id = 0000000000000000 From b559fca0a1b97a1af5234ba59975342cecb65964 Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 12 Feb 2021 15:48:49 -0300 Subject: [PATCH 29/45] Update .gitignore --- .gitignore | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index 46b047f..b1f528d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1 @@ -pkgxtra/pkcs1/__pycache__/ -pkgxtra/__pycache__/ -pkgxtra/gpsoauth/__pycache__/ -.vscode/ -.pyc WhatsApp/ From a96da334d396f7ba0c5a03e74f61a94168a2da5c Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Fri, 12 Feb 2021 16:33:35 -0300 Subject: [PATCH 30/45] Delete .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b1f528d..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -WhatsApp/ From 88bf6ddd6feeebf9b7a4fa76ed9a6eadd5f000c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alcazer=20Bar=C4=B1=C5=9F=20T=C3=9CRKG=C3=9CC=C3=9C?= Date: Tue, 16 Feb 2021 06:54:48 +0300 Subject: [PATCH 31/45] Fix % character If file name is as follows, it gives an 404 not found error. file%20name.txt We need to change url like; file%2520name.txt --- WhatsAppGDExtract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 2eeb154..463e56e 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -110,7 +110,7 @@ def fetch(self, file): if not have_file(name, int(file["sizeBytes"]), md5Hash): download_file( name, - self.get(file["name"], {"alt": "media"}, stream=True) + self.get(file["name"].replace("%", "%25"), {"alt": "media"}, stream=True) ) return name, int(file["sizeBytes"]), md5Hash From d4515724cb7cc414148ec4ff94b5b7d3f8a5e297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alcazer=20Bar=C4=B1=C5=9F=20T=C3=9CRKG=C3=9CC=C3=9C?= Date: Sat, 6 Mar 2021 20:29:19 +0300 Subject: [PATCH 32/45] Fix UnicodeEncodeError UnicodeEncodeError: 'charmap' codec can't encode character '\u200e' in position xx: character maps to --- WhatsAppGDExtract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 463e56e..2c490aa 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -217,7 +217,7 @@ def main(args): print("{} files ({})".format(num_files, human_size(total_size))) elif args[1] == "sync": - with open("md5sum.txt", "w", buffering=1) as cksums: + with open("md5sum.txt", "w", encoding="utf-8", buffering=1) as cksums: for backup in backups: print("Backup {} ({}):".format( backup["name"], From 5254d703ec1d48bff25f9972fb07d9d47fc0536c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alcazer=20Bar=C4=B1=C5=9F=20T=C3=9CRKG=C3=9CC=C3=9C?= Date: Sun, 13 Jun 2021 16:19:19 +0300 Subject: [PATCH 33/45] Fix + character If file name is as follows, it gives an 404 not found error. file+name.txt We need to change url like; file%2Bname.txt --- WhatsAppGDExtract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 2c490aa..1266d68 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -110,7 +110,7 @@ def fetch(self, file): if not have_file(name, int(file["sizeBytes"]), md5Hash): download_file( name, - self.get(file["name"].replace("%", "%25"), {"alt": "media"}, stream=True) + self.get(file["name"].replace("%", "%25").replace("+", "%2B"), {"alt": "media"}, stream=True) ) return name, int(file["sizeBytes"]), md5Hash From a0dced2978d3aacfe5d1186207def6f59548bacb Mon Sep 17 00:00:00 2001 From: YuriCosta Date: Wed, 14 Jul 2021 13:26:34 -0300 Subject: [PATCH 34/45] fixed bug where a corrupt backup would end the whole script --- WhatsAppGDExtract.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 1266d68..dc1a732 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -219,12 +219,16 @@ def main(args): elif args[1] == "sync": with open("md5sum.txt", "w", encoding="utf-8", buffering=1) as cksums: for backup in backups: - print("Backup {} ({}):".format( - backup["name"], - human_size(int(backup["sizeBytes"])), - )) - - wa_backup.fetch_all(backup, cksums) + try: + print("Backup {} ({}):".format( + backup["name"], + human_size(int(backup["sizeBytes"])), + )) + except: + print("Corrupted/Incomplete Backup!"); + continue + + wa_backup.fetch_all(backup, cksums) if __name__ == "__main__": main(sys.argv) From fc3023a4c696758dfdf1035750a149f3bbc68607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alcazer=20Bar=C4=B1=C5=9F=20T=C3=9CRKG=C3=9CC=C3=9C?= Date: Wed, 22 Sep 2021 00:42:53 +0300 Subject: [PATCH 35/45] fix #30, fix #32 --- WhatsAppGDExtract.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index dc1a732..180edfc 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -72,13 +72,22 @@ def __init__(self, gmail, password, android_id): ) def get(self, path, params=None, **kwargs): - response = requests.get( - "https://backup.googleapis.com/v1/{}".format(path), - headers={"Authorization": "Bearer {}".format(self.auth["Auth"])}, - params=params, - **kwargs, - ) - response.raise_for_status() + try: + response = requests.get( + "https://backup.googleapis.com/v1/{}".format(path), + headers={"Authorization": "Bearer {}".format(self.auth["Auth"])}, + params=params, + **kwargs, + ) + response.raise_for_status() + except requests.exceptions.HTTPError as errh: + print ("\n\nHttp Error:",errh) + except requests.exceptions.ConnectionError as errc: + print ("\n\nError Connecting:",errc) + except requests.exceptions.Timeout as errt: + print ("\n\nTimeout Error:",errt) + except requests.exceptions.RequestException as err: + print ("\n\nOOps: Something Else",err) return response def get_page(self, path, page_token=None): @@ -228,7 +237,7 @@ def main(args): print("Corrupted/Incomplete Backup!"); continue - wa_backup.fetch_all(backup, cksums) + wa_backup.fetch_all(backup, cksums) if __name__ == "__main__": main(sys.argv) From e32ecb2ad71d7ab56ff1aaeee81f5b306d54c1da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alcazer=20Bar=C4=B1=C5=9F=20T=C3=9CRKG=C3=9CC=C3=9C?= Date: Wed, 22 Sep 2021 01:51:43 +0300 Subject: [PATCH 36/45] fix #30 --- WhatsAppGDExtract.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index 180edfc..bed9a09 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -233,11 +233,10 @@ def main(args): backup["name"], human_size(int(backup["sizeBytes"])), )) + wa_backup.fetch_all(backup, cksums) except: print("Corrupted/Incomplete Backup!"); continue - wa_backup.fetch_all(backup, cksums) - if __name__ == "__main__": main(sys.argv) From dfbba450d46e9ca48984f3a8dc93ce73c53288b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alcazer=20Bar=C4=B1=C5=9F=20T=C3=9CRKG=C3=9CC=C3=9C?= Date: Sun, 7 Nov 2021 17:46:27 +0300 Subject: [PATCH 37/45] cosmetic and fix #30, #32, #34 --- WhatsAppGDExtract.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index bed9a09..a3f1531 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -155,14 +155,14 @@ def fetch_all(self, backup, cksums): def getConfigs(): config = configparser.ConfigParser() try: - config.read('settings.cfg') - gmail = config.get('auth', 'gmail') + config.read("settings.cfg") + gmail = config.get("auth", "gmail") password = config.get("auth", "password", fallback="") if not password: try: password = getpass("Enter your password for {}: ".format(gmail)) except KeyboardInterrupt: - quit('\nCancelled!') + quit("\nCancelled!") android_id = config.get("auth", "android_id") return { "android_id": android_id, @@ -170,10 +170,10 @@ def getConfigs(): "password": password, } except (configparser.NoSectionError, configparser.NoOptionError): - quit('The "settings.cfg" file is missing or corrupt!') + quit("The 'settings.cfg' file is missing or corrupt!") def createSettingsFile(): - with open('settings.cfg', 'w') as cfg: + with open("settings.cfg", "w") as cfg: cfg.write(dedent(""" [auth] gmail = alias@gmail.com @@ -206,7 +206,7 @@ def main(args): if len(args) != 2 or args[1] not in ("info", "list", "sync"): quit(__doc__.format(args[0])) - if not os.path.isfile('settings.cfg'): + if not os.path.isfile("settings.cfg"): createSettingsFile() wa_backup = WaBackup(**getConfigs()) backups = wa_backup.backups() @@ -220,23 +220,37 @@ def main(args): total_size = 0 for backup in backups: for file in wa_backup.backup_files(backup): - num_files += 1 - total_size += int(file["sizeBytes"]) - print(os.path.sep.join(file["name"].split("/")[3:])) + try: + num_files += 1 + total_size += int(file["sizeBytes"]) + print(os.path.sep.join(file["name"].split("/")[3:])) + except: + print("\n#####\n\nWarning: Unexpected error in file: {}\n\nDetail: {}\n\n#####\n".format( + os.path.sep.join(file["name"].split("/")[3:]), + file + )) + input("Press the key to continue...") + continue print("{} files ({})".format(num_files, human_size(total_size))) elif args[1] == "sync": with open("md5sum.txt", "w", encoding="utf-8", buffering=1) as cksums: for backup in backups: try: + answer = input("Do you want {}? [y/n] : ".format(backup["name"])) + if not answer or answer[0].lower() != 'y': + continue print("Backup {} ({}):".format( backup["name"], human_size(int(backup["sizeBytes"])), )) wa_backup.fetch_all(backup, cksums) except: - print("Corrupted/Incomplete Backup!"); - continue + print("\n#####\n\nWarning: Unexpected error in backup: {}\n\nDetail: {}\n\n#####\n".format( + backup["name"], + backup + )) + input("Press the key to continue...") if __name__ == "__main__": main(sys.argv) From d959773ed36ffa7a32e3c896e556d45cc05069bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alcazer=20Bar=C4=B1=C5=9F=20T=C3=9CRKG=C3=9CC=C3=9C?= Date: Sun, 7 Nov 2021 20:45:52 +0300 Subject: [PATCH 38/45] cosmetic and fix backup info, #30, #32, #34 --- WhatsAppGDExtract.py | 60 +++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index a3f1531..b7e5e4d 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -20,6 +20,7 @@ import os import requests import sys +import traceback def human_size(size): for s in ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]: @@ -188,19 +189,16 @@ def backup_info(backup): metadata = json.loads(backup["metadata"]) for size in "backupSize", "chatdbSize", "mediaSize", "videoSize": metadata[size] = human_size(int(metadata[size])) - return dedent(""" - Backup {name} ({backupSize}): - WhatsApp version: {versionOfAppWhenBackup} - Password protected: {passwordProtectedBackupEnabled} - Messages: {numOfMessages} ({chatdbSize}) - Media files: {numOfMediaFiles} ({mediaSize}) - Photos: {numOfPhotos} - Videos: included={includeVideosInBackup} ({videoSize}) - """.format( - name=backup["name"].split("/")[-1], - **metadata - ) - ) + print("Backup {} Size:({}) Upload Time:".format(backup["name"].split("/")[-1], metadata["backupSize"]), backup["updateTime"]) + print(" WhatsApp version : {}".format(metadata["versionOfAppWhenBackup"])) + try: + print(" Password protected: {}".format(metadata["passwordProtectedBackupEnabled"])) + except: + pass + print(" Messages : {} ({})".format(metadata["numOfMessages"], metadata["chatdbSize"])) + print(" Media files : {} ({})".format(metadata["numOfMediaFiles"], metadata["mediaSize"])) + print(" Photos : {}".format(metadata["numOfPhotos"])) + print(" Videos : included={} ({})".format(metadata["includeVideosInBackup"], metadata["videoSize"])) def main(args): if len(args) != 2 or args[1] not in ("info", "list", "sync"): @@ -213,42 +211,48 @@ def main(args): if args[1] == "info": for backup in backups: - print(backup_info(backup)) + answer = input("\nDo you want {}? [y/n] : ".format(backup["name"].split("/")[-1])) + if not answer or answer[0].lower() != 'y': + continue + backup_info(backup) elif args[1] == "list": - num_files = 0 - total_size = 0 for backup in backups: + answer = input("\nDo you want {}? [y/n] : ".format(backup["name"].split("/")[-1])) + if not answer or answer[0].lower() != 'y': + continue + num_files = 0 + total_size = 0 for file in wa_backup.backup_files(backup): try: num_files += 1 total_size += int(file["sizeBytes"]) print(os.path.sep.join(file["name"].split("/")[3:])) except: - print("\n#####\n\nWarning: Unexpected error in file: {}\n\nDetail: {}\n\n#####\n".format( + print("\n#####\n\nWarning: Unexpected error in file: {}\n\nDetail: {}\n\nException: {}\n\n#####\n".format( os.path.sep.join(file["name"].split("/")[3:]), - file + json.dumps(file, indent=4, sort_keys=True), + traceback.format_exc() )) input("Press the key to continue...") continue - print("{} files ({})".format(num_files, human_size(total_size))) + print("{} files ({})".format(num_files, human_size(total_size))) elif args[1] == "sync": with open("md5sum.txt", "w", encoding="utf-8", buffering=1) as cksums: for backup in backups: try: - answer = input("Do you want {}? [y/n] : ".format(backup["name"])) + answer = input("\nDo you want {}? [y/n] : ".format(backup["name"].split("/")[-1])) if not answer or answer[0].lower() != 'y': continue - print("Backup {} ({}):".format( - backup["name"], - human_size(int(backup["sizeBytes"])), - )) + print("Backup Size:{} Upload Time: {}".format(human_size(int(backup["sizeBytes"])), backup["updateTime"])) wa_backup.fetch_all(backup, cksums) - except: - print("\n#####\n\nWarning: Unexpected error in backup: {}\n\nDetail: {}\n\n#####\n".format( - backup["name"], - backup + except Exception as err: + print("\n#####\n\nWarning: Unexpected error in backup: {} (Size:{} Upload Time: {})\n\nException: {}\n\n#####\n".format( + backup["name"].split("/")[-1], + human_size(int(backup["sizeBytes"])), + backup["updateTime"], + traceback.format_exc() )) input("Press the key to continue...") From 7513942828b1d9a3cc6ef86bef7b7f0e21a0843a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alcazer=20Bar=C4=B1=C5=9F=20T=C3=9CRKG=C3=9CC=C3=9C?= Date: Sun, 7 Nov 2021 21:00:35 +0300 Subject: [PATCH 39/45] cosmetic and fix backup info, #30, #32, #34 --- WhatsAppGDExtract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index b7e5e4d..efc871f 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -189,7 +189,7 @@ def backup_info(backup): metadata = json.loads(backup["metadata"]) for size in "backupSize", "chatdbSize", "mediaSize", "videoSize": metadata[size] = human_size(int(metadata[size])) - print("Backup {} Size:({}) Upload Time:".format(backup["name"].split("/")[-1], metadata["backupSize"]), backup["updateTime"]) + print("Backup {} Size:({}) Upload Time:{}".format(backup["name"].split("/")[-1], metadata["backupSize"]), backup["updateTime"]) print(" WhatsApp version : {}".format(metadata["versionOfAppWhenBackup"])) try: print(" Password protected: {}".format(metadata["passwordProtectedBackupEnabled"])) From 09e3707ff8535bdc9eff80a6b7f061e237b150ae Mon Sep 17 00:00:00 2001 From: Brian Choromanski Date: Sun, 14 Nov 2021 17:22:09 -0500 Subject: [PATCH 40/45] Fixed error on list option --- WhatsAppGDExtract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhatsAppGDExtract.py b/WhatsAppGDExtract.py index efc871f..6b406dd 100755 --- a/WhatsAppGDExtract.py +++ b/WhatsAppGDExtract.py @@ -189,7 +189,7 @@ def backup_info(backup): metadata = json.loads(backup["metadata"]) for size in "backupSize", "chatdbSize", "mediaSize", "videoSize": metadata[size] = human_size(int(metadata[size])) - print("Backup {} Size:({}) Upload Time:{}".format(backup["name"].split("/")[-1], metadata["backupSize"]), backup["updateTime"]) + print("Backup {} Size:({}) Upload Time:{}".format(backup["name"].split("/")[-1], metadata["backupSize"], backup["updateTime"])) print(" WhatsApp version : {}".format(metadata["versionOfAppWhenBackup"])) try: print(" Password protected: {}".format(metadata["passwordProtectedBackupEnabled"])) From c09df765b97f2e4d5129592e8a1fe9eb383945a0 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Mon, 25 Apr 2022 13:30:16 +0530 Subject: [PATCH 41/45] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ceebd2f..1bd1f61 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Prerequisites 2. Android device with WhatsApp installed and the Google Drive backup feature enabled. 3. The device's Android ID (if you want to reduce the risk of being logged - out of Google). Search Google Play for "device id" for plenty of apps + out of Google). Run `adb shell settings get secure android_id` or Search Google Play for "device id" for plenty of apps that can reveal this information. 4. Google account login credentials (username and password). App password when using 2-factor authentication. From 300b0ab3b0fdb561589e9e2f72a0f249bf7dad63 Mon Sep 17 00:00:00 2001 From: Yuri Date: Wed, 30 Nov 2022 23:00:37 -0300 Subject: [PATCH 42/45] requirements and password instructions, script working again! --- requirements.txt | 2 +- settings.cfg | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0c690b9..36d2841 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -gpsoauth +gpsoauth==1.0.2 \ No newline at end of file diff --git a/settings.cfg b/settings.cfg index 8870104..61afedf 100644 --- a/settings.cfg +++ b/settings.cfg @@ -1,7 +1,9 @@ [auth] gmail = alias@gmail.com -# Optional. The account password or app password when using 2FA. + +# The account app password # You will be prompted if omitted. password = yourpassword + # The result of "adb shell settings get secure android_id". android_id = 0000000000000000 From a34e1b5b6fe5130ff7c45b2bf10a91c9ac3eccd9 Mon Sep 17 00:00:00 2001 From: Yuri Date: Wed, 30 Nov 2022 23:09:36 -0300 Subject: [PATCH 43/45] hotfix --- README.md | 2 +- settings.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1bd1f61..faa745a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Instructions 1. Extract `WhatsApp-GD-Extractor-master.zip`. 2. Install dependencies: Run `python3 -m pip install -r requirements.txt` - from your command console. + from your command console. Make sure gpsoauth is the latest version. 3. Edit the `[auth]` section in `settings.cfg`. 4. Run `python3 WhatsAppGDExtract.py` from your command console. 5. Read the usage examples that are displayed. diff --git a/settings.cfg b/settings.cfg index 61afedf..936b55b 100644 --- a/settings.cfg +++ b/settings.cfg @@ -1,9 +1,9 @@ [auth] gmail = alias@gmail.com -# The account app password +# The account app password ou plain text password # You will be prompted if omitted. -password = yourpassword +password = # The result of "adb shell settings get secure android_id". android_id = 0000000000000000 From e6376f818793e9574cd0bee347d50d643a1a4d9f Mon Sep 17 00:00:00 2001 From: joanroig Date: Fri, 23 Jun 2023 10:38:58 +0200 Subject: [PATCH 44/45] fix: set correct version for the dependency urllib3 --- .gitignore | 164 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 +- requirements.txt | 3 +- 3 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cc1375 --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Whatsapp extractor +*/* +md5sum.txt diff --git a/README.md b/README.md index faa745a..95cae2a 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,7 @@ Prerequisites 3. The device's Android ID (if you want to reduce the risk of being logged out of Google). Run `adb shell settings get secure android_id` or Search Google Play for "device id" for plenty of apps that can reveal this information. - 4. Google account login credentials (username and password). App password - when using 2-factor authentication. + 4. Google account login credentials (username and password). Create and use an App password when using 2-factor authentication: https://myaccount.google.com/apppasswords Instructions diff --git a/requirements.txt b/requirements.txt index 36d2841..c42cb6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -gpsoauth==1.0.2 \ No newline at end of file +gpsoauth==1.0.2 +urllib3<2 \ No newline at end of file From 35decacfe1eb0c1bcbe42728030942acec047172 Mon Sep 17 00:00:00 2001 From: Yuri Costa Date: Thu, 29 May 2025 19:19:13 -0300 Subject: [PATCH 45/45] Update requirements.txt --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c42cb6a..dbe1a4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -gpsoauth==1.0.2 -urllib3<2 \ No newline at end of file +gpsoauth==1.1.1 +urllib3<2