From 0e6319eb4e3cadda969f4bc901bffd211251e568 Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Tue, 22 Feb 2022 18:33:54 +0800 Subject: [PATCH] Support crypt15 --- Whatsapp_Chat_Exporter/__main__.py | 20 +++++++++--- Whatsapp_Chat_Exporter/extract.py | 52 ++++++++++++++++++++++++++---- setup.py | 3 +- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/Whatsapp_Chat_Exporter/__main__.py b/Whatsapp_Chat_Exporter/__main__.py index 06a4e39..21aca32 100644 --- a/Whatsapp_Chat_Exporter/__main__.py +++ b/Whatsapp_Chat_Exporter/__main__.py @@ -1,11 +1,13 @@ from .__init__ import __version__ from Whatsapp_Chat_Exporter import extract, extract_iphone from Whatsapp_Chat_Exporter import extract_iphone_media +from Whatsapp_Chat_Exporter.extract import Crypt from optparse import OptionParser import os import sqlite3 import shutil import json +import string from sys import exit @@ -101,14 +103,22 @@ def main(): print("You must specify the backup file with -b") exit(1) print("Decryption key specified, decrypting WhatsApp backup...") - key = open(options.key, "rb").read() + if "crypt12" in options.backup: + crypt = Crypt.CRYPT12 + elif "crypt14" in options.backup: + crypt = Crypt.CRYPT14 + elif "crypt15" in options.backup: + crypt = Crypt.CRYPT15 + if os.path.isfile(options.key): + key = open(options.key, "rb").read() + elif all(char in string.hexdigits for char in options.key): + key = bytes.fromhex(options.key) db = open(options.backup, "rb").read() - is_crypt14 = False if "crypt12" in options.backup else True - error = extract.decrypt_backup(db, key, msg_db, is_crypt14) + error = extract.decrypt_backup(db, key, msg_db, crypt) if error != 0: if error == 1: - print("Dependencies of decrypt_backup are not " - "present. For details, see README.md.") + print("Dependencies of decrypt_backup and/or extract_encrypted_key" + " are not present. For details, see README.md.") exit(3) elif error == 2: print("Failed when decompressing the decrypted backup." diff --git a/Whatsapp_Chat_Exporter/extract.py b/Whatsapp_Chat_Exporter/extract.py index 7eeebaa..9c4f355 100644 --- a/Whatsapp_Chat_Exporter/extract.py +++ b/Whatsapp_Chat_Exporter/extract.py @@ -12,6 +12,7 @@ from pathlib import Path from bleach import clean as sanitize from markupsafe import Markup from datetime import datetime +from enum import Enum from mimetypes import MimeTypes try: import zlib @@ -20,7 +21,12 @@ except ModuleNotFoundError: support_backup = False else: support_backup = True - +try: + import javaobj +except ModuleNotFoundError: + support_crypt15 = False +else: + support_crypt15 = True def sanitize_except(html): return Markup(sanitize(html, tags=["br"])) @@ -39,18 +45,39 @@ CRYPT14_OFFSETS = [ {"iv": 66, "db": 99} ] + +class Crypt(Enum): + CRYPT15 = 15 + CRYPT14 = 14 + CRYPT12 = 12 + + def brute_force_offset(): for iv in range(60, 80): for db in range(80, 130): yield iv, iv + 16, db -def decrypt_backup(database, key, output, crypt14=True): + +def extract_encrypted_key(keyfile): + from hashlib import sha256 + import hmac + key_stream = b"" + for byte in javaobj.loads(keyfile): + key_stream += byte.to_bytes(1, "big", signed=True) + key = hmac.new( + hmac.new(b'\x00' * 32, key_stream, sha256).digest(), + b"backup encryption\x01", + sha256 + ) + return key.digest() + +def decrypt_backup(database, key, output, crypt=Crypt.CRYPT14): if not support_backup: return 1 - if len(key) != 158: + if crypt is not Crypt.CRYPT15 and len(key) != 158: raise ValueError("The key file must be 158 bytes") t1 = key[30:62] - if crypt14: + if crypt == Crypt.CRYPT14: if len(database) < 191: raise ValueError("The crypt14 file must be at least 191 bytes") current_try = 0 @@ -58,16 +85,27 @@ def decrypt_backup(database, key, output, crypt14=True): t2 = database[15:47] iv = database[offsets["iv"]:offsets["iv"] + 16] db_ciphertext = database[offsets["db"]:] - else: + elif crypt == Crypt.CRYPT12: if len(database) < 67: raise ValueError("The crypt12 file must be at least 67 bytes") t2 = database[3:35] iv = database[51:67] db_ciphertext = database[67:-20] + elif crypt == Crypt.CRYPT15: + if not support_crypt15: + return 1 + if len(database) < 131: + raise ValueError("The crypt15 file must be at least 131 bytes") + t1 = t2 = None + iv = database[8:24] + db_ciphertext = database[131:] if t1 != t2: raise ValueError("The signature of key file and backup file mismatch") - main_key = key[126:] + if crypt == Crypt.CRYPT15: + main_key = extract_encrypted_key(key) + else: + main_key = key[126:] decompressed = False while not decompressed: cipher = AES.new(main_key, AES.MODE_GCM, iv) @@ -75,7 +113,7 @@ def decrypt_backup(database, key, output, crypt14=True): try: db = zlib.decompress(db_compressed) except zlib.error: - if crypt14: + if crypt == Crypt.CRYPT14: current_try += 1 if current_try < len(CRYPT14_OFFSETS): offsets = CRYPT14_OFFSETS[current_try] diff --git a/setup.py b/setup.py index 6633374..57b87ce 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,8 @@ setuptools.setup( 'bleach' ], extras_require={ - 'android_backup': ["pycryptodome"] + 'android_backup': ["pycryptodome"], + 'crypt15': ["pycryptodome", "javaobj-py3"] }, entry_points={ "console_scripts": [