| 
 | 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 | +from dataclasses import dataclass  | 
 | 19 | + | 
 | 20 | +logger = logging.getLogger("snagrecover")  | 
 | 21 | +from snagrecover.protocols import rockchip  | 
 | 22 | +from snagrecover.utils import BinFileHeader  | 
 | 23 | + | 
 | 24 | +# List generated with a grep on rkbin repository  | 
 | 25 | +NEWIDB_LIST = [ "rk3506", "rk3506b", "rk3528", "rk3562", "rk3566", "rk3568", "rk3576", "rk3583", "rk3588", "rv1103b", "rv1106" ]  | 
 | 26 | + | 
 | 27 | +BOOTTAG = b"BOOT"  | 
 | 28 | +LDRTAG = b"LDR "  | 
 | 29 | +TAG_LIST = [ BOOTTAG, LDRTAG ]  | 
 | 30 | +BOOTENTRYSIZE = 57  | 
 | 31 | +BOOTHEADERENTRYSIZE = 6  | 
 | 32 | +BOOTHEADERSIZE = 102  | 
 | 33 | +BOOTHEADERTIMESIZE = 7  | 
 | 34 | +RC4_KEY = bytearray([124, 78, 3, 4, 85, 5, 9, 7, 45, 44, 123, 56, 23, 13, 23, 17])  | 
 | 35 | + | 
 | 36 | +@dataclass  | 
 | 37 | +class BootEntry(BinFileHeader):  | 
 | 38 | +	size: int  | 
 | 39 | +	type: int  | 
 | 40 | +	name: bytes  | 
 | 41 | +	data_offset: int  | 
 | 42 | +	data_size: int  | 
 | 43 | +	data_delay: int  | 
 | 44 | + | 
 | 45 | +	fmt = "<BI40sIII"  | 
 | 46 | +	class_size = BOOTENTRYSIZE  | 
 | 47 | + | 
 | 48 | +	def __str__(self):  | 
 | 49 | +		name = self.name.decode('utf-16le')  | 
 | 50 | +		return f"Entry {name} (type: {self.type}, size: {self.size}, data offset: {self.data_offset}, data size: {self.data_size}, delay: {self.data_delay})"  | 
 | 51 | + | 
 | 52 | + | 
 | 53 | +@dataclass  | 
 | 54 | +class BootHeaderEntry(BinFileHeader):  | 
 | 55 | +	count: int  | 
 | 56 | +	offset: int  | 
 | 57 | +	size: int  | 
 | 58 | + | 
 | 59 | +	fmt = "<BIB"  | 
 | 60 | +	class_size = BOOTHEADERENTRYSIZE  | 
 | 61 | + | 
 | 62 | + | 
 | 63 | +@dataclass  | 
 | 64 | +class BootReleaseTime(BinFileHeader):  | 
 | 65 | +	year: int  | 
 | 66 | +	month: int  | 
 | 67 | +	day: int  | 
 | 68 | +	hour: int  | 
 | 69 | +	minute: int  | 
 | 70 | +	second: int  | 
 | 71 | + | 
 | 72 | +	fmt = "<HBBBBB"  | 
 | 73 | +	class_size = BOOTHEADERTIMESIZE  | 
 | 74 | + | 
 | 75 | +	def __str__(self):  | 
 | 76 | +		return f"{self.year}/{self.month}/{self.day} {self.hour}:{self.minute}:{self.second}"  | 
 | 77 | + | 
 | 78 | +class LoaderFileError(Exception):  | 
 | 79 | +	def __init__(self, message):  | 
 | 80 | +		self.message = message  | 
 | 81 | +		super().__init__(self.message)  | 
 | 82 | + | 
 | 83 | +	def __str__(self):  | 
 | 84 | +		return f"File format error: {self.message}"  | 
 | 85 | + | 
 | 86 | +@dataclass  | 
 | 87 | +class BootHeader(BinFileHeader):  | 
 | 88 | +	tag: bytes  | 
 | 89 | +	size: int  | 
 | 90 | +	version: int  | 
 | 91 | +	merge_version: int  | 
 | 92 | +	releasetime: BootReleaseTime  | 
 | 93 | +	chip: bytes  | 
 | 94 | +	entry471: BootHeaderEntry  | 
 | 95 | +	entry472: BootHeaderEntry  | 
 | 96 | +	loader: BootHeaderEntry  | 
 | 97 | +	sign: int  | 
 | 98 | +	# 1 : disable rc4  | 
 | 99 | +	rc4: int  | 
 | 100 | +	reserved: bytes  | 
 | 101 | + | 
 | 102 | +	fmt = f"<4sHII{BOOTHEADERTIMESIZE}s4s{BOOTHEADERENTRYSIZE}s{BOOTHEADERENTRYSIZE}s{BOOTHEADERENTRYSIZE}sBB57s"  | 
 | 103 | +	class_size = BOOTHEADERSIZE  | 
 | 104 | + | 
 | 105 | +	def __post_init__(self):  | 
 | 106 | +		if self.tag not in TAG_LIST:  | 
 | 107 | +			raise LoaderFileError(f"Invalid tag {self.header.tag}")  | 
 | 108 | +		# not sure how to exactly parse version/merge_version  | 
 | 109 | +		self.maj_ver = self.version >> 8  | 
 | 110 | +		self.min_ver = self.version & 0xff  | 
 | 111 | +		self.releasetime = BootReleaseTime.read(self.releasetime, 0)  | 
 | 112 | +		# the code should possible check that the soc_model in cfg is matching  | 
 | 113 | +		# this information but a mapping is needed.  | 
 | 114 | +		self.chip = self.chip[::-1]  | 
 | 115 | +		self.entry471 = BootHeaderEntry.read(self.entry471, 0)  | 
 | 116 | +		self.entry472 = BootHeaderEntry.read(self.entry472, 0)  | 
 | 117 | +		self.loader = BootHeaderEntry.read(self.loader, 0)  | 
 | 118 | +		if self.rc4:  | 
 | 119 | +			self.rc4 = False  | 
 | 120 | +		else:  | 
 | 121 | +			self.rc4 = True  | 
 | 122 | +		if self.sign == 'S':  | 
 | 123 | +			self.sign = True  | 
 | 124 | +		else:  | 
 | 125 | +			self.sign = False  | 
 | 126 | + | 
 | 127 | +	def __str__(self):  | 
 | 128 | +		return f"{self.tag}, {self.size} ,{self.maj_ver}.{self.min_ver}, 0x{self.merge_version:0x}, {self.releasetime}, {self.chip}, {self.entry471}, {self.entry472}, {self.loader}, sign: {self.sign}, enc: {self.rc4}"  | 
 | 129 | + | 
 | 130 | +class RkCrc32(crc.Crc32Base):  | 
 | 131 | +	"""CRC-32/ROCKCHIP  | 
 | 132 | +	"""  | 
 | 133 | +	_names = ('CRC-32/ROCKCHIP')  | 
 | 134 | +	_width = 32  | 
 | 135 | +	_poly = 0x04c10db7  | 
 | 136 | +	_initvalue = 0x00000000  | 
 | 137 | +	_reflect_input = False  | 
 | 138 | +	_reflect_output = False  | 
 | 139 | +	_xor_output = 0  | 
 | 140 | + | 
 | 141 | +class LoaderFile():  | 
 | 142 | +	def __init__(self, blob):  | 
 | 143 | +		self.blob = blob  | 
 | 144 | +		offset = BOOTHEADERSIZE  | 
 | 145 | +		self.header = BootHeader.read(self.blob, 0)  | 
 | 146 | + | 
 | 147 | +		offset = self.header.entry471.offset  | 
 | 148 | +		self.entry471 = []  | 
 | 149 | +		for _i in range(self.header.entry471.count):  | 
 | 150 | +			entry = BootEntry.read(self.blob, offset)  | 
 | 151 | +			self.entry471.append(entry)  | 
 | 152 | +			offset += BOOTENTRYSIZE  | 
 | 153 | + | 
 | 154 | +		offset = self.header.entry472.offset  | 
 | 155 | +		self.entry472 = []  | 
 | 156 | +		for _i in range(self.header.entry472.count):  | 
 | 157 | +			entry = BootEntry.read(self.blob, offset)  | 
 | 158 | +			self.entry472.append(entry)  | 
 | 159 | +			offset += BOOTENTRYSIZE  | 
 | 160 | + | 
 | 161 | +		offset = self.header.loader.offset  | 
 | 162 | +		self.loader = []  | 
 | 163 | +		for _i in range(self.header.loader.count):  | 
 | 164 | +			entry = BootEntry.read(self.blob, offset)  | 
 | 165 | +			self.loader.append(entry)  | 
 | 166 | +			offset += BOOTENTRYSIZE  | 
 | 167 | +		crc32 = self.blob[-4:]  | 
 | 168 | +		calc_crc32 = RkCrc32.calc(self.blob[:-4])  | 
 | 169 | +		(self.crc32,) = struct.unpack("<I", crc32)  | 
 | 170 | +		assert self.crc32 == calc_crc32  | 
 | 171 | + | 
 | 172 | +	def entry_data(self, name, idx = 0):  | 
 | 173 | +		entry = None  | 
 | 174 | +		if name == "471":  | 
 | 175 | +			entry = self.entry471  | 
 | 176 | +		elif name == "472":  | 
 | 177 | +			entry = self.entry472  | 
 | 178 | +		elif name == "loader":  | 
 | 179 | +			entry = self.loader  | 
 | 180 | +		else:  | 
 | 181 | +			raise LoaderFileError(f"Invalid name {name}")  | 
 | 182 | + | 
 | 183 | +		if idx > len(entry):  | 
 | 184 | +			raise LoaderFileError(f"Invalid index {idx}. Only has {len(entry)} entries.")  | 
 | 185 | +		e = entry[idx]  | 
 | 186 | +		logger.debug(f"{e}")  | 
 | 187 | +		return (self.blob[e.data_offset:e.data_offset+e.data_size], e.data_delay)  | 
 | 188 | + | 
 | 189 | +	def __str__(self):  | 
 | 190 | +		return f"{self.header} crc: {self.crc32:02x}"  | 
 | 191 | + | 
 | 192 | +def rc4_encrypt(fw_blob):  | 
 | 193 | + | 
 | 194 | +	# Round to 4096 block size  | 
 | 195 | +	blob_len = len(fw_blob)  | 
 | 196 | +	padded_len = (blob_len+4095)//4096 * 4096  | 
 | 197 | +	fw_blob = bytearray(fw_blob)  | 
 | 198 | +	fw_blob += bytearray([0]*(padded_len - blob_len))  | 
 | 199 | +	a = ARC4(RC4_KEY)  | 
 | 200 | +	c = Cipher(a, mode=None)  | 
 | 201 | +	d = c.encryptor()  | 
 | 202 | +	obuf = bytearray()  | 
 | 203 | +	for i in range(padded_len):  | 
 | 204 | +		obuf += d.update(fw_blob[i*512:(i+1)*512])  | 
 | 205 | +	return obuf  | 
 | 206 | + | 
 | 207 | +def rockchip_run(dev, fw_name, fw_blob):  | 
 | 208 | +	rom = rockchip.RochipBootRom(dev)  | 
 | 209 | + | 
 | 210 | +	if fw_name == 'code471':  | 
 | 211 | +		logger.info("Downloading code471...")  | 
 | 212 | +		blob = rc4_encrypt(fw_blob)  | 
 | 213 | +		rom.write_blob(blob, 0x471)  | 
 | 214 | +	elif fw_name == 'code472':  | 
 | 215 | +		logger.info("Downloading code472...")  | 
 | 216 | +		blob = rc4_encrypt(fw_blob)  | 
 | 217 | +		rom.write_blob(blob, 0x472)  | 
 | 218 | +	else:  | 
 | 219 | +		fw = LoaderFile(fw_blob)  | 
 | 220 | +		logger.info(f"{fw}")  | 
 | 221 | +		for i in range(fw.header.entry471.count):  | 
 | 222 | +			logger.info(f"Downloading entry 471 {i}...")  | 
 | 223 | +			(data, delay) = fw.entry_data("471", i)  | 
 | 224 | +			rom.write_blob(data, 0x471)  | 
 | 225 | +			logger.info(f"Sleeping {delay}ms")  | 
 | 226 | +			time.sleep(delay / 1000)  | 
 | 227 | +			logger.info("Done")  | 
 | 228 | +		for i in range(fw.header.entry472.count):  | 
 | 229 | +			logger.info(f"Downloading entry 472 {i}...")  | 
 | 230 | +			(data, delay) = fw.entry_data("472", i)  | 
 | 231 | +			rom.write_blob(data, 0x472)  | 
 | 232 | +			logger.info(f"Sleeping {delay}ms")  | 
 | 233 | +			time.sleep(delay / 1000)  | 
 | 234 | +			logger.info("Done")  | 
0 commit comments