Support crypt15

This commit is contained in:
KnugiHK
2022-02-22 18:33:54 +08:00
parent 734bb78cd8
commit 0e6319eb4e
3 changed files with 62 additions and 13 deletions

View File

@@ -1,11 +1,13 @@
from .__init__ import __version__ from .__init__ import __version__
from Whatsapp_Chat_Exporter import extract, extract_iphone from Whatsapp_Chat_Exporter import extract, extract_iphone
from Whatsapp_Chat_Exporter import extract_iphone_media from Whatsapp_Chat_Exporter import extract_iphone_media
from Whatsapp_Chat_Exporter.extract import Crypt
from optparse import OptionParser from optparse import OptionParser
import os import os
import sqlite3 import sqlite3
import shutil import shutil
import json import json
import string
from sys import exit from sys import exit
@@ -101,14 +103,22 @@ def main():
print("You must specify the backup file with -b") print("You must specify the backup file with -b")
exit(1) exit(1)
print("Decryption key specified, decrypting WhatsApp backup...") 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() db = open(options.backup, "rb").read()
is_crypt14 = False if "crypt12" in options.backup else True error = extract.decrypt_backup(db, key, msg_db, crypt)
error = extract.decrypt_backup(db, key, msg_db, is_crypt14)
if error != 0: if error != 0:
if error == 1: if error == 1:
print("Dependencies of decrypt_backup are not " print("Dependencies of decrypt_backup and/or extract_encrypted_key"
"present. For details, see README.md.") " are not present. For details, see README.md.")
exit(3) exit(3)
elif error == 2: elif error == 2:
print("Failed when decompressing the decrypted backup." print("Failed when decompressing the decrypted backup."

View File

@@ -12,6 +12,7 @@ from pathlib import Path
from bleach import clean as sanitize from bleach import clean as sanitize
from markupsafe import Markup from markupsafe import Markup
from datetime import datetime from datetime import datetime
from enum import Enum
from mimetypes import MimeTypes from mimetypes import MimeTypes
try: try:
import zlib import zlib
@@ -20,7 +21,12 @@ except ModuleNotFoundError:
support_backup = False support_backup = False
else: else:
support_backup = True support_backup = True
try:
import javaobj
except ModuleNotFoundError:
support_crypt15 = False
else:
support_crypt15 = True
def sanitize_except(html): def sanitize_except(html):
return Markup(sanitize(html, tags=["br"])) return Markup(sanitize(html, tags=["br"]))
@@ -39,18 +45,39 @@ CRYPT14_OFFSETS = [
{"iv": 66, "db": 99} {"iv": 66, "db": 99}
] ]
class Crypt(Enum):
CRYPT15 = 15
CRYPT14 = 14
CRYPT12 = 12
def brute_force_offset(): def brute_force_offset():
for iv in range(60, 80): for iv in range(60, 80):
for db in range(80, 130): for db in range(80, 130):
yield iv, iv + 16, db 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: if not support_backup:
return 1 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") raise ValueError("The key file must be 158 bytes")
t1 = key[30:62] t1 = key[30:62]
if crypt14: if crypt == Crypt.CRYPT14:
if len(database) < 191: if len(database) < 191:
raise ValueError("The crypt14 file must be at least 191 bytes") raise ValueError("The crypt14 file must be at least 191 bytes")
current_try = 0 current_try = 0
@@ -58,16 +85,27 @@ def decrypt_backup(database, key, output, crypt14=True):
t2 = database[15:47] t2 = database[15:47]
iv = database[offsets["iv"]:offsets["iv"] + 16] iv = database[offsets["iv"]:offsets["iv"] + 16]
db_ciphertext = database[offsets["db"]:] db_ciphertext = database[offsets["db"]:]
else: elif crypt == Crypt.CRYPT12:
if len(database) < 67: if len(database) < 67:
raise ValueError("The crypt12 file must be at least 67 bytes") raise ValueError("The crypt12 file must be at least 67 bytes")
t2 = database[3:35] t2 = database[3:35]
iv = database[51:67] iv = database[51:67]
db_ciphertext = database[67:-20] 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: if t1 != t2:
raise ValueError("The signature of key file and backup file mismatch") 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 decompressed = False
while not decompressed: while not decompressed:
cipher = AES.new(main_key, AES.MODE_GCM, iv) cipher = AES.new(main_key, AES.MODE_GCM, iv)
@@ -75,7 +113,7 @@ def decrypt_backup(database, key, output, crypt14=True):
try: try:
db = zlib.decompress(db_compressed) db = zlib.decompress(db_compressed)
except zlib.error: except zlib.error:
if crypt14: if crypt == Crypt.CRYPT14:
current_try += 1 current_try += 1
if current_try < len(CRYPT14_OFFSETS): if current_try < len(CRYPT14_OFFSETS):
offsets = CRYPT14_OFFSETS[current_try] offsets = CRYPT14_OFFSETS[current_try]

View File

@@ -41,7 +41,8 @@ setuptools.setup(
'bleach' 'bleach'
], ],
extras_require={ extras_require={
'android_backup': ["pycryptodome"] 'android_backup': ["pycryptodome"],
'crypt15': ["pycryptodome", "javaobj-py3"]
}, },
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [