With the requirement set by Apple and MDM for force encryption on backups, is there a way to decrypt and encrypt iPhone backups?
We need access to the raw files that are saved within the backup to make slight changes to a few plist files inside, but the problem is that we need to do this with in code as doing this by hand will take forever with 1000+ phones. I have most of my code written in python and editing the plist seems to be working just fine until we put on the mdm profile resulting in the phone requiring encrypted backups from that point on.
Here is what I found so far: but unable to make this as 10> change the file from a mbdb to a db file. Reading from others, it also seems to that that might have change how the files are encrypted. Any help or update on this would be very helpful!
#!/usr/bin/env python2.7
# coding: UTF-8
# default to True to avoid leaking secrets
ANONYMIZE_OUTPUT = True
import PBKDF2 # http://iphone-dataprotection.googlecode.com/hg-history/tip/python_scripts/crypto/PBKDF2.py
import bplist # https://github.com/farcaller/bplist-python/raw/master/bplist.py
import Crypto.Cipher.AES # https://www.dlitz.net/software/pycrypto/
import hashlib
import os.path
import pprint
import sys
BACKUP_DIR = "data/encrypted"
def main():
with open(os.path.join(BACKUP_DIR, 'Manifest.plist'), 'rb') as infile:
manifest_plist = bplist.BPlistReader.plistWithString(infile.read())
keybag = Keybag(manifest_plist['BackupKeyBag'])
# the actual keys are unknown, but the wrapped keys are known
keybag.printClassKeys()
if not keybag.unlockWithPasscode('test'):
raise Exception('Could not unlock keybag; bad password?')
# now the keys are known too
keybag.printClassKeys()
for item in process_mbdb_file(
os.path.join(BACKUP_DIR, 'Manifest.mbdb')).values():
filename = item['filename']
if not filename.endswith('calculator.plist'):
continue
encryption_key = item['unknown1'][4:]
protection_class = item['flag']
backup_filename = os.path.join(
BACKUP_DIR,
hashlib.sha1(item['domain'] + '-' + item['filename']).hexdigest())
with open(backup_filename, 'rb') as infile:
data = infile.read()
print '== encrypted data:'
print wrap(data)
print
key = keybag.unwrapKeyForClass(protection_class, encryption_key)
# truncate to actual length, because encryption may introduce padding
decrypted_data = AESdecryptCBC(data, key)[:item['filelen']]
print '== decrypted data:'
print wrap(decrypted_data)
print
print '== pretty-printed calculator preferences'
pprint.pprint(bplist.BPlistReader.plistWithString(decrypted_data))
##
# this section is mostly copied from parts of iphone-dataprotection
# http://code.google.com/p/iphone-dataprotection/
import struct
CLASSKEY_TAGS = ["CLAS","WRAP","WPKY", "KTYP", "PBKY"] #UUID
KEYBAG_TYPES = ["System", "Backup", "Escrow", "OTA (icloud)"]
KEY_TYPES = ["AES", "Curve25519"]
PROTECTION_CLASSES={
1:"NSFileProtectionComplete",
2:"NSFileProtectionCompleteUnlessOpen",
3:"NSFileProtectionCompleteUntilFirstUserAuthentication",
4:"NSFileProtectionNone",
5:"NSFileProtectionRecovery?",
6: "kSecAttrAccessibleWhenUnlocked",
7: "kSecAttrAccessibleAfterFirstUnlock",
8: "kSecAttrAccessibleAlways",
9: "kSecAttrAccessibleWhenUnlockedThisDeviceOnly",
10: "kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly",
11: "kSecAttrAccessibleAlwaysThisDeviceOnly"
}
WRAP_DEVICE = 1
WRAP_PASSCODE = 2
class Keybag(object):
def __init__(self, data):
self.type = None
self.uuid = None
self.wrap = None
self.deviceKey = None
self.attrs = {}
self.classKeys = {}
self.KeyBagKeys = None #DATASIGN blob
self.parseBinaryBlob(data)
def parseBinaryBlob(self, data):
currentClassKey = None
for tag, data in loopTLVBlocks(data):
if len(data) == 4:
data = struct.unpack(">L", data)[0]
if tag == "TYPE":
self.type = data
if self.type > 3:
print "FAIL: keybag type > 3 : %d" % self.type
elif tag == "UUID" and self.uuid is None:
self.uuid = data
elif tag == "WRAP" and self.wrap is None:
self.wrap = data
elif tag == "UUID":
if currentClassKey:
self.classKeys[currentClassKey["CLAS"]] = currentClassKey
currentClassKey = {"UUID": data}
elif tag in CLASSKEY_TAGS:
currentClassKey[tag] = data
else:
self.attrs[tag] = data
if currentClassKey:
self.classKeys[currentClassKey["CLAS"]] = currentClassKey
def unlockWithPasscode(self, passcode):
passcodekey = PBKDF2.PBKDF2(passcode, self.attrs["SALT"],
iterations=self.attrs["ITER"]).read(32)
for classkey in self.classKeys.values():
if not classkey.has_key("WPKY"):
continue
k = classkey["WPKY"]
if classkey["WRAP"] & WRAP_PASSCODE:
k = AESUnwrap(passcodekey, classkey["WPKY"])
if not k:
return False
classkey["KEY"] = k
return True
def unwrapKeyForClass(self, protection_class, persistent_key):
ck = self.classKeys[protection_class]["KEY"]
if len(persistent_key) != 0x28:
raise Exception("Invalid key length")
return AESUnwrap(ck, persistent_key)
def printClassKeys(self):
print "== Keybag"
print "Keybag type: %s keybag (%d)" % (KEYBAG_TYPES[self.type], self.type)
print "Keybag version: %d" % self.attrs["VERS"]
print "Keybag iterations: %d, iv=%s" % (
self.attrs["ITER"], anonymize(self.attrs["SALT"].encode('hex')))
print "Keybag UUID: %s" % anonymize(self.uuid.encode("hex"))
print "-"*209
print "".join(["Class".ljust(53),
"WRAP".ljust(5),
"Type".ljust(11),
"Key".ljust(65),
"WPKY".ljust(65),
"Public key"])
print "-"*208
for k, ck in self.classKeys.items():
if k == 6: print ""
print "".join(
[PROTECTION_CLASSES.get(k).ljust(53),
str(ck.get("WRAP","")).ljust(5),
KEY_TYPES[ck.get("KTYP",0)].ljust(11),
anonymize(ck.get("KEY", "").encode("hex")).ljust(65),
anonymize(ck.get("WPKY", "").encode("hex")).ljust(65),
ck.get("PBKY", "").encode("hex")])
print
def loopTLVBlocks(blob):
i = 0
while i + 8 <= len(blob):
tag = blob[i:i+4]
length = struct.unpack(">L",blob[i+4:i+8])[0]
data = blob[i+8:i+8+length]
yield (tag,data)
i += 8 + length
def unpack64bit(s):
return struct.unpack(">Q",s)[0]
def pack64bit(s):
return struct.pack(">Q",s)
def AESUnwrap(kek, wrapped):
C = []
for i in xrange(len(wrapped)/8):
C.append(unpack64bit(wrapped[i*8:i*8+8]))
n = len(C) - 1
R = [0] * (n+1)
A = C[0]
for i in xrange(1,n+1):
R[i] = C[i]
for j in reversed(xrange(0,6)):
for i in reversed(xrange(1,n+1)):
todec = pack64bit(A ^ (n*j+i))
todec += pack64bit(R[i])
B = Crypto.Cipher.AES.new(kek).decrypt(todec)
A = unpack64bit(B[:8])
R[i] = unpack64bit(B[8:])
if A != 0xa6a6a6a6a6a6a6a6:
return None
res = "".join(map(pack64bit, R[1:]))
return res
ZEROIV = "\x00"*16
def AESdecryptCBC(data, key, iv=ZEROIV, padding=False):
if len(data) % 16:
print "AESdecryptCBC: data length not /16, truncating"
data = data[0:(len(data)/16) * 16]
data = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv).decrypt(data)
if padding:
return removePadding(16, data)
return data
##
# this .mbdb-parsing code is from http://stackoverflow.com/q/3085153/14558:
def getint(data, offset, intsize):
"""Retrieve an integer (big-endian) and new offset from the current offset"""
value = 0
while intsize > 0:
value = (value<<8) + ord(data[offset])
offset = offset + 1
intsize = intsize - 1
return value, offset
def getstring(data, offset):
"""Retrieve a string and new offset from the current offset into the data"""
if data[offset] == chr(0xFF) and data[offset+1] == chr(0xFF):
return '', offset+2 # Blank string
length, offset = getint(data, offset, 2) # 2-byte length
value = data[offset:offset+length]
return value, (offset + length)
def process_mbdb_file(filename):
mbdb = {} # Map offset of info in this file => file info
data = open(filename).read()
if data[0:4] != "mbdb": raise Exception("This does not look like an MBDB file")
offset = 4
offset = offset + 2 # value x05 x00, not sure what this is
while offset < len(data):
fileinfo = {}
fileinfo['start_offset'] = offset
fileinfo['domain'], offset = getstring(data, offset)
fileinfo['filename'], offset = getstring(data, offset)
fileinfo['linktarget'], offset = getstring(data, offset)
fileinfo['datahash'], offset = getstring(data, offset)
fileinfo['unknown1'], offset = getstring(data, offset)
fileinfo['mode'], offset = getint(data, offset, 2)
fileinfo['unknown2'], offset = getint(data, offset, 4)
fileinfo['unknown3'], offset = getint(data, offset, 4)
fileinfo['userid'], offset = getint(data, offset, 4)
fileinfo['groupid'], offset = getint(data, offset, 4)
fileinfo['mtime'], offset = getint(data, offset, 4)
fileinfo['atime'], offset = getint(data, offset, 4)
fileinfo['ctime'], offset = getint(data, offset, 4)
fileinfo['filelen'], offset = getint(data, offset, 8)
fileinfo['flag'], offset = getint(data, offset, 1)
fileinfo['numprops'], offset = getint(data, offset, 1)
fileinfo['properties'] = {}
for ii in range(fileinfo['numprops']):
propname, offset = getstring(data, offset)
propval, offset = getstring(data, offset)
fileinfo['properties'][propname] = propval
mbdb[fileinfo['start_offset']] = fileinfo
return mbdb
##
# and here are some utility functions, one making sure I don’t leak my
# secret keys when posting the output on Stack Exchange
if ANONYMIZE_OUTPUT:
memo = {}
def anonymize(s):
global memo
if s in memo:
return memo[s]
import random
import string
r = random.Random(0)
possible_alphabets = [
string.digits,
string.digits + 'abcdef',
"".join(chr(x) for x in range(0, 256)),
]
for a in possible_alphabets:
if all(c in a for c in s):
alphabet = a
break
ret = "".join([r.choice(alphabet) for i in range(len(s))])
memo[s] = ret
return ret
else:
def anonymize(s): return s
def wrap(s, width=78):
"Return a width-wrapped repr(s)-like string without breaking on \’s"
s = repr(s)
quote = s[0]
s = s[1:-1]
ret = []
while len(s):
i = s.rfind('\\', 0, width)
if i <= width - 4: # "\x??" is four characters
i = width
ret.append(s[:i])
s = s[i:]
return '\n'.join("%s%s%s" % (quote, line ,quote) for line in ret)
if __name__ == '__main__':
main()