mirror of
https://github.com/mvt-project/mvt.git
synced 2024-06-28 07:09:00 +00:00
706c429595
The two most common reasons that `mvt-ios decrypt-backup` can fail are wrong passwords and not pointing to an actual backup. We can distinguish these cases based on the kinds of errors thrown from iOSbackup (at least for the current versions that i'm testing with). When we encounter those particular exceptions, just report a simple summary and don't overwhelm the user with a backtrace. If we encounter an unexpected exception, leave the reporting as-is. Closes: #28, #36
140 lines
5.8 KiB
Python
140 lines
5.8 KiB
Python
# Mobile Verification Toolkit (MVT)
|
|
# Copyright (c) 2021 The MVT Project Authors.
|
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
|
# https://license.mvt.re/1.1/
|
|
|
|
import binascii
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import sqlite3
|
|
|
|
from iOSbackup import iOSbackup
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
class DecryptBackup:
|
|
"""This class provides functions to decrypt an encrypted iTunes backup
|
|
using either a password or a key file.
|
|
"""
|
|
|
|
def __init__(self, backup_path, dest_path=None):
|
|
"""Decrypts an encrypted iOS backup.
|
|
:param backup_path: Path to the encrypted backup folder
|
|
:param dest_path: Path to the folder where to store the decrypted backup
|
|
"""
|
|
self.backup_path = backup_path
|
|
self.dest_path = dest_path
|
|
self._backup = None
|
|
self._decryption_key = None
|
|
|
|
def process_backup(self):
|
|
if not os.path.exists(self.dest_path):
|
|
os.makedirs(self.dest_path)
|
|
|
|
manifest_path = os.path.join(self.dest_path, "Manifest.db")
|
|
# We extract a decrypted Manifest.db.
|
|
self._backup.getManifestDB()
|
|
# We store it to the destination folder.
|
|
shutil.copy(self._backup.manifestDB, manifest_path)
|
|
|
|
for item in self._backup.getBackupFilesList():
|
|
try:
|
|
file_id = item["backupFile"]
|
|
relative_path = item["relativePath"]
|
|
domain = item["domain"]
|
|
|
|
# This may be a partial backup. Skip files from the manifest which do not exist locally.
|
|
source_file_path = os.path.join(self.backup_path, file_id[0:2], file_id)
|
|
if not os.path.exists(source_file_path):
|
|
log.debug("Skipping file %s. File not found in encrypted backup directory.",
|
|
source_file_path)
|
|
continue
|
|
|
|
item_folder = os.path.join(self.dest_path, file_id[0:2])
|
|
if not os.path.exists(item_folder):
|
|
os.makedirs(item_folder)
|
|
|
|
# iOSBackup getFileDecryptedCopy() claims to read a "file" parameter
|
|
# but the code actually is reading the "manifest" key.
|
|
# Add manifest plist to both keys to handle this.
|
|
item["manifest"] = item["file"]
|
|
|
|
self._backup.getFileDecryptedCopy(manifestEntry=item,
|
|
targetName=file_id,
|
|
targetFolder=item_folder)
|
|
log.info("Decrypted file %s [%s] to %s/%s", relative_path, domain, item_folder, file_id)
|
|
except Exception as e:
|
|
log.error("Failed to decrypt file %s: %s", relative_path, e)
|
|
|
|
def decrypt_with_password(self, password):
|
|
"""Decrypts an encrypted iOS backup.
|
|
:param password: Password to use to decrypt the original backup
|
|
"""
|
|
log.info("Decrypting iOS backup at path %s with password", self.backup_path)
|
|
|
|
try:
|
|
self._backup = iOSbackup(udid=os.path.basename(self.backup_path),
|
|
cleartextpassword=password,
|
|
backuproot=os.path.dirname(self.backup_path))
|
|
except Exception as e:
|
|
if isinstance(e, KeyError) and len(e.args) > 0 and e.args[0] == b"KEY":
|
|
log.critical("Failed to decrypt backup. Password is probably wrong.")
|
|
elif isinstance(e, FileNotFoundError) and os.path.basename(e.filename) == "Manifest.plist":
|
|
log.critical(f"Failed to find backup at {self.backup_path}. Did you need to specify the full path?")
|
|
else:
|
|
log.exception(e)
|
|
log.critical("Failed to decrypt backup. Did you provide the correct password? Did you point to the right backup path?")
|
|
|
|
def decrypt_with_key_file(self, key_file):
|
|
"""Decrypts an encrypted iOS backup using a key file.
|
|
:param key_file: File to read the key bytes to decrypt the backup
|
|
"""
|
|
log.info("Decrypting iOS backup at path %s with key file %s",
|
|
self.backup_path, key_file)
|
|
|
|
with open(key_file, "rb") as handle:
|
|
key_bytes = handle.read()
|
|
|
|
# Key should be 64 hex encoded characters (32 raw bytes)
|
|
if len(key_bytes) != 64:
|
|
log.critical("Invalid key from key file. Did you provide the correct key file?")
|
|
return
|
|
|
|
try:
|
|
key_bytes_raw = binascii.unhexlify(key_bytes)
|
|
self._backup = iOSbackup(udid=os.path.basename(self.backup_path),
|
|
derivedkey=key_bytes_raw,
|
|
backuproot=os.path.dirname(self.backup_path))
|
|
except Exception as e:
|
|
log.exception(e)
|
|
log.critical("Failed to decrypt backup. Did you provide the correct key file?")
|
|
|
|
def get_key(self):
|
|
"""Retrieve and prints the encryption key.
|
|
"""
|
|
if not self._backup:
|
|
return
|
|
|
|
self._decryption_key = self._backup.getDecryptionKey()
|
|
log.info("Derived decryption key for backup at path %s is: \"%s\"",
|
|
self.backup_path, self._decryption_key)
|
|
|
|
def write_key(self, key_path):
|
|
"""Save extracted key to file.
|
|
:param key_path: Path to the file where to write the derived decryption key.
|
|
"""
|
|
if not self._decryption_key:
|
|
return
|
|
|
|
try:
|
|
with open(key_path, 'w') as handle:
|
|
handle.write(self._decryption_key)
|
|
except Exception as e:
|
|
log.exception(e)
|
|
log.critical("Failed to write key to file: %s", key_path)
|
|
return
|
|
else:
|
|
log.info("Wrote decryption key to file: %s. This file is equivalent to a plaintext password. Keep it safe!",
|
|
key_path)
|