├── decryptor ├── requirements.txt ├── tests │ ├── resources │ │ ├── fota_package.bin │ │ └── unencrypted_blob.bin │ ├── test_sanity.py │ └── README.md └── airoha_decrypt.py ├── resources └── 010_editor_screenshot.png ├── .github └── workflows │ └── tests.yml ├── LICENSE ├── README.md └── AirohaFirmware.bt /decryptor/requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome -------------------------------------------------------------------------------- /resources/010_editor_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramikg/airoha-firmware-parser/HEAD/resources/010_editor_screenshot.png -------------------------------------------------------------------------------- /decryptor/tests/resources/fota_package.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramikg/airoha-firmware-parser/HEAD/decryptor/tests/resources/fota_package.bin -------------------------------------------------------------------------------- /decryptor/tests/resources/unencrypted_blob.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramikg/airoha-firmware-parser/HEAD/decryptor/tests/resources/unencrypted_blob.bin -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | Test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-python@v5 10 | - name: Install dependencies 11 | run: | 12 | pip install -U pytest 13 | pip install -U -r decryptor/requirements.txt 14 | - name: Run tests 15 | run: | 16 | cd decryptor/tests 17 | pytest 18 | -------------------------------------------------------------------------------- /decryptor/tests/test_sanity.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | KEY = '000102030405060708090a0b0c0d0e0f' 4 | IV = '62633636633839306334636432383763' 5 | ENCRYPTED_FILE = 'resources/fota_package.bin' 6 | DECRYPTED_TARGET_FILE = 'resources/unencrypted_blob.bin' 7 | PADDING_BYTE = b'\xFF' 8 | 9 | 10 | def _assert_file_contents_are_equal(file1, file2): 11 | with open(file1, 'rb') as f1, open(file2, 'rb') as f2: 12 | return f1.read().rstrip(PADDING_BYTE) == f2.read().rstrip(PADDING_BYTE) 13 | 14 | 15 | def test_sanity(): 16 | output_path = '/tmp/decrypted.bin' 17 | 18 | command = [ 19 | 'python', '../airoha_decrypt.py', 20 | f'--key={KEY}', 21 | f'--iv={IV}', 22 | f'--from={ENCRYPTED_FILE}', 23 | f'--to={output_path}' 24 | ] 25 | run_result = subprocess.run(command) 26 | assert run_result.returncode == 0 27 | 28 | assert _assert_file_contents_are_equal(DECRYPTED_TARGET_FILE, output_path) 29 | -------------------------------------------------------------------------------- /decryptor/tests/README.md: -------------------------------------------------------------------------------- 1 | # Sanity test 2 | 3 | A sanity test for CI purposes. 4 | 5 | ## How to build a dummy FOTA package 6 | 7 | Below are the instructions used for building the FOTA package used for testing. 8 | 9 | 1. Download the Airoha Tool Kit (ATK) from [here](https://github.com/npnet/Airoha_AB1585EVK/blob/main/mcu/tools/pc_tool/atk/AB158x_Airoha_Tool_Kit(ATK)_v3.1.6_20220525_144824.7z). 10 | 2. On a Windows PC, open the _FOTA Package Tool_. 11 | 3. Use the sample `flash_download.cfg` provided below. You can find more examples [here](https://github.com/npnet/Airoha_AB1565EVK/tree/main/mcu/tools/config). 12 | 4. Build the FOTA package using the default key & IV. 13 | 14 | ### Sample `flash_download.cfg` 15 | 16 | ``` 17 | general: 18 | config_version : v2.0 19 | platform: AB155x 20 | 21 | main_region: 22 | address_type: physical 23 | rom_list: 24 | - rom: 25 | file: unencrypted_blob.bin 26 | name: ROFS 27 | begin_address: 0x08000000 28 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airoha firmware parser & decryptor 2 | 3 | An _010 Editor_ template for parsing the Airoha firmware format, and a Python script for decrypting the firmware's encrypted part. 4 | 5 | Products using this firmware format include the AirReps (an AirPods clone) and numerous Sony headphones (notably WH-1000XM4 and WH-1000XM5, whose MediaTek chips are rebranded Airoha chips. Additional models may be found in [this](https://github.com/lzghzr/MDR_Proxy) repository). 6 | 7 | The only plaintext strings present in the firmware are "verion_string" (Sony) and "version_string" (AirReps). 8 | 9 | ## Parser usage 10 | 11 | Simply load the template in 010 Editor and run it on your firmware file. 12 | 13 | ![Screenshot](resources/010_editor_screenshot.png) 14 | 15 | To produce the parser I've analyzed most firmware fields by hand, until I've found out that an Airoha evaluation kit is [publicly available](https://github.com/haltsai/Airoha_AB1565EVK) and used it to complete the analysis. 16 | 17 | ## Decryptor usage 18 | 19 | To decrypt and decompress an Airoha firmware package, run: 20 | 21 | ```bash 22 | cd decryptor 23 | pip install -Ur requirements.txt 24 | 25 | python airoha_decrypt.py --key=000102030405060708090a0b0c0d0e0f --iv=62633636633839306334636432383763 --from=fw.encrypted --to=fw.decrypted 26 | ``` 27 | 28 | Additional flags include: 29 | - `--no-decompress`: Do not decompress after decryption. 30 | Note that LZMA decompression should fail if you've provided the wrong key/IV. 31 | - `--no-decrypt`: Do not decrypt. (Useful for compressed non-encrypted files.) 32 | - `--reverse-key-and-iv`: A convenience flag for reversing the bytes in the key and in the IV. 33 | 34 | For the full list, run `python airoha_decrypt.py -h`. -------------------------------------------------------------------------------- /decryptor/airoha_decrypt.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import binascii 3 | import lzma 4 | import os 5 | import struct 6 | 7 | from Crypto.Cipher import AES 8 | 9 | 10 | ENCRYPTED_PART_OFFSET_STRING = '0x1000' 11 | 12 | 13 | class AirohaDecryptInputAndOutputFilesMustBeDifferent(Exception): 14 | pass 15 | 16 | 17 | def _parse_args(): 18 | parser = argparse.ArgumentParser() 19 | 20 | parser.add_argument('--from', dest='_from', metavar='FROM', type=argparse.FileType('rb'), required=True, 21 | help='Encrypted firmware file.') 22 | parser.add_argument('--key', type=binascii.unhexlify, 23 | help='AES-128 key. Hex format.') 24 | parser.add_argument('--iv', type=binascii.unhexlify, 25 | help='A 16-bytes IV for the AES-CBC. Hex format.') 26 | parser.add_argument('--to', required=True, 27 | help='Decrypted & decompressed firmware file.') 28 | parser.add_argument('--offset', type=lambda x: int(x, 0), default=ENCRYPTED_PART_OFFSET_STRING, 29 | help=f'Offset of the encrypted part in the input file. Default is {ENCRYPTED_PART_OFFSET_STRING}.') 30 | parser.add_argument('--no-decompress', action='store_true', 31 | help='Do not decompress the decrypted data.') 32 | parser.add_argument('--no-decrypt', action='store_true', 33 | help='Do not decrypt (useful for compressed non-encrypted files).') 34 | parser.add_argument('--reverse-key-and-iv', action='store_false', 35 | help='Reverse the bytes order in the input key and IV.') 36 | return parser.parse_args() 37 | 38 | 39 | def _decrypt(ciphertext, key, iv): 40 | cipher = AES.new(key, AES.MODE_CBC, iv=iv) 41 | # Note that decrypted data may be padded with 0xFF bytes 42 | return cipher.decrypt(ciphertext) 43 | 44 | 45 | def _decompress(decrypted_data): 46 | # CPython can't handle an initialized size field in the LZMA header, so we set it to -1 47 | fixed_lzma = decrypted_data[:5] + struct.pack(' COMPRESSION_TYPE 2 | { 3 | NONE = 0, 4 | LZMA = 1, 5 | LZMA_AES = 2 6 | }; 7 | 8 | enum INTEGRITY_CHECK_TYPE 9 | { 10 | CRC_32 = 0, 11 | SHA256 = 1, 12 | SHA256_RSA = 2 13 | }; 14 | 15 | enum TLV_TYPE 16 | { 17 | BASIC_INFO = 0x11, 18 | MOVER_INFO = 0x12, 19 | VERSION_INFO = 0x13, 20 | INTEGRITY_VERIFY_INFO = 0x14, 21 | DEVICE_NAME_INFO = 0x20, 22 | DEVICE_TYPE_INFO = 0x21, 23 | IS_NVDM_INCOMPATIBLE_FLAG = 0xF0 24 | }; 25 | 26 | local uint32 g_firmware_offset; 27 | local uint32 g_firmware_size; 28 | 29 | typedef struct { 30 | uint32 source_offset ; 31 | uint32 decompressed_size ; 32 | uint32 dest_offset ; 33 | } Section; 34 | 35 | void VerifySections(Section sections[]) 36 | { 37 | local uint32 number_of_sections = sizeof(sections) / sizeof(Section); 38 | local uint64 section_index; 39 | local uint32 previous_offset; 40 | local uint32 previous_size; 41 | local uint32 current_offset; 42 | for (section_index = 1; section_index < number_of_sections; ++section_index) 43 | { 44 | previous_offset = sections[section_index - 1].source_offset; 45 | previous_size = sections[section_index - 1].decompressed_size; 46 | current_offset = sections[section_index].source_offset; 47 | Assert(previous_offset + previous_size == current_offset, "Invalid section offset."); 48 | } 49 | } 50 | 51 | void VerifyPadding(char buffer[], char value) 52 | { 53 | local uint64 i = 0; 54 | for(i = 0; i < sizeof(buffer); ++i) 55 | { 56 | Assert(buffer[i] == value); 57 | } 58 | } 59 | 60 | typedef struct { 61 | uchar checksum[32]; 62 | } SHA2Checksum; 63 | 64 | typedef struct { 65 | TLV_TYPE type ; 66 | uint16 length; 67 | local string type_string; 68 | switch (type) 69 | { 70 | case BASIC_INFO: 71 | COMPRESSION_TYPE compression_type; 72 | INTEGRITY_CHECK_TYPE integrity_check_type; 73 | Assert(integrity_check_type == 1, "Only SHA256 integrity check is supported."); 74 | uint32 firmware_offset ; 75 | uint32 firmware_size; 76 | Assert(firmware_offset + firmware_size == FileSize()); 77 | 78 | g_firmware_offset = firmware_offset; 79 | g_firmware_size = firmware_size; 80 | type_string = "BASIC_INFO"; 81 | break; 82 | case VERSION_INFO: 83 | char version_string[length]; 84 | 85 | type_string = "VERSION_INFO"; 86 | break; 87 | case MOVER_INFO: 88 | uint32 number_of_sections; 89 | Assert(length == 4 + number_of_sections * 12); 90 | Section sections_table[number_of_sections] ; 91 | VerifySections(sections_table); 92 | 93 | type_string = "MOVER_INFO"; 94 | break; 95 | case INTEGRITY_VERIFY_INFO: 96 | uint32 number_of_checksums; 97 | // Checksum for the decompressed section 98 | SHA2Checksum checksum[number_of_checksums]; 99 | 100 | type_string = "INTEGRITY_VERIFY_INFO"; 101 | break; 102 | case DEVICE_NAME_INFO: 103 | char device_name[length]; 104 | 105 | type_string = "DEVICE_NAME_INFO"; 106 | break; 107 | case DEVICE_TYPE_INFO: 108 | char device_type[length]; 109 | 110 | type_string = "DEVICE_TYPE_INFO"; 111 | break; 112 | case IS_NVDM_INCOMPATIBLE_FLAG: 113 | byte is_nvdm_incompatible; 114 | 115 | type_string = "IS_NVDM_INCOMPATIBLE_FLAG"; 116 | break; 117 | default: 118 | Printf("%d\n", type); 119 | Assert(false, "Unsupported TLV type."); 120 | } 121 | } TLV ; 122 | 123 | typedef struct { 124 | uchar file_checksum[32]; 125 | char padding1[224]; 126 | VerifyPadding(padding1, '\xFF'); 127 | 128 | local uchar calculated_checksum[32]; 129 | ChecksumAlgBytes(CHECKSUM_SHA256, calculated_checksum, FTell(), FileSize() - FTell()); 130 | Assert(!Memcmp(calculated_checksum, file_checksum, Strlen(calculated_checksum)), "Bad file checksum."); 131 | 132 | TLV tlv ; 133 | while (ReadUShort() != 0xFFFF) 134 | { 135 | TLV tlv; 136 | } 137 | 138 | local uint64 number_of_bytes_til_firmware_start = g_firmware_offset - FTell(); 139 | char padding2[number_of_bytes_til_firmware_start]; 140 | VerifyPadding(padding2, '\xFF'); 141 | char firmware[g_firmware_size]; 142 | } AirohaFirmware; 143 | 144 | LittleEndian(); 145 | AirohaFirmware firmware ; 146 | Assert(FTell() == FileSize(), "Part of the file wasn't parsed."); 147 | --------------------------------------------------------------------------------