|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +# This is a fork from original work by BlueC0re <coding@bluec0re.eu> |
| 4 | +# https://github.com/bluec0re/android-backup-tools |
| 5 | +# License: GPLv3 |
| 6 | +import tarfile |
| 7 | +import zlib |
| 8 | +import enum |
| 9 | +import io |
| 10 | +import pickle |
| 11 | +import os |
| 12 | +import binascii |
| 13 | + |
| 14 | +try: |
| 15 | + from Crypto.Cipher import AES |
| 16 | + from Crypto.Protocol.KDF import PBKDF2 |
| 17 | + from Crypto import Random |
| 18 | +except ImportError: |
| 19 | + AES = None |
| 20 | + |
| 21 | + |
| 22 | +class CompressionType(enum.IntEnum): |
| 23 | + NONE = 0 |
| 24 | + ZLIB = 1 |
| 25 | + |
| 26 | + |
| 27 | +class EncryptionType(enum.Enum): |
| 28 | + NONE = 'none' |
| 29 | + AES256 = 'AES-256' |
| 30 | + |
| 31 | + |
| 32 | +class AndroidBackup: |
| 33 | + def __init__(self, fname=None): |
| 34 | + if fname: |
| 35 | + self.open(fname) |
| 36 | + |
| 37 | + def open(self, fname, mode='rb'): |
| 38 | + self.fp = open(fname, mode) |
| 39 | + |
| 40 | + def close(self): |
| 41 | + self.fp.close() |
| 42 | + |
| 43 | + def parse(self): |
| 44 | + self.fp.seek(0) |
| 45 | + magic = self.fp.readline() |
| 46 | + assert magic == b'ANDROID BACKUP\n' |
| 47 | + self.version = int(self.fp.readline().strip()) |
| 48 | + self.compression = CompressionType(int(self.fp.readline().strip())) |
| 49 | + self.encryption = EncryptionType(self.fp.readline().strip().decode()) |
| 50 | + |
| 51 | + def is_encrypted(self): |
| 52 | + return self.encryption == EncryptionType.AES256 |
| 53 | + |
| 54 | + def _decrypt(self, enc, password): |
| 55 | + if AES is None: |
| 56 | + raise ImportError("PyCrypto required") |
| 57 | + |
| 58 | + user_salt, enc = enc.split(b'\n', 1) |
| 59 | + user_salt = binascii.a2b_hex(user_salt) |
| 60 | + ck_salt, enc = enc.split(b'\n', 1) |
| 61 | + ck_salt = binascii.a2b_hex(ck_salt) |
| 62 | + rounds, enc = enc.split(b'\n', 1) |
| 63 | + rounds = int(rounds) |
| 64 | + iv, enc = enc.split(b'\n', 1) |
| 65 | + iv = binascii.a2b_hex(iv) |
| 66 | + master_key, enc = enc.split(b'\n', 1) |
| 67 | + master_key = binascii.a2b_hex(master_key) |
| 68 | + |
| 69 | + user_key = PBKDF2(password, user_salt, dkLen=256//8, count=rounds) |
| 70 | + cipher = AES.new(user_key, |
| 71 | + mode=AES.MODE_CBC, |
| 72 | + IV=iv) |
| 73 | + |
| 74 | + master_key = list(cipher.decrypt(master_key)) |
| 75 | + l = master_key.pop(0) |
| 76 | + master_iv = bytes(master_key[:l]) |
| 77 | + master_key = master_key[l:] |
| 78 | + l = master_key.pop(0) |
| 79 | + mk = bytes(master_key[:l]) |
| 80 | + master_key = master_key[l:] |
| 81 | + l = master_key.pop(0) |
| 82 | + master_ck = bytes(master_key[:l]) |
| 83 | + |
| 84 | + # gen checksum |
| 85 | + |
| 86 | + # double encode utf8 |
| 87 | + utf8mk = self.encode_utf8(mk) |
| 88 | + calc_ck = PBKDF2(utf8mk, ck_salt, dkLen=256//8, count=rounds) |
| 89 | + assert calc_ck == master_ck |
| 90 | + |
| 91 | + cipher = AES.new(mk, |
| 92 | + mode=AES.MODE_CBC, |
| 93 | + IV=master_iv) |
| 94 | + |
| 95 | + dec = cipher.decrypt(enc) |
| 96 | + pad = dec[-1] |
| 97 | + |
| 98 | + return dec[:-pad] |
| 99 | + |
| 100 | + @staticmethod |
| 101 | + def encode_utf8(mk): |
| 102 | + utf8mk = mk.decode('raw_unicode_escape') |
| 103 | + utf8mk = list(utf8mk) |
| 104 | + for i in range(len(utf8mk)): |
| 105 | + c = ord(utf8mk[i]) |
| 106 | + # fix java encoding (add 0xFF00 to non ascii chars) |
| 107 | + if 0x7f < c < 0x100: |
| 108 | + c += 0xff00 |
| 109 | + utf8mk[i] = chr(c) |
| 110 | + return ''.join(utf8mk).encode('utf-8') |
| 111 | + |
| 112 | + def _encrypt(self, dec, password): |
| 113 | + if AES is None: |
| 114 | + raise ImportError("PyCrypto required") |
| 115 | + |
| 116 | + master_key = Random.get_random_bytes(32) |
| 117 | + master_salt = Random.get_random_bytes(64) |
| 118 | + user_salt = Random.get_random_bytes(64) |
| 119 | + master_iv = Random.get_random_bytes(16) |
| 120 | + user_iv = Random.get_random_bytes(16) |
| 121 | + rounds = 10000 |
| 122 | + |
| 123 | + l = len(dec) |
| 124 | + pad = 16 - (l % 16) |
| 125 | + dec += bytes([pad] * pad) |
| 126 | + cipher = AES.new(master_key, IV=master_iv, mode=AES.MODE_CBC) |
| 127 | + enc = cipher.encrypt(dec) |
| 128 | + |
| 129 | + master_ck = PBKDF2(self.encode_utf8(master_key), |
| 130 | + master_salt, dkLen=256//8, count=rounds) |
| 131 | + |
| 132 | + user_key = PBKDF2(password, |
| 133 | + user_salt, dkLen=256//8, count=rounds) |
| 134 | + |
| 135 | + master_dec = b"\x10" + master_iv + b"\x20" + master_key + b"\x20" + master_ck |
| 136 | + l = len(master_dec) |
| 137 | + pad = 16 - (l % 16) |
| 138 | + master_dec += bytes([pad] * pad) |
| 139 | + cipher = AES.new(user_key, IV=user_iv, mode=AES.MODE_CBC) |
| 140 | + master_enc = cipher.encrypt(master_dec) |
| 141 | + |
| 142 | + enc = binascii.b2a_hex(user_salt).upper() + b"\n" + \ |
| 143 | + binascii.b2a_hex(master_salt).upper() + b"\n" + \ |
| 144 | + str(rounds).encode() + b"\n" + \ |
| 145 | + binascii.b2a_hex(user_iv).upper() + b"\n" + \ |
| 146 | + binascii.b2a_hex(master_enc).upper() + b"\n" + enc |
| 147 | + |
| 148 | + return enc |
| 149 | + |
| 150 | + def read_data(self, password): |
| 151 | + """Reads from the file and returns a TarFile object.""" |
| 152 | + data = self.fp.read() |
| 153 | + |
| 154 | + if self.encryption == EncryptionType.AES256: |
| 155 | + if password is None: |
| 156 | + raise Exception("Password need to be provided to extract encrypted archives") |
| 157 | + data = self._decrypt(data, password) |
| 158 | + |
| 159 | + if self.compression == CompressionType.ZLIB: |
| 160 | + data = zlib.decompress(data, zlib.MAX_WBITS) |
| 161 | + |
| 162 | + tar = tarfile.TarFile(fileobj=io.BytesIO(data)) |
| 163 | + return tar |
| 164 | + |
| 165 | + def unpack(self, target_dir=None, password=None): |
| 166 | + tar = self.read_data(password) |
| 167 | + |
| 168 | + members = tar.getmembers() |
| 169 | + |
| 170 | + if target_dir is None: |
| 171 | + target_dir = os.path.basename(self.fp.name) + '_unpacked' |
| 172 | + pickle_fname = os.path.basename(self.fp.name) + '.pickle' |
| 173 | + if not os.path.exists(target_dir): |
| 174 | + os.mkdir(target_dir) |
| 175 | + |
| 176 | + tar.extractall(path=target_dir) |
| 177 | + |
| 178 | + with open(pickle_fname, 'wb') as fp: |
| 179 | + pickle.dump(members, fp) |
| 180 | + |
| 181 | + def list(self, password=None): |
| 182 | + tar = self.read_tar(password) |
| 183 | + return tar.list() |
| 184 | + |
| 185 | + def pack(self, fname, password=None): |
| 186 | + target_dir = os.path.basename(fname) + '_unpacked' |
| 187 | + pickle_fname = os.path.basename(fname) + '.pickle' |
| 188 | + |
| 189 | + data = io.BytesIO() |
| 190 | + tar = tarfile.TarFile(name=fname, |
| 191 | + fileobj=data, |
| 192 | + mode='w', |
| 193 | + format=tarfile.PAX_FORMAT) |
| 194 | + |
| 195 | + with open(pickle_fname, 'rb') as fp: |
| 196 | + members = pickle.load(fp) |
| 197 | + |
| 198 | + os.chdir(target_dir) |
| 199 | + for member in members: |
| 200 | + if member.isreg(): |
| 201 | + tar.addfile(member, open(member.name, 'rb')) |
| 202 | + else: |
| 203 | + tar.addfile(member) |
| 204 | + |
| 205 | + tar.close() |
| 206 | + |
| 207 | + data.seek(0) |
| 208 | + if self.compression == CompressionType.ZLIB: |
| 209 | + data = zlib.compress(data.read()) |
| 210 | + if self.encryption == EncryptionType.AES256: |
| 211 | + data = self._encrypt(data, password) |
| 212 | + |
| 213 | + with open(fname, 'wb') as fp: |
| 214 | + fp.write(b'ANDROID BACKUP\n') |
| 215 | + fp.write('{}\n'.format(self.version).encode()) |
| 216 | + fp.write('{:d}\n'.format(self.compression).encode()) |
| 217 | + fp.write('{}\n'.format(self.encryption.value).encode()) |
| 218 | + |
| 219 | + fp.write(data) |
| 220 | + |
| 221 | + def __exit__(self, *args, **kwargs): |
| 222 | + self.close() |
| 223 | + |
| 224 | + def __enter__(self): |
| 225 | + self.parse() |
| 226 | + return self |
0 commit comments