Skip to content

Commit

Permalink
Fix Encryption bug + new option + Remove domain folders
Browse files Browse the repository at this point in the history
Fix encryption bug of not decrypting files correctly

Added a new -d option to export an encrypted backup to a decrypted backup without recreating the structure

Remove domain root folders on export for a MUCH cleaner and understandable output directory of recreated file structures
  • Loading branch information
jfarley248 committed Sep 22, 2020
1 parent 59a6680 commit 07c25c7
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 44 deletions.
5 changes: 3 additions & 2 deletions helpers/decryptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def __init__(self, input_dir, output_dir, password, logger):
self.output_dir = output_dir
self.logger = logger
self.password = password
self.decryptor_object = None
self.decrypted_manifest_db = None
self.start_decryption()

Expand All @@ -37,8 +38,8 @@ def start_decryption(self):


backup_path = self.input_dir
decrypt = EncryptedBackup(backup_directory=backup_path, passphrase=self.password, outputdir=self.output_dir, log= self.logger)
self.decrypted_manifest_db = decrypt._decrypted_manifest_db_path
self.decryptor_object = EncryptedBackup(backup_directory=backup_path, passphrase=self.password, outputdir=self.output_dir, log= self.logger)
self.decrypted_manifest_db = self.decryptor_object._decrypted_manifest_db_path



Expand Down
166 changes: 166 additions & 0 deletions helpers/encryptedDbParser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
'''
Copyright (c) 2019 Jack Farley
This file is part of iTunesBackupAnalyzer
Usage or distribution of this software/code is subject to the
terms of the GNU GENERAL PUBLIC LICENSE.
manifestDbParser.py
------------
'''
from __future__ import unicode_literals
from __future__ import print_function

import os
import helpers.deserializer as deserializer
from biplist import *
import logging
import plistlib
import datetime
import io
import os
import re
import errno
import sqlite3
from pathlib_revised import Path2
from shutil import copyfile




def getFileInfo(plist_blob):
'''Read the NSKeyedArchive plist, deserialize it and return file metadata as a dictionary'''
info = {}
try:
f = io.BytesIO(plist_blob)
info = deserializer.process_nsa_plist("", f)
ea = info.get('ExtendedAttributes', None)
if ea:
#INVESTIGATE THIS MORE
if type(ea) is bytes:
pass
else:
ea = ea['NS.data']
info['ExtendedAttributes'] = ea #str(biplist.readPlistFromString(ea))
except Exception as ex:
logging.exception("Failed to parse file metadata from db, exception was: " + str(ex))

return info




def ReadUnixTime(unix_time): # Unix timestamp is time epoch beginning 1970/1/1
'''Returns datetime object, or None upon error'''
if unix_time not in ( 0, None, ''):
try:
if isinstance(unix_time, str):
unix_time = float(unix_time)
return datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=unix_time)
except (ValueError, OverflowError, TypeError) as ex:
logging.error("ReadUnixTime() Failed to convert timestamp from value " + str(unix_time) + " Error was: " + str(ex))
return None

def createFolder(folderPath, logger):

if not os.path.exists(folderPath):

try:
fixedFolderPath = Path2(folderPath)

Path2(fixedFolderPath).makedirs()
except Exception as ex:
logger.exception("Could not make root directory: " + folderPath + "\nError was: " + str(ex))

def OpenDb(inputPath, logger):
try:
conn = sqlite3.connect(inputPath)
logger.debug ("Opened database: " + inputPath + " successfully")
return conn
except Exception as ex:
logger.exception ("Failed to open database: " + inputPath + " Exception was: " + str(ex))
return None


def WriteMetaDataToDb(file_meta_list, outputDir, logger):
outputFileInfoDb = os.path.join(outputDir, "File_Metadata.db")
conn2 = OpenDb(outputFileInfoDb, logger)

createMetaQuery = "CREATE TABLE IF NOT EXISTS Metadata (RelativePath TEXT, LastModified DATE, " \
"LastStatusChange DATE, Birth DATE, " \
"Size INTEGER, InodeNumber INTEGER, Flags INTEGER, UserID INTEGER, GroupID INTEGER, " \
"Mode INTEGER, ProtectionClass INTEGER, ExtendedAttributes BLOB)"

try:
conn2.execute(createMetaQuery)
except sqlite3.Error:
logger.exception("Failed to execute query: " + createMetaQuery)

try:
conn2.executemany('''INSERT INTO Metadata(RelativePath, LastModified, LastStatusChange, Birth,
Size, InodeNumber, Flags, UserID, GroupID,
Mode, ProtectionClass, ExtendedAttributes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)''',
file_meta_list)

except sqlite3.Error:
logger.exception("Error filling Metadata table.")

conn2.commit()
conn2.close()

def readEncManiDb(manifestPath, sourceDir, outputDir, decrypt_object, decrypt_only, logger):

'''Creates Root folder for recreated file structure'''
root = outputDir
createFolder(root, logger)


'''Copy Decrypted manifest db to decrypted backup'''
if decrypt_only:
dest_manifest = os.path.join(outputDir, "BACKUP", "Manifest.db")
copyfile(decrypt_object.decrypted_manifest_db, dest_manifest)

conn = OpenDb(manifestPath, logger)
c = conn.cursor()
query = '''SELECT fileId, domain, relativePath, flags, file FROM files'''
try:
logger.debug("Trying to execute query: " + query + " against database " + manifestPath)
c.execute(query)
logger.debug("Successfully executed query: " + query + " against database " + manifestPath)

except Exception as ex:
logger.exception("Could not execute query: " + query + " against database " + manifestPath
+ " Exception was: " + str(ex))
file_meta_list = []
for fileListing in c:
fileId = fileListing[0]
domain = fileListing[1]
relativePath = fileListing[2]
fType = fileListing[3]
info = getFileInfo(fileListing[4])
ea = info.get('ExtendedAttributes', None)
if ea:
ea = bytes(ea)

file_meta_list.append([ (domain + "/" + relativePath) if relativePath else domain,
ReadUnixTime(info.get('LastModified', None)),
ReadUnixTime(info.get('LastStatusChange', None)), ReadUnixTime(info.get('Birth', None)),
info.get('Size', None), info.get('InodeNumber', None), info.get('Flags', None),
info.get('UserID', None), info.get('UserID', None),
info.get('Mode', None), info.get('ProtectionClass', None), ea
])
if len(file_meta_list) > 50000:
WriteMetaDataToDb(file_meta_list, outputDir, logger)
file_meta_list = []
try:
# Potential area to extract to decrypted backup instead of recreated structure

if fType == 1:
if decrypt_only:
file_path = os.path.join(outputDir, "BACKUP", fileId[0:2], fileId)
else:
file_path = os.path.join(outputDir, "Recreated_Structure", relativePath)
decrypt_object.decryptor_object.extract_file(relative_path=relativePath, output_filename=file_path)
except Exception as ex:
logger.exception("Recreation failed for file {}/{}".format(domain, relativePath))

if len(file_meta_list):
WriteMetaDataToDb(file_meta_list, outputDir, logger)
76 changes: 76 additions & 0 deletions helpers/iphone_backup_decrypt/iphone_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,82 @@ def _open_temp_database(self):
except sqlite3.Error:
return False

def _decrypt_inner_file(self, *, file_id, file_bplist):
# Ensure we've already unlocked the Keybag:
self._read_and_unlock_keybag()
# Extract the decryption key from the PList data:
plist = biplist.readPlistFromString(file_bplist)
file_data = plist['$objects'][plist['$top']['root'].integer]
protection_class = file_data['ProtectionClass']
if "EncryptionKey" not in file_data:
return None # This file is not encrypted; either a directory or empty.
encryption_key = plist['$objects'][file_data['EncryptionKey'].integer]['NS.data'][4:]
inner_key = self._keybag.unwrapKeyForClass(protection_class, encryption_key)
# Find the encrypted version of the file on disk and decrypt it:
filename_in_backup = os.path.join(self._backup_directory, file_id[:2], file_id)
with open(filename_in_backup, 'rb') as encrypted_file_filehandle:
encrypted_data = encrypted_file_filehandle.read()
# Decrypt the file contents:
decrypted_data = google_iphone_dataprotection.AESdecryptCBC(encrypted_data, inner_key)
# Remove any padding introduced by the CBC encryption:
return google_iphone_dataprotection.removePadding(decrypted_data)


def extract_file_as_bytes(self, relative_path):
"""
Decrypt a single named file and return the bytes.
:param relative_path:
The iOS 'relativePath' of the file to be decrypted. Common relative paths are provided by the
'RelativePath' class, otherwise these can be found by opening the decrypted Manifest.db file
and examining the Files table.
:return: decrypted bytes of the file.
"""
# Ensure that we've initialised everything:
if self._temp_manifest_db_conn is None:
self._decrypt_manifest_db_file()
# Use Manifest.db to find the on-disk filename and file metadata, including the keys, for the file.
# The metadata is contained in the 'file' column, as a binary PList file:
try:
cur = self._temp_manifest_db_conn.cursor()
query = """
SELECT fileID, file
FROM Files
WHERE relativePath = ?
ORDER BY domain, relativePath
LIMIT 1;
"""
cur.execute(query, (relative_path,))
result = cur.fetchone()
except sqlite3.Error:
return None
file_id, file_bplist = result
# Decrypt the requested file:
return self._decrypt_inner_file(file_id=file_id, file_bplist=file_bplist)


def extract_file(self, *, relative_path, output_filename):
"""
Decrypt a single named file and save it to disk.
This is a helper method and is exactly equivalent to extract_file_as_bytes(...) and then
writing that data to a file.
:param relative_path:
The iOS 'relativePath' of the file to be decrypted. Common relative paths are provided by the
'RelativePath' class, otherwise these can be found by opening the decrypted Manifest.db file
and examining the Files table.
:param output_filename:
The filename to write the decrypted file contents to.
"""
# Get the decrypted bytes of the requested file:
decrypted_data = self.extract_file_as_bytes(relative_path)
# Output them to disk:
output_directory = os.path.dirname(output_filename)
if output_directory:
os.makedirs(output_directory, exist_ok=True)
if decrypted_data is not None:
with open(output_filename, 'wb') as outfile:
outfile.write(decrypted_data)


def _decrypt_manifest_db_file(self):

# Ensure we've already unlocked the Keybag:
Expand Down
57 changes: 48 additions & 9 deletions helpers/manifestDbParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,43 @@ def OpenDb(inputPath, logger):


'''Ingests all files/folders/plists'''
def recreate(fileId, domain, relativePath, fType, root, sourceDir, logger, a_time, m_time):
def recreate(fileId, relativePath, fType, root, sourceDir, logger, a_time, m_time):

'''Fields with types of 4 have not been found in backups to my knowledge'''
if fType == 4:
logger.info("Found file with type of 4: " + relativePath)
logger.info("Type 4 files aren't found in iTunes Backups... But we'll check anyway")
type4File = os.path.join(sourceDir, fileId[0:2], fileId)
if os.path.isfile(type4File):
logger.info("The file actually exists... Please contact jfarley248@gmail.com to correct this code\n")
else:
logger.info("Nope, file: " + relativePath + " does not exist")

'''Fields with types of 2 are Folders'''
if fType == 2:
logger.debug("Trying to recreate directory: " + "\\" + relativePath + " from source file: " + fileId)
try:
recreateFolder(relativePath, root, logger)
logger.debug("Successfully recreated directory: " + "\\" + relativePath + " from source file: " + fileId)
except Exception as ex:
logger.exception("Failed to recreate directory: " + relativePath + " from source file: "
+ fileId + " Exception was: " + str(ex))

'''Fields with types of 1 are Files'''
if fType == 1:
logger.debug("Trying to recreate file: " + "\\" + relativePath + " from source file: " + fileId)
try:
recreateFile(fileId, relativePath, root, sourceDir, logger, a_time, m_time)
logger.debug(
"Successfully recreated file: " + "\\" + relativePath + " from source file: " + fileId)
except Exception as ex:
logger.exception("Failed to recreate file: " + relativePath + " from source file: "
+ fileId + " Exception was: " + str(ex))



'''Ingests all files/folders/plists'''
def recreateEnc(fileId, domain, relativePath, fType, root, sourceDir, logger, a_time, m_time, decrypt_object):

'''Fields with types of 4 have not been found in backups to my knowledge'''
if fType == 4:
Expand Down Expand Up @@ -88,25 +124,25 @@ def recreate(fileId, domain, relativePath, fType, root, sourceDir, logger, a_tim
+ fileId + " Exception was: " + str(ex))



'''Recreates the folder structures in the output directory based on type = 2'''
def recreateFolder(domain, relativePath, root, logger):
def recreateFolder(relativePath, root, logger):

'''If the relative path is empty, then the domain is the root folder'''
domain = re.sub('[<>:"|?*]', '_', domain)
relativePath = re.sub('[<>:"|?*]', '_', relativePath)
relativePath = relativePath.replace("/", "\\")
if not relativePath:
newFolder = os.path.join(root, domain)
newFolder = root
createFolder(newFolder, logger)

else:
newFolder = os.path.join(root, domain, relativePath)
newFolder = os.path.join(root, relativePath)
createFolder(newFolder, logger)



'''Recreates the file structures in the output directory based on type = 3'''
def recreateFile(fileId, domain, relativePath, root, sourceDir, logger, a_time, m_time):
def recreateFile(fileId, relativePath, root, sourceDir, logger, a_time, m_time):


'''Source file created from taking first two characters of fileID,
Expand All @@ -117,7 +153,7 @@ def recreateFile(fileId, domain, relativePath, root, sourceDir, logger, a_time,
'''Gets rid of folder slashes and replaces with backslashes, offending characters with underscores'''
sanitizedRelPath = relativePath.replace("/", "\\")
sanitizedRelPath = re.sub('[<>:"|?*]', '_', sanitizedRelPath)
destFile = os.path.join(root, domain, sanitizedRelPath)
destFile = os.path.join(root, sanitizedRelPath)


if not os.path.exists(os.path.dirname(destFile)):
Expand Down Expand Up @@ -180,7 +216,8 @@ def readManiDb(manifestPath, sourceDir, outputDir, logger):
WriteMetaDataToDb(file_meta_list, outputDir, logger)
file_meta_list = []
try:
recreate(fileId, domain, relativePath, fType, root, sourceDir, logger, info.get('LastStatusChange', 0), info.get('LastModified', 0))
# Potential area to extract to decrypted backup instead of recreated structure
recreate(fileId, relativePath, fType, root, sourceDir, logger, info.get('LastStatusChange', 0), info.get('LastModified', 0))
except Exception as ex:
logger.exception("Recreation failed for file {}/{}".format(domain, relativePath))

Expand Down Expand Up @@ -232,4 +269,6 @@ def getFileInfo(plist_blob):
logging.exception("Failed to parse file metadata from db, exception was: " + str(ex))

return info




Loading

0 comments on commit 07c25c7

Please sign in to comment.