from __future__ import annotations import enum import logging import typing import zlib from zigpy.ota.image import BaseOTAImage, ElementTagId, OTAImage VALID_SILABS_CRC = 0x2144DF1C # CRC32(anything | CRC32(anything)) == CRC32(0x00000000) LOGGER = logging.getLogger(__name__) class ValidationResult(enum.Enum): INVALID = 0 VALID = 1 UNKNOWN = 2 class ValidationError(Exception): pass def parse_silabs_ebl(data: bytes) -> typing.Iterable[tuple[bytes, bytes]]: """Parses a Silicon Labs EBL firmware image.""" if len(data) % 64 != 0: raise ValidationError( f"Image size ({len(data)}) must be a multiple of 64 bytes" ) orig_data = data while True: if len(data) < 4: raise ValidationError( "Image is truncated: not long enough to contain a valid tag" ) tag = data[:2] length = int.from_bytes(data[2:4], "big") value = data[4 : 4 + length] if len(value) < length: raise ValidationError("Image is truncated: tag value is cut off") data = data[4 + length :] yield tag, value # EBL end tag if tag != b"\xfc\x04": continue # At this point the EBL should contain nothing but padding if data.strip(b"\xff"): raise ValidationError("Image padding contains invalid bytes") unpadded_image = orig_data[: -len(data)] if data else orig_data computed_crc = zlib.crc32(unpadded_image) if computed_crc != VALID_SILABS_CRC: raise ValidationError( f"Image CRC-32 is invalid:" f" expected 0x{VALID_SILABS_CRC:08X}, got 0x{computed_crc:08X}" ) break # pragma: no cover def parse_silabs_gbl(data: bytes) -> typing.Iterable[tuple[bytes, bytes]]: """Parses a Silicon Labs GBL firmware image.""" orig_data = data while True: if len(data) < 8: raise ValidationError( "Image is truncated: not long enough to contain a valid tag" ) tag = data[:4] length = int.from_bytes(data[4:8], "little") value = data[8 : 8 + length] if len(value) < length: raise ValidationError("Image is truncated: tag value is cut off") data = data[8 + length :] yield tag, value # GBL end tag if tag != b"\xfc\x04\x04\xfc": continue # GBL images aren't expected to contain padding but some are (i.e. Hue) unpadded_image = orig_data[: -len(data)] if data else orig_data computed_crc = zlib.crc32(unpadded_image) if computed_crc != VALID_SILABS_CRC: raise ValidationError( f"Image CRC-32 is invalid:" f" expected 0x{VALID_SILABS_CRC:08X}, got 0x{computed_crc:08X}" ) break # pragma: no cover def validate_firmware(data: bytes) -> ValidationResult: """Validates a firmware image.""" parser = None if data.startswith(b"\xeb\x17\xa6\x03"): parser = parse_silabs_gbl elif data.startswith(b"\x00\x00\x00\x8c"): parser = parse_silabs_ebl else: return ValidationResult.UNKNOWN tuple(parser(data)) return ValidationResult.VALID def validate_ota_image(image: BaseOTAImage) -> ValidationResult: """Validates a Zigbee OTA image's embedded firmwares and indicates if an image is valid, invalid, or of an unknown type. """ if not isinstance(image, OTAImage): return ValidationResult.UNKNOWN results = [ validate_firmware(subelement.data) for subelement in image.subelements if subelement.tag_id == ElementTagId.UPGRADE_IMAGE ] if not results or any(r == ValidationResult.UNKNOWN for r in results): return ValidationResult.UNKNOWN return ValidationResult.VALID def check_invalid(image: BaseOTAImage) -> bool: """Checks if an image is invalid or not. Unknown image types are considered valid.""" try: validate_ota_image(image) except ValidationError as e: LOGGER.warning("Image %s is invalid: %s", image.header, e) return True else: return False