def getBackupKeyBag(backupfolder, passphrase): manifest = readPlist(backupfolder + "/Manifest.plist") kb = Keybag(manifest["BackupKeyBag"].data) if kb.unlockBackupKeybagWithPasscode(passphrase): print "BackupKeyBag unlock OK" return kb else: return None
def getBackupKeyBag(backupfolder, passphrase): manifest = BPlistReader.plistWithFile(backupfolder + "/Manifest.plist") kb = Keybag(manifest["BackupKeyBag"].data) if kb.unlockBackupKeybagWithPasscode(passphrase): print "BackupKeyBag unlock OK" return kb else: return None
class MobileBackupClient(object): def __init__(self, account_settings, dsPrsID, auth, output_folder): mobilebackup_url = account_settings["com.apple.mobileme"][ "com.apple.Dataclass.Backup"]["url"] content_url = account_settings["com.apple.mobileme"][ "com.apple.Dataclass.Content"]["url"] self.mobilebackup_host = host_from_url(mobilebackup_url) self.content_host = host_from_url(content_url) self.dsPrsID = dsPrsID self.headers = { 'Authorization': auth, 'X-MMe-Client-Info': CLIENT_INFO, 'User-Agent': USER_AGENT_MOBILE_BACKUP, 'X-Apple-MBS-Protocol-Version': "1.7" } self.headers2 = { 'x-apple-mmcs-proto-version': "3.3", 'x-apple-mmcs-dataclass': "com.apple.Dataclass.Backup", 'x-apple-mme-dsid': str(self.dsPrsID), 'User-Agent': USER_AGENT_BACKUPD, 'Accept': "application/vnd.com.apple.me.ubchunk+protobuf", 'Content-Type': "application/vnd.com.apple.me.ubchunk+protobuf", 'x-mme-client-info': CLIENT_INFO_BACKUP } self.files = {} self.output_folder = output_folder self.chosen_snapshot_id = None self.combined = False self.itunes_style = False self.downloaded_files = [] self.domain_filter = None self.threads = DEFAULT_THREADS def mobile_backup_request(self, method, url, msg=None, body=""): return probobuf_request(self.mobilebackup_host, method, url, body, self.headers, msg) def get_account(self): return self.mobile_backup_request("GET", MBS[self.dsPrsID](), MBSAccount) def get_backup(self, backupUDID): return self.mobile_backup_request( "GET", MBS[self.dsPrsID][backupUDID.encode("hex")](), MBSBackup) def get_keys(self, backupUDID): return self.mobile_backup_request( "GET", MBS[self.dsPrsID][backupUDID.encode("hex")].getKeys(), MBSKeySet) def list_files(self, backupUDID, snapshotId): limit = 5000 files = "" offset = 0 new_files = self.mobile_backup_request( "GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId] ['listFiles'](offset=offset, limit=limit)) while new_files: files = files + new_files offset += limit new_files = self.mobile_backup_request( "GET", MBS[self.dsPrsID][backupUDID.encode("hex")] [snapshotId].listFiles(offset=offset, limit=limit)) print "\tShifting offset: ", offset return decode_protobuf_array(files, MBSFile) def get_files(self, backupUDID, snapshotId, files): r = [] h = {} for file in files: if file.Size == 0: continue ff = MBSFile() ff.FileID = file.FileID h[file.FileID] = file.Signature r.append(ff) self.files[file.Signature] = file body = encode_protobuf_array(r) z = self.mobile_backup_request( "POST", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].getFiles(), None, body) tokens = decode_protobuf_array(z, MBSFileAuthToken) z = MBSFileAuthTokens() for token in tokens: toto = z.tokens.add() toto.FileID = h[token.FileID] toto.AuthToken = token.AuthToken return z def authorize_get(self, tokens, snapshot): if len(tokens.tokens) == 0: return self.headers2["x-apple-mmcs-auth"] = "%s %s" % ( tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken) body = tokens.SerializeToString() file_groups = probobuf_request(self.content_host, "POST", URL[self.dsPrsID].authorizeGet(), body, self.headers2, FileGroups) file_chunks = {} pool = Pool(self.threads) containers = [] for group in file_groups.file_groups: for container_index, container in enumerate( group.storage_host_chunk_list): containers.append([group, container_index, container]) for res in pool.imap_unordered(self.download_chunks, containers): args, data = res group, container_index, container = args for file_ref in group.file_checksum_chunk_references: if file_ref.file_checksum not in self.files: continue decrypted_chunks = file_chunks.setdefault( file_ref.file_checksum, {}) for i, reference in enumerate(file_ref.chunk_references): if reference.container_index == container_index: decrypted_chunks[i] = data[reference.chunk_index] if len(decrypted_chunks) == len(file_ref.chunk_references): file = self.files[file_ref.file_checksum] try: self.write_file(file, decrypted_chunks, snapshot) except: raise else: # With iTunes style we need to keep the file if self.itunes_style: self.downloaded_files.append(file) del self.files[file_ref.file_checksum] pool.join() return file_groups def get_complete(self, mmcs_auth): self.headers2["x-apple-mmcs-auth"] = mmcs_auth body = "" probobuf_request(self.content_host, "POST", URL[self.dsPrsID].getComplete(), body, self.headers2) def download_chunks(self, args): group, container_index, container = args headers = {} # XXX for header in container.host_info.headers: headers[header.name] = header.value d = probobuf_request(container.host_info.hostname, container.host_info.method, container.host_info.uri, "", headers) decrypted = [] i = 0 for chunk in container.chunk_info: dchunk = decrypt_chunk(d[i:i + chunk.chunk_length], chunk.chunk_encryption_key, chunk.chunk_checksum) if dchunk: decrypted.append(dchunk) i += chunk.chunk_length return args, decrypted def write_file(self, file, decrypted_chunks, snapshot): # If the filename should be left in the iTunes backup style if self.itunes_style: if self.combined: directory = self.output_folder else: directory = os.path.join(self.output_folder, "snapshot_" + str(snapshot)) path_hash = hashlib.sha1( file.Domain.encode('utf-8') + "-" + file.RelativePath.encode('utf-8')).hexdigest() path = os.path.join(directory, path_hash) else: if self.combined: directory = os.path.join( self.output_folder, re.sub(r'[:|*<>?"]', "_", file.Domain)) path = os.path.join(directory, file.RelativePath) else: directory = os.path.join( self.output_folder, re.sub(r'[:|*<>?"]', "_", "snapshot_" + str(snapshot) + "/" + file.Domain)) path = os.path.join(directory, file.RelativePath) mkdir_p(os.path.dirname(path)) print '\t', file.Domain, '\t', path with open(path, "wb") as ff: hash = hashlib.sha1() for key, chunk in decrypted_chunks.iteritems(): hash.update(chunk) ff.write(chunk) # If file is encrypted if file.Attributes.EncryptionKey: key = file.Attributes.EncryptionKey ProtectionClass = struct.unpack(">L", key[0x18:0x1C])[0] if ProtectionClass == file.Attributes.ProtectionClass: wrapped_key = None filekey = None if file.Attributes.EncryptionKeyVersion and file.Attributes.EncryptionKeyVersion == 2: if self.kb.uuid == key[:0x10]: keyLength = struct.unpack(">L", key[0x20:0x24])[0] if keyLength == 0x48: wrapped_key = key[0x24:] else: wrapped_key = key[0x1C:] if wrapped_key: filekey = self.kb.unwrapCurve25519(ProtectionClass, wrapped_key) if not filekey: print "Failed to unwrap file key for file %s !!!" % file.RelativePath else: print "\tfilekey", filekey.encode("hex") self.decrypt_protected_file(path, filekey, file.Attributes.DecryptedSize) else: print "\tUnable to decrypt file, possible old backup format", file.RelativePath def decrypt_protected_file(self, path, filekey, decrypted_size=0): ivkey = hashlib.sha1(filekey).digest()[:16] hash = hashlib.sha1() sz = os.path.getsize(path) oldpath = path + ".encrypted" try: os.rename(path, oldpath) except: pass with open(oldpath, "rb") as old_file: with open(path, "wb") as new_file: n = sz / 0x1000 if decrypted_size: n += 1 for block in xrange(n): iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey) old_data = old_file.read(0x1000) hash.update(old_data) new_file.write(AESdecryptCBC(old_data, filekey, iv)) if decrypted_size == 0: #old iOS 5 format trailer = old_file.read(0x1C) decrypted_size = struct.unpack(">Q", trailer[:8])[0] assert hash.digest() == trailer[8:] new_file.truncate(decrypted_size) os.remove(oldpath) # Delete the encrypted file def computeIV(self, lba): iv = "" lba &= 0xffffffff for _ in xrange(4): if (lba & 1): lba = 0x80000061 ^ (lba >> 1) else: lba = lba >> 1 iv += struct.pack("<L", lba) return iv def download(self, backupUDID, item_types): mbsbackup = self.get_backup(backupUDID) self.output_folder = os.path.join(self.output_folder, backupUDID.encode("hex")) print "Downloading backup {} to {}".format(backupUDID.encode("hex"), self.output_folder) try: mkdir_p(self.output_folder) except OSError: print "Directory \"{}\" already exists.".format(self.output_folder) return keys = self.get_keys(backupUDID) if not keys or not len(keys.Key): print "get_keys FAILED!" return print "Got OTA Keybag" self.kb = Keybag(keys.Key[-1].KeyData) if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData): print "Unable to unlock OTA keybag !" return print "Available Snapshots: %d" % (mbsbackup.Snapshot.SnapshotID) if self.chosen_snapshot_id == None: snapshot_list = [ 1, mbsbackup.Snapshot.SnapshotID - 1, mbsbackup.Snapshot.SnapshotID ] elif self.chosen_snapshot_id < 0: snapshot_list = [ mbsbackup.Snapshot.SnapshotID + self.chosen_snapshot_id + 1 ] # Remember chosen_snapshot_id is negative else: snapshot_list = [self.chosen_snapshot_id] for snapshot in snapshot_list: print "Listing snapshot %d..." % (snapshot) files = self.list_files(backupUDID, snapshot) print "Files in snapshot %d" % (len(files)) def matches_allowed_domain(a_file): return self.domain_filter in a_file.Domain def matches_allowed_item_types(a_file): return any(ITEM_TYPES_TO_FILE_NAMES[item_type] in a_file.RelativePath.lower() \ for item_type in item_types) if self.domain_filter: files = filter(matches_allowed_domain, files) if len(item_types) > 0: files = filter(matches_allowed_item_types, files) print "Downloading %d files due to filter" % (len(files)) if len(files): authTokens = self.get_files(backupUDID, snapshot, files) if len(authTokens.tokens) > 0: self.authorize_get(authTokens, snapshot) if self.itunes_style: self.write_info_plist(mbsbackup, snapshot) self.write_manifest_mbdb(snapshot) else: print "Unable to download snapshot. This snapshot may not have finished uploading yet." # Clean up self.files if not self.combined: self.downloaded_files = [] # Writes a plist file in the output_directory simular to that created by iTunes during backup def write_info_plist(self, mbsbackup, snapshot): if self.combined: directory = self.output_folder else: directory = os.path.join(self.output_folder, "snapshot_" + str(snapshot)) info_plist = { "Device Name": mbsbackup.Attributes.DeviceClass, "Display Name": mbsbackup.Attributes.DeviceClass, "Product Type": mbsbackup.Attributes.ProductType, "Serial Number": mbsbackup.Attributes.SerialNumber, "Target Type": "Device", "iTunes Version": "11.1", "Product Version": "8.1.1", # Must be higher than 4.0, current iTunes backup sets to 8.1.1 "Target Identifier": mbsbackup.backupUDID.encode("hex"), "Unique Identifier": mbsbackup.backupUDID.encode("hex") } with open(directory + "/Info.plist", 'wb') as fp: plistlib.writePlist(info_plist, fp) def write_manifest_mbdb(self, snapshot): if self.combined: directory = self.output_folder else: directory = os.path.join(self.output_folder, "snapshot_" + str(snapshot)) filename = os.path.join(directory, "Manifest.mbdb") # Generate the bare minimum MBDB file # Open file mbdb_file = open(filename, "wb") # Write file header mbdb_file.write("mbdb") mbdb_file.write("\x00\x00") # For each file for file in self.downloaded_files: # Write App Domain length mbdb_file.write(struct.pack('>h', len(file.Domain))) # Write App Domain mbdb_file.write(file.Domain) # Write iPhone Filename length mbdb_file.write(struct.pack('>h', len(file.RelativePath))) # Write iPhone Filename mbdb_file.write(file.RelativePath) # Write 0xFFFF for Link Target Length signifying that it is not present mbdb_file.write("\xFF\xFF") # Write 0xFFFF for SHAChecksum Length signifying the checksum is not present mbdb_file.write("\xFF\xFF") # Write 0xFFFF for the length of some unknown value, signifying it is not present mbdb_file.write("\xFF\xFF") # Write 0x27 bytes of 0x00, for the file properties as set by the iPhone during restore mbdb_file.write( "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ) # Write 0x00 for extended property count mbdb_file.write("\x00") # Close file mbdb_file.close()
class MobileBackupClient(object): def __init__(self, account_settings, dsPrsID, auth, output_folder): mobilebackup_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Backup"]["url"] content_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Content"]["url"] self.mobilebackup_host = host_from_url(mobilebackup_url) self.content_host = host_from_url(content_url) self.dsPrsID = dsPrsID self.headers = { 'Authorization': auth, 'X-MMe-Client-Info': CLIENT_INFO, 'User-Agent': USER_AGENT_MOBILE_BACKUP, 'X-Apple-MBS-Protocol-Version': "1.7" } self.headers2 = { 'x-apple-mmcs-proto-version': "3.3", 'x-apple-mmcs-dataclass': "com.apple.Dataclass.Backup", 'x-apple-mme-dsid': str(self.dsPrsID), 'User-Agent': USER_AGENT_BACKUPD, 'Accept': "application/vnd.com.apple.me.ubchunk+protobuf", 'Content-Type': "application/vnd.com.apple.me.ubchunk+protobuf", 'x-mme-client-info': CLIENT_INFO_BACKUP } self.files = {} self.output_folder = output_folder self.chosen_snapshot_id = None self.combined = False self.itunes_style = False self.downloaded_files = [] self.domain_filter = None def mobile_backup_request(self, method, url, msg=None, body=""): return probobuf_request(self.mobilebackup_host, method, url, body, self.headers, msg) def get_account(self): return self.mobile_backup_request("GET", MBS[self.dsPrsID](), MBSAccount) def get_backup(self, backupUDID): return self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")](), MBSBackup) def get_keys(self, backupUDID): return self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")].getKeys(), MBSKeySet) def list_files(self, backupUDID, snapshotId): limit = 5000 files = "" offset = 0 new_files = self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].listFiles(offset=offset, limit=limit)) while new_files: files = files + new_files offset += limit new_files = self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].listFiles(offset=offset, limit=limit)) print "\tShifting offset: ", offset return decode_protobuf_array(files, MBSFile) def get_files(self, backupUDID, snapshotId, files): r = [] h = {} for file in files: if file.Size == 0: continue ff = MBSFile() ff.FileID = file.FileID h[file.FileID] = file.Signature r.append(ff) self.files[file.Signature] = file body = encode_protobuf_array(r) z = self.mobile_backup_request("POST", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].getFiles(), None, body) tokens = decode_protobuf_array(z, MBSFileAuthToken) z = MBSFileAuthTokens() for token in tokens: toto = z.tokens.add() toto.FileID = h[token.FileID] toto.AuthToken = token.AuthToken return z def authorize_get(self, tokens, snapshot): if len(tokens.tokens) == 0: return self.headers2["x-apple-mmcs-auth"]= "%s %s" % (tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken) body = tokens.SerializeToString() file_groups = probobuf_request(self.content_host, "POST", URL[self.dsPrsID].authorizeGet(), body, self.headers2, FileGroups) file_chunks = {} for group in file_groups.file_groups: for container_index, container in enumerate(group.storage_host_chunk_list): data = self.download_chunks(container) for file_ref in group.file_checksum_chunk_references: if file_ref.file_checksum not in self.files: continue decrypted_chunks = file_chunks.setdefault(file_ref.file_checksum, {}) for i, reference in enumerate(file_ref.chunk_references): if reference.container_index == container_index: decrypted_chunks[i] = data[reference.chunk_index] if len(decrypted_chunks) == len(file_ref.chunk_references): file = self.files[file_ref.file_checksum] try: self.write_file(file, decrypted_chunks, snapshot) except: raise else: # With iTunes style we need to keep the file if self.itunes_style : self.downloaded_files.append(file) del self.files[file_ref.file_checksum] return file_groups def get_complete(self, mmcs_auth): self.headers2["x-apple-mmcs-auth"] = mmcs_auth body = "" probobuf_request(self.content_host, "POST", URL[self.dsPrsID].getComplete(), body, self.headers2) def download_chunks(self, container): headers = {} # XXX for header in container.host_info.headers: headers[header.name] = header.value d = probobuf_request(container.host_info.hostname, container.host_info.method, container.host_info.uri, "", headers) decrypted = [] i = 0 for chunk in container.chunk_info: dchunk = decrypt_chunk(d[i:i+chunk.chunk_length], chunk.chunk_encryption_key, chunk.chunk_checksum) if dchunk: decrypted.append(dchunk) i += chunk.chunk_length return decrypted def write_file(self, file, decrypted_chunks, snapshot): # If the filename should be left in the iTunes backup style if self.itunes_style: if self.combined: directory = self.output_folder else: directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot)) path_hash = hashlib.sha1(file.Domain.encode('utf-8')+"-"+file.RelativePath.encode('utf-8')).hexdigest() path = os.path.join(directory, path_hash) else: if self.combined: directory = os.path.join(self.output_folder, re.sub(r'[:|*<>?"]', "_", file.Domain)) path = os.path.join(directory, file.RelativePath) else: directory = os.path.join(self.output_folder, re.sub(r'[:|*<>?"]', "_", "snapshot_"+str(snapshot)+"/"+file.Domain)) path = os.path.join(directory, file.RelativePath) mkdir_p(os.path.dirname(path)) print '\t', file.Domain, '\t', path with open(path, "wb") as ff: hash = hashlib.sha1() for key, chunk in decrypted_chunks.iteritems(): hash.update(chunk) ff.write(chunk) # If file is encrypted if file.Attributes.EncryptionKey: key = file.Attributes.EncryptionKey ProtectionClass = struct.unpack(">L", key[0x18:0x1C])[0] if ProtectionClass == file.Attributes.ProtectionClass: wrapped_key = None filekey = None if file.Attributes.EncryptionKeyVersion and file.Attributes.EncryptionKeyVersion == 2: if self.kb.uuid == key[:0x10]: keyLength = struct.unpack(">L", key[0x20:0x24])[0] if keyLength == 0x48: wrapped_key = key[0x24:] else: wrapped_key = key[0x1C:] if wrapped_key: filekey = self.kb.unwrapCurve25519(ProtectionClass, wrapped_key) if not filekey: print "Failed to unwrap file key for file %s !!!" % file.RelativePath else: print "\tfilekey", filekey.encode("hex") self.decrypt_protected_file(path, filekey, file.Attributes.DecryptedSize) else: print "\tUnable to decrypt file, possible old backup format", file.RelativePath def decrypt_protected_file(self, path, filekey, decrypted_size=0): ivkey = hashlib.sha1(filekey).digest()[:16] hash = hashlib.sha1() sz = os.path.getsize(path) oldpath = path + ".encrypted" try: os.rename(path, oldpath) except: pass with open(oldpath, "rb") as old_file: with open(path, "wb") as new_file: n = sz / 0x1000 if decrypted_size: n += 1 for block in xrange(n): iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey) old_data = old_file.read(0x1000) hash.update(old_data) new_file.write(AESdecryptCBC(old_data, filekey, iv)) if decrypted_size == 0: #old iOS 5 format trailer = old_file.read(0x1C) decrypted_size = struct.unpack(">Q", trailer[:8])[0] assert hash.digest() == trailer[8:] new_file.truncate(decrypted_size) os.remove(oldpath) # Delete the encrypted file def computeIV(self, lba): iv = "" lba &= 0xffffffff for _ in xrange(4): if (lba & 1): lba = 0x80000061 ^ (lba >> 1) else: lba = lba >> 1 iv += struct.pack("<L", lba) return iv def download(self, backupUDID, item_types): mbsbackup = self.get_backup(backupUDID) self.output_folder = os.path.join(self.output_folder, backupUDID.encode("hex")) print "Downloading backup {} to {}".format(backupUDID.encode("hex"), self.output_folder) try: mkdir_p(self.output_folder) except OSError: print "Directory \"{}\" already exists.".format(self.output_folder) return keys = self.get_keys(backupUDID) if not keys or not len(keys.Key): print "get_keys FAILED!" return print "Got OTA Keybag" self.kb = Keybag(keys.Key[1].KeyData) if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData): print "Unable to unlock OTA keybag !" return print "Available Snapshots: %d" % (mbsbackup.Snapshot.SnapshotID) if self.chosen_snapshot_id == None: snapshot_list = [1, mbsbackup.Snapshot.SnapshotID - 1, mbsbackup.Snapshot.SnapshotID] elif self.chosen_snapshot_id < 0: snapshot_list = [mbsbackup.Snapshot.SnapshotID + self.chosen_snapshot_id + 1] # Remember chosen_snapshot_id is negative else: snapshot_list = [self.chosen_snapshot_id] for snapshot in snapshot_list: print "Listing snapshot %d..." % (snapshot) files = self.list_files(backupUDID, snapshot) print "Files in snapshot %d" % (len(files)) def matches_allowed_domain(a_file): return self.domain_filter in a_file.Domain def matches_allowed_item_types(a_file): return any(ITEM_TYPES_TO_FILE_NAMES[item_type] in a_file.RelativePath.lower() \ for item_type in item_types) if self.domain_filter: files = filter(matches_allowed_domain, files) if len(item_types) > 0: files = filter(matches_allowed_item_types, files) print "Downloading %d files due to filter" % (len(files)) if len(files): authTokens = self.get_files(backupUDID, snapshot, files) if len(authTokens.tokens) > 0: self.authorize_get(authTokens, snapshot) if self.itunes_style: self.write_info_plist(mbsbackup, snapshot) self.write_manifest_mbdb(snapshot) else: print "Unable to download snapshot. This snapshot may not have finished uploading yet." # Clean up self.files if not self.combined : self.downloaded_files = [] # Writes a plist file in the output_directory simular to that created by iTunes during backup def write_info_plist(self, mbsbackup, snapshot): if self.combined: directory = self.output_folder else: directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot)) info_plist = { "Device Name" : mbsbackup.Attributes.DeviceClass, "Display Name" : mbsbackup.Attributes.DeviceClass, "Product Type" : mbsbackup.Attributes.ProductType, "Serial Number" : mbsbackup.Attributes.SerialNumber, "Target Type" : "Device", "iTunes Version" : "11.1", "Product Version" : "8.1.1", # Must be higher than 4.0, current iTunes backup sets to 8.1.1 "Target Identifier" : mbsbackup.backupUDID.encode("hex"), "Unique Identifier" : mbsbackup.backupUDID.encode("hex") } with open(directory+"/Info.plist", 'wb') as fp: plistlib.writePlist(info_plist, fp) def write_manifest_mbdb(self, snapshot): if self.combined: directory = self.output_folder else: directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot)) filename = os.path.join(directory, "Manifest.mbdb") # Generate the bare minimum MBDB file # Open file mbdb_file = open(filename, "wb") # Write file header mbdb_file.write("mbdb") mbdb_file.write("\x00\x00") # For each file for file in self.downloaded_files: # Write App Domain length mbdb_file.write( struct.pack('>h', len(file.Domain)) ) # Write App Domain mbdb_file.write( file.Domain ) # Write iPhone Filename length mbdb_file.write( struct.pack('>h', len(file.RelativePath)) ) # Write iPhone Filename mbdb_file.write( file.RelativePath ) # Write 0xFFFF for Link Target Length signifying that it is not present mbdb_file.write("\xFF\xFF") # Write 0xFFFF for SHAChecksum Length signifying the checksum is not present mbdb_file.write("\xFF\xFF") # Write 0xFFFF for the length of some unknown value, signifying it is not present mbdb_file.write("\xFF\xFF") # Write 0x27 bytes of 0x00, for the file properties as set by the iPhone during restore mbdb_file.write("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") # Write 0x00 for extended property count mbdb_file.write("\x00") # Close file mbdb_file.close()
class MobileBackupClient(object): def __init__(self, account_settings, dsPrsID, auth, outputFolder): mobilebackup_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Backup"]["url"] content_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Content"]["url"] self.mobilebackup_host = re.match("https://(.*):443", mobilebackup_url).group(1) self.content_host = re.match("https://(.*):443", content_url).group(1) self.dsPrsID = dsPrsID self.headers = {"Authorization": auth, "X-MMe-Client-Info": Client_Info, "User-Agent": USER_AGENT_MOBILE_BACKUP, "X-Apple-MBS-Protocol-Version": "1.7" #error 400 without this } self.headers2 = {"x-apple-mmcs-proto-version": "3.3", "x-apple-mmcs-dataclass": "com.apple.Dataclass.Backup", "x-apple-mme-dsid": str(self.dsPrsID), "User-Agent":USER_AGENT_BACKUPD, "Accept": "application/vnd.com.apple.me.ubchunk+protobuf", "Content-Type": "application/vnd.com.apple.me.ubchunk+protobuf", "x-mme-client-info": Client_Info_backup } self.files = {} self.outputFolder = outputFolder def mobileBackupRequest(self, method, url, msg=None, body=""): return probobuf_request(self.mobilebackup_host, method, url, body, self.headers, msg) def getAccount(self): return self.mobileBackupRequest("GET", "/mbs/%d" % self.dsPrsID, MBSAccount) def getBackup(self, backupUDID): return self.mobileBackupRequest("GET", "/mbs/%d/%s" % (self.dsPrsID, backupUDID.encode("hex")), MBSBackup) def getKeys(self, backupUDID): return self.mobileBackupRequest("GET", "/mbs/%d/%s/getKeys" % (self.dsPrsID, backupUDID.encode("hex")), MBSKeySet) def listFiles(self, backupUDID, snapshotId): files = self.mobileBackupRequest("GET", "/mbs/%d/%s/%d/listFiles" % (self.dsPrsID, backupUDID.encode("hex"), snapshotId)) return decode_protobuf_array(files, MBSFile) def getFiles(self, backupUDID, snapshotId, files): r = [] h = {} for f in files: if f.Size == 0: continue ff = MBSFile() ff.FileID = f.FileID h[f.FileID] = f.Signature r.append(ff) self.files[f.Signature] = f body = encode_protobuf_array(r) z = self.mobileBackupRequest("POST", "/mbs/%d/%s/%d/getFiles" % (self.dsPrsID, backupUDID.encode("hex"), snapshotId), None, body) tokens = decode_protobuf_array(z, MBSFileAuthToken) z = MBSFileAuthTokens() for t in tokens: toto = z.tokens.add() toto.FileID = h[t.FileID] #use signature toto.AuthToken = t.AuthToken return z def authorizeGet(self, tokens): self.headers2["x-apple-mmcs-auth"]= "%s %s" % (tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken) body = tokens.SerializeToString() filegroups = probobuf_request(self.content_host, "POST", "/%d/authorizeGet" % self.dsPrsID, body, self.headers2, FileGroups) #print filegroups filechunks = {} for group in filegroups.file_groups: for container_index in xrange(len(group.storage_host_chunk_list)): data = self.downloadChunks(group.storage_host_chunk_list[container_index]) for file_ref in group.file_checksum_chunk_references: if not self.files.has_key(file_ref.file_checksum): continue decrypted_chunks = filechunks.setdefault(file_ref.file_checksum, {}) for i in xrange(len(file_ref.chunk_references)): ref = file_ref.chunk_references[i] if ref.container_index == container_index: decrypted_chunks[i] = data[ref.chunk_index] if len(decrypted_chunks) == len(file_ref.chunk_references): f = self.files[file_ref.file_checksum] self.writeFile(f, decrypted_chunks) del self.files[file_ref.file_checksum] pprint(self.files) return filegroups def getComplete(self, mmcs_auth): self.headers2["x-apple-mmcs-auth"] = mmcs_auth body = "" probobuf_request(self.content_host, "POST", "/%d/getComplete" % self.dsPrsID, body, self.headers2) def downloadChunks(self, storage_host): headers = {} for h in storage_host.host_info.headers: headers[h.name] = h.value d = probobuf_request(storage_host.host_info.hostname, storage_host.host_info.method, storage_host.host_info.uri, "", headers) decrypted = [] i = 0 for chunk in storage_host.chunk_info: decrypted.append(decrypt_chunk(d[i:i+chunk.chunk_length], chunk.chunk_encryption_key, chunk.chunk_checksum)) i += chunk.chunk_length return decrypted def writeFile(self, f, decrypted_chunks): path = os.path.join(self.outputFolder, re.sub(r'[:|*<>?"]', "_", f.RelativePath)) print path makedirs(os.path.dirname(path)) ff = open(path, "wb") h = hashlib.sha1() for i in xrange(len(decrypted_chunks)): d = decrypted_chunks[i] h.update(d) ff.write(d) ff.close() if f.Attributes.EncryptionKey: EncryptionKey = f.Attributes.EncryptionKey #ProtectionClass = f.Attributes.ProtectionClass hexdump(EncryptionKey) ProtectionClass = struct.unpack(">L", EncryptionKey[0x18:0x1C])[0] assert ProtectionClass == f.Attributes.ProtectionClass #EncryptionKeyVersion=2 => starts with keybag uuid if f.Attributes.EncryptionKeyVersion and f.Attributes.EncryptionKeyVersion == 2: assert self.kb.uuid == EncryptionKey[:0x10] keyLength = struct.unpack(">L", EncryptionKey[0x20:0x24])[0] assert keyLength == 0x48 wrapped_key = EncryptionKey[0x24:] else:#XXX old format ios 5 backup wrapped_key = EncryptionKey[0x1C:] print "ProtectionClass= %d" % ProtectionClass filekey = self.kb.unwrapCurve25519(ProtectionClass, wrapped_key) if not filekey: print "Failed to unwrap file key for file %s !!!" % f.RelativePath else: print "filekey",filekey.encode("hex") self.decryptProtectedFile(path, filekey, f.Attributes.DecryptedSize) def decryptProtectedFile(self, path, filekey, DecryptedSize=0): ivkey = hashlib.sha1(filekey).digest()[:16] h = hashlib.sha1() sz = os.path.getsize(path) #iOS 5 trailer = uint64 sz + sha1 of encrypted file #assert (sz % 0x1000) == 0x1C oldpath = path + ".encrypted" try: os.rename(path, oldpath) except: pass f1 = open(oldpath, "rb") f2 = open(path, "wb") n = (sz / 0x1000) if DecryptedSize: n += 1 for block in xrange(n): iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey) data = f1.read(0x1000) h.update(data) f2.write(AESdecryptCBC(data, filekey, iv)) if DecryptedSize == 0: #old iOS 5 format trailer = f1.read(0x1C) DecryptedSize = struct.unpack(">Q", trailer[:8])[0] assert h.digest() == trailer[8:] f1.close() f2.truncate(DecryptedSize) f2.close() def computeIV(self, lba): iv = "" lba &= 0xffffffff for _ in xrange(4): if (lba & 1): lba = 0x80000061 ^ (lba >> 1); else: lba = lba >> 1; iv += struct.pack("<L", lba) return iv def download(self, backupUDID): mbsbackup = self.getBackup(backupUDID) print "Downloading backup %s" % backupUDID.encode("hex") self.outputFolder = os.path.join(self.outputFolder, backupUDID.encode("hex")) makedirs(self.outputFolder) print backup_summary(mbsbackup) #print mbsbackup.Snapshot.Attributes.KeybagUUID.encode("hex") keys = self.getKeys(backupUDID) if not keys or not len(keys.Key): print "getKeys FAILED!" return print "Got OTA Keybag" self.kb = Keybag(keys.Key[1].KeyData) if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData): print "Unable to unlock OTA keybag !" return for snapshot in xrange(1, mbsbackup.Snapshot.SnapshotID+1): files = self.listFiles(backupUDID, snapshot) print "%d files" % len(files) files2 = [] for f in files: if f.Attributes.EncryptionKey: files2.append(f) print f if len(files2): authTokens = self.getFiles(backupUDID, snapshot, files) self.authorizeGet(authTokens)
class MobileBackupClient(object): def __init__(self, account_settings, dsPrsID, auth, output_folder): mobilebackup_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Backup"]["url"] content_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Content"]["url"] self.mobilebackup_host = host_from_url(mobilebackup_url) self.content_host = host_from_url(content_url) self.dsPrsID = dsPrsID self.headers = { 'Authorization': auth, 'X-MMe-Client-Info': CLIENT_INFO, 'User-Agent': USER_AGENT_MOBILE_BACKUP, 'X-Apple-MBS-Protocol-Version': "1.7" } self.headers2 = { 'x-apple-mmcs-proto-version': "3.3", 'x-apple-mmcs-dataclass': "com.apple.Dataclass.Backup", 'x-apple-mme-dsid': str(self.dsPrsID), 'User-Agent': USER_AGENT_BACKUPD, 'Accept': "application/vnd.com.apple.me.ubchunk+protobuf", 'Content-Type': "application/vnd.com.apple.me.ubchunk+protobuf", 'x-mme-client-info': CLIENT_INFO_BACKUP } self.files = {} self.output_folder = output_folder def mobile_backup_request(self, method, url, msg=None, body=""): return probobuf_request(self.mobilebackup_host, method, url, body, self.headers, msg) def get_account(self): return self.mobile_backup_request("GET", MBS[self.dsPrsID](), MBSAccount) def get_backup(self, backupUDID): return self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")](), MBSBackup) def get_keys(self, backupUDID): return self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")].getKeys(), MBSKeySet) def list_files(self, backupUDID, snapshotId): limit = 5000 files = "" offset = 0 new_files = self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId]['listFiles'](offset=offset, limit=limit)) while new_files: files = files + new_files offset += limit; new_files = self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId](offset=offset, limit=limit)) print "\tShifting offset: ", offset return decode_protobuf_array(files, MBSFile) def get_files(self, backupUDID, snapshotId, files): r = [] h = {} for file in files: if file.Size == 0: continue ff = MBSFile() ff.FileID = file.FileID h[file.FileID] = file.Signature r.append(ff) self.files[file.Signature] = file body = encode_protobuf_array(r) z = self.mobile_backup_request("POST", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].getFiles(), None, body) tokens = decode_protobuf_array(z, MBSFileAuthToken) z = MBSFileAuthTokens() for token in tokens: toto = z.tokens.add() toto.FileID = h[token.FileID] toto.AuthToken = token.AuthToken return z def authorize_get(self, tokens, snapshot): self.headers2["x-apple-mmcs-auth"]= "%s %s" % (tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken) body = tokens.SerializeToString() file_groups = probobuf_request(self.content_host, "POST", URL[self.dsPrsID].authorizeGet(), body, self.headers2, FileGroups) file_chunks = {} for group in file_groups.file_groups: for container_index, container in enumerate(group.storage_host_chunk_list): data = self.download_chunks(container) for file_ref in group.file_checksum_chunk_references: if file_ref.file_checksum not in self.files: continue decrypted_chunks = file_chunks.setdefault(file_ref.file_checksum, {}) for i, reference in enumerate(file_ref.chunk_references): if reference.container_index == container_index: decrypted_chunks[i] = data[reference.chunk_index] if len(decrypted_chunks) == len(file_ref.chunk_references): file = self.files[file_ref.file_checksum] try: self.write_file(file, decrypted_chunks, snapshot) except: raise else: del self.files[file_ref.file_checksum] return file_groups def get_complete(self, mmcs_auth): self.headers2["x-apple-mmcs-auth"] = mmcs_auth body = "" probobuf_request(self.content_host, "POST", URL[self.dsPrsID].getComplete(), body, self.headers2) def download_chunks(self, container): headers = {} # XXX for header in container.host_info.headers: headers[header.name] = header.value d = probobuf_request(container.host_info.hostname, container.host_info.method, container.host_info.uri, "", headers) decrypted = [] i = 0 for chunk in container.chunk_info: dchunk = decrypt_chunk(d[i:i+chunk.chunk_length], chunk.chunk_encryption_key, chunk.chunk_checksum) if dchunk: decrypted.append(dchunk) i += chunk.chunk_length return decrypted def write_file(self, file, decrypted_chunks, snapshot): # If the filename should be left in the iTunes backup style if self.itunes_style: if self.combined: directory = self.output_folder else: directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot)) path_hash = hashlib.sha1(file.Domain+"-"+file.RelativePath).hexdigest() path = os.path.join(directory, path_hash) else: if self.combined: directory = os.path.join(self.output_folder, re.sub(r'[:|*<>?"]', "_",file.Domain)) path = os.path.join(directory, file.RelativePath) else: directory = os.path.join(self.output_folder, re.sub(r'[:|*<>?"]', "_", "snapshot_"+str(snapshot)+"/"+file.Domain)) path = os.path.join(directory, file.RelativePath) mkdir_p(os.path.dirname(path)) print '\t', file.Domain, '\t', path with open(path, "wb") as ff: hash = hashlib.sha1() for key, chunk in decrypted_chunks.iteritems(): hash.update(chunk) ff.write(chunk) # If file is encrypted if file.Attributes.EncryptionKey: key = file.Attributes.EncryptionKey ProtectionClass = struct.unpack(">L", key[0x18:0x1C])[0] if ProtectionClass == file.Attributes.ProtectionClass: if file.Attributes.EncryptionKeyVersion and file.Attributes.EncryptionKeyVersion == 2: assert self.kb.uuid == key[:0x10] keyLength = struct.unpack(">L", key[0x20:0x24])[0] assert keyLength == 0x48 wrapped_key = key[0x24:] else: wrapped_key = key[0x1C:] filekey = self.kb.unwrapCurve25519(ProtectionClass, wrapped_key) if not filekey: print "Failed to unwrap file key for file %s !!!" % file.RelativePath else: print "\tfilekey", filekey.encode("hex") self.decrypt_protected_file(path, filekey, file.Attributes.DecryptedSize) else: print "\tUnable to decrypt file, possible old backup format", file.RelativePath def decrypt_protected_file(self, path, filekey, decrypted_size=0): ivkey = hashlib.sha1(filekey).digest()[:16] hash = hashlib.sha1() sz = os.path.getsize(path) oldpath = path + ".encrypted" try: os.rename(path, oldpath) except: pass with open(oldpath, "rb") as old_file: with open(path, "wb") as new_file: n = sz / 0x1000 if decrypted_size: n += 1 for block in xrange(n): iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey) old_data = old_file.read(0x1000) hash.update(old_data) new_file.write(AESdecryptCBC(old_data, filekey, iv)) if decrypted_size == 0: #old iOS 5 format trailer = old_file.read(0x1C) decrypted_size = struct.unpack(">Q", trailer[:8])[0] assert hash.digest() == trailer[8:] new_file.truncate(decrypted_size) def computeIV(self, lba): iv = "" lba &= 0xffffffff for _ in xrange(4): if (lba & 1): lba = 0x80000061 ^ (lba >> 1); else: lba = lba >> 1; iv += struct.pack("<L", lba) return iv def download(self, backupUDID, item_types): mbsbackup = self.get_backup(backupUDID) self.output_folder = os.path.join(self.output_folder, backupUDID.encode("hex")) print "Downloading backup {} to {}".format(backupUDID.encode("hex"), self.output_folder) try: mkdir_p(self.output_folder) except OSError: print "Directory \"{}\" already exists.".format(self.output_folder) return keys = self.get_keys(backupUDID) if not keys or not len(keys.Key): print "get_keys FAILED!" return print "Got OTA Keybag" self.kb = Keybag(keys.Key[1].KeyData) if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData): print "Unable to unlock OTA keybag !" return print "Available Snapshots: ", mbsbackup.Snapshot.SnapshotID #for snapshot in xrange(1, mbsbackup.Snapshot.SnapshotID+1): for snapshot in [1, mbsbackup.Snapshot.SnapshotID - 1, mbsbackup.Snapshot.SnapshotID]: print "Listing snapshot..." files = self.list_files(backupUDID, snapshot) print "Files in snapshot %s : %s" % (snapshot, len(files)) def matches_allowed_item_types(file): return any(ITEM_TYPES_TO_FILE_NAMES[item_type] in file.RelativePath \ for item_type in item_types) if len(item_types) > 0: files = filter(matches_allowed_item_types, files) if len(files): authTokens = self.get_files(backupUDID, snapshot, files) self.authorize_get(authTokens, snapshot) if self.itunes_style: self.write_info_plist(mbsbackup, snapshot) # Writes a plist file in the output_directory simular to that created by iTunes during backup def write_info_plist(self, mbsbackup, snapshot): if self.combined: directory = self.output_folder else: directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot)) plist_file = open(directory+"/Info.plist", "w") # TODO: Use plistlib to generate the XML plist_file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") plist_file.write("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n") plist_file.write("<plist version=\"1.0\">\n") plist_file.write("<dict>\n") plist_file.write(" <key>Build Version</key>\n") plist_file.write(" <string>10B329</string>\n") plist_file.write(" <key>Device Name</key>\n") plist_file.write(" <string>{}</string>\n".format(mbsbackup.Attributes.DeviceClass)) plist_file.write(" <key>Display Name</key>\n") plist_file.write(" <string>{}</string>\n".format(mbsbackup.Attributes.DeviceClass)) plist_file.write(" <key>GUID</key>\n") plist_file.write(" <string></string>\n") plist_file.write(" <key>IMEI</key>\n") plist_file.write(" <string></string>\n") plist_file.write(" <key>Product Type</key>\n") plist_file.write(" <string>{}</string>\n".format(mbsbackup.Attributes.HardwareModel)) plist_file.write(" <key>Product Version</key>\n") plist_file.write(" <string>6.1.3</string>\n") plist_file.write(" <key>Serial Number</key>\n") plist_file.write(" <string></string>\n") plist_file.write(" <key>Target Identifier</key>\n") plist_file.write(" <string></string>\n") plist_file.write(" <key>Target Type</key>\n") plist_file.write(" <string>Device</string>\n") plist_file.write(" <key>Unique Identifier</key>\n") plist_file.write(" <string></string>\n") plist_file.write(" <key>iTunes Settings</key>\n") plist_file.write(" <dict/>\n") plist_file.write(" <key>iTunes Version</key>\n") plist_file.write(" <string>11.1</string>\n") plist_file.write("</dict>\n") plist_file.write("</plist>\n") plist_file.close()
class MobileBackupClient(object): def __init__(self, account_settings, dsPrsID, auth, outputFolder): mobilebackup_url = account_settings["com.apple.mobileme"][ "com.apple.Dataclass.Backup"]["url"] content_url = account_settings["com.apple.mobileme"][ "com.apple.Dataclass.Content"]["url"] self.mobilebackup_host = re.match("https://(.*):443", mobilebackup_url).group(1) self.content_host = re.match("https://(.*):443", content_url).group(1) self.dsPrsID = dsPrsID self.headers = { "Authorization": auth, "X-MMe-Client-Info": Client_Info, "User-Agent": USER_AGENT_MOBILE_BACKUP, "X-Apple-MBS-Protocol-Version": "1.7" } self.headers2 = { "x-apple-mmcs-proto-version": "3.3", "x-apple-mmcs-dataclass": "com.apple.Dataclass.Backup", "x-apple-mme-dsid": str(self.dsPrsID), "User-Agent": USER_AGENT_BACKUPD, "Accept": "application/vnd.com.apple.me.ubchunk+protobuf", "Content-Type": "application/vnd.com.apple.me.ubchunk+protobuf", "x-mme-client-info": Client_Info_backup } self.files = {} self.outputFolder = outputFolder def mobileBackupRequest(self, method, url, msg=None, body=""): return probobuf_request(self.mobilebackup_host, method, url, body, self.headers, msg) def getAccount(self): return self.mobileBackupRequest("GET", "/mbs/%d" % self.dsPrsID, MBSAccount) def getBackup(self, backupUDID): return self.mobileBackupRequest( "GET", "/mbs/%d/%s" % (self.dsPrsID, backupUDID.encode("hex")), MBSBackup) def getKeys(self, backupUDID): return self.mobileBackupRequest( "GET", "/mbs/%d/%s/getKeys" % (self.dsPrsID, backupUDID.encode("hex")), MBSKeySet) def listFiles(self, backupUDID, snapshotId): files = self.mobileBackupRequest( "GET", "/mbs/%d/%s/%d/listFiles?offset=0&limit=100" % (self.dsPrsID, backupUDID.encode("hex"), snapshotId)) i = 100 files2 = 1 while files2: files2 = self.mobileBackupRequest( "GET", "/mbs/%d/%s/%d/listFiles?offset=%s&limit=100" % (self.dsPrsID, backupUDID.encode("hex"), snapshotId, str(i))) if files2: i = i + 100 files = files + files2 print "\tShifting offset: ", i #print files return decode_protobuf_array(files, MBSFile) def getFiles(self, backupUDID, snapshotId, files): r = [] h = {} for f in files: if f.Size == 0: continue ff = MBSFile() ff.FileID = f.FileID h[f.FileID] = f.Signature r.append(ff) self.files[f.Signature] = f body = encode_protobuf_array(r) z = self.mobileBackupRequest( "POST", "/mbs/%d/%s/%d/getFiles" % (self.dsPrsID, backupUDID.encode("hex"), snapshotId), None, body) tokens = decode_protobuf_array(z, MBSFileAuthToken) z = MBSFileAuthTokens() for t in tokens: toto = z.tokens.add() toto.FileID = h[t.FileID] toto.AuthToken = t.AuthToken return z def authorizeGet(self, tokens, snapshot): self.headers2["x-apple-mmcs-auth"] = "%s %s" % ( tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken) body = tokens.SerializeToString() filegroups = probobuf_request(self.content_host, "POST", "/%d/authorizeGet" % self.dsPrsID, body, self.headers2, FileGroups) #print filegroups filechunks = {} for group in filegroups.file_groups: for container_index in xrange(len(group.storage_host_chunk_list)): data = self.downloadChunks( group.storage_host_chunk_list[container_index]) for file_ref in group.file_checksum_chunk_references: if not self.files.has_key(file_ref.file_checksum): continue decrypted_chunks = filechunks.setdefault( file_ref.file_checksum, {}) for i in xrange(len(file_ref.chunk_references)): ref = file_ref.chunk_references[i] if ref.container_index == container_index: decrypted_chunks[i] = data[ref.chunk_index] if len(decrypted_chunks) == len(file_ref.chunk_references): f = self.files[file_ref.file_checksum] self.writeFile(f, decrypted_chunks, snapshot) del self.files[file_ref.file_checksum] return filegroups def getComplete(self, mmcs_auth): self.headers2["x-apple-mmcs-auth"] = mmcs_auth body = "" probobuf_request(self.content_host, "POST", "/%d/getComplete" % self.dsPrsID, body, self.headers2) def downloadChunks(self, storage_host): headers = {} for h in storage_host.host_info.headers: headers[h.name] = h.value d = probobuf_request(storage_host.host_info.hostname, storage_host.host_info.method, storage_host.host_info.uri, "", headers) decrypted = [] i = 0 for chunk in storage_host.chunk_info: dchunk = decrypt_chunk(d[i:i + chunk.chunk_length], chunk.chunk_encryption_key, chunk.chunk_checksum) if dchunk: decrypted.append(dchunk) i += chunk.chunk_length return decrypted def writeFile(self, f, decrypted_chunks, snapshot): if not os.path.exists( os.path.join( self.outputFolder, re.sub(r'[:|*<>?"]', "_", "snapshot_" + str(snapshot) + "/" + f.Domain))): os.makedirs( os.path.join( self.outputFolder, re.sub(r'[:|*<>?"]', "_", "snapshot_" + str(snapshot) + "/" + f.Domain))) path = os.path.join( self.outputFolder, re.sub( r'[:|*<>?"]', "_", "snapshot_" + str(snapshot) + "/" + f.Domain + "/" + f.RelativePath)) #print '\t',f print '\t', f.Domain, '\t', path makedirs(os.path.dirname(path)) ff = open(path, "wb") h = hashlib.sha1() for i in xrange(len(decrypted_chunks)): d = decrypted_chunks[i] h.update(d) ff.write(d) ff.close() #If file is encrypted if f.Attributes.EncryptionKey: EncryptionKey = f.Attributes.EncryptionKey ProtectionClass = struct.unpack(">L", EncryptionKey[0x18:0x1C])[0] if ProtectionClass == f.Attributes.ProtectionClass: if f.Attributes.EncryptionKeyVersion and f.Attributes.EncryptionKeyVersion == 2: assert self.kb.uuid == EncryptionKey[:0x10] keyLength = struct.unpack(">L", EncryptionKey[0x20:0x24])[0] assert keyLength == 0x48 wrapped_key = EncryptionKey[0x24:] else: wrapped_key = EncryptionKey[0x1C:] filekey = self.kb.unwrapCurve25519(ProtectionClass, wrapped_key) if not filekey: print "Failed to unwrap file key for file %s !!!" % f.RelativePath else: print "\tfilekey", filekey.encode("hex") self.decryptProtectedFile(path, filekey, f.Attributes.DecryptedSize) else: print "\tUnable to decrypt file, possible old backup format", f.RelativePath #hexdump(EncryptionKey) def decryptProtectedFile(self, path, filekey, DecryptedSize=0): ivkey = hashlib.sha1(filekey).digest()[:16] h = hashlib.sha1() sz = os.path.getsize(path) #iOS 5 trailer = uint64 sz + sha1 of encrypted file #assert (sz % 0x1000) == 0x1C oldpath = path + ".encrypted" try: os.rename(path, oldpath) except: pass f1 = open(oldpath, "rb") f2 = open(path, "wb") n = (sz / 0x1000) if DecryptedSize: n += 1 for block in xrange(n): iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey) data = f1.read(0x1000) h.update(data) f2.write(AESdecryptCBC(data, filekey, iv)) if DecryptedSize == 0: #old iOS 5 format trailer = f1.read(0x1C) DecryptedSize = struct.unpack(">Q", trailer[:8])[0] assert h.digest() == trailer[8:] f1.close() f2.truncate(DecryptedSize) f2.close() def computeIV(self, lba): iv = "" lba &= 0xffffffff for _ in xrange(4): if (lba & 1): lba = 0x80000061 ^ (lba >> 1) else: lba = lba >> 1 iv += struct.pack("<L", lba) return iv def download(self, backupUDID, fast): mbsbackup = self.getBackup(backupUDID) print "Downloading backup %s" % backupUDID.encode("hex") self.outputFolder = os.path.join(self.outputFolder, backupUDID.encode("hex")) makedirs(self.outputFolder) #print backup_summary(mbsbackup) #print mbsbackup.Snapshot.Attributes.KeybagUUID.encode("hex") keys = self.getKeys(backupUDID) if not keys or not len(keys.Key): print "getKeys FAILED!" return print "Got OTA Keybag" self.kb = Keybag(keys.Key[1].KeyData) if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData): print "Unable to unlock OTA keybag !" return print "Available Snapshots: ", mbsbackup.Snapshot.SnapshotID for snapshot in xrange(1, mbsbackup.Snapshot.SnapshotID + 1): print "Listing snapshot..." files = self.listFiles(backupUDID, snapshot) print "Files in snapshot %s : %s" % (snapshot, len(files)) files2 = [] if fast == 'y': for f in files: if 'AddressBook.sqlitedb' in f.RelativePath or 'Calendar.sqlitedb' in f.RelativePath or 'sms.db' in f.RelativePath or 'call_history.db' in f.RelativePath or '.JPG' in f.RelativePath: files2.append(f) files = files2 if len(files): authTokens = self.getFiles(backupUDID, snapshot, files) #print authTokens self.authorizeGet(authTokens, snapshot) if fast == 'n': if len(files): authTokens = self.getFiles(backupUDID, snapshot, files) #print authTokens self.authorizeGet(authTokens, snapshot)