Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/unblob/handlers/archive/airoha.py: 47%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

66 statements  

1import io 

2import lzma 

3from enum import IntEnum 

4from pathlib import Path 

5 

6from unblob.file_utils import ( 

7 Endian, 

8 File, 

9 FileSystem, 

10 InvalidInputFormat, 

11 StructParser, 

12) 

13from unblob.models import ( 

14 Extractor, 

15 ExtractResult, 

16 HandlerDoc, 

17 HandlerType, 

18 HexString, 

19 Reference, 

20 StructHandler, 

21 ValidChunk, 

22) 

23from unblob.report import ExtractionProblem 

24 

25C_DEFINITIONS = r""" 

26 typedef struct airoha_header { 

27 uint8 file_checksum[32]; /* SHA256 of the rest of the file */ 

28 uint8 padding[224]; /* 0xFF padding */ 

29 uint16 basic_info_tlv_type; /* 0x11 BASIC_INFO */ 

30 uint16 basic_info_tlv_length; /* 10 */ 

31 uint8 compression_type; /* 0=NONE, 1=LZMA, 2=LZMA_AES */ 

32 uint8 integrity_check_type; /* 0=CRC32, 1=SHA256, 2=SHA256_RSA */ 

33 uint32 firmware_offset; 

34 uint32 firmware_size; 

35 } airoha_header_t; 

36 

37 typedef struct airoha_tlv_header { 

38 uint16 tlv_type; 

39 uint16 tlv_length; 

40 } airoha_tlv_header_t; 

41 

42 typedef struct airoha_mover_info_header { 

43 uint32 number_of_sections; 

44 } airoha_mover_info_header_t; 

45 

46 typedef struct airoha_section { 

47 uint32 source_offset; /* offset in decompressed stream */ 

48 uint32 decompressed_size; 

49 uint32 dest_offset; /* in-memory load address */ 

50 } airoha_section_t; 

51""" 

52 

53PRELUDE_SIZE = 256 

54PATTERN_FF_LEN = 16 

55 

56 

57class CompressionType(IntEnum): 

58 NONE = 0 

59 LZMA = 1 

60 LZMA_AES = 2 

61 

62 

63class TlvType(IntEnum): 

64 MOVER_INFO = 0x12 

65 

66 

67class AirohaExtractor(Extractor): 

68 def __init__(self): 

69 self._struct_parser = StructParser(C_DEFINITIONS) 

70 

71 def _read_sections(self, file: File) -> list: 

72 tlv = self._struct_parser.parse("airoha_tlv_header_t", file, Endian.LITTLE) 

73 while tlv.tlv_type != TlvType.MOVER_INFO: 

74 file.seek(tlv.tlv_length, io.SEEK_CUR) 

75 tlv = self._struct_parser.parse("airoha_tlv_header_t", file, Endian.LITTLE) 

76 

77 mover = self._struct_parser.parse( 

78 "airoha_mover_info_header_t", file, Endian.LITTLE 

79 ) 

80 return [ 

81 self._struct_parser.parse("airoha_section_t", file, Endian.LITTLE) 

82 for _ in range(mover.number_of_sections) 

83 ] 

84 

85 def extract(self, inpath: Path, outdir: Path) -> ExtractResult: 

86 fs = FileSystem(outdir) 

87 with File.from_path(inpath) as file: 

88 header = self._struct_parser.parse("airoha_header_t", file, Endian.LITTLE) 

89 sections = self._read_sections(file) 

90 if not sections: 

91 raise InvalidInputFormat("Airoha file has no MOVER_INFO sections") 

92 base_address = min(section.dest_offset for section in sections) 

93 firmware_name = Path(f"firmware_{base_address:08x}.bin") 

94 

95 match header.compression_type: 

96 case CompressionType.LZMA_AES: 

97 fs.record_problem( 

98 ExtractionProblem( 

99 problem=( 

100 "Firmware blob is AES-encrypted (LZMA_AES); " 

101 "decryption requires a per-vendor key/IV" 

102 ), 

103 resolution="Carved encrypted blob", 

104 ) 

105 ) 

106 fs.carve( 

107 Path("firmware.encrypted.bin"), 

108 file, 

109 header.firmware_offset, 

110 header.firmware_size, 

111 ) 

112 return ExtractResult(reports=fs.problems) 

113 case CompressionType.LZMA: 

114 file.seek(header.firmware_offset, io.SEEK_SET) 

115 payload = lzma.decompress(file.read(header.firmware_size)) 

116 case _: 

117 file.seek(header.firmware_offset, io.SEEK_SET) 

118 payload = file.read(header.firmware_size) # ~5 MB max 

119 

120 with fs.open(firmware_name, "wb+") as outfile: 

121 for section in sections: 

122 outfile.seek(section.dest_offset - base_address, io.SEEK_SET) 

123 outfile.write( 

124 payload[ 

125 section.source_offset : section.source_offset 

126 + section.decompressed_size 

127 ] 

128 ) 

129 

130 return ExtractResult(reports=fs.problems) 

131 

132 

133class AirohaHandler(StructHandler): 

134 NAME = "airoha" 

135 PATTERNS = [ 

136 HexString( 

137 """ 

138 FF FF FF FF FF FF FF FF 

139 FF FF FF FF FF FF FF FF // 0xFF padding 

140 11 00 0A 00 // BASIC_INFO TLV header (type=0x11, length=0x0A) 

141 ( 00 | 01 | 02 ) // compression_type: NONE | LZMA | LZMA_AES 

142 ( 00 | 01 | 02 ) // integrity_check_type: CRC32 | SHA256 | SHA256+RSA 

143 """ 

144 ), 

145 ] 

146 PATTERN_MATCH_OFFSET = -(PRELUDE_SIZE - PATTERN_FF_LEN) 

147 C_DEFINITIONS = C_DEFINITIONS 

148 HEADER_STRUCT = "airoha_header_t" 

149 EXTRACTOR = AirohaExtractor() 

150 DOC = HandlerDoc( 

151 name="Airoha BT firmware", 

152 description=( 

153 "Airoha Bluetooth audio firmware files are either compressed or encrypted. These firmwares are used on Bluetooth audio chips that can be found in popular earbuds and headphones." 

154 ), 

155 handler_type=HandlerType.ARCHIVE, 

156 vendor="Airoha", 

157 references=[ 

158 Reference( 

159 title="Airoha firmware parser (010 Editor template + decryptor)", 

160 url="https://github.com/ramikg/airoha-firmware-parser", 

161 ), 

162 Reference( 

163 title="Airoha Bluetooth security vulnerabilities", 

164 url="https://insinuator.net/2025/06/airoha-bluetooth-security-vulnerabilities/", 

165 ), 

166 ], 

167 limitations=[ 

168 "AES-encrypted firmware blobs are only carved, decryption requires a per-vendor AES key/IV not embedded in the file.", 

169 ], 

170 ) 

171 

172 def is_valid_header(self, header) -> bool: 

173 return header.firmware_offset >= PRELUDE_SIZE and header.firmware_size > 0 

174 

175 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk: 

176 header = self.parse_header(file, endian=Endian.LITTLE) 

177 

178 if not self.is_valid_header(header): 

179 raise InvalidInputFormat("Invalid Airoha header.") 

180 

181 return ValidChunk( 

182 start_offset=start_offset, 

183 end_offset=start_offset + header.firmware_offset + header.firmware_size, 

184 )