The great thing about encrypted iPhone backups is that they contain things
like WiFi passwords that aren’t in regular unencrypted backups. As
discussed in the iOS Security Whitepaper, encrypted backups
are considered more “secure,” so Apple considers it ok to include more
sensitive information in them.
An important warning: obviously, decrypting your iOS device’s backup
removes its encryption. To protect your privacy and security, you should
only run these scripts on a machine with full-disk encryption. While it
is possible for a security expert to write software that protects keys in
memory, e.g. by using functions like VirtualLock() and
SecureZeroMemory() among many other things, these
Python scripts will store your encryption keys and passwords in strings to
be garbage-collected by Python. This means your secret keys and passwords
will live in RAM for a while, from whence they will leak into your swap
file and onto your disk, where an adversary can recover them. This
completely defeats the point of having an encrypted backup.
How to decrypt backups: in theory
The iOS Security Whitepaper explains the fundamental concepts
of per-file keys, protection classes, protection class keys, and keybags
better than I can. If you’re not already familiar with these, take a few
minutes to read the relevant parts.
Now you know that every file in iOS is encrypted with its own random
per-file encryption key, belongs to a protection class, and the per-file
encryption keys are stored in the filesystem metadata, wrapped in the
protection class key.
To decrypt:
Decode the keybag stored in the BackupKeyBag entry of
Manifest.plist. A high-level overview of this structure is given in
the whitepaper. The iPhone Wiki
describes the binary format: a 4-byte string type field, a 4-byte
big-endian length field, and then the value itself.
The important values are the PBKDF2 ITERations and SALT, the double
protection salt DPSL and iteration count DPIC, and then for each
protection CLS, the WPKY wrapped key.
Using the backup password derive a 32-byte key using the correct PBKDF2
salt and number of iterations. First use a SHA256 round with DPSL and
DPIC, then a SHA1 round with ITER and SALT.
Unwrap each wrapped key according to
RFC 3394.
Decrypt the manifest database by stripping the initial four-byte length
tag from the ManifestKey in Manifest.plist, and unwrapping it using
the data protection class 4 NSFileProtectionNone. You now have a
SQLite database with all file metadata.
For each file of interest, get the class-encrypted per-file encryption
key and protection class code by looking in the Files.file database
column for a binary plist containing EncryptionKey and
ProtectionClass entries. Strip the initial four-byte length tag from
EncryptionKey before using.
Then, derive the final decryption key by unwrapping it with the class
key that was unwrapped with the backup password. Then decrypt the file
using AES in CBC mode with a zero IV.
How to decrypt backups: in practice
In runnable source code form, here is how to decrypt the calculator
preferences file from an encrypted iPhone backup:
#!/usr/bin/env python2.7
# coding: UTF-8
import argparse
import base64
import getpass
import hashlib
import os.path
import pprint
import random
import shutil
import sqlite3
import stat
import string
import sys
import tempfile
import Crypto.Cipher.AES # https://www.dlitz.net/software/pycrypto/
import biplist
import fastpbkdf2
def main():
## Parse options
parser = argparse.ArgumentParser()
parser.add_argument('--backup-directory', dest='backup_directory',
default='data/encrypted')
parser.add_argument('--password-pipe', dest='password_pipe',
help="""\
Keeps password from being visible in system process list.
Typical use: --password-pipe=<(echo -n foo)
""")
parser.add_argument('--no-anonymize-output', dest='anonymize',
action='store_false')
parser.add_argument('--base64-passcode-key-pipe', dest='passcode_key_pipe',
help="""\
Provide a previously derived passcode key to save time doing PBDKF2 when
developing""")
args = parser.parse_args()
global ANONYMIZE_OUTPUT
ANONYMIZE_OUTPUT = args.anonymize
if ANONYMIZE_OUTPUT:
print 'Warning: All output keys are FAKE to protect your privacy'
manifest_file = os.path.join(args.backup_directory, 'Manifest.plist')
with open(manifest_file, 'rb') as infile:
manifest_plist = biplist.readPlist(infile)
keybag = Keybag(manifest_plist['BackupKeyBag'])
# the actual keys are unknown, but the wrapped keys are known
keybag.printClassKeys()
if args.password_pipe:
password = readpipe(args.password_pipe)
else:
password = getpass.getpass('Backup password: ')
if args.passcode_key_pipe:
passcode_key = base64.decodestring(readpipe(args.passcode_key_pipe))
else:
passcode_key = None
## Unlock keybag with password
if not keybag.unlockWithPasscode(password, passcode_key):
raise Exception('Could not unlock keybag; bad password?')
# now the keys are known too
keybag.printClassKeys()
## Decrypt metadata DB
manifest_key = manifest_plist['ManifestKey'][4:]
with open(os.path.join(args.backup_directory, 'Manifest.db'), 'r') as db:
encrypted_db = db.read()
key = keybag.unwrapKeyForClass(4, # NSFileProtectionNone
manifest_key)
decrypted_data = AESdecryptCBC(encrypted_db, key)
temp_dir = tempfile.mkdtemp()
try:
# Does anyone know how to get Python’s SQLite module to open some
# bytes in memory as a database?
db_filename = os.path.join(temp_dir, 'db.sqlite3')
with open(db_filename, 'w') as db_file:
db_file.write(decrypted_data)
conn = sqlite3.connect(db_filename)
c = conn.cursor()
c.execute("""
SELECT fileID, domain, relativePath, file
FROM Files
WHERE relativePath LIKE '%/Preferences/com.apple.calculator.plist'
ORDER BY relativePath""")
results = c.fetchall()
finally:
shutil.rmtree(temp_dir)
for item in results:
fileID, domain, relativePath, file_bplist = item
plist = biplist.readPlistFromString(file_bplist)
file_data = plist['$objects'][plist['$top']['root'].integer]
size = file_data['Size']
protection_class = file_data['ProtectionClass']
encryption_key = plist['$objects'][
file_data['EncryptionKey'].integer]['NS.data'][4:]
backup_filename = os.path.join(args.backup_directory,
fileID[:2], fileID)
with open(backup_filename, 'rb') as infile:
data = infile.read()
key = keybag.unwrapKeyForClass(protection_class, encryption_key)
# truncate to actual length, as encryption may introduce padding
decrypted_data = AESdecryptCBC(data, key)[:size]
print '== decrypted data:'
print wrap(decrypted_data)
print
print '== pretty-printed calculator preferences'
pprint.pprint(biplist.readPlistFromString(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, passcode_key=None):
if passcode_key is None:
passcode1 = fastpbkdf2.pbkdf2_hmac('sha256', passcode,
self.attrs["DPSL"],
self.attrs["DPIC"], 32)
passcode_key = fastpbkdf2.pbkdf2_hmac('sha1', passcode1,
self.attrs["SALT"],
self.attrs["ITER"], 32)
print '== Passcode key'
print base64.encodestring(anonymize(passcode_key))
for classkey in self.classKeys.values():
if not classkey.has_key("WPKY"):
continue
k = classkey["WPKY"]
if classkey["WRAP"] & WRAP_PASSCODE:
k = AESUnwrap(passcode_key, 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 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
##
# here are some utility functions, one making sure I don’t leak my
# secret keys when posting the output on Stack Exchange
anon_random = random.Random(0)
memo = {}
def anonymize(s):
global anon_random, memo
if ANONYMIZE_OUTPUT:
if s in memo:
return memo[s]
possible_alphabets = [
string.digits,
string.digits + 'abcdef',
string.letters,
"".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([anon_random.choice(alphabet) for i in range(len(s))])
memo[s] = ret
return ret
else:
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)
def readpipe(path):
if stat.S_ISFIFO(os.stat(path).st_mode):
with open(path, 'rb') as pipe:
return pipe.read()
else:
raise Exception("Not a pipe: {!r}".format(path))
if __name__ == '__main__':
main()
The iphone-dataprotection code posted by Bédrune and Sigwald can
decrypt the keychain from a backup, including fun things like saved wifi
and website passwords:
That code no longer works on backups from phones using the latest iOS, but
not all that much has changed ... leave a comment if you’d like me to
update the above code to dump out saved passwords as well ;P
Security researchers Jean-Baptiste Bédrune and Jean Sigwald presented how to do this at Hack-in-the-box Amsterdam 2011.
Since then, Apple has released an iOS Security Whitepaper with more details about keys and algorithms, and Charlie Miller et al. have released the iOS Hacker’s Handbook, which covers some of the same ground in a how-to fashion. When iOS 10 first came out there were changes to the backup format which Apple did not publicize at first, but various people reverse-engineered the format changes.
Encrypted backups are great
The great thing about encrypted iPhone backups is that they contain things like WiFi passwords that aren’t in regular unencrypted backups. As discussed in the iOS Security Whitepaper, encrypted backups are considered more “secure,” so Apple considers it ok to include more sensitive information in them.
An important warning: obviously, decrypting your iOS device’s backup removes its encryption. To protect your privacy and security, you should only run these scripts on a machine with full-disk encryption. While it is possible for a security expert to write software that protects keys in memory, e.g. by using functions like
VirtualLock()
andSecureZeroMemory()
among many other things, these Python scripts will store your encryption keys and passwords in strings to be garbage-collected by Python. This means your secret keys and passwords will live in RAM for a while, from whence they will leak into your swap file and onto your disk, where an adversary can recover them. This completely defeats the point of having an encrypted backup.How to decrypt backups: in theory
The iOS Security Whitepaper explains the fundamental concepts of per-file keys, protection classes, protection class keys, and keybags better than I can. If you’re not already familiar with these, take a few minutes to read the relevant parts.
Now you know that every file in iOS is encrypted with its own random per-file encryption key, belongs to a protection class, and the per-file encryption keys are stored in the filesystem metadata, wrapped in the protection class key.
To decrypt:
Decode the keybag stored in the
BackupKeyBag
entry ofManifest.plist
. A high-level overview of this structure is given in the whitepaper. The iPhone Wiki describes the binary format: a 4-byte string type field, a 4-byte big-endian length field, and then the value itself.The important values are the PBKDF2
ITER
ations andSALT
, the double protection saltDPSL
and iteration countDPIC
, and then for each protectionCLS
, theWPKY
wrapped key.Using the backup password derive a 32-byte key using the correct PBKDF2 salt and number of iterations. First use a SHA256 round with
DPSL
andDPIC
, then a SHA1 round withITER
andSALT
.Unwrap each wrapped key according to RFC 3394.
Decrypt the manifest database by stripping the initial four-byte length tag from the
ManifestKey
inManifest.plist
, and unwrapping it using the data protection class 4NSFileProtectionNone
. You now have a SQLite database with all file metadata.For each file of interest, get the class-encrypted per-file encryption key and protection class code by looking in the
Files.file
database column for a binary plist containingEncryptionKey
andProtectionClass
entries. Strip the initial four-byte length tag fromEncryptionKey
before using.Then, derive the final decryption key by unwrapping it with the class key that was unwrapped with the backup password. Then decrypt the file using AES in CBC mode with a zero IV.
How to decrypt backups: in practice
In runnable source code form, here is how to decrypt the calculator preferences file from an encrypted iPhone backup:
Which then prints this output:
Extra credit
The iphone-dataprotection code posted by Bédrune and Sigwald can decrypt the keychain from a backup, including fun things like saved wifi and website passwords:
That code no longer works on backups from phones using the latest iOS, but not all that much has changed ... leave a comment if you’d like me to update the above code to dump out saved passwords as well ;P