├── .gitattributes ├── .gitignore ├── ab_decrypt.py └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /ab_decrypt.py: -------------------------------------------------------------------------------- 1 | # ab_decrypt.py 2 | # to decrypt Android backups done using "adb backup" 3 | # lclevy@free.fr (@lorenzo2472) Nov 2016 4 | # requirements : PyCryptoDome 3.9.0, Python 3.7.3 5 | # not memory optimized, as decryption and decompression are done in memory ! 6 | # checked with Android 5.1, 7.0 and 8.0 7 | # references: 8 | # https://nelenkov.blogspot.fr/2012/06/unpacking-android-backups.html 9 | # https://android.googlesource.com/platform/frameworks/base/+/master/services/backup/java/com/android/server/backup/BackupManagerService.java 10 | 11 | #from __future__ import print_function 12 | 13 | import sys 14 | import platform 15 | import codecs 16 | import ctypes 17 | import zlib 18 | from binascii import unhexlify, hexlify 19 | from Crypto.Protocol.KDF import PBKDF2 20 | from Crypto.Cipher import AES 21 | from optparse import OptionParser 22 | from struct import pack 23 | 24 | VERBOSITY=0 25 | CHUNK_SIZE=128*1024 26 | 27 | def dprint(*args, **kwargs): 28 | kwargs.setdefault('file', sys.stderr) 29 | kwargs.setdefault('flush', True) 30 | print(*args, **kwargs) 31 | 32 | def inputtty(prompt=""): 33 | if platform.system() == "Windows": 34 | return input(prompt) 35 | with open('/dev/tty', 'rb') as ftty: 36 | if prompt: 37 | with open('/dev/tty', 'wb') as fwtty: 38 | fwtty.write(prompt.encode('utf8')) 39 | fwtty.flush() 40 | return ftty.readline().decode('utf8').rstrip("\n") 41 | 42 | def readHeader(f): 43 | header = dict() 44 | header['version'] = f.readline()[:-1] 45 | header['compression'] = f.readline()[:-1] 46 | header['encryption'] = f.readline()[:-1] 47 | 48 | if header['encryption']==b'none': 49 | pass 50 | elif header['encryption']==b'AES-256': 51 | #get PBKDF2 parameters to decrypt master key blob 52 | header['upSalt'] = unhexlify( f.readline()[:-1] ) 53 | header['mkSumSalt'] = unhexlify( f.readline()[:-1] ) 54 | header['round'] = int( f.readline()[:-1] ) 55 | header['ukIV'] = unhexlify( f.readline()[:-1] ) 56 | header['mkBlob'] = unhexlify( f.readline()[:-1] ) 57 | if VERBOSITY>1: 58 | dprint('user password salt:', hexlify( header['upSalt']) ) 59 | dprint('master key checksum salt:', hexlify(header['mkSumSalt']) ) 60 | dprint('number of PBKDF2 rounds:', header['round'] ) 61 | dprint('user key IV:', hexlify(header['ukIV']) ) 62 | dprint('master key blob:', hexlify(header['mkBlob']) ) 63 | else: 64 | raise RuntimeError(f"Unsupported encryption scheme: {header['encryption']}") 65 | return header 66 | 67 | def masterKeyJavaConversion(k): 68 | """ 69 | because of byte to Java char before using password data as PBKDF2 key, special handling is required 70 | 71 | from : https://android.googlesource.com/platform/frameworks/base/+/master/services/backup/java/com/android/server/backup/BackupManagerService.java 72 | private byte[] makeKeyChecksum(byte[] pwBytes, byte[] salt, int rounds) { 73 | char[] mkAsChar = new char[pwBytes.length]; 74 | for (int i = 0; i < pwBytes.length; i++) { 75 | mkAsChar[i] = (char) pwBytes[i]; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< HERE 76 | } 77 | Key checksum = buildCharArrayKey(mkAsChar, salt, rounds); 78 | return checksum.getEncoded(); 79 | } 80 | 81 | Java byte to char conversion (as "Widening and Narrowing Primitive Conversion") is defined here: 82 | https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.4 83 | First, the byte is converted to an int via widening primitive conversion (chapter 5.1.2), 84 | and then the resulting int is converted to a char by narrowing primitive conversion (chapter 5.1.3) 85 | 86 | """ 87 | # Widening Primitive Conversion : https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.2 88 | toSigned = [ ctypes.c_byte(x).value for x in k ] #sign extension 89 | if VERBOSITY>2: dprint(toSigned) 90 | # Narrowing Primitive Conversion : https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.3 91 | toUnsigned16bits = [ ctypes.c_ushort(x).value & 0xffff for x in toSigned ] 92 | if VERBOSITY>2: 93 | dprint(('{:x} '*len(toUnsigned16bits)).format(*toUnsigned16bits)) 94 | """ 95 | The Java programming language represents text in sequences of 16-bit code UNITS, using the UTF-16 encoding. 96 | https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.1 97 | """ 98 | toBytes = pack(f'>{len(toUnsigned16bits)}H', *toUnsigned16bits ) #unsigned short to bytes 99 | if VERBOSITY>2: 100 | dprint(hexlify(toBytes, sep=',').decode('ascii')) 101 | 102 | toUtf16be = codecs.decode(toBytes,'UTF-16BE') #from bytes to Utf16 103 | if VERBOSITY>2: 104 | dprint(hexlify(toUtf16be.encode('UTF-16BE'), sep='+').decode('ascii')) 105 | """ 106 | https://developer.android.com/reference/javax/crypto/spec/PBEKeySpec.html 107 | \"Different PBE mechanisms may consume different bits of each password character. 108 | For example, the PBE mechanism defined in PKCS #5 looks at only the low order 8 bits of each character, 109 | whereas PKCS #12 looks at all 16 bits of each character. \" 110 | """ 111 | toUft8 = codecs.encode(toUtf16be,'UTF-8') # char must be encoded as UTF-8 first 112 | if VERBOSITY>2: 113 | dprint(hexlify(toUft8, sep='+').decode('ascii')) 114 | 115 | return toUft8 116 | 117 | def getAESDecrypter(header, password): 118 | assert header['encryption']==b'AES-256', f"Not using AES decryption: {header['encryption']}" 119 | # generate AES key from password and salt 120 | key = PBKDF2(password, header['upSalt'], 32, header['round']) #default algo is sha1 121 | # decrypt master key blob 122 | decrypted = AES.new(key, AES.MODE_CBC, header['ukIV']).decrypt( header['mkBlob'] ) 123 | # parse decrypted blob 124 | Niv = decrypted[0] # IV length 125 | iv = decrypted[1:1+Niv] # AES CBC IV 126 | Nmk = ord( decrypted[1+Niv:1+Niv+1] ) # master key length 127 | mk = decrypted[1+Niv+1:1+Niv+1+Nmk] # AES 256 key 128 | Nck = ord( decrypted[1+Niv+1+Nmk:1+Niv+1+Nmk+1] ) # check value length 129 | ck = decrypted[1+Niv+1+Nmk+1:1+Niv+1+Nmk+1+Nck] # check value 130 | if VERBOSITY>1: 131 | dprint('IV length:',Niv) 132 | dprint('IV:',hexlify(iv)) 133 | dprint('master key length:',Nmk) 134 | dprint('master key:',hexlify(mk)) 135 | dprint('check value length:',Nck) 136 | dprint('check value:',hexlify(ck)) 137 | 138 | #verify password 139 | toBytes2 = masterKeyJavaConversion( bytearray(mk) ) # consider data as bytes, not str 140 | if VERBOSITY>1: 141 | dprint('PBKDF2 secret value for password verification is: ', end='') 142 | dprint( hexlify(toBytes2) ) 143 | ck2 = PBKDF2( toBytes2, header['mkSumSalt'], Nck, header['round'] ) 144 | if ck2!=ck: 145 | dprint( 'computed ck:', hexlify(ck2), 'is different than embedded ck:', hexlify(ck) ) 146 | else: 147 | dprint('password verification is OK') 148 | # decryption using master key and iv 149 | return AES.new(mk, AES.MODE_CBC, iv) 150 | 151 | def chunkReader(f, chunkSize=CHUNK_SIZE): 152 | data = f.read(chunkSize) 153 | while data: 154 | yield data 155 | data = f.read(chunkSize) 156 | 157 | def decrypt(encryptedIter, aesobj, chunkSize=CHUNK_SIZE): 158 | for encrypted in encryptedIter: 159 | yield aesobj.decrypt(encrypted) 160 | 161 | def decompress(compressedDataIter, blockSize=CHUNK_SIZE): 162 | decompressobj = zlib.decompressobj() 163 | for compressedData in compressedDataIter: 164 | yield decompressobj.decompress(compressedData) 165 | yield decompressobj.flush() 166 | if not decompressobj.eof: 167 | raise RuntimeError("incomplete or truncated zlib stream") 168 | 169 | def ab2tar(f, fout, password=None): 170 | if f.readline()[:-1]!=b'ANDROID BACKUP': 171 | dprint('not ANDROID BACKUP') 172 | return False 173 | 174 | #parse header 175 | header = readHeader(f) 176 | if VERBOSITY>1: 177 | dprint(header) 178 | 179 | if header['encryption']==b'AES-256': 180 | if password is None: 181 | password = inputtty("Enter Password: ") 182 | password = password.encode('utf-8') 183 | compressedIter = decrypt(chunkReader(f), getAESDecrypter(header, password)) 184 | elif header['encryption']==b'none': 185 | dprint('no encryption') 186 | compressedIter = chunkReader(f) 187 | else: 188 | dprint('unknown encryption') 189 | return False 190 | 191 | # decompression (zlib stream) 192 | dprint('writing backup as .tar ... ', end='', flush=True) 193 | for decData in decompress(compressedIter): 194 | fout.write(decData) 195 | dprint(f'OK. Filename is \'{fout.name}\', {fout.tell()} bytes written.') 196 | return True 197 | 198 | def main(argv): 199 | global VERBOSITY 200 | parser = OptionParser() 201 | parser.add_option("-p", "--pw", dest="password", help="password") 202 | parser.add_option("-o", "--out", dest="output", default="-", help="output file") 203 | parser.add_option("-v", "--verbose", type='int', dest="verbose", default=0, help="verbose mode") 204 | parser.add_option("-b", "--backup", dest="backup", help="input file") 205 | (options, args) = parser.parse_args() 206 | 207 | VERBOSITY = options.verbose 208 | if options.backup is None: 209 | dprint('-b argument is mandatory') 210 | return 1 211 | 212 | if options.output == '-': 213 | fout = sys.stdout.buffer 214 | else: 215 | fout = open(options.output,'wb') 216 | 217 | with open(options.backup,'rb') as abfile: 218 | return int(not ab2tar(abfile, fout, options.password)) 219 | fout.close() 220 | 221 | if __name__ == "__main__": 222 | exit(main(sys.argv[1:])) 223 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # ab_decrypt.py # 3 | v 1.1 4 | 5 | ## Introduction ## 6 | 7 | An educational python tool to decrypt Android backups (created using "adb backup"). 8 | 9 | Not memory optimized, as decryption and decompression are done in memory ! 10 | 11 | The tricky thing is to prepare the PBKDF2 secret value for password verification, as Java/Android implementation does byte to UTF16BE char to UTF8 strange conversions! 12 | 13 | References documents: 14 | 15 | - [Unpacking android backups](https://nelenkov.blogspot.fr/2012/06/unpacking-android-backups.html "Unpacking android backups"), Nikolay Elenkov, 8th June 2012 16 | - [BackupManagerService.java](https://android.googlesource.com/platform/frameworks/base/+/master/services/backup/java/com/android/server/backup/BackupManagerService.java "BackupManagerService.java"), Android code source 17 | - [Java Widening and Narrowing Primitive Conversion](https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.4 "Java Widening and Narrowing Primitive Conversion "), Java specification 18 | 19 | Requirements : PyCryptoDome 3.9.0, Python 3.7.3. Tested with Android 5.1, 7.0 and 8.1 backups. 20 | 21 | Copyright Laurent Clévy (@lorenzo2472), november 2016 22 | 23 | License is GPLv3 24 | 25 | ## Contributions ## 26 | 27 | - Glenn Wasbhurn (@crass) : reading and decompressing using chunks, for efficient memory usage. Better code organisation 28 | 29 | ## Backup tool 30 | 31 | adb tool can be downloaded from https://developer.android.com/studio/releases/platform-tools 32 | 33 | 34 | 35 | ## Usage ## 36 | 37 | >python ab_decrypt.py -h 38 | Usage: ab_decrypt.py [options] 39 | 40 | Options: 41 | -h, --helpshow this help message and exit 42 | -p PASSWORD, --pw=PASSWORD 43 | password 44 | -o OUTPUT, --out=OUTPUT 45 | output file 46 | -v VERBOSE, --verbose=VERBOSE 47 | verbose mode 48 | -b BACKUP, --backup=BACKUP 49 | input file 50 | 51 | ### level 1 verbosity 52 | 53 | >python ab_decrypt.py -b nexus4_20161101_1234.ab -p 1234 -v 1 54 | password verification is OK 55 | decrypting 176040976 bytes... OK 56 | decompression... done ( 263501824 bytes ) 57 | writing backup as .tar ... OK. Filename is 'backup.tar' 58 | 59 | ### level 2 verbosity 60 | 61 | to get crypto values, use **-v 2** : 62 | 63 | >python ab_decrypt.py -b nexus4_20161101_1234.ab -p 1234 -v 2 64 | {'encryption': 'AES-256', 'version': '3', 'compression': '1'} 65 | user password salt: eff5fff9d380affdb615a1b8f0ff9aee96f63777c9931e61845a290447a2280514c481dcebe1ab6175d159ba1e2225f61275 66 | b44b8d2e3485a3b6e1ac1bb6f711 67 | master key checksum salt: 2faa6feaf812be9f0641613f8378fb890840aea00c8ead6d81a1c16127f02fb7c37907c3c88bc08ccd2cd70aed162c 68 | 62fbdf9d2a0856c149ae5a7b9877d73347 69 | number of PBKDF2 rounds: 10000 70 | user key IV: e19804bb75fb9ccd0d7e09b1ee5e0173 71 | master key blob: 3d3fc39048cf5322f12db4ecf9374ec5059ee2e0565b5b24739c1fb7dbda902210197574d2c709874a7022673fd1a9b2e67e8b6 72 | 0f4832be54bbd8aca130cbc184ffc4d7316e334f6fcf7eb28604c5d5c210464b8cc995c75d9be9dabf7dbfd35 73 | IV length: 16 74 | IV: 000bfc38507701b2babf008befd7a09b 75 | master key length: 32 76 | master key: de7afb611aeeea2c3e4b8a5841a7854b4d08aaedb0cacfbdf83eda5b1a807cba 77 | check value length: 32 78 | check value: 636dd3626057ebf16991eba31a0bc0d828809b7631ca866bac96d5ff853b7730 79 | PBKDF2 secret value for password verification is: efbf9e7aefbfbb611aefbfaeefbfaa2c3e4befbe8a5841efbea7efbe854b4d08efbeaa 80 | efbfadefbeb0efbf8aefbf8fefbebdefbfb83eefbf9a5b1aefbe807cefbeba 81 | password verification is OK 82 | decrypting 176040976 bytes... OK 83 | decompression... done ( 263501824 bytes ) 84 | writing backup as .tar ... OK. Filename is 'backup.tar' 85 | 86 | >python ab_decrypt.py -v 2 -p 1234 -b i:\dev\platform-tools_r30.0.0-windows\platform-tools\backup.ab 87 | {'version': b'5', 'compression': b'1', 'encryption': b'AES-256'} 88 | user password salt: b'e126eddc7772e044d31b78429961c3fd36900fb0467b55fa22f569c851dab56bd6c5c7bf4df194c3ff5572d67ab243567d7a732d76484a5d5813df28db20a44a' 89 | master key checksum salt: b'a10883dceadbffd6d1b73c74c58633e2f018c679a25b62773c4965eff2aa3ff8b6d35ac520463e2f7c3ad7ff319b092aaddab8a4287ade365bd70b2d2ced60eb' 90 | number of PBKDF2 rounds: 10000 91 | user key IV: b'1b2dc97e1eb6377fd984318e2b12cc10' 92 | master key blob: b'e2bbd7aa2612812e4d89f9637899bd6e649d701f899803713ddc455f34736ea3e39fd6c6ee0e817e636de36082cb441214a53ecc7d16f3dd26bdf95b833e901c5f7d97debde9842d602cf635245f04839ae2e7f26f9cfd5804ba4100d698049b' 93 | IV length: 16 94 | IV: b'131e7c02716b0db57e6624d10cb174bc' 95 | master key length: 32 96 | master key: b'52155e98c52a173eee635ebbded652cea999fa218e9083bd775f2dcdd1235f67' 97 | check value length: 32 98 | check value: b'660ff9040b56f3a7eda6ac1fcafdbd72e28762cff2af5fc93b5969cf9f2b6303' 99 | PBKDF2 secret value for password verification is: b'52155eefbe98efbf852a173eefbfae635eefbebbefbf9eefbf9652efbf8eefbea9efbe99efbfba21efbe8eefbe90efbe83efbebd775f2defbf8defbf91235f67' 100 | password verification is OK 101 | decrypting 406437680 bytes... OK 102 | decompression... done ( 580716032 bytes ) 103 | writing backup as .tar ... OK. Filename is 'backup.tar' 104 | 105 | ### unencrypted backups are supported too: 106 | 107 | >python ab_decrypt.py -b nexus4_20161101_nopw.ab -v 1 -o out.tar 108 | no encryption 109 | decompression... done ( 263516160 bytes ) 110 | writing backup as .tar ... OK. Filename is 'out.tar' 111 | 112 | --------------------------------------------------------------------------------