summaryrefslogtreecommitdiff
path: root/plugins/python/abackup/backup.py
diff options
context:
space:
mode:
authorCyrille Bagard <nocbos@gmail.com>2019-07-01 19:38:13 (GMT)
committerCyrille Bagard <nocbos@gmail.com>2019-07-01 19:38:13 (GMT)
commitc8a643f54649574777e6b5d5f5d332160c8c72ea (patch)
tree6acc977c19203c8fd92b287c523910e5d47930ba /plugins/python/abackup/backup.py
parent6e8544094334d134d51cd9ca549a7c75b2e8fdab (diff)
Added support for Android backup files.
Diffstat (limited to 'plugins/python/abackup/backup.py')
-rw-r--r--plugins/python/abackup/backup.py211
1 files changed, 211 insertions, 0 deletions
diff --git a/plugins/python/abackup/backup.py b/plugins/python/abackup/backup.py
new file mode 100644
index 0000000..e2599da
--- /dev/null
+++ b/plugins/python/abackup/backup.py
@@ -0,0 +1,211 @@
+
+# Chrysalide - Outil d'analyse de fichiers binaires
+# backup.py - gestionnaire du format de fichier des sauvegardes Android
+#
+# Copyright (C) 2019 Cyrille Bagard
+#
+# This file is part of Chrysalide.
+#
+# Chrysalide is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Chrysalide is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+try:
+ from Crypto.Cipher import AES
+except:
+ AES = None
+
+import hashlib
+import io
+import pychrysalide
+from pychrysalide.analysis.contents import FileContent
+from pychrysalide.arch import vmpa
+import zlib
+
+
+PBKDF2_KEY_SIZE = 32
+
+
+class AndroidBackup():
+ """Reader for Android backups."""
+
+
+ def _read_lines(self, pos, lcount):
+ """Read lcount lines from a given pos in a binary content."""
+
+ count = 0
+
+ buf = ''
+
+ while count < lcount:
+
+ got = chr(self._content.read_u8(pos))
+
+ buf += got
+
+ if got == '\n':
+ count += 1
+
+ if pos.phys == self._content.size:
+ break
+
+ return buf
+
+
+ def __init__(self, content):
+ """Create an Android backup reader from a binary content."""
+
+ # Refs from https://android.googlesource.com/platform/frameworks/base.git/+/refs/heads/master/ :
+
+ # - versions : services/backup/java/com/android/server/backup/BackupManagerService.java#184
+ # - content : services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java#335
+
+ self._content = content
+
+ pos = vmpa(0, vmpa.VMPA_NO_VIRTUAL)
+
+ # Read the global file header
+
+ self._header = self._read_lines(pos, 4)
+
+ lines = self._header.split('\n')
+
+ if len(lines) != 5 or lines[0] != 'ANDROID BACKUP':
+ raise ValueError('Invalid header')
+
+ self._version = lines[1]
+
+ if not(lines[2] in ['0', '1']):
+ raise ValueError('Content should be compressed or uncompressed')
+
+ self._compressed = (lines[2] == '1')
+
+ if not(lines[3] in ['none', 'AES-256']):
+ raise ValueError('Encryption not supported!')
+
+ self._encrypted = (lines[3] == 'AES-256')
+
+ # Read the encryption header
+
+ if not(self._encrypted):
+
+ self._enc_header = None
+
+ self._user_password_salt = None
+ self._master_key_checksum_salt = None
+ self._pbkdf2_rounds_numer = None
+ self._user_key_iv = None
+ self._master_key_blob = None
+
+ else:
+
+ self._enc_header = self._read_lines(pos, 5)
+
+ lines = self._enc_header.split('\n')
+
+ if len(lines) != 6:
+ raise ValueError('Invalid encryption header')
+
+ self._user_password_salt = bytes.fromhex(lines[0])
+ self._master_key_checksum_salt = bytes.fromhex(lines[1])
+ self._pbkdf2_rounds_numer = int(lines[2])
+ self._user_key_iv = bytes.fromhex(lines[3])
+ self._master_key_blob = bytes.fromhex(lines[4])
+
+ self._backup_start = pos.phys
+
+
+ def is_encrypted(self):
+ """Tell if the backup is encrypted."""
+
+ return self._encrypted
+
+
+ def _convert_master_key(self, input_bytes):
+ """Convert a master key into an UTF-8 byte array."""
+
+ output = []
+
+ for byte in input_bytes:
+
+ if byte < ord(b'\x80'):
+ output.append(byte)
+ else:
+ output.append(ord('\xef') | (byte >> 12))
+ output.append(ord('\xbc') | ((byte >> 6) & ord('\x3f')))
+ output.append(ord('\x80') | (byte & ord('\x3f')))
+
+ return bytes(output)
+
+
+ def get_master_key(self, password):
+ """Get a verified master key and its IV for the backup encryption."""
+
+ if AES is None:
+ raise OSError('No AES support')
+
+ key = hashlib.pbkdf2_hmac('sha1', password.encode('utf-8'),
+ self._user_password_salt,
+ self._pbkdf2_rounds_numer, PBKDF2_KEY_SIZE)
+
+ alg = AES.new(key, AES.MODE_CBC, self._user_key_iv)
+
+ master_key = alg.decrypt(self._master_key_blob)
+
+ blob = io.BytesIO(master_key)
+
+ master_iv_length = ord(blob.read(1))
+ master_iv = blob.read(master_iv_length)
+ master_key_length = ord(blob.read(1))
+ master_key = blob.read(master_key_length)
+ master_key_checksum_length = ord(blob.read(1))
+ master_key_checksum = blob.read(master_key_checksum_length)
+
+ checksum = hashlib.pbkdf2_hmac('sha1', self._convert_master_key(master_key),
+ self._master_key_checksum_salt,
+ self._pbkdf2_rounds_numer, PBKDF2_KEY_SIZE)
+
+ if not master_key_checksum == checksum:
+ raise ValueError('Invalid decryption password')
+
+ return master_key, master_iv
+
+
+ def get_content(self, password=None):
+ """Extract the backup content as a simple tarball content."""
+
+ data = self._content.data[self._backup_start:]
+
+ if self._encrypted:
+
+ assert(password)
+
+ # Cf. https://f-o.org.uk/2017/decrypting-android-backups-with-python.html
+
+ master_key, master_iv = self.get_master_key(password)
+
+ alg = AES.new(master_key, AES.MODE_CBC, master_iv)
+
+ data = alg.decrypt(data)
+
+ if self._compressed:
+
+ d = zlib.decompressobj()
+ extracted = d.decompress(data)
+
+ else:
+
+ extracted = data
+
+ return extracted