[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**NOTE**\nPlease consider that some errors could be handled only by providing the info.xml file and the files related to the issue (e.g. a file that cannot be decrypted). If the files needed to understand the bug could contain personal data of any kind, DO NOT SEND THEM. Instead, provide samples that can be shared and with a limited size. Thanks.\n\n**Required info (please complete the following information):**\n - Huawei Kobackup version: \n - Host: [Windows / Linux ]\n - Kobackup script version:\n - Kobackup output log (use -vvv)\n\n**Additional context**\nAdd any other context about the problem here.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Francesco \"dfirfpi\" Picasso, Reality Net System Solutions\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# kobackupdec\nHuawei backup decryptor\n\n_This script is introduced by the blog post at https://blog.digital-forensics.it/2019/07/huawei-backup-decryptor.html._\n\nThe `kobackupdec` is a Python3 script aimed to decrypt Huawei *HiSuite* or *KoBackup* (the Android app) backups. When decrypting and uncompressing the archives, it will re-organize the output folders structure trying to _mimic_ the typical Android one. The script will work both on Windows and Linux hosts, provided the PyCryptoDome dependency. Starting from **20100107** the script was rewritten to handle v9 and v10 kobackup backups structures.\n\n## _EOL_\n\nOn 1.1.2021 the script will get its _end of life_ status. It was needed two years ago to overcome issues for some Huawei devices' forensics acquisitions. Now commercial forensics solutions include the very same capabilities, and much more: there are no more reasons to maintain it. We've got messages from guys using this script to manage theirs backups: we do not recommend it, and we did not write it for this reason. Anyhow we're happy some of you did find it useful, and we thank you for the feedback. We shared it to the community, trying to give back something: if someone has any interest in maintaining it, please let us know so we can include a link to the project. \n\n## Usage\n\nThe script *assumes* that backups are encrypted with a user-provided password. Actually it does not support the HiSuite _self_ generated password, when the user does not provide its own.\n\n```\nusage: kobackupdec.py [-h] [-v] password backup_path dest_path\n\nHuawei KoBackup decryptor version 20200611\n\npositional arguments:\n  password       user password for the backup\n  backup_path    backup folder\n  dest_path      decrypted backup folder\n\noptional arguments:\n  -h, --help       show this help message and exit\n  -e, --expandtar  expand tar files\n  -w, --writable   do not set RO pemission on decrypted data\n  -v, --verbose    verbose level, -v to -vvv\n```\n\n- `password`, is the user provided password.\n- `backup_path`, is the folder containing the Huawei backup, relative or absolute paths can be used.\n- `dest_path`, is the folder to be created in the specified path, absolute or relative. It will complain if the provided folder already exists.\n- `[-v]` (from `-v` to `-vvv`) verbosity level, written on *stderr*. It's suggested to use *-vvv* with a redirect to get a log of the process.\n\n### Example\n\n```\nZ:\\> py -3 kobackupdec.py -vvv 123456 \"Z:\\HUAWEI P30 Pro_2019-06-28 22.56.31\" Z:\\HiSuiteBackup\nINFO:root:getting files and folder from Z:\\HUAWEI P30 Pro_2019-06-28 22.56.31\nINFO:root:parsing XML files...\nINFO:root:parsing xml audio.xml\nDEBUG:root:parsing xml file audio.xml\nINFO:root:parsing xml document.xml\nDEBUG:root:parsing xml file document.xml\nINFO:root:parsing xml info.xml\nDEBUG:root:ignoring entry HeaderInfo\nDEBUG:root:ignoring entry BackupFilePhoneInfo\nDEBUG:root:ignoring entry BackupFileVersionInfo\nINFO:root:parsing xml picture.xml\nDEBUG:root:parsing xml file picture.xml\nINFO:root:parsing xml soundrecorder.xml\nDEBUG:root:parsing xml file soundrecorder.xml\nINFO:root:parsing xml video.xml\nDEBUG:root:parsing xml file video.xml\nDEBUG:root:crypto_init: using version 3.\nDEBUG:root:SHA256(BKEY)[16] = b'8d969eef6ecad3c29a3a629280e686cf'\n...\n```\n\nThe **output** folder structure will be similar to the following one: *data/data* applications will be exploded in their proper paths, and the APKs will be *restored* too (not icons, actually). Note that the **db** folder will contain the *special* databases as created by the Huawei backups.\n\n```\nHiSuiteBackup\n|-- data\n|   |-- app\n|   |   |-- de.sec.mobile.apk-1\n|   |   | [...]\n|   |   `-- org.telegram.messenger.apk-1\n|   `-- data\n|       |-- de.sec.mobile\n|       | [...]\n|       `-- org.telegram.messenger\n|-- db\n|   |-- HWlanucher.db\n|   |-- Memo.db\n|   |-- alarm.db\n|   |-- calendar.db\n|   |-- calllog.db\n|   |-- camera.db\n|   |-- clock.db\n|   |-- contact.db\n|   |-- harassment.db\n|   |-- phoneManager.db\n|   |-- setting.db\n|   |-- sms.db\n|   |-- soundrecorder.db\n|   |-- systemUI.db\n|   |-- weather.db\n|   `-- wifiConfig.db\n`-- storage\n    |-- DCIM\n    |-- Download\n    |-- Huawei\n    |-- MagazineUnlock\n    |-- Notifications\n    |-- Pictures\n    |-- WhatsApp\n    |-- mp3\n    |-- parallel_intl\n    `-- s8-wallpapers-9011.PNG\n```\n"
  },
  {
    "path": "kobackupdec.py",
    "content": "#!/usr/bin/python3\n# -*- coding: utf-8 -*-\n#\n# Huawei KoBackup backups decryptor.\n#\n# Version History\n# - 20200705: fixed decrypt_large_package to read input's chunks\n# - 20200611: added 'expandtar' option, to avoid automatic expansion of TARs\n#             added 'writable' option, to allow user RW on decrypted files\n#             large TAR files are not managed in chunk but not expanded\n# - 20200607: merged empty CheckMsg, update folder_to_media_type by @realSnoopy\n# - 20200406: merged pull by @lp4n6, related to files and folders permissions\n# - 20200405: added Python minor version check and note (thanks @lp4n6)\n# - 2020test: rewritten to handle v9 and v10 backups\n# - 20200107: merged pull by @lp4n6, fixed current version\n# - 20191113: fixed double folder creation error\n# - 20190729: first public release\n# - 20190729: first public release\n#\n# Note: it needs Python version >= 3.7\n#\n# Released under MIT License\n#\n# Copyright (c) 2019 Francesco \"dfirfpi\" Picasso, Reality Net System Solutions\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n#\n'''Huawei KoBackup decryptor.'''\n\nimport argparse\nimport binascii\nimport enum\nimport io\nimport logging\nimport os\nimport os.path\nimport pathlib\nimport sys\nimport tarfile\nimport xml.dom.minidom\n\nfrom Crypto.Cipher import AES\nfrom Crypto.Hash import SHA256\nfrom Crypto.Hash import HMAC\nfrom Crypto.Protocol.KDF import PBKDF2\nfrom Crypto.Util import Counter\n\nVERSION = '20200705'\n\n# Disabling check on doc strings and naming convention.\n# pylint: disable=C0111,C0103\n\nMAX_FILE_SIZE = 536870912 # Files larger than that needs to be 'chuncked'.\n\n# --- DecryptMaterial ---------------------------------------------------------\n\nclass DecryptMaterial:\n\n    def __init__(self, type_name):\n        self._type_name = type_name\n        self._name = None\n        self._encMsgV3 = None\n        self._iv = None\n        self._path = None\n        self._records_num = None\n        self._copy_file_path = None\n\n    @property\n    def type_name(self):\n        return self._type_name\n\n    @property\n    def name(self):\n        return self._name\n\n    @name.setter\n    def name(self, value_string):\n        if value_string:\n            self._name = value_string\n        else:\n            logging.error('empty entry name!')\n\n    @property\n    def records_num(self):\n        return self._records_num\n\n    @records_num.setter\n    def records_num(self, value_string):\n        self._records_num = value_string\n\n    @property\n    def encMsgV3(self):\n        return self._encMsgV3\n\n    @encMsgV3.setter\n    def encMsgV3(self, value_hex_string):\n        if value_hex_string:\n            self._encMsgV3 = binascii.unhexlify(value_hex_string)\n            if len(self._encMsgV3) != 48:\n                logging.error('encMsgV3 should be 48 bytes long!')\n\n    @property\n    def iv(self):\n        return self._iv\n\n    @iv.setter\n    def iv(self, value_hex_string):\n        if value_hex_string:\n            self._iv = binascii.unhexlify(value_hex_string)\n            if len(self._iv) != 16:\n                logging.error('iv should be 16 bytes long!')\n\n    @property\n    def copy_file_path(self):\n        return self._copy_file_path\n\n    @copy_file_path.setter\n    def copy_file_path(self, value_string):\n        self._copy_file_path = value_string\n\n    @property\n    def path(self):\n        return self._path\n\n    @path.setter\n    def path(self, value_string):\n        if value_string:\n            self._path = value_string\n        else:\n            logging.error('empty file path!')\n\n    def do_check(self):\n        if self._name and (self._encMsgV3 or self._iv):\n            return True\n        return False\n\n    def dump(self):\n        dump = 'NAME: {}, TYPE: {}, '.format(self._name, self._type_name)\n        if self._path:\n            dump += 'PATH: {}, '.format(self._path)\n        if self._copy_file_path:\n            dump += 'COPY_FILEPATH: {}, '.format(self._copy_file_path)\n        if self._records_num:\n            dump += 'RECORDS_NUM: {}'.format(self._records_num)\n        # Not reported: self._encMsgV3, self._iv\n        dump += '\\n'\n        return dump\n\n\n# --- Decryptor ---------------------------------------------------------------\n\nclass Decryptor:\n    '''It provides algo and key derivations to decrypt files.'''\n\n    count = 5000\n    dklen = 32\n    chunk_size = 1024*1024*64\n\n    def __init__(self, password):\n        '''Initialize the object by setting a password.'''\n        self._upwd = password\n        self._good = False\n        self._e_perbackupkey = None\n        self._pwkey_salt = None\n        self._type_attch = 0\n        self._checkMsg = None\n        self._bkey = None\n        self._bkey_sha256 = None\n\n    @property\n    def good(self):\n        return self._good\n\n    @property\n    def password(self):\n        return self._upwd\n\n    @property\n    def e_perbackupkey(self):\n        return self._e_perbackupkey\n\n    @e_perbackupkey.setter\n    def e_perbackupkey(self, value_hex_string):\n        if value_hex_string:\n            self._e_perbackupkey = binascii.unhexlify(value_hex_string)\n            if len(self._e_perbackupkey) != 48:\n                logging.error('e_perbackupkey should be 48 bytes long!')\n\n    @property\n    def pwkey_salt(self):\n        return self._pwkey_salt\n\n    @pwkey_salt.setter\n    def pwkey_salt(self, value_hex_string):\n        if value_hex_string:\n            self._pwkey_salt = binascii.unhexlify(value_hex_string)\n            if len(self._pwkey_salt) != 32:\n                logging.error('pwkey_salt should be 32 bytes long!')\n\n    @property\n    def type_attch(self):\n        return self._type_attch\n\n    @type_attch.setter\n    def type_attch(self, value_int):\n        self._type_attch = value_int\n\n    @property\n    def checkMsg(self):\n        return self._checkMsg\n\n    @checkMsg.setter\n    def checkMsg(self, value_hex_string):\n        if value_hex_string:\n            self._checkMsg = binascii.unhexlify(value_hex_string)\n            if len(self._checkMsg) != 64:\n                logging.error('checkMsg should be 64 bytes long!')\n\n    @staticmethod\n    def prf(p, s):\n        return HMAC.new(p, s, SHA256).digest()\n\n    def __decrypt_bkey_v4(self):\n        key_salt = self._pwkey_salt[:16]\n        logging.debug('KEY_SALT[%s] = %s', len(key_salt),\n                      binascii.hexlify(key_salt))\n\n        key = PBKDF2(self._upwd, key_salt, Decryptor.dklen, Decryptor.count,\n                     Decryptor.prf)\n        logging.debug('KEY[%s] = %s', len(key), binascii.hexlify(key))\n\n        nonce = self._pwkey_salt[16:]\n        logging.debug('KEY NONCE[%s] = %s', len(nonce),\n                      binascii.hexlify(nonce))\n\n        cipher = AES.new(key, mode=AES.MODE_GCM, nonce=nonce)\n        self._bkey = cipher.decrypt(self._e_perbackupkey)[:32]\n        logging.debug('BKEY[%s] =   %s',\n                      len(self._bkey), binascii.hexlify(self._bkey))\n\n    def crypto_init(self):\n        if self._good:\n            logging.info('crypto_init: already done with success!')\n            return\n\n        if self._type_attch != 3:\n            logging.error('crypto_init: type_attch *should be* 3!')\n            return\n\n        if self._e_perbackupkey and self._pwkey_salt:\n            logging.debug('crypto_init: using version 4.')\n            self.__decrypt_bkey_v4()\n        else:\n            logging.debug('crypto_init: using version 3.')\n            self._bkey = self._upwd\n\n        self._bkey_sha256 = SHA256.new(self._bkey).digest()[:16]\n        logging.debug('SHA256(BKEY)[%s] = %s', len(self._bkey_sha256),\n                      binascii.hexlify(self._bkey_sha256))\n\n        # [TBR][TODO] This check should be refactored.\n        if self._checkMsg:\n            salt = self._checkMsg[32:]\n\n            logging.debug('SALT[%s] = %s', len(salt), binascii.hexlify(salt))\n\n            res = PBKDF2(self._bkey, salt, Decryptor.dklen, Decryptor.count,\n                         Decryptor.prf, hmac_hash_module=None)\n            logging.debug('KEY check expected = %s',\n                          binascii.hexlify(self._checkMsg[:32]))\n            logging.debug('RESULT = %s', binascii.hexlify(res))\n\n            if res == self._checkMsg[:32]:\n                logging.info('OK, backup key is correct!')\n                self._good = True\n            else:\n                logging.error('KO, backup key is wrong!')\n                self._good = False\n        else:\n            logging.warning('Empty CheckMsg! Cannot check backup password!')\n            logging.warning('Assuming the provided password is correct...')\n            self._good = True\n\n    def decrypt_package(self, dec_material, data):\n        if not self._good:\n            logging.warning('well, it is hard to decrypt with a wrong key.')\n\n        if not dec_material.encMsgV3:\n            logging.error('cannot decrypt with an empty encMsgV3!')\n            return None\n\n        salt = dec_material.encMsgV3[:32]\n        counter_iv = dec_material.encMsgV3[32:]\n\n        key = PBKDF2(self._bkey, salt, Decryptor.dklen, Decryptor.count,\n                     Decryptor.prf, hmac_hash_module=None)\n\n        counter_obj = Counter.new(128, initial_value=int.from_bytes(\n            counter_iv, byteorder='big'), little_endian=False)\n\n        decryptor = AES.new(key, mode=AES.MODE_CTR, counter=counter_obj)\n        return decryptor.decrypt(data)\n\n    def decrypt_large_package(self, dec_material, entry):\n        if not self._good:\n            logging.warning('well, it is hard to decrypt with a wrong key.')\n\n        if not dec_material.encMsgV3:\n            logging.error('cannot decrypt with an empty encMsgV3!')\n            return None\n\n        salt = dec_material.encMsgV3[:32]\n        counter_iv = dec_material.encMsgV3[32:]\n\n        key = PBKDF2(self._bkey, salt, Decryptor.dklen, Decryptor.count,\n                     Decryptor.prf, hmac_hash_module=None)\n\n        counter_obj = Counter.new(128, initial_value=int.from_bytes(\n            counter_iv, byteorder='big'), little_endian=False)\n\n        decryptor = AES.new(key, mode=AES.MODE_CTR, counter=counter_obj)\n        data_len = entry.stat().st_size\n        with open(entry, 'rb') as entry_fd:\n            for x in range(0, data_len, self.chunk_size):\n                logging.debug('decrypting chunk %d of %s', x, entry)\n                data = entry_fd.read(self.chunk_size)\n                yield decryptor.decrypt(data)\n\n    def decrypt_file(self, dec_material, data):\n        if not self._good:\n            logging.warning('well, it is hard to decrypt with a wrong key.')\n\n        if not dec_material.iv:\n            logging.error('cannot decrypt with an empty iv!')\n            return None\n\n        counter_obj = Counter.new(\n            128,\n            initial_value=int.from_bytes(dec_material.iv, byteorder='big'),\n            little_endian=False)\n\n        decryptor = AES.new(\n            self._bkey_sha256, mode=AES.MODE_CTR, counter=counter_obj)\n        return decryptor.decrypt(data)\n\n# --- DecryptInfo -------------------------------------------------------------\n\nclass DecryptInfo:\n    '''It provides the information and keys to decrypt files.'''\n\n    class info_type(enum.Enum):\n        FILE = 1\n        MEDIA = 2\n        MULTIMEDIA = 3\n        SYSTEM_DATA = 4\n        SYSTEM_DATA_FOLDER = 5\n\n    def __init__(self):\n        self._decryptor = None\n        self._file_info = {}\n        self._media_info = {}\n        self._multimedia_file = {}\n        self._system_data_info = {}\n        self._system_data_folder_info = {}\n\n    def search_decrypt_material(self, key):\n        assert key\n        decrypt_material = None\n        if key in self._file_info:\n            decrypt_material = self._file_info[key]\n        elif key in self._media_info:\n            decrypt_material = self._media_info[key]\n        elif key in self._multimedia_file:\n            decrypt_material = self._multimedia_file[key]\n        elif key in self._system_data_info:\n            decrypt_material = self._system_data_info[key]\n        elif key in self._system_data_folder_info:\n            decrypt_material = self._system_data_folder_info[key]\n        else:\n            pass\n        return decrypt_material\n\n    def get_decrypt_material(self, key, di_type, search=False):\n        assert key\n        assert isinstance(di_type, DecryptInfo.info_type)\n        decrypt_material = None\n        logging.debug('searching key [%s] of %s', key, di_type)\n        if di_type is DecryptInfo.info_type.FILE:\n            if key in self._file_info:\n                decrypt_material = self._file_info[key]\n        elif di_type is DecryptInfo.info_type.MEDIA:\n            if key in self._media_info:\n                decrypt_material = self._media_info[key]\n        elif di_type is DecryptInfo.info_type.MULTIMEDIA:\n            if key in self._multimedia_file:\n                decrypt_material = self._multimedia_file[key]\n        elif di_type is DecryptInfo.info_type.SYSTEM_DATA:\n            if key in self._system_data_info:\n                decrypt_material = self._system_data_info[key]\n        elif di_type is DecryptInfo.info_type.SYSTEM_DATA_FOLDER:\n            if key in self._system_data_folder_info:\n                decrypt_material = self._system_data_folder_info[key]\n        else:\n            logging.critical('Unknown decrypt info type %s', di_type)\n            return None\n        if decrypt_material is None:\n            if search is True:\n                logging.debug('unable to get [%s], trying on all types', key)\n                decrypt_material = self.search_decrypt_material(key)\n        if decrypt_material is None:\n            logging.debug('unable to get [%s] in decrypt material!', key)\n        else:\n            logging.debug('decrypt info  [%s] found', key)\n        return decrypt_material\n\n    @property\n    def decryptor(self):\n        return self._decryptor\n\n    @decryptor.setter\n    def decryptor(self, new_decryptor):\n        assert new_decryptor\n        new_decryptor.crypto_init()\n        if not new_decryptor.good:\n            logging.warning('Setting a new decryptor which is not working!')\n        self._decryptor = new_decryptor\n\n    @property\n    def has_media(self):\n        '''Checks if media categories decryption info is provided.'''\n        return bool(self._media_info)\n\n    def add_file_info(self, decrypt_material):\n        '''Add the decryption material for a BackupFileModuleInfo entry to the\n           proper internal object.\n        '''\n        assert decrypt_material.type_name == 'BackupFileModuleInfo'\n        if decrypt_material.name in self._file_info:\n            logging.error('Duplicate file info, cannot insert %s',\n                          decrypt_material.name)\n            return\n        self._file_info[decrypt_material.name] = decrypt_material\n\n    def add_media_info(self, decrypt_material):\n        '''Add the decryption material for a BackupFileModuleInfo_Media\n           entry to the proper internal object.\n        '''\n        assert decrypt_material.type_name == 'BackupFileModuleInfo_Media'\n        if decrypt_material.name in self._file_info:\n            logging.error('Duplicate media info, cannot insert %s',\n                          decrypt_material.name)\n            return\n        self._media_info[decrypt_material.name] = decrypt_material\n\n    def add_multimedia_file(self, decrypt_material):\n        '''Add the decryption material for a multimedia file\n           entry to the proper internal object.\n        '''\n        assert decrypt_material.type_name == 'Multimedia'\n        if decrypt_material.path in self._multimedia_file:\n            logging.error('Duplicate multimedia file path, cannot insert %s',\n                          decrypt_material.path)\n            return\n        # Note path is used for the key, not name.\n        self._multimedia_file[decrypt_material.path] = decrypt_material\n\n    def add_system_data_info(self, decrypt_material):\n        '''Add the decryption material for a BackupFileModuleInfo_SystemData\n           entry to the proper internal object. It handles the scenario where\n           the entry is related to folders, double copying the material.\n        '''\n        assert decrypt_material.type_name == 'BackupFileModuleInfo_SystemData'\n        name = decrypt_material.name\n        if name in self._system_data_info:\n            logging.error('Duplicated system data info, cannot insert %s',\n                          decrypt_material.name)\n            return\n        self._system_data_info[decrypt_material.name] = decrypt_material\n        copyfilepath = decrypt_material.copy_file_path\n        if copyfilepath and copyfilepath.startswith('/'):\n            if copyfilepath in self._system_data_folder_info:\n                logging.error('Duplicated system data folder info, cannot '\n                              'insert %s', copyfilepath)\n            else:\n                self._system_data_folder_info[copyfilepath] = decrypt_material\n\n    def dump(self):\n        dump = 'DecryptInfo dump ---\\n'\n        dump += 'password:{}, '.format(self._decryptor.password)\n        dump += 'good:{}, '.format(self._decryptor.good)\n        dump += 'has media:{}, '.format(self.has_media)\n        dump += 'file info:{}, '.format(len(self._file_info))\n        dump += 'media info:{}, '.format(len(self._media_info))\n        dump += 'multimedia file:{}, '.format(len(self._multimedia_file))\n        dump += 'system data info:{}, '.format(len(self._system_data_info))\n        dump += 'system folder data info:{}\\n'.format(len(\n            self._system_data_folder_info))\n\n        dump += 'DUMPING FILE INFO ITEMS\\n'\n        for _, ev in self._file_info.items():\n            dump += ev.dump()\n        dump += 'DUMPING MEDIA INFO ITEMS\\n'\n        for _, ev in self._media_info.items():\n            dump += ev.dump()\n        dump += 'DUMPING MULTIMEDIA FILE ITEMS\\n'\n        for _, ev in self._multimedia_file.items():\n            dump += ev.dump()\n        dump += 'DUMPING SYSTEM DATA INFO ITEMS\\n'\n        for _, ev in self._system_data_info.items():\n            dump += ev.dump()\n        dump += 'DUMPING SYSTEM DATA FOLDER INFO ITEMS\\n'\n        for _, ev in self._system_data_folder_info.items():\n            dump += ev.dump()\n        return dump\n\n# --- xml_get_column_value ----------------------------------------------------\n\ndef xml_get_column_value(xml_node):\n    '''Helper to get xml 'column' value.'''\n    child = xml_node.firstChild\n    column_value = None\n    try:\n        if child.tagName == 'value':\n            if child.hasAttribute('String'):\n                column_value = str(child.getAttribute('String'))\n            elif child.hasAttribute('Integer'):\n                column_value = int(child.getAttribute('Integer'))\n            elif child.hasAttribute('Null'):\n                column_value = None\n            else:\n                logging.warning('xml column value: unknown value attribute.')\n        else:\n            logging.warning('xml_get_column_value: entry has no values!')\n    except:\n        logging.warning('*exception*, xml_get_column_value, child: %s', child)\n\n    return column_value\n\n# --- parse_backup_files_type_info --------------------------------------------\n\ndef parse_backup_files_type_info(decryptor, xml_entry):\n    for entry in xml_entry.getElementsByTagName('column'):\n        name = entry.getAttribute('name')\n        if name == 'e_perbackupkey':\n            decryptor.e_perbackupkey = xml_get_column_value(entry)\n        elif name == 'pwkey_salt':\n            decryptor.pwkey_salt = xml_get_column_value(entry)\n        elif name == 'type_attch':\n            decryptor.type_attch = xml_get_column_value(entry)\n        elif name == 'checkMsg':\n            decryptor.checkMsg = xml_get_column_value(entry)\n\n# --- parse_backup_file_module_info -------------------------------------------\n\ndef parse_backup_file_module_info(xml_entry):\n    decm = DecryptMaterial(xml_entry.getAttribute('table'))\n    for entry in xml_entry.getElementsByTagName('column'):\n        tag_name = entry.getAttribute('name')\n        if tag_name == 'encMsgV3':\n            decm.encMsgV3 = xml_get_column_value(entry)\n        elif tag_name == 'name':\n            decm.name = xml_get_column_value(entry)\n        elif tag_name == 'copyFilePath':\n            decm.copy_file_path = xml_get_column_value(entry)\n        elif tag_name == 'checkMsgV3':\n            # [TBR][TODO] Reverse this double sized checkMsgV3.\n            pass\n\n    if decm.do_check() is False:\n        logging.warning('Decryption material checks failed for %s, type %s',\n                        decm.name, decm.type_name)\n    return decm\n\n# --- parse_info_xml ----------------------------------------------------------\n\ndef parse_info_xml(filepath, password):\n    '''Parses the info.xml backup file.\n       Creates and returns a DecryptInfo object.\n    '''\n    logging.info('Parsing file %s', filepath.absolute())\n    info_dom = None\n    with filepath.open('r', encoding='utf-8') as info_xml:\n        info_dom = xml.dom.minidom.parse(info_xml)\n\n    if info_dom.firstChild.tagName != 'info.xml':\n        logging.error('First tag should be \\'info.xml\\', not %s',\n                      info_dom.firstChild.tagName)\n        return None\n\n    dec_info = DecryptInfo()\n\n    for entry in info_dom.getElementsByTagName('row'):\n        title = entry.getAttribute('table')\n        if title == 'BackupFileModuleInfo':\n            dec_info.add_file_info(parse_backup_file_module_info(entry))\n        elif title == 'BackupFileModuleInfo_SystemData':\n            dec_info.add_system_data_info(parse_backup_file_module_info(entry))\n        elif title == 'BackupFileModuleInfo_Media':\n            dec_info.add_media_info(parse_backup_file_module_info(entry))\n        elif title == 'BackupFilesTypeInfo':\n            logging.debug('Parsing BackupFilesTypeInfo')\n            decryptor = Decryptor(password)\n            parse_backup_files_type_info(decryptor, entry)\n            dec_info.decryptor = decryptor\n        elif title == 'BackupFileModuleInfo_Contact':\n            logging.debug('Ignoring BackupFileModuleInfo_Contact entry')\n        elif title == 'HeaderInfo':\n            logging.debug('Ignoring HeaderInfo entry.')\n        elif title == 'BackupFilePhoneInfo':\n            logging.debug('Ignoring BackupFilePhoneInfo entry')\n        elif title == 'BackupFileVersionInfo':\n            logging.debug('Ignoring BackupFileVersionInfo entry')\n        else:\n            logging.warning('Unknown entry in info.xml: %s', title)\n\n    return dec_info\n\n# --- parse_generic_xml -------------------------------------------------------\n\ndef parse_generic_xml(xml_file_path, decrypt_info):\n    '''Parses a generic XML file, which contain single media (video, documents,\n       pictures, etc.) decryption material.\n    '''\n    xml_dom = None\n    logging.info('parsing xml file %s', xml_file_path.name)\n\n    with xml_file_path.open('r', encoding='utf-8') as xml_file:\n        xml_dom = xml.dom.minidom.parse(xml_file)\n\n    if xml_dom.firstChild.tagName != 'Multimedia':\n        logging.error('First tag should be \\'Multimedia\\', not %s',\n                      xml_dom.firstChild.tagName)\n        return\n\n    for entry in xml_dom.getElementsByTagName('File'):\n        path = entry.getElementsByTagName('Path')[0].firstChild.data\n        iv = entry.getElementsByTagName('Iv')[0].firstChild.data\n        if path and iv:\n            if os.name != 'nt':\n                path = path.replace('\\\\', '/')\n            decrypt_material = DecryptMaterial('Multimedia')\n            decrypt_material.path = path.lstrip('\\\\').lstrip('/')\n            decrypt_material.iv = iv\n            decrypt_info.add_multimedia_file(decrypt_material)\n        else:\n            logging.warning('No path and/or iv for %s!', entry)\n\n# --- tar_extract_win ---------------------------------------------------------\n\ndef tar_extract_win(tar_obj, dest_dir):\n    win_illegal = ':<>|\"?*\\n'\n    table = str.maketrans(win_illegal, '_' * len(win_illegal))\n    for member in tar_obj.getmembers():\n        if member.isdir():\n            new_dir = dest_dir.joinpath(member.path.translate(table))\n            new_dir.mkdir(parents=True, exist_ok=True)\n        else:\n            dest_file = dest_dir.joinpath(member.path.translate(table))\n            try:\n                with open(dest_file, \"wb\") as fout:\n                    fout.write(tarfile.ExFileObject(tar_obj, member).read())\n            except FileNotFoundError:\n                logging.warning('unable to extract %s', dest_file)\n\n# --- decrypt_entry -----------------------------------------------------------\n\ndef decrypt_entry(decrypt_info, entry, type_info, search=False):\n    cleartext = None\n    skey = entry.stem\n    decrypt_material = decrypt_info.get_decrypt_material(skey, type_info,\n                                                         search)\n    if decrypt_material:\n        cleartext = decrypt_info.decryptor.decrypt_package(\n            decrypt_material, entry.read_bytes())\n    else:\n        logging.warning('entry %s has no decrypt material!', skey)\n    return cleartext\n\n# --- decrypt_large_entry -----------------------------------------------------\n\ndef decrypt_large_entry(decrypt_info, entry, type_info, search=False):\n    skey = entry.stem\n    decrypt_material = decrypt_info.get_decrypt_material(skey, type_info,\n                                                         search)\n    if decrypt_material:\n        for x in decrypt_info.decryptor.decrypt_large_package(\n                decrypt_material, entry):\n            yield x\n    else:\n        logging.warning('entry %s has no decrypt material!', skey)\n\n# --- decrypt_files_in_root ---------------------------------------------------\n\ndef decrypt_files_in_root(decrypt_info, path_in, path_out, expandtar):\n\n    data_apk_dir = path_out.absolute().joinpath('data/app')\n    data_app_dir = path_out.absolute().joinpath('data/data')\n    #data_app_dir.mkdir(0o755, parents=True, exist_ok=True)\n    data_unk_dir = path_out.absolute().joinpath('unknown')\n\n    for entry in path_in.glob('*'):\n        if entry.is_dir():\n            continue\n        cleartext = None\n        extension = entry.suffix.lower()\n\n        # XML files in the 'root' were already managed.\n        if extension == '.xml':\n            continue\n        logging.info('working on %s', entry.name)\n\n        if extension == '.apk':\n            dest_file = data_apk_dir.joinpath(entry.name + '-1')\n            dest_file.mkdir(0o755, parents=True, exist_ok=True)\n            dest_file = dest_file.joinpath('base.apk')\n            dest_file.write_bytes(entry.read_bytes())\n\n        elif extension == '.db':\n            cleartext = decrypt_entry(decrypt_info, entry,\n                                      DecryptInfo.info_type.SYSTEM_DATA,\n                                      search=True)\n            if cleartext:\n                dest_file = data_app_dir.joinpath(entry.name)\n                dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n                dest_file.write_bytes(cleartext)\n            else:\n                logging.warning('unable to decrypt entry %s', entry.name)\n\n        elif extension == '.tar' and entry.stat().st_size < MAX_FILE_SIZE:\n            cleartext = decrypt_entry(decrypt_info, entry,\n                                      DecryptInfo.info_type.FILE)\n            if cleartext and expandtar:\n                with tarfile.open(fileobj=io.BytesIO(cleartext)) as tar_data:\n                    if os.name == 'nt':\n                        tar_extract_win(tar_data, data_app_dir)\n                    else:\n                        tar_data.extractall(path=data_app_dir)\n            elif cleartext:\n                logging.info('Not expanding TAR file %s', entry.name)\n                dest_file = data_app_dir.joinpath(entry.name)\n                dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n                dest_file.write_bytes(cleartext)\n            else:\n                logging.warning('unable to decrypt entry %s', entry.name)\n\n        elif extension == '.tar' and entry.stat().st_size >= MAX_FILE_SIZE:\n            logging.info('Decrypting LARGE entry %s', entry.name)\n            logging.info('TAR will not be expanded')\n            dest_file = data_app_dir.joinpath(entry.name)\n            dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n            with open(dest_file, 'wb') as fd:\n                for x in decrypt_large_entry(decrypt_info, entry,\n                                             DecryptInfo.info_type.FILE):\n                    fd.write(x)\n\n        else:\n            logging.warning('entry %s unmanged, copying it', entry.name)\n            dest_file = data_unk_dir.joinpath(entry.name)\n            dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n            dest_file.write_bytes(entry.read_bytes())\n\n# --- decrypt_files_in_folder -------------------------------------------------\n\ndef decrypt_files_in_folder(decrypt_info, folder, path_out, expandtar):\n\n    folder_to_media_type = {'movies': 'video', 'pictures': 'photo',\n                            'audios': 'audio', }\n\n    media_out_dir = path_out.absolute().joinpath('storage')\n    media_unk_dir = path_out.absolute().joinpath('unknown')\n\n    # Dirty 'hack' to see if an XML file is inside the folder with IVs\n    # needed to decrypt .enc files... Not tested for side effects.\n    xml_files = folder.glob('*.xml')\n    for entry in xml_files:\n        parse_generic_xml(entry, decrypt_info)\n\n    for entry in folder.glob('**/*'):\n        if entry.is_dir():\n            continue\n\n        logging.info('working on [%s]', entry.name)\n        extension = entry.suffix.lower()\n\n        cleartext = None\n\n        if extension == '.enc':\n            skey = str(entry.relative_to(folder).with_suffix(''))\n            decrypt_material = decrypt_info.get_decrypt_material(\n                skey, DecryptInfo.info_type.MULTIMEDIA)\n            if decrypt_material:\n                cleartext = decrypt_info.decryptor.decrypt_file(\n                    decrypt_material, entry.read_bytes())\n\n            if cleartext and decrypt_material:\n                tmp_path = decrypt_material.path.lstrip('/').lstrip('\\\\')\n                dest_file = path_out.joinpath(tmp_path)\n                dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n                dest_file.write_bytes(cleartext)\n                continue\n\n        decrypt_material = decrypt_info.get_decrypt_material(\n            folder.name, DecryptInfo.info_type.MEDIA)\n        if not decrypt_material:\n            # Some folders share a common type even if with different names.\n            if folder.name in folder_to_media_type:\n                decrypt_material = decrypt_info.get_decrypt_material(\n                    folder_to_media_type[folder.name],\n                    DecryptInfo.info_type.MEDIA)\n        if decrypt_material:\n            cleartext = decrypt_info.decryptor.decrypt_package(\n                decrypt_material, entry.read_bytes())\n            if cleartext:\n                dest_file = media_out_dir.joinpath(entry.relative_to(folder))\n                dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n                dest_file.write_bytes(cleartext)\n                continue\n\n        skey = '/' +  str(entry.relative_to(folder).parent)\n        decrypt_material = decrypt_info.get_decrypt_material(\n            skey, DecryptInfo.info_type.SYSTEM_DATA_FOLDER)\n        if decrypt_material:\n            cleartext = decrypt_info.decryptor.decrypt_package(\n                decrypt_material, entry.read_bytes())\n            if cleartext:\n                dest_file = media_out_dir.joinpath(entry.relative_to(folder))\n                dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n                if entry.suffix.lower() == '.tar' and expandtar:\n                    with tarfile.open(fileobj=io.BytesIO(cleartext)) as tdata:\n                        if os.name == 'nt':\n                            tar_extract_win(tdata, dest_file.parent)\n                        else:\n                            tdata.extractall(path=dest_file.parent)\n                # Double copy here the tar and the extracted one, no overwrite.\n                if dest_file.exists():\n                    new_name = str(folder.name) + '_' + str(dest_file.name)\n                    dest_file = dest_file.parent.joinpath(new_name)\n                    dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n                dest_file.write_bytes(cleartext)\n                continue\n\n        if cleartext is None:\n            logging.warning('decrypting [%s] failed, copying it', entry.name)\n            dest_file = media_unk_dir.joinpath(entry.name)\n            dest_file.parent.mkdir(0o755, parents=True, exist_ok=True)\n            dest_file.write_bytes(entry.read_bytes())\n\n\n# --- decrypt_backup ----------------------------------------------------------\n\ndef decrypt_backup(password, path_in, path_out, expandtar):\n\n    decrypt_info = parse_info_xml(path_in.joinpath('info.xml'), password)\n    if not decrypt_info:\n        logging.critical('failed to parse info.xml')\n        return\n\n    if not decrypt_info.decryptor.good:\n        logging.critical('Decryptor checks failed. Unable to decrypt')\n        return\n\n    xml_files = path_in.glob('*.xml')\n    for entry in xml_files:\n        if entry.name != 'info.xml' and not entry.name.startswith('._'):\n            parse_generic_xml(entry, decrypt_info)\n\n    logging.debug(decrypt_info.dump())\n\n    decrypt_files_in_root(decrypt_info, path_in, path_out, expandtar)\n\n    for entry in path_in.glob('*'):\n        if entry.is_dir():\n            decrypt_files_in_folder(decrypt_info, entry, path_out, expandtar)\n\n# --- decrypt_media -----------------------------------------------------------\n\ndef decrypt_media(password, path_in, path_out, expandtar):\n\n    # [TODO][TBR] Should parse media.db sqlite.\n\n    decrypt_info = None\n    subfolder = None\n    for entry in path_in.glob('**/info.xml'):\n        decrypt_info = parse_info_xml(entry, password)\n        subfolder = entry.parent\n\n    if decrypt_info is None or subfolder is None:\n        logging.error('unable to find or parse info.xml in media folder!')\n        return\n\n    if not decrypt_info.decryptor.good:\n        logging.critical('Decryptor checks failed. Unable to decrypt')\n        return\n\n    logging.debug(decrypt_info.dump())\n\n    for entry in subfolder.glob('*'):\n        if entry.is_dir():\n            decrypt_files_in_folder(decrypt_info, entry, path_out, expandtar)\n\n# --- main --------------------------------------------------------------------\n\ndef main(password, backup_path_in, dest_path_out, expandtar, writable):\n\n    logging.info('searching backup in [%s]', backup_path_in)\n\n    files_folder = None\n    if backup_path_in.joinpath('info.xml').exists():\n        files_folder = backup_path_in\n    else:\n        if backup_path_in.joinpath('backupFiles1').is_dir():\n            files_folder = backup_path_in.joinpath('backupFiles1')\n            info_xml = next(files_folder.glob('**/info.xml'), None)\n            if info_xml:\n                files_folder = info_xml.parent\n            else:\n                logging.error('Unable to find info.xml in backupFiles1!')\n                return\n        else:\n            logging.error('No backup1 folder nor info.xml file found!')\n            return\n\n    if files_folder:\n        logging.info('got info.xml, going to decrypt backup files')\n        decrypt_backup(password, files_folder, dest_path_out, expandtar)\n\n    media_folder = None\n    if backup_path_in.joinpath('media').is_dir():\n        logging.info('got media folder, going to decrypt media files')\n        media_folder = backup_path_in.joinpath('media')\n    else:\n        logging.info('No media folder found.')\n\n    if media_folder:\n        decrypt_media(password, media_folder, dest_path_out, expandtar)\n\n    if writable:\n        logging.info('Not setting read-only on decrypted files')\n    else:\n        logging.info('setting all decrypted files to read-only')\n        for entry in dest_path_out.glob('**/*'):\n            # Set read-only permission if entry is a file.\n            if os.path.isfile(entry):\n                os.chmod(entry, 0o444)\n            # *nix directories require execute permission to read/traverse\n            elif os.path.isdir(entry):\n                os.chmod(entry, 0o555)\n\n\n# --- entry point and parameters checks ---------------------------------------\n\nif __name__ == '__main__':\n\n    if sys.version_info[0] < 3:\n        sys.exit('Python 3 or a more recent version is required.')\n    elif sys.version_info[1] < 7:\n        sys.exit('Python 3.7 or a more recent version is required.')\n\n    description = 'Huawei KoBackup decryptor version {}'.format(VERSION)\n    parser = argparse.ArgumentParser(description=description)\n    parser.add_argument('password', help='user password for the backup')\n    parser.add_argument('backup_path', help='backup folder')\n    parser.add_argument('dest_path', help='decrypted backup folder')\n    parser.add_argument('-e', '--expandtar', action='store_true',\n                        help='expand tar files')\n    parser.add_argument('-w', '--writable', action='store_true',\n                        help='do not set RO pemission on decrypted data')\n    parser.add_argument('-v', '--verbose', action='count',\n                        help='verbose level, -v to -vvv')\n    args = parser.parse_args()\n\n    log_level = logging.CRITICAL\n    if not args.verbose:\n        log_level = logging.ERROR\n    elif args.verbose == 1:\n        log_level = logging.WARNING\n    elif args.verbose == 2:\n        log_level = logging.INFO\n    elif args.verbose >= 3:\n        log_level = logging.DEBUG\n\n    logging.basicConfig(level=log_level)\n\n    user_password = args.password.encode('utf-8')\n\n    backup_path = pathlib.Path(args.backup_path)\n    if not backup_path.is_dir():\n        sys.exit('Backup folder does not exist!')\n\n    dest_path = pathlib.Path(args.dest_path)\n    if dest_path.is_dir():\n        sys.exit('Destination folder already exists!')\n\n    # Make directory with read and execute permission (=read and traverse)\n    dest_path.mkdir(0o755, parents=True)\n\n    main(user_password, backup_path, dest_path, args.expandtar, args.writable)\n"
  },
  {
    "path": "requirements.txt",
    "content": "pycryptodome\n"
  },
  {
    "path": "setup.py",
    "content": "# Setup file for compiling the python script with cx_Freeze (https://github.com/anthony-tuininga/cx_Freeze)\n\nfrom cx_Freeze import setup, Executable\n\nexecutables = [\n    Executable('kobackupdec.py')\n]\n\nsetup(name='KoBackupDec',\n# Change build number to the current one\n    version='20200607',\n    description='HiSuite / KoBackup Decryptor',\n    executables=executables\n)\n\n# Compile the python script to an executable with: python setup.py build\n# Build an Windows installation Package with: python setup.py bdist_msi\n"
  }
]