b.liu | e958203 | 2025-04-17 19:18:16 +0800 | [diff] [blame] | 1 | #! /usr/bin/env python3 |
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | |
| 4 | import argparse |
| 5 | import struct |
| 6 | |
| 7 | from binascii import crc32 |
| 8 | from dataclasses import dataclass |
| 9 | from itertools import cycle |
| 10 | from typing import List |
| 11 | |
| 12 | |
| 13 | def xor(data: bytes) -> bytes: |
| 14 | passphrase = "Seek AGREEMENT for the date of completion.\0" |
| 15 | pw = cycle(bytearray(passphrase.encode('ascii'))) |
| 16 | return bytearray(b ^ next(pw) for b in data) |
| 17 | |
| 18 | |
| 19 | def add_fw_header(data: bytes, magic: int, hwid: int, build_id: int, |
| 20 | offsets: List[int]) -> bytes: |
| 21 | unknown_1 = 0x01 |
| 22 | unknown_2 = 0x0000 |
| 23 | unknown_3 = 0x00000000 |
| 24 | unknown_4 = 0x01000000 |
| 25 | file_crc = crc(data, 0) |
| 26 | |
| 27 | header_struct = struct.Struct('>QIBBHIIIIII' + 'I' * len(offsets)) |
| 28 | header_size = header_struct.size |
| 29 | file_size = header_size + len(data) |
| 30 | |
| 31 | header_offsets = map(lambda x: x + header_size, offsets) |
| 32 | |
| 33 | header_data = header_struct.pack(magic, file_size, unknown_1, len(offsets), |
| 34 | unknown_2, hwid, build_id, unknown_3, |
| 35 | build_id, unknown_4, *header_offsets, |
| 36 | file_crc) |
| 37 | return header_data + data |
| 38 | |
| 39 | |
| 40 | def add_file_header(data: bytes, filename: str, build_id: int) -> bytes: |
| 41 | unknown1 = 0x01000000 |
| 42 | unknown2 = 0x00000000 |
| 43 | file_crc = crc(data, 0) |
| 44 | |
| 45 | header_struct = struct.Struct(">16sIIIII") |
| 46 | file_size = header_struct.size + len(data) |
| 47 | |
| 48 | header_data = header_struct.pack(filename.encode('ascii'), file_size, |
| 49 | unknown1, build_id, unknown2, file_crc) |
| 50 | return header_data + data |
| 51 | |
| 52 | |
| 53 | def crc(data: bytes, init_val: int) -> int: |
| 54 | return 0xffffffff ^ (crc32(data, 0xffffffff ^ init_val)) |
| 55 | |
| 56 | |
| 57 | @dataclass |
| 58 | class Partition: |
| 59 | name: str |
| 60 | size: int |
| 61 | |
| 62 | |
| 63 | def main(): |
| 64 | partitions = [ |
| 65 | Partition(name='kernel', size=2048 * 1024), |
| 66 | Partition(name='root', size=9216 * 1024), |
| 67 | Partition(name='userdisk', size=3076 * 1024), |
| 68 | ] |
| 69 | |
| 70 | parser = argparse.ArgumentParser(prog='moxa-encode-fw', |
| 71 | description='MOXA IW firmware encoder') |
| 72 | parser.add_argument('-i', '--input', required=True, type=str, help='Firmware file') |
| 73 | parser.add_argument('-o', '--output', required=True, type=str, help="Output path for encoded firmware file") |
| 74 | parser.add_argument('-m', '--magic', required=True, type=lambda x: int(x,0), help="Magic for firmware header") |
| 75 | parser.add_argument('-d', '--hwid', required=True, type=lambda x: int(x,0), help="Hardware id of device") |
| 76 | parser.add_argument('-b', '--buildid', required=True, type=lambda x: int(x,0), help="Build id of firmware") |
| 77 | args = parser.parse_args() |
| 78 | |
| 79 | with open(args.input, 'rb') as input_file: |
| 80 | firmware = bytearray(input_file.read()) |
| 81 | |
| 82 | offsets = [] |
| 83 | pos_input = 0 |
| 84 | pos_output = 0 |
| 85 | firmware_seg = bytearray() |
| 86 | |
| 87 | for partition in partitions: |
| 88 | part_data = firmware[pos_input:pos_input + partition.size] |
| 89 | |
| 90 | # just to make sure that no partition is empty |
| 91 | if len(part_data) == 0: |
| 92 | part_data = bytearray([0x00]) |
| 93 | |
| 94 | header = add_file_header(part_data, partition.name, args.buildid) |
| 95 | firmware_seg += header |
| 96 | |
| 97 | offsets.append(pos_output) |
| 98 | pos_input += partition.size |
| 99 | pos_output += len(header) |
| 100 | |
| 101 | moxa_firmware = add_fw_header(firmware_seg, args.magic, args.hwid, args.buildid, offsets) |
| 102 | |
| 103 | encrypted = xor(moxa_firmware) |
| 104 | with open(args.output, 'wb') as output_file: |
| 105 | output_file.write(encrypted) |
| 106 | |
| 107 | |
| 108 | if __name__ == '__main__': |
| 109 | main() |