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