|
| 1 | +#!/usr/bin/python3 |
| 2 | +# Copyright 2025 Collabora Ltd. |
| 3 | +# |
| 4 | +# SPDX-License-Identifier: GPL-2.0+ |
| 5 | +# |
| 6 | +# Author: Arnaud Patard <[email protected]> |
| 7 | +# |
| 8 | +# Notes: |
| 9 | +# to unpack / descramble encrypted parts, the rc4 key is inside u-boot's code. |
| 10 | +# Some information used here are coming from rkdeveloptool, which is GPL-2.0+ |
| 11 | + |
| 12 | +import logging |
| 13 | +import struct |
| 14 | +import time |
| 15 | +from cryptography.hazmat.primitives.ciphers import Cipher |
| 16 | +from cryptography.hazmat.decrepit.ciphers.algorithms import ARC4 |
| 17 | +from crccheck import crc |
| 18 | + |
| 19 | +logger = logging.getLogger("snagrecover") |
| 20 | +from snagrecover.protocols import rockchip |
| 21 | + |
| 22 | +BOOTTAG = b"BOOT" |
| 23 | +LDRTAG = b"LDR " |
| 24 | +TAG_LIST = [ BOOTTAG, LDRTAG ] |
| 25 | +BOOTENTRYSIZE = 57 |
| 26 | +BOOTHEADERENTRYSIZE = 6 |
| 27 | +BOOTHEADERSIZE = 102 |
| 28 | +BOOTHEADERTIMESIZE = 7 |
| 29 | +RC4_KEY = bytearray([124, 78, 3, 4, 85, 5, 9, 7, 45, 44, 123, 56, 23, 13, 23, 17]) |
| 30 | + |
| 31 | +class BootEntryError(Exception): |
| 32 | + pass |
| 33 | + |
| 34 | +class BootEntry(): |
| 35 | + def __init__(self, entry): |
| 36 | + if len(entry) != BOOTENTRYSIZE: |
| 37 | + raise BootEntryError(f"Invalid boot entry size {len(entry)}") |
| 38 | + self.entry = entry |
| 39 | + |
| 40 | + def unpack(self): |
| 41 | + (self.size, self.type) = struct.unpack("<BI", self.entry[:5]) |
| 42 | + self.name = self.entry[5:45].decode('utf-16le') |
| 43 | + (self.data_offset, self.data_size, self.data_delay) = struct.unpack("<III", self.entry[45:57]) |
| 44 | + |
| 45 | + def __str__(self): |
| 46 | + return f"Entry {self.name} (type: {self.type}, size: {self.size}, data offset: {self.data_offset}, data size: {self.data_size}, delay: {self.data_delay})" |
| 47 | + |
| 48 | + |
| 49 | +class BootHeaderEntryError(Exception): |
| 50 | + pass |
| 51 | + |
| 52 | +class BootHeaderEntry(): |
| 53 | + def __init__(self, name, entry): |
| 54 | + self.name = name |
| 55 | + if len(entry) != BOOTHEADERENTRYSIZE: |
| 56 | + raise BootHeaderEntryError(f"Invalid boot header entry size {len(entry)}") |
| 57 | + self.entry = entry |
| 58 | + |
| 59 | + def unpack(self): |
| 60 | + (self.count, self.offset, self.size) = struct.unpack("<BIB", self.entry) |
| 61 | + if self.size != BOOTENTRYSIZE: |
| 62 | + raise BootHeaderError("Invalid boot entry size in the header definition") |
| 63 | + |
| 64 | + def __str__(self): |
| 65 | + return f"Header entry {self.name}: ({self.count}, {self.offset})" |
| 66 | + |
| 67 | +class BootHeaderError(Exception): |
| 68 | + pass |
| 69 | + |
| 70 | +class BootHeader(): |
| 71 | + def __init__(self, header): |
| 72 | + if len(header) != BOOTHEADERSIZE: |
| 73 | + raise BootHeaderError(f"Invalid boot header size {len(header)}") |
| 74 | + self.header = header |
| 75 | + |
| 76 | + def unpack(self): |
| 77 | + offset = 0 |
| 78 | + self.tag = self.header[offset:offset+4] |
| 79 | + offset += 4 |
| 80 | + (self.size, version, self.merge_version) = struct.unpack("<HII", self.header[offset:offset+10]) |
| 81 | + if self.size != BOOTHEADERSIZE: |
| 82 | + raise BootHeaderError("Invalid header size in the header definition") |
| 83 | + # not sure how to exactly parse version/merge_version |
| 84 | + self.maj_ver = version >> 8 |
| 85 | + self.min_ver = version & 0xff |
| 86 | + offset += 10 |
| 87 | + self.releasetime = self.header[offset:offset+BOOTHEADERTIMESIZE] |
| 88 | + offset += BOOTHEADERTIMESIZE |
| 89 | + self.chip = self.header[offset:offset+4][::-1] |
| 90 | + offset += 4 |
| 91 | + self.entry471 = BootHeaderEntry("471", self.header[offset:offset+BOOTHEADERENTRYSIZE]) |
| 92 | + self.entry471.unpack() |
| 93 | + offset += BOOTHEADERENTRYSIZE |
| 94 | + self.entry472 = BootHeaderEntry("472", self.header[offset:offset+BOOTHEADERENTRYSIZE]) |
| 95 | + self.entry472.unpack() |
| 96 | + offset += BOOTHEADERENTRYSIZE |
| 97 | + self.loader = BootHeaderEntry("loader", self.header[offset:offset+BOOTHEADERENTRYSIZE]) |
| 98 | + self.loader.unpack() |
| 99 | + offset += BOOTHEADERENTRYSIZE |
| 100 | + # rc4 = 1: disable rc4 on 471/472. Not sure about rc4 on loader. |
| 101 | + (self.sign, self.rc4) = struct.unpack("<BB", self.header[offset:offset+2]) |
| 102 | + if self.rc4: |
| 103 | + self.rc4 = False |
| 104 | + else: |
| 105 | + self.rc4 = True |
| 106 | + if self.sign == 'S': |
| 107 | + self.sign = True |
| 108 | + else: |
| 109 | + self.sign = False |
| 110 | + |
| 111 | + def __str__(self): |
| 112 | + return f"{self.tag},{self.maj_ver}.{self.min_ver},{self.merge_version},{self.chip},{self.entry471},{self.entry472},{self.loader},sign:{self.sign},enc:{self.rc4}" |
| 113 | + |
| 114 | +class LoaderFileError(Exception): |
| 115 | + pass |
| 116 | + |
| 117 | +class RkCrc32(crc.Crc32Base): |
| 118 | + """CRC-32/ROCKCHIP |
| 119 | + """ |
| 120 | + _names = ('CRC-32/ROCKCHIP') |
| 121 | + _width = 32 |
| 122 | + _poly = 0x04c10db7 |
| 123 | + _initvalue = 0x00000000 |
| 124 | + _reflect_input = False |
| 125 | + _reflect_output = False |
| 126 | + _xor_output = 0 |
| 127 | + |
| 128 | +class LoaderFile(): |
| 129 | + def __init__(self, blob): |
| 130 | + self.blob = blob |
| 131 | + buf = self.blob[0:BOOTHEADERSIZE] |
| 132 | + offset = BOOTHEADERSIZE |
| 133 | + self.header = BootHeader(buf) |
| 134 | + self.header.unpack() |
| 135 | + if self.header.tag not in TAG_LIST: |
| 136 | + raise LoaderFileError(f"Invalid tag {self.header.tag}") |
| 137 | + |
| 138 | + offset = self.header.entry471.offset |
| 139 | + self.entry471 = [] |
| 140 | + for _i in range(self.header.entry471.count): |
| 141 | + self.entry471.append(BootEntry(self.blob[offset:offset+BOOTENTRYSIZE])) |
| 142 | + offset += BOOTENTRYSIZE |
| 143 | + |
| 144 | + offset = self.header.entry472.offset |
| 145 | + self.entry472 = [] |
| 146 | + for _i in range(self.header.entry472.count): |
| 147 | + self.entry472.append(BootEntry(self.blob[offset:offset+BOOTENTRYSIZE])) |
| 148 | + offset += BOOTENTRYSIZE |
| 149 | + |
| 150 | + offset = self.header.loader.offset |
| 151 | + self.loader = [] |
| 152 | + for _i in range(self.header.loader.count): |
| 153 | + self.loader.append(BootEntry(self.blob[offset:offset+BOOTENTRYSIZE])) |
| 154 | + offset += BOOTENTRYSIZE |
| 155 | + crc32 = self.blob[-4:] |
| 156 | + calc_crc32 = RkCrc32.calc(self.blob[:-4]) |
| 157 | + (self.crc32,) = struct.unpack("<I", crc32) |
| 158 | + assert self.crc32 == calc_crc32 |
| 159 | + |
| 160 | + def entry_data(self, name, idx = 0): |
| 161 | + entry = None |
| 162 | + if name == "471": |
| 163 | + entry = self.entry471 |
| 164 | + elif name == "472": |
| 165 | + entry = self.entry472 |
| 166 | + elif name == "loader": |
| 167 | + entry = self.loader |
| 168 | + else: |
| 169 | + raise LoaderFileError(f"Invalid name {name}") |
| 170 | + |
| 171 | + if idx > len(entry): |
| 172 | + raise LoaderFileError(f"Invalid index {idx}. Only has {len(entry)} entries.") |
| 173 | + e = entry[idx] |
| 174 | + e.unpack() |
| 175 | + logger.debug(f"{e}") |
| 176 | + return (self.blob[e.data_offset:e.data_offset+e.data_size], e.data_delay) |
| 177 | + |
| 178 | + def __str__(self): |
| 179 | + return f"{self.header} crc: {self.crc32:02x}" |
| 180 | + |
| 181 | +def rc4_encrypt(fw_blob): |
| 182 | + |
| 183 | + # Round to 4096 block size |
| 184 | + blob_len = len(fw_blob) |
| 185 | + padded_len = (blob_len+4095)//4096 * 4096 |
| 186 | + fw_blob = bytearray(fw_blob) |
| 187 | + fw_blob += bytearray([0]*(padded_len - blob_len)) |
| 188 | + a = ARC4(RC4_KEY) |
| 189 | + c = Cipher(a, mode=None) |
| 190 | + d = c.encryptor() |
| 191 | + obuf = bytearray() |
| 192 | + for i in range(padded_len): |
| 193 | + obuf += d.update(fw_blob[i*512:(i+1)*512]) |
| 194 | + return obuf |
| 195 | + |
| 196 | +def rockchip_run(dev, fw_name, fw_blob): |
| 197 | + rom = rockchip.RochipBootRom(dev) |
| 198 | + |
| 199 | + if fw_name == 'code471': |
| 200 | + logger.info("Downloading code471...") |
| 201 | + blob = rc4_encrypt(fw_blob) |
| 202 | + rom.write_blob(blob, 0x471) |
| 203 | + elif fw_name == 'code472': |
| 204 | + logger.info("Downloading code472...") |
| 205 | + blob = rc4_encrypt(fw_blob) |
| 206 | + rom.write_blob(blob, 0x472) |
| 207 | + else: |
| 208 | + fw = LoaderFile(fw_blob) |
| 209 | + logger.info(f"{fw}") |
| 210 | + for i in range(fw.header.entry471.count): |
| 211 | + logger.info(f"Downloading entry 471 {i}...") |
| 212 | + (data, delay) = fw.entry_data("471", i) |
| 213 | + rom.write_blob(data, 0x471) |
| 214 | + logger.info(f"Sleeping {delay}ms") |
| 215 | + time.sleep(delay / 1000) |
| 216 | + logger.info("Done") |
| 217 | + for i in range(fw.header.entry472.count): |
| 218 | + logger.info(f"Downloading entry 472 {i}...") |
| 219 | + (data, delay) = fw.entry_data("472", i) |
| 220 | + rom.write_blob(data, 0x472) |
| 221 | + logger.info(f"Sleeping {delay}ms") |
| 222 | + time.sleep(delay / 1000) |
| 223 | + logger.info("Done") |
0 commit comments