From 326b99d8600e429a93b8e1543b35902b4a738c23 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 15 Aug 2023 00:32:07 -0400 Subject: [PATCH 01/12] feat(whatsapp_business)!: extraction script for smb Duplicated original extract_iphone_media to include smb specfic fileIDs. --- .../extract_iphone_media_smb.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 Whatsapp_Chat_Exporter/extract_iphone_media_smb.py diff --git a/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py b/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py new file mode 100644 index 0000000..0f9511d --- /dev/null +++ b/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py @@ -0,0 +1,126 @@ +#!/usr/bin/python3 + +import shutil +import sqlite3 +import os +import time +import getpass +import threading +try: + from iphone_backup_decrypt import EncryptedBackup, RelativePath + from iphone_backup_decrypt import FailedToDecryptError, Domain +except ModuleNotFoundError: + support_encrypted = False +else: + support_encrypted = True + + +def extract_encrypted(base_dir, password): + backup = EncryptedBackup(backup_directory=base_dir, passphrase=password, cleanup=False, check_same_thread=False) + print("Decrypting WhatsApp database...") + try: + backup.extract_file(relative_path=RelativePath.WHATSAPP_MESSAGES, + output_filename="724bd3b98b18518b455a87c1f3ac3a0d189c4466") + backup.extract_file(relative_path=RelativePath.WHATSAPP_CONTACTS, + output_filename="d7246a707f51ddf8b17ee2dddabd9e0a4da5c552") + except FailedToDecryptError: + print("Failed to decrypt backup: incorrect password?") + exit() + extract_thread = threading.Thread( + target=backup.extract_files_by_domain, + args=(Domain.WHATSAPP, Domain.WHATSAPP) + ) + extract_thread.daemon = True + extract_thread.start() + dot = 0 + while extract_thread.is_alive(): + print(f"Decrypting and extracting files{'.' * dot}{' ' * (3 - dot)}", end="\r") + if dot < 3: + dot += 1 + time.sleep(0.5) + else: + dot = 0 + time.sleep(0.4) + print(f"All required files decrypted and extracted.", end="\n") + extract_thread.handled = True + return backup + + +def is_encrypted(base_dir): + with sqlite3.connect(os.path.join(base_dir, "Manifest.db")) as f: + c = f.cursor() + try: + c.execute("""SELECT count() + FROM Files + """) + except sqlite3.OperationalError as e: + raise e # These error cannot be used to determine if the backup is encrypted + except sqlite3.DatabaseError: + return True + else: + return False + + +def extract_media(base_dir): + if is_encrypted(base_dir): + if not support_encrypted: + print("You don't have the dependencies to handle encrypted backup.") + print("Read more on how to deal with encrypted backup:") + print("https://github.com/KnugiHK/Whatsapp-Chat-Exporter/blob/main/README.md#usage") + return False + print("Encryption detected on the backup!") + password = getpass.getpass("Enter the password for the backup:") + extract_encrypted(base_dir, password) + else: + wts_db = os.path.join(base_dir, "72/724bd3b98b18518b455a87c1f3ac3a0d189c4466") + contact_db = os.path.join(base_dir, "d7/d7246a707f51ddf8b17ee2dddabd9e0a4da5c552") + if not os.path.isfile(wts_db): + print("WhatsApp database not found.") + exit() + else: + shutil.copyfile(wts_db, "724bd3b98b18518b455a87c1f3ac3a0d189c4466") + if not os.path.isfile(contact_db): + print("Contact database not found.") + exit() + else: + shutil.copyfile(contact_db, "d7246a707f51ddf8b17ee2dddabd9e0a4da5c552") + _wts_id = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared" + with sqlite3.connect(os.path.join(base_dir, "Manifest.db")) as manifest: + manifest.row_factory = sqlite3.Row + c = manifest.cursor() + c.execute( + f"""SELECT count() + FROM Files + WHERE domain = '{_wts_id}'""" + ) + total_row_number = c.fetchone()[0] + print(f"Extracting WhatsApp files...(0/{total_row_number})", end="\r") + c.execute(f"""SELECT fileID, + relativePath, + flags, + ROW_NUMBER() OVER(ORDER BY relativePath) AS _index + FROM Files + WHERE domain = '{_wts_id}' + ORDER BY relativePath""") + if not os.path.isdir(_wts_id): + os.mkdir(_wts_id) + row = c.fetchone() + while row is not None: + if row["relativePath"] == "": + row = c.fetchone() + continue + destination = os.path.join(_wts_id, row["relativePath"]) + hashes = row["fileID"] + folder = hashes[:2] + flags = row["flags"] + if flags == 2: + try: + os.mkdir(destination) + except FileExistsError: + pass + elif flags == 1: + shutil.copyfile(os.path.join(base_dir, folder, hashes), destination) + if row["_index"] % 100 == 0: + print(f"Extracting WhatsApp files...({row['_index']}/{total_row_number})", end="\r") + row = c.fetchone() + print(f"Extracting WhatsApp files...({total_row_number}/{total_row_number})", end="\n") From d8b434e169b3ecdef42dab1fcc8719c42e0bc2eb Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 15 Aug 2023 00:35:32 -0400 Subject: [PATCH 02/12] feat(whatsapp_business)!: args and default values for smb Added corresponding logic for whatsapp business and default values. --- Whatsapp_Chat_Exporter/__main__.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Whatsapp_Chat_Exporter/__main__.py b/Whatsapp_Chat_Exporter/__main__.py index 14e1f6d..370b929 100644 --- a/Whatsapp_Chat_Exporter/__main__.py +++ b/Whatsapp_Chat_Exporter/__main__.py @@ -7,7 +7,7 @@ import json import string import glob from Whatsapp_Chat_Exporter import extract_exported, extract_iphone -from Whatsapp_Chat_Exporter import extract, extract_iphone_media +from Whatsapp_Chat_Exporter import extract, extract_iphone_media, extract_iphone_media_smb from Whatsapp_Chat_Exporter.data_model import ChatStore from Whatsapp_Chat_Exporter.utility import Crypt, check_update, import_from_json from argparse import ArgumentParser, SUPPRESS @@ -177,6 +177,13 @@ def main(): action='store_true', help="Import JSON file and convert to HTML output" ) + parser.add_argument( + "--smb", + dest="smb", + default=False, + action='store_true', + help="Use Whatsapp Business default files (iphone only)" + ) args = parser.parse_args() # Check for updates @@ -265,14 +272,23 @@ def main(): vcard = extract_iphone.vcard create_html = extract.create_html if args.media is None: - args.media = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared" + if args.smb: + args.media = "AppDomainGroup-group.net.whatsapp.WhatsAppSMB.shared" + else: + args.media = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared" if args.backup is not None: if not os.path.isdir(args.media): - extract_iphone_media.extract_media(args.backup) + if args.smb: + extract_iphone_media_smb.extract_media(args.backup) + else: + extract_iphone_media.extract_media(args.backup) else: print("WhatsApp directory already exists, skipping WhatsApp file extraction.") if args.db is None: - msg_db = "7c7fba66680ef796b916b067077cc246adacf01d" + if args.smb: + msg_db = "724bd3b98b18518b455a87c1f3ac3a0d189c4466" + else: + msg_db = "7c7fba66680ef796b916b067077cc246adacf01d" else: msg_db = args.db if args.wa is None: From 269a59c1e21e695ef94dfc36281aa01b0d8583b7 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 15 Aug 2023 00:36:15 -0400 Subject: [PATCH 03/12] feat(whatsapp_business): made vcard path dynamic --- Whatsapp_Chat_Exporter/__main__.py | 2 +- Whatsapp_Chat_Exporter/extract_iphone.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Whatsapp_Chat_Exporter/__main__.py b/Whatsapp_Chat_Exporter/__main__.py index 370b929..66deabe 100644 --- a/Whatsapp_Chat_Exporter/__main__.py +++ b/Whatsapp_Chat_Exporter/__main__.py @@ -306,7 +306,7 @@ def main(): db.row_factory = sqlite3.Row messages(db, data, args.media) media(db, data, args.media) - vcard(db, data) + vcard(db, data, args.media) if args.android: extract.calls(db, data) if not args.no_html: diff --git a/Whatsapp_Chat_Exporter/extract_iphone.py b/Whatsapp_Chat_Exporter/extract_iphone.py index 822ad24..dadd695 100644 --- a/Whatsapp_Chat_Exporter/extract_iphone.py +++ b/Whatsapp_Chat_Exporter/extract_iphone.py @@ -244,7 +244,7 @@ def media(db, data, media_folder): f"Processing media...({total_row_number}/{total_row_number})", end="\r") -def vcard(db, data): +def vcard(db, data, media_folder): c = db.cursor() c.execute("""SELECT DISTINCT ZWAVCARDMENTION.ZMEDIAITEM, ZWAMEDIAITEM.ZMESSAGE, @@ -260,13 +260,13 @@ def vcard(db, data): contents = c.fetchall() total_row_number = len(contents) print(f"\nProcessing vCards...(0/{total_row_number})", end="\r") - base = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared/Message/vCards" - if not os.path.isdir(base): - Path(base).mkdir(parents=True, exist_ok=True) + path = f'{media_folder}/Message/vCards' + if not os.path.isdir(path): + Path(path).mkdir(parents=True, exist_ok=True) for index, content in enumerate(contents): file_name = "".join(x for x in content["ZVCARDNAME"] if x.isalnum()) file_name = file_name.encode('utf-8')[:230].decode('utf-8', 'ignore') - file_path = os.path.join(base, f"{file_name}.vcf") + file_path = os.path.join(path, f"{file_name}.vcf") if not os.path.isfile(file_path): with open(file_path, "w", encoding="utf-8") as f: f.write(content["ZVCARDSTRING"]) From f4888949429c9411728a9f40a91b5d7a9bc9b6bc Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 15 Aug 2023 00:46:54 -0400 Subject: [PATCH 04/12] fix(whatsapp_business): missing domain ref --- Whatsapp_Chat_Exporter/extract_iphone_media_smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py b/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py index 0f9511d..459344a 100644 --- a/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py +++ b/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py @@ -84,7 +84,7 @@ def extract_media(base_dir): exit() else: shutil.copyfile(contact_db, "d7246a707f51ddf8b17ee2dddabd9e0a4da5c552") - _wts_id = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared" + _wts_id = "AppDomainGroup-group.net.whatsapp.WhatsAppSMB.shared" with sqlite3.connect(os.path.join(base_dir, "Manifest.db")) as manifest: manifest.row_factory = sqlite3.Row c = manifest.cursor() From ee4e95c75f17eff48abafda747d920a324ecd9f6 Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:22:08 +0800 Subject: [PATCH 05/12] Bug fix on the possible breakage mentioned in #60 --- Whatsapp_Chat_Exporter/extract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Whatsapp_Chat_Exporter/extract.py b/Whatsapp_Chat_Exporter/extract.py index 6441eff..9dd552d 100644 --- a/Whatsapp_Chat_Exporter/extract.py +++ b/Whatsapp_Chat_Exporter/extract.py @@ -528,7 +528,7 @@ def media(db, data, media_folder): f"Processing media...({total_row_number}/{total_row_number})", end="\r") -def vcard(db, data): +def vcard(db, data, media_folder): c = db.cursor() try: c.execute("""SELECT message_row_id, From a5cb46e095c70aeaa0a6aa32955914965650a76f Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:22:43 +0800 Subject: [PATCH 06/12] Also make vcard path dynamic in Android --- Whatsapp_Chat_Exporter/extract.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Whatsapp_Chat_Exporter/extract.py b/Whatsapp_Chat_Exporter/extract.py index 9dd552d..2b5fb90 100644 --- a/Whatsapp_Chat_Exporter/extract.py +++ b/Whatsapp_Chat_Exporter/extract.py @@ -558,14 +558,14 @@ def vcard(db, data, media_folder): rows = c.fetchall() total_row_number = len(rows) print(f"\nProcessing vCards...(0/{total_row_number})", end="\r") - base = "WhatsApp/vCards" - if not os.path.isdir(base): - Path(base).mkdir(parents=True, exist_ok=True) + path = f"{media_folder}/vCards" + if not os.path.isdir(path): + Path(path).mkdir(parents=True, exist_ok=True) for index, row in enumerate(rows): media_name = row["media_name"] if row["media_name"] is not None else "" file_name = "".join(x for x in media_name if x.isalnum()) file_name = file_name.encode('utf-8')[:230].decode('utf-8', 'ignore') - file_path = os.path.join(base, f"{file_name}.vcf") + file_path = os.path.join(path, f"{file_name}.vcf") if not os.path.isfile(file_path): with open(file_path, "w", encoding="utf-8") as f: f.write(row["vcard"]) From 448ba892cc1b650356fd4881f319dd665ea5d8ad Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Tue, 15 Aug 2023 17:33:56 +0800 Subject: [PATCH 07/12] Change the option from --smb to --business --- Whatsapp_Chat_Exporter/__main__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Whatsapp_Chat_Exporter/__main__.py b/Whatsapp_Chat_Exporter/__main__.py index 66deabe..45f708a 100644 --- a/Whatsapp_Chat_Exporter/__main__.py +++ b/Whatsapp_Chat_Exporter/__main__.py @@ -178,11 +178,11 @@ def main(): help="Import JSON file and convert to HTML output" ) parser.add_argument( - "--smb", - dest="smb", + "--business", + dest="business", default=False, action='store_true', - help="Use Whatsapp Business default files (iphone only)" + help="Use Whatsapp Business default files (iOS only)" ) args = parser.parse_args() @@ -206,6 +206,9 @@ def main(): elif args.import_json and not os.path.isfile(args.json): print("JSON file not found.") exit(1) + if args.android and args.business: + print("WhatsApp Business is only available on iOS for now.") + exit(1) data = {} From 2944d00ca29296a67a533e5744e9d7794b69995d Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Tue, 15 Aug 2023 17:40:20 +0800 Subject: [PATCH 08/12] Refactoring so that no file needs to be introduced --- Whatsapp_Chat_Exporter/__main__.py | 21 ++- .../extract_iphone_media.py | 20 +-- .../extract_iphone_media_smb.py | 126 ------------------ Whatsapp_Chat_Exporter/utility.py | 12 ++ 4 files changed, 32 insertions(+), 147 deletions(-) delete mode 100644 Whatsapp_Chat_Exporter/extract_iphone_media_smb.py diff --git a/Whatsapp_Chat_Exporter/__main__.py b/Whatsapp_Chat_Exporter/__main__.py index 45f708a..c20d8bd 100644 --- a/Whatsapp_Chat_Exporter/__main__.py +++ b/Whatsapp_Chat_Exporter/__main__.py @@ -7,7 +7,7 @@ import json import string import glob from Whatsapp_Chat_Exporter import extract_exported, extract_iphone -from Whatsapp_Chat_Exporter import extract, extract_iphone_media, extract_iphone_media_smb +from Whatsapp_Chat_Exporter import extract, extract_iphone_media from Whatsapp_Chat_Exporter.data_model import ChatStore from Whatsapp_Chat_Exporter.utility import Crypt, check_update, import_from_json from argparse import ArgumentParser, SUPPRESS @@ -274,24 +274,19 @@ def main(): media = extract_iphone.media vcard = extract_iphone.vcard create_html = extract.create_html + if args.business: + from Whatsapp_Chat_Exporter.utility import WhatsAppBusinessIdentifier as identifiers + else: + from Whatsapp_Chat_Exporter.utility import WhatsAppIdentifier as identifiers if args.media is None: - if args.smb: - args.media = "AppDomainGroup-group.net.whatsapp.WhatsAppSMB.shared" - else: - args.media = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared" + args.media = identifiers.DOMAIN if args.backup is not None: if not os.path.isdir(args.media): - if args.smb: - extract_iphone_media_smb.extract_media(args.backup) - else: - extract_iphone_media.extract_media(args.backup) + extract_iphone_media.extract_media(args.backup, identifiers) else: print("WhatsApp directory already exists, skipping WhatsApp file extraction.") if args.db is None: - if args.smb: - msg_db = "724bd3b98b18518b455a87c1f3ac3a0d189c4466" - else: - msg_db = "7c7fba66680ef796b916b067077cc246adacf01d" + msg_db = identifiers.MESSAGE else: msg_db = args.db if args.wa is None: diff --git a/Whatsapp_Chat_Exporter/extract_iphone_media.py b/Whatsapp_Chat_Exporter/extract_iphone_media.py index b293ea7..250d4d6 100644 --- a/Whatsapp_Chat_Exporter/extract_iphone_media.py +++ b/Whatsapp_Chat_Exporter/extract_iphone_media.py @@ -6,6 +6,7 @@ import os import time import getpass import threading +from Whatsapp_Chat_Exporter.utility import WhatsAppIdentifier try: from iphone_backup_decrypt import EncryptedBackup, RelativePath from iphone_backup_decrypt import FailedToDecryptError, Domain @@ -15,7 +16,7 @@ else: support_encrypted = True -def extract_encrypted(base_dir, password): +def extract_encrypted(base_dir, password, identifiers): backup = EncryptedBackup(backup_directory=base_dir, passphrase=password, cleanup=False, check_same_thread=False) print("Decrypting WhatsApp database...") try: @@ -61,7 +62,7 @@ def is_encrypted(base_dir): return False -def extract_media(base_dir): +def extract_media(base_dir, identifiers): if is_encrypted(base_dir): if not support_encrypted: print("You don't have the dependencies to handle encrypted backup.") @@ -70,20 +71,23 @@ def extract_media(base_dir): return False print("Encryption detected on the backup!") password = getpass.getpass("Enter the password for the backup:") - extract_encrypted(base_dir, password) + extract_encrypted(base_dir, password, identifiers) else: - wts_db = os.path.join(base_dir, "7c/7c7fba66680ef796b916b067077cc246adacf01d") - contact_db = os.path.join(base_dir, "b8/b8548dc30aa1030df0ce18ef08b882cf7ab5212f") + wts_db = os.path.join(base_dir, identifiers.MESSAGE[:2], identifiers.MESSAGE) + contact_db = os.path.join(base_dir, identifiers.CONTACT[:2], identifiers.CONTACT) if not os.path.isfile(wts_db): - print("WhatsApp database not found.") + if identifiers is WhatsAppIdentifier: + print("WhatsApp database not found.") + else: + print("WhatsApp Business database not found.") exit() else: - shutil.copyfile(wts_db, "7c7fba66680ef796b916b067077cc246adacf01d") + shutil.copyfile(wts_db, identifiers.MESSAGE) if not os.path.isfile(contact_db): print("Contact database not found.") exit() else: - shutil.copyfile(contact_db, "b8548dc30aa1030df0ce18ef08b882cf7ab5212f") + shutil.copyfile(contact_db, identifiers.CONTACT) _wts_id = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared" with sqlite3.connect(os.path.join(base_dir, "Manifest.db")) as manifest: manifest.row_factory = sqlite3.Row diff --git a/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py b/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py deleted file mode 100644 index 459344a..0000000 --- a/Whatsapp_Chat_Exporter/extract_iphone_media_smb.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/python3 - -import shutil -import sqlite3 -import os -import time -import getpass -import threading -try: - from iphone_backup_decrypt import EncryptedBackup, RelativePath - from iphone_backup_decrypt import FailedToDecryptError, Domain -except ModuleNotFoundError: - support_encrypted = False -else: - support_encrypted = True - - -def extract_encrypted(base_dir, password): - backup = EncryptedBackup(backup_directory=base_dir, passphrase=password, cleanup=False, check_same_thread=False) - print("Decrypting WhatsApp database...") - try: - backup.extract_file(relative_path=RelativePath.WHATSAPP_MESSAGES, - output_filename="724bd3b98b18518b455a87c1f3ac3a0d189c4466") - backup.extract_file(relative_path=RelativePath.WHATSAPP_CONTACTS, - output_filename="d7246a707f51ddf8b17ee2dddabd9e0a4da5c552") - except FailedToDecryptError: - print("Failed to decrypt backup: incorrect password?") - exit() - extract_thread = threading.Thread( - target=backup.extract_files_by_domain, - args=(Domain.WHATSAPP, Domain.WHATSAPP) - ) - extract_thread.daemon = True - extract_thread.start() - dot = 0 - while extract_thread.is_alive(): - print(f"Decrypting and extracting files{'.' * dot}{' ' * (3 - dot)}", end="\r") - if dot < 3: - dot += 1 - time.sleep(0.5) - else: - dot = 0 - time.sleep(0.4) - print(f"All required files decrypted and extracted.", end="\n") - extract_thread.handled = True - return backup - - -def is_encrypted(base_dir): - with sqlite3.connect(os.path.join(base_dir, "Manifest.db")) as f: - c = f.cursor() - try: - c.execute("""SELECT count() - FROM Files - """) - except sqlite3.OperationalError as e: - raise e # These error cannot be used to determine if the backup is encrypted - except sqlite3.DatabaseError: - return True - else: - return False - - -def extract_media(base_dir): - if is_encrypted(base_dir): - if not support_encrypted: - print("You don't have the dependencies to handle encrypted backup.") - print("Read more on how to deal with encrypted backup:") - print("https://github.com/KnugiHK/Whatsapp-Chat-Exporter/blob/main/README.md#usage") - return False - print("Encryption detected on the backup!") - password = getpass.getpass("Enter the password for the backup:") - extract_encrypted(base_dir, password) - else: - wts_db = os.path.join(base_dir, "72/724bd3b98b18518b455a87c1f3ac3a0d189c4466") - contact_db = os.path.join(base_dir, "d7/d7246a707f51ddf8b17ee2dddabd9e0a4da5c552") - if not os.path.isfile(wts_db): - print("WhatsApp database not found.") - exit() - else: - shutil.copyfile(wts_db, "724bd3b98b18518b455a87c1f3ac3a0d189c4466") - if not os.path.isfile(contact_db): - print("Contact database not found.") - exit() - else: - shutil.copyfile(contact_db, "d7246a707f51ddf8b17ee2dddabd9e0a4da5c552") - _wts_id = "AppDomainGroup-group.net.whatsapp.WhatsAppSMB.shared" - with sqlite3.connect(os.path.join(base_dir, "Manifest.db")) as manifest: - manifest.row_factory = sqlite3.Row - c = manifest.cursor() - c.execute( - f"""SELECT count() - FROM Files - WHERE domain = '{_wts_id}'""" - ) - total_row_number = c.fetchone()[0] - print(f"Extracting WhatsApp files...(0/{total_row_number})", end="\r") - c.execute(f"""SELECT fileID, - relativePath, - flags, - ROW_NUMBER() OVER(ORDER BY relativePath) AS _index - FROM Files - WHERE domain = '{_wts_id}' - ORDER BY relativePath""") - if not os.path.isdir(_wts_id): - os.mkdir(_wts_id) - row = c.fetchone() - while row is not None: - if row["relativePath"] == "": - row = c.fetchone() - continue - destination = os.path.join(_wts_id, row["relativePath"]) - hashes = row["fileID"] - folder = hashes[:2] - flags = row["flags"] - if flags == 2: - try: - os.mkdir(destination) - except FileExistsError: - pass - elif flags == 1: - shutil.copyfile(os.path.join(base_dir, folder, hashes), destination) - if row["_index"] % 100 == 0: - print(f"Extracting WhatsApp files...({row['_index']}/{total_row_number})", end="\r") - row = c.fetchone() - print(f"Extracting WhatsApp files...({total_row_number}/{total_row_number})", end="\n") diff --git a/Whatsapp_Chat_Exporter/utility.py b/Whatsapp_Chat_Exporter/utility.py index 62350d0..26f1df1 100644 --- a/Whatsapp_Chat_Exporter/utility.py +++ b/Whatsapp_Chat_Exporter/utility.py @@ -290,3 +290,15 @@ def setup_template(template, no_avatar): # iOS Specific APPLE_TIME = datetime.timestamp(datetime(2001, 1, 1)) + + +class WhatsAppIdentifier(StrEnum): + MESSAGE = "7c7fba66680ef796b916b067077cc246adacf01d" + CONTACT = "b8548dc30aa1030df0ce18ef08b882cf7ab5212f" + DOMAIN = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared" + + +class WhatsAppBusinessIdentifier(StrEnum): + MESSAGE = "724bd3b98b18518b455a87c1f3ac3a0d189c4466" + CONTACT = "d7246a707f51ddf8b17ee2dddabd9e0a4da5c552" + DOMAIN = "AppDomainGroup-group.net.whatsapp.WhatsAppSMB.shared" From b9f123fbea69d65bdacba019e50e4715641146e8 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Thu, 17 Aug 2023 19:59:11 -0400 Subject: [PATCH 09/12] fix: use corresponding identifier --- Whatsapp_Chat_Exporter/extract_iphone_media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Whatsapp_Chat_Exporter/extract_iphone_media.py b/Whatsapp_Chat_Exporter/extract_iphone_media.py index 250d4d6..468e694 100644 --- a/Whatsapp_Chat_Exporter/extract_iphone_media.py +++ b/Whatsapp_Chat_Exporter/extract_iphone_media.py @@ -88,7 +88,7 @@ def extract_media(base_dir, identifiers): exit() else: shutil.copyfile(contact_db, identifiers.CONTACT) - _wts_id = "AppDomainGroup-group.net.whatsapp.WhatsApp.shared" + _wts_id = identifiers.DOMAIN with sqlite3.connect(os.path.join(base_dir, "Manifest.db")) as manifest: manifest.row_factory = sqlite3.Row c = manifest.cursor() From dbd1802dd63e0b0ac1210d1bc6d9fc2f4899cf49 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Fri, 18 Aug 2023 01:51:13 -0400 Subject: [PATCH 10/12] feat: use dynamic domains when encrypted --- Whatsapp_Chat_Exporter/extract_iphone_media.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Whatsapp_Chat_Exporter/extract_iphone_media.py b/Whatsapp_Chat_Exporter/extract_iphone_media.py index 468e694..f2ef72c 100644 --- a/Whatsapp_Chat_Exporter/extract_iphone_media.py +++ b/Whatsapp_Chat_Exporter/extract_iphone_media.py @@ -9,7 +9,7 @@ import threading from Whatsapp_Chat_Exporter.utility import WhatsAppIdentifier try: from iphone_backup_decrypt import EncryptedBackup, RelativePath - from iphone_backup_decrypt import FailedToDecryptError, Domain + from iphone_backup_decrypt import FailedToDecryptError except ModuleNotFoundError: support_encrypted = False else: @@ -21,15 +21,15 @@ def extract_encrypted(base_dir, password, identifiers): print("Decrypting WhatsApp database...") try: backup.extract_file(relative_path=RelativePath.WHATSAPP_MESSAGES, - output_filename="7c7fba66680ef796b916b067077cc246adacf01d") + output_filename=identifiers.MESSAGE) backup.extract_file(relative_path=RelativePath.WHATSAPP_CONTACTS, - output_filename="b8548dc30aa1030df0ce18ef08b882cf7ab5212f") + output_filename=identifiers.CONTACT) except FailedToDecryptError: print("Failed to decrypt backup: incorrect password?") exit() extract_thread = threading.Thread( target=backup.extract_files_by_domain, - args=(Domain.WHATSAPP, Domain.WHATSAPP) + args=(identifiers.DOMAIN, identifiers.DOMAIN) ) extract_thread.daemon = True extract_thread.start() From a08f44e6ed45812466ce65073f4392995b7b6ad8 Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Wed, 23 Aug 2023 16:06:25 +0800 Subject: [PATCH 11/12] Fix the potential collision of Whatsapp and Whatsapp Business --- Whatsapp_Chat_Exporter/extract_iphone_media.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Whatsapp_Chat_Exporter/extract_iphone_media.py b/Whatsapp_Chat_Exporter/extract_iphone_media.py index f2ef72c..1cf4f6b 100644 --- a/Whatsapp_Chat_Exporter/extract_iphone_media.py +++ b/Whatsapp_Chat_Exporter/extract_iphone_media.py @@ -20,10 +20,16 @@ def extract_encrypted(base_dir, password, identifiers): backup = EncryptedBackup(backup_directory=base_dir, passphrase=password, cleanup=False, check_same_thread=False) print("Decrypting WhatsApp database...") try: - backup.extract_file(relative_path=RelativePath.WHATSAPP_MESSAGES, - output_filename=identifiers.MESSAGE) - backup.extract_file(relative_path=RelativePath.WHATSAPP_CONTACTS, - output_filename=identifiers.CONTACT) + backup.extract_file( + relative_path=RelativePath.WHATSAPP_MESSAGES, + domain=identifiers.DOMAIN, + output_filename=identifiers.MESSAGE + ) + backup.extract_file( + relative_path=RelativePath.WHATSAPP_CONTACTS, + domain=identifiers.DOMAIN, + output_filename=identifiers.CONTACT + ) except FailedToDecryptError: print("Failed to decrypt backup: incorrect password?") exit() From decea88028e7952e8343c77ef9abe7b371c9866d Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Wed, 23 Aug 2023 16:07:04 +0800 Subject: [PATCH 12/12] Better UI I am too lazy to switch branch. --- Whatsapp_Chat_Exporter/extract_iphone_media.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Whatsapp_Chat_Exporter/extract_iphone_media.py b/Whatsapp_Chat_Exporter/extract_iphone_media.py index 1cf4f6b..4698723 100644 --- a/Whatsapp_Chat_Exporter/extract_iphone_media.py +++ b/Whatsapp_Chat_Exporter/extract_iphone_media.py @@ -18,7 +18,7 @@ else: def extract_encrypted(base_dir, password, identifiers): backup = EncryptedBackup(backup_directory=base_dir, passphrase=password, cleanup=False, check_same_thread=False) - print("Decrypting WhatsApp database...") + print("Decrypting WhatsApp database...", end="") try: backup.extract_file( relative_path=RelativePath.WHATSAPP_MESSAGES, @@ -33,6 +33,8 @@ def extract_encrypted(base_dir, password, identifiers): except FailedToDecryptError: print("Failed to decrypt backup: incorrect password?") exit() + else: + print("Done") extract_thread = threading.Thread( target=backup.extract_files_by_domain, args=(identifiers.DOMAIN, identifiers.DOMAIN)