feat: 开放小爱音箱补丁固件构建源代码和教程
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Source: https://github.com/csftech/Xiaomi-OpenWrt-firmware-toolkit
|
||||
# Author: @csftech (Sheng-Fu Chang)
|
||||
# MIT Licensed
|
||||
|
||||
import argparse
|
||||
import binascii
|
||||
import ctypes
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
class ImageHeader(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("magic", ctypes.c_uint),
|
||||
("signature_offset", ctypes.c_uint),
|
||||
("crc32_checksum", ctypes.c_uint),
|
||||
("file_type", ctypes.c_ushort),
|
||||
("model", ctypes.c_ushort),
|
||||
("segment_offsets", ctypes.c_uint * 8),
|
||||
]
|
||||
|
||||
|
||||
class SegmentHeader(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("magic", ctypes.c_uint),
|
||||
("flash_address", ctypes.c_uint),
|
||||
("length", ctypes.c_uint),
|
||||
("partition", ctypes.c_uint),
|
||||
("segment_name", ctypes.c_char * 32),
|
||||
]
|
||||
|
||||
|
||||
class Firmware:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.image_header = ImageHeader()
|
||||
self.fd = open(self.path, "rb")
|
||||
|
||||
def verify(self, ignore_hash=False):
|
||||
logging.info("[Jobs] Verifying firmware image...")
|
||||
assert self.fd.readinto(self.image_header) == ctypes.sizeof(self.image_header)
|
||||
|
||||
# magic
|
||||
logging.info(f"firmware magic: {hex(self.image_header.magic)}")
|
||||
assert self.image_header.magic in [0x31524448, 0x32524448]
|
||||
|
||||
if self.image_header.magic == 0x32524448:
|
||||
logging.info("NOTE: firmware seems to be encrypted?")
|
||||
|
||||
# signature
|
||||
self.fd.seek(self.image_header.signature_offset)
|
||||
signature_length = int.from_bytes(self.fd.read(16), "little")
|
||||
signature = self.fd.read()
|
||||
logging.info(
|
||||
f"firmware signature_offset: {hex(self.image_header.signature_offset)}"
|
||||
)
|
||||
logging.info(f"firmware signature_length: {signature_length}")
|
||||
logging.info(f"firmware signature: {signature.hex()}")
|
||||
assert len(signature) == signature_length
|
||||
|
||||
# crc32
|
||||
self.fd.seek(12)
|
||||
logging.info(
|
||||
f"firmware crc32_checksum: {hex(self.image_header.crc32_checksum)}"
|
||||
)
|
||||
computed_checksum = ~binascii.crc32(self.fd.read()) & 0xFFFFFFFF
|
||||
logging.info(f"computed crc32_checksum: {hex(computed_checksum)}")
|
||||
assert self.image_header.crc32_checksum == computed_checksum
|
||||
|
||||
# md5
|
||||
self.fd.seek(0)
|
||||
m = hashlib.md5()
|
||||
m.update(self.fd.read())
|
||||
hash = m.hexdigest()
|
||||
logging.info(f"computed md5 hash: {hash}")
|
||||
filename_hash = os.path.basename(self.path).split("_")[-2]
|
||||
try:
|
||||
assert hash[(len(filename_hash) * -1) :] == filename_hash
|
||||
except AssertionError:
|
||||
if ignore_hash:
|
||||
logging.warning(
|
||||
f'Warning: hash "{filename_hash}" does not match expected!'
|
||||
)
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
self.fd.seek(0)
|
||||
return True
|
||||
|
||||
def extract(self, dest: str = None):
|
||||
logging.info("[Jobs] Extracting firmware...")
|
||||
current_time = time.strftime("%Y%m%d_%H%M%S", time.localtime())
|
||||
self.dest_dir = os.path.join(
|
||||
dest, f'{os.path.basename(self.path).replace(".bin", "")}_{current_time}'
|
||||
)
|
||||
if dest is not None:
|
||||
self.dest_dir = dest
|
||||
os.mkdir(self.dest_dir)
|
||||
logging.info(f"create destination directory: {self.dest_dir}")
|
||||
|
||||
for address in self.image_header.segment_offsets:
|
||||
if address:
|
||||
self.fd.seek(address)
|
||||
segment_header = SegmentHeader()
|
||||
assert self.fd.readinto(segment_header) == ctypes.sizeof(segment_header)
|
||||
|
||||
# extract segment
|
||||
with open(
|
||||
os.path.join(self.dest_dir, segment_header.segment_name.decode()),
|
||||
"wb",
|
||||
) as s:
|
||||
s.write(self.fd.read(segment_header.length))
|
||||
logging.info(
|
||||
f'extracting segment: {segment_header.segment_name.decode("ascii")}'
|
||||
)
|
||||
|
||||
|
||||
def run(path, extract=False, dest=None, ignore_hash=False):
|
||||
logging.info(f"[MSG] Input file: {path}")
|
||||
firmware = Firmware(path)
|
||||
if firmware.verify(ignore_hash):
|
||||
logging.info(
|
||||
"[Jobs] Verification success: it's a genuine firmware from Xiaomi."
|
||||
)
|
||||
|
||||
if extract:
|
||||
firmware.extract(dest)
|
||||
logging.info("[Jobs] Extraction complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--dest",
|
||||
help="Destination directory to store extracted files, default=current working directory",
|
||||
action="store",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--ignore-hash",
|
||||
help="Do not fail if md5 hash does not match",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument("-e", "--extract", help="Input your firmware", action="store")
|
||||
parser.add_argument("-s", "--show", help="Show firmware info", action="store")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.extract:
|
||||
firmware_path = os.path.abspath(args.extract)
|
||||
dest_path = os.getcwd()
|
||||
if args.dest:
|
||||
dest_path = args.dest
|
||||
run(firmware_path, extract=True, dest=dest_path, ignore_hash=args.ignore_hash)
|
||||
elif args.show:
|
||||
firmware_path = os.path.abspath(args.show)
|
||||
run(firmware_path, ignore_hash=args.ignore_hash)
|
||||
else:
|
||||
parser.print_help()
|
||||
Reference in New Issue
Block a user