From ece88744ed6ab37297e7ce7e5e3645e4f8faf18d Mon Sep 17 00:00:00 2001 From: Pavel Kirkovsky Date: Thu, 22 Jul 2021 23:52:39 -0700 Subject: [PATCH 1/5] KeyUtils class for working with decryption keys --- mvt/ios/keyutils.py | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 mvt/ios/keyutils.py diff --git a/mvt/ios/keyutils.py b/mvt/ios/keyutils.py new file mode 100644 index 0000000..361df45 --- /dev/null +++ b/mvt/ios/keyutils.py @@ -0,0 +1,52 @@ +import os +import logging +from iOSbackup import iOSbackup + +log = logging.getLogger(__name__) + +class KeyUtils: + """This class provides functions to extract a backup key from a password. + """ + + def __init__(self, password, backup_path): + """Generates a key file for an iOS backup. + :param password: Backup encryption password + :param key_file: Path to the file where to store the generated key file + """ + self.password = password + self.backup_path = backup_path + self._backup = None + + def get_key(self): + try: + self._backup = iOSbackup(udid=os.path.basename(self.backup_path), + cleartextpassword=self.password, + 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 password?") + return + else: + self.decryption_key = self._backup.getDecryptionKey() + log.info("Extracted decryption key.") + + def print_key(self): + self.get_key() + log.info("Decryption key for backup at path %s is:\n %s", + self.backup_path, self.decryption_key) + + def write_key(self, key_file): + self.get_key() + + try: + with open(key_file, 'w') as writer: + writer.write(self.decryption_key) + except Exception as e: + log.exception(e) + log.critical("Failed to write key file.") + return + else: + log.info("Wrote decryption key for backup at path %s to file %s", + self.backup_path, key_file) + log.warn("The file %s is equivalent to a plaintext password. Keep this file safe!", + key_file) From 9fbcce4340755d0e7dfbde98eeb9803edfa1062d Mon Sep 17 00:00:00 2001 From: Pavel Kirkovsky Date: Thu, 22 Jul 2021 23:52:52 -0700 Subject: [PATCH 2/5] Add `extract-key` command --- mvt/ios/cli.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/mvt/ios/cli.py b/mvt/ios/cli.py index 47a45cb..bd939c6 100644 --- a/mvt/ios/cli.py +++ b/mvt/ios/cli.py @@ -15,6 +15,7 @@ from mvt.common.options import MutuallyExclusiveOption from mvt.common.indicators import Indicators from .decrypt import DecryptBackup +from .keyutils import KeyUtils from .modules.fs import BACKUP_MODULES, FS_MODULES # Setup logging using Rich. @@ -60,6 +61,26 @@ def decrypt_backup(destination, password, key_file, backup_path): raise click.ClickException("Missing required option. Specify either " "--password or --key-file.") +#============================================================================== +# Command: extract-key +#============================================================================== +@cli.command("extract-key", help="Extract decryption key from an iTunes backup") +@click.option("--password", "-p", + help="Password to use to decrypt the backup", + prompt="Backup password", + hide_input=True, prompt_required=False, required=True) +@click.option("--key-file", "-k", + help="Key file to be written (if unset, will print to STDOUT)", + required=False, + type=click.Path(exists=False, file_okay=True, dir_okay=False, writable=True)) +@click.argument("BACKUP_PATH", type=click.Path(exists=True)) +def extract_key(password, backup_path, key_file): + key_utils = KeyUtils(password, backup_path) + if key_file: + key_utils.write_key(key_file) + else: + key_utils.print_key() + #============================================================================== # Command: check-backup From af4826070a6e7bb3df6e62f53d61a81da7150224 Mon Sep 17 00:00:00 2001 From: Pavel Kirkovsky Date: Thu, 22 Jul 2021 23:55:08 -0700 Subject: [PATCH 3/5] Update README with `extract-key` command --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2a802a0..cfa7221 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ MVT provides two commands `mvt-ios` and `mvt-android` with the following subcomm * `check-fs`: Extract artifacts from a full filesystem dump * `check-iocs`: Compare stored JSON results to provided indicators * `decrypt-backup`: Decrypt an encrypted iTunes backup + * `extract-key`: Extract decryption key from an iTunes backup * `mvt-android`: * `check-backup`: Check an Android Backup * `download-apks`: Download all or non-safelisted installed APKs From 30d03482560f1d3fb121dac45fea828ccf814ab1 Mon Sep 17 00:00:00 2001 From: Pavel Kirkovsky Date: Fri, 23 Jul 2021 03:46:48 -0700 Subject: [PATCH 4/5] Added `extract-key` info to main docs --- docs/ios/backup/check.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/ios/backup/check.md b/docs/ios/backup/check.md index 3a1fd70..551205c 100644 --- a/docs/ios/backup/check.md +++ b/docs/ios/backup/check.md @@ -2,6 +2,32 @@ The backup might take some time. It is best to make sure the phone remains unlocked during the backup process. Afterwards, a new folder will be created under the path you specified using the UDID of the iPhone you backed up. +## Extracting and saving the decryption key (optional) + +If you do not wish to enter a password every time when decrypting a backup, MVT can accept a key file instead. This key can be used with the `decrypt-backup` command. + +To generate a key file, you will need your device backup and the backup password: + + $ mvt-ios extract-key --help + Usage: mvt-ios extract-key [OPTIONS] BACKUP_PATH + + Extract decryption key from an iTunes backup + + Options: + -p, --password TEXT Password to use to decrypt the backup [required] + -k, --key-file FILE Key file to be written (if unset, will print to STDOUT) + --help Show this message and exit. + +You can specify the password on the command line, or omit the `-p` option to have MVT prompt for a password. The `-k` option specifies where to save the file containing the decryption key. If `-k` is omitted, MVT will display the decryption key without saving. + +_Note_: This decryption key is sensitive data! Keep the file safe. + +To extract the key and have MVT prompt for a password: + +```bash +mvt-ios extract-key -k /path/to/save/key /path/to/backup +``` + ## Decrypting a backup In case you have an encrypted backup, you will need to decrypt it first. This can be done with `mvt-ios` as well: @@ -25,7 +51,7 @@ In case you have an encrypted backup, you will need to decrypt it first. This ca --help Show this message and exit. -You can specify either a password via command-line or pass a key file, and you need to specify a destination path where the decrypted backup will be stored. Following is an example usage of `decrypt-backup`: +You can specify either a password via command-line or pass a key file, and you need to specify a destination path where the decrypted backup will be stored. If `-p` is omitted, MVT will ask for a password. Following is an example usage of `decrypt-backup`: ```bash mvt-ios decrypt-backup -p password -d /path/to/decrypted /path/to/backup From b264ae946d39a4b6584da50b8f3bd2f5086c2503 Mon Sep 17 00:00:00 2001 From: Nex Date: Fri, 30 Jul 2021 18:46:45 +0200 Subject: [PATCH 5/5] Refactored to include functionality in existing DecryptBackup class --- mvt/ios/cli.py | 16 ++++++++------ mvt/ios/decrypt.py | 44 +++++++++++++++++++++++++++----------- mvt/ios/keyutils.py | 52 --------------------------------------------- 3 files changed, 42 insertions(+), 70 deletions(-) delete mode 100644 mvt/ios/keyutils.py diff --git a/mvt/ios/cli.py b/mvt/ios/cli.py index 49d3552..78b1793 100644 --- a/mvt/ios/cli.py +++ b/mvt/ios/cli.py @@ -17,7 +17,6 @@ from mvt.common.module import run_module, save_timeline from mvt.common.options import MutuallyExclusiveOption from .decrypt import DecryptBackup -from .keyutils import KeyUtils from .modules.fs import BACKUP_MODULES, FS_MODULES # Setup logging using Rich. @@ -55,6 +54,7 @@ def cli(): @click.argument("BACKUP_PATH", type=click.Path(exists=True)) def decrypt_backup(destination, password, key_file, backup_path): backup = DecryptBackup(backup_path, destination) + if password: backup.decrypt_with_password(password) elif key_file: @@ -63,13 +63,16 @@ def decrypt_backup(destination, password, key_file, backup_path): raise click.ClickException("Missing required option. Specify either " "--password or --key-file.") + backup.process_backup() + + #============================================================================== # Command: extract-key #============================================================================== @cli.command("extract-key", help="Extract decryption key from an iTunes backup") @click.option("--password", "-p", help="Password to use to decrypt the backup", - prompt="Backup password", + prompt="Enter backup password", hide_input=True, prompt_required=False, required=True) @click.option("--key-file", "-k", help="Key file to be written (if unset, will print to STDOUT)", @@ -77,11 +80,12 @@ def decrypt_backup(destination, password, key_file, backup_path): type=click.Path(exists=False, file_okay=True, dir_okay=False, writable=True)) @click.argument("BACKUP_PATH", type=click.Path(exists=True)) def extract_key(password, backup_path, key_file): - key_utils = KeyUtils(password, backup_path) + backup = DecryptBackup(backup_path) + backup.decrypt_with_password(password) + backup.get_key() + if key_file: - key_utils.write_key(key_file) - else: - key_utils.print_key() + backup.write_key(key_file) #============================================================================== diff --git a/mvt/ios/decrypt.py b/mvt/ios/decrypt.py index 385a3a9..db93bc8 100644 --- a/mvt/ios/decrypt.py +++ b/mvt/ios/decrypt.py @@ -18,7 +18,7 @@ class DecryptBackup: using either a password or a key file. """ - def __init__(self, backup_path, dest_path): + 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 @@ -26,8 +26,12 @@ class DecryptBackup: 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) - def _process_backup(self): manifest_path = os.path.join(self.dest_path, "Manifest.db") # We extract a decrypted Manifest.db. self._backup.getManifestDB() @@ -69,9 +73,6 @@ class DecryptBackup: """ log.info("Decrypting iOS backup at path %s with password", self.backup_path) - if not os.path.exists(self.dest_path): - os.makedirs(self.dest_path) - try: self._backup = iOSbackup(udid=os.path.basename(self.backup_path), cleartextpassword=password, @@ -79,9 +80,6 @@ class DecryptBackup: except Exception as e: log.exception(e) log.critical("Failed to decrypt backup. Did you provide the correct password?") - return - else: - self._process_backup() def decrypt_with_key_file(self, key_file): """Decrypts an encrypted iOS backup using a key file. @@ -90,9 +88,6 @@ class DecryptBackup: log.info("Decrypting iOS backup at path %s with key file %s", self.backup_path, key_file) - if not os.path.exists(self.dest_path): - os.makedirs(self.dest_path) - with open(key_file, "rb") as handle: key_bytes = handle.read() @@ -109,6 +104,31 @@ class DecryptBackup: 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: - self._process_backup() + log.info("Wrote decryption key to file: %s. This file is equivalent to a plaintext password. Keep it safe!", + key_path) diff --git a/mvt/ios/keyutils.py b/mvt/ios/keyutils.py deleted file mode 100644 index 361df45..0000000 --- a/mvt/ios/keyutils.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import logging -from iOSbackup import iOSbackup - -log = logging.getLogger(__name__) - -class KeyUtils: - """This class provides functions to extract a backup key from a password. - """ - - def __init__(self, password, backup_path): - """Generates a key file for an iOS backup. - :param password: Backup encryption password - :param key_file: Path to the file where to store the generated key file - """ - self.password = password - self.backup_path = backup_path - self._backup = None - - def get_key(self): - try: - self._backup = iOSbackup(udid=os.path.basename(self.backup_path), - cleartextpassword=self.password, - 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 password?") - return - else: - self.decryption_key = self._backup.getDecryptionKey() - log.info("Extracted decryption key.") - - def print_key(self): - self.get_key() - log.info("Decryption key for backup at path %s is:\n %s", - self.backup_path, self.decryption_key) - - def write_key(self, key_file): - self.get_key() - - try: - with open(key_file, 'w') as writer: - writer.write(self.decryption_key) - except Exception as e: - log.exception(e) - log.critical("Failed to write key file.") - return - else: - log.info("Wrote decryption key for backup at path %s to file %s", - self.backup_path, key_file) - log.warn("The file %s is equivalent to a plaintext password. Keep this file safe!", - key_file)