mirror of
https://github.com/KnugiHK/WhatsApp-Chat-Exporter.git
synced 2026-04-25 15:31:38 +00:00
Support crypt15
This commit is contained in:
@@ -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."
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -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": [
|
||||||
|
|||||||
Reference in New Issue
Block a user