Add support for decrypting contact db from crypt15 backup

This commit is contained in:
KnugiHK
2023-12-03 13:40:21 +08:00
parent 3847836ed6
commit 0d626519ec
3 changed files with 38 additions and 14 deletions

View File

@@ -9,7 +9,7 @@ import glob
from Whatsapp_Chat_Exporter import extract_exported, extract_iphone 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
from Whatsapp_Chat_Exporter.data_model import ChatStore from Whatsapp_Chat_Exporter.data_model import ChatStore
from Whatsapp_Chat_Exporter.utility import Crypt, check_update, import_from_json from Whatsapp_Chat_Exporter.utility import Crypt, DbType, check_update, import_from_json
from argparse import ArgumentParser, SUPPRESS from argparse import ArgumentParser, SUPPRESS
from sys import exit from sys import exit
try: try:
@@ -191,6 +191,13 @@ def main():
action='store_true', action='store_true',
help="Preserve the modification timestamp of the extracted files (iOS only)" help="Preserve the modification timestamp of the extracted files (iOS only)"
) )
parser.add_argument(
"--wab",
"--wa-backup",
dest="wab",
default=None,
help="Path to contact database in crypt15 format"
)
args = parser.parse_args() args = parser.parse_args()
# Check for updates # Check for updates
@@ -229,6 +236,10 @@ def main():
msg_db = "msgstore.db" msg_db = "msgstore.db"
else: else:
msg_db = args.db msg_db = args.db
if args.wa is None:
contact_db = "wa.db"
else:
contact_db = args.wa
if args.key is not None: if args.key is not None:
if args.backup is None: if args.backup is None:
print("You must specify the backup file with -b") print("You must specify the backup file with -b")
@@ -245,7 +256,19 @@ def main():
elif all(char in string.hexdigits for char in args.key): elif all(char in string.hexdigits for char in args.key):
key = bytes.fromhex(args.key) key = bytes.fromhex(args.key)
db = open(args.backup, "rb").read() db = open(args.backup, "rb").read()
error = extract.decrypt_backup(db, key, msg_db, crypt, args.showkey) if args.wab:
wab = open(args.wab, "rb").read()
error_wa = extract.decrypt_backup(wab, key, contact_db, crypt, args.showkey, DbType.CONTACT)
key.seek(0)
else:
error_wa = 0
error_message = extract.decrypt_backup(db, key, msg_db, crypt, args.showkey, DbType.MESSAGE)
if error_wa != 0:
error = error_wa
elif error_message != 0:
error = error_message
else:
error = 0
if error != 0: if error != 0:
if error == 1: if error == 1:
print("Dependencies of decrypt_backup and/or extract_encrypted_key" print("Dependencies of decrypt_backup and/or extract_encrypted_key"
@@ -258,10 +281,6 @@ def main():
else: else:
print("Unknown error occurred.", error) print("Unknown error occurred.", error)
exit(5) exit(5)
if args.wa is None:
contact_db = "wa.db"
else:
contact_db = args.wa
if args.media is None: if args.media is None:
args.media = "WhatsApp" args.media = "WhatsApp"

View File

@@ -1,11 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
import sqlite3 import sqlite3
import json
import jinja2
import os import os
import shutil
import re
import io import io
import hmac import hmac
from pathlib import Path from pathlib import Path
@@ -13,7 +9,7 @@ from mimetypes import MimeTypes
from hashlib import sha256 from hashlib import sha256
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from Whatsapp_Chat_Exporter.data_model import ChatStore, Message from Whatsapp_Chat_Exporter.data_model import ChatStore, Message
from Whatsapp_Chat_Exporter.utility import MAX_SIZE, ROW_SIZE, determine_metadata, get_status_location from Whatsapp_Chat_Exporter.utility import MAX_SIZE, ROW_SIZE, DbType, determine_metadata, get_status_location
from Whatsapp_Chat_Exporter.utility import rendering, Crypt, Device, get_file_name, setup_template from Whatsapp_Chat_Exporter.utility import rendering, Crypt, Device, get_file_name, setup_template
from Whatsapp_Chat_Exporter.utility import brute_force_offset, CRYPT14_OFFSETS, JidType from Whatsapp_Chat_Exporter.utility import brute_force_offset, CRYPT14_OFFSETS, JidType
@@ -53,7 +49,7 @@ def _extract_encrypted_key(keyfile):
return _generate_hmac_of_hmac(key_stream) return _generate_hmac_of_hmac(key_stream)
def decrypt_backup(database, key, output, crypt=Crypt.CRYPT14, show_crypt15=False): def decrypt_backup(database, key, output, crypt=Crypt.CRYPT14, show_crypt15=False, db_type=DbType.MESSAGE):
if not support_backup: if not support_backup:
return 1 return 1
if isinstance(key, io.IOBase): if isinstance(key, io.IOBase):
@@ -83,8 +79,12 @@ def decrypt_backup(database, key, output, crypt=Crypt.CRYPT14, show_crypt15=Fals
if len(database) < 131: if len(database) < 131:
raise ValueError("The crypt15 file must be at least 131 bytes") raise ValueError("The crypt15 file must be at least 131 bytes")
t1 = t2 = None t1 = t2 = None
iv = database[8:24] if db_type == DbType.MESSAGE:
db_offset = database[0] + 2 # Skip protobuf + protobuf size and backup type iv = database[8:24]
db_offset = database[0] + 2 # Skip protobuf + protobuf size and backup type
elif db_type == DbType.CONTACT:
iv = database[7:23]
db_offset = database[0] + 1 # Skip protobuf + protobuf size
db_ciphertext = database[db_offset:] db_ciphertext = database[db_offset:]
if t1 != t2: if t1 != t2:

View File

@@ -172,6 +172,11 @@ class Crypt(IntEnum):
CRYPT12 = 12 CRYPT12 = 12
class DbType(StrEnum):
MESSAGE = "message"
CONTACT = "contact"
def brute_force_offset(max_iv=200, max_db=200): def brute_force_offset(max_iv=200, max_db=200):
for iv in range(0, max_iv): for iv in range(0, max_iv):
for db in range(0, max_db): for db in range(0, max_db):