1from pathlib import Path
2
3from structlog import get_logger
4
5from unblob.file_utils import Endian, File, InvalidInputFormat, StructParser
6from unblob.models import (
7 Extractor,
8 HandlerDoc,
9 HandlerType,
10 HexString,
11 Reference,
12 StructHandler,
13 ValidChunk,
14)
15
16logger = get_logger()
17
18C_DEFINITIONS = r"""
19 typedef struct engenius_header {
20 uint32 unknown_1;
21 uint32 vendor_id;
22 uint32 product_id;
23 char version[20];
24 uint32 length;
25 uint32 unknown_2;
26 char checksum[16];
27 char padding[32];
28 uint32 unknown_3;
29 char magic[4];
30 char reg_dom[8];
31 uint32 major_version;
32 uint32 minor_version;
33 uint32 micro_version;
34 uint32 release_date;
35 uint32 c_major_version;
36 uint32 c_minor_version;
37 uint32 c_micro_version;
38 uint32 model_len;
39 char model[model_len];
40 } engenius_header_t;
41"""
42
43XOR_KEY = b"\xac\x78\x3c\x9e\xcf\x67\xb3\x59"
44XOR_KEY_LEN = len(XOR_KEY)
45
46
47def decrypter(reference):
48 def decrypt(value, offset):
49 nonlocal reference
50 return value ^ XOR_KEY[(offset - reference) % XOR_KEY_LEN]
51
52 return decrypt
53
54
55class EngeniusExtractor(Extractor):
56 def __init__(self):
57 self._struct_parser = StructParser(C_DEFINITIONS)
58
59 def extract(self, inpath: Path, outdir: Path):
60 outpath = outdir.joinpath(f"{inpath.name}.decrypted")
61
62 with File.from_path(inpath) as f:
63 engenius_header = self._struct_parser.parse(
64 "engenius_header_t", f, Endian.BIG
65 )
66 logger.debug(
67 "engenius_header_t",
68 engenius_header=engenius_header,
69 size=len(engenius_header),
70 _verbosity=3,
71 )
72 decrypt = decrypter(f.find(XOR_KEY))
73 with outpath.open("wb") as outfile:
74 decrypted = bytearray()
75 for offset in range(f.tell(), engenius_header.length):
76 decrypted.append(decrypt(f[offset], offset))
77 outfile.write(decrypted)
78
79
80class EngeniusHandler(StructHandler):
81 NAME = "engenius"
82
83 PATTERNS = [HexString("12 34 56 78 61 6c 6c")]
84
85 C_DEFINITIONS = C_DEFINITIONS
86 HEADER_STRUCT = "engenius_header_t"
87 EXTRACTOR = EngeniusExtractor()
88 PATTERN_MATCH_OFFSET = -0x5C
89
90 DOC = HandlerDoc(
91 name="Engenius",
92 description="Engenius firmware files contain a custom header with metadata, followed by encrypted data using an XOR cipher.",
93 handler_type=HandlerType.ARCHIVE,
94 vendor="Engenius",
95 references=[
96 Reference(
97 title="enfringement - Tools for working with EnGenius WiFi hardware.",
98 url="https://github.com/ryancdotorg/enfringement", # Replace with actual reference if available
99 )
100 ],
101 limitations=["Does not support all firmware versions."],
102 )
103
104 def is_valid_header(self, header) -> bool:
105 if header.length <= len(header):
106 return False
107 try:
108 header.model.decode("utf-8")
109 except UnicodeDecodeError:
110 return False
111 return True
112
113 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
114 header = self.parse_header(file, endian=Endian.BIG)
115
116 if not self.is_valid_header(header):
117 raise InvalidInputFormat("Invalid Engenius header.")
118
119 return ValidChunk(
120 start_offset=start_offset,
121 end_offset=start_offset + len(header) + header.length,
122 )