packet-tracer-skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LICENSES/LICENSE.Twofish-BSD-3-Clause.txt +29 -0
- package/README.md +687 -0
- package/SKILL.md +221 -0
- package/bin/packet-tracer-skill.js +635 -0
- package/examples/blueprint_minimal.json +46 -0
- package/package.json +42 -0
- package/references/packettracer-sample-catalog.json +21410 -0
- package/references/packettracer-sample-catalog.md +1124 -0
- package/references/pkt-format.md +57 -0
- package/references/xml-skeleton-notes.md +44 -0
- package/requirements-dev.txt +1 -0
- package/requirements.txt +6 -0
- package/scripts/build_sample_catalog.py +65 -0
- package/scripts/donor_diagnostics.py +35 -0
- package/scripts/generate_pkt.py +1264 -0
- package/scripts/install_skill.py +71 -0
- package/scripts/intent_parser.py +712 -0
- package/scripts/packet_tracer_env.py +278 -0
- package/scripts/pkt_builder.py +15 -0
- package/scripts/pkt_codec.py +181 -0
- package/scripts/pkt_editor.py +752 -0
- package/scripts/pkt_transformer.py +541 -0
- package/scripts/sample_catalog.py +385 -0
- package/scripts/sample_selector.py +156 -0
- package/scripts/setup.ps1 +26 -0
- package/scripts/twofish_diagnostics.py +91 -0
- package/scripts/vendor/README.md +46 -0
- package/scripts/vendor/twofish.py +81 -0
- package/scripts/workspace_repair.py +441 -0
- package/templates/pt900/base_empty.xml +21 -0
- package/templates/pt900/device_library/pc.xml +20 -0
- package/templates/pt900/device_library/printer.xml +432 -0
- package/templates/pt900/device_library/router.xml +16 -0
- package/templates/pt900/device_library/switch.xml +38 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_INSTALL_CANDIDATES = [
|
|
10
|
+
Path(r"C:\Program Files\Cisco Packet Tracer 9.0.0"),
|
|
11
|
+
Path(r"C:\Program Files\Cisco Packet Tracer"),
|
|
12
|
+
Path(r"C:\Program Files (x86)\Cisco Packet Tracer 9.0.0"),
|
|
13
|
+
Path(r"C:\Program Files (x86)\Cisco Packet Tracer"),
|
|
14
|
+
]
|
|
15
|
+
DEFAULT_PACKET_TRACER_TARGET_VERSION = "9.0.0.0810"
|
|
16
|
+
DEFAULT_DONOR_FALLBACKS = [
|
|
17
|
+
Path.home() / "Downloads",
|
|
18
|
+
Path.home() / "Documents",
|
|
19
|
+
Path.home() / "Desktop",
|
|
20
|
+
]
|
|
21
|
+
DEFAULT_SAMPLE_DONOR_FILES = [
|
|
22
|
+
Path(r"01 Networking\FTP\FTP.pkt"),
|
|
23
|
+
Path(r"01 Networking\HTTPS\HTTPS.pkt"),
|
|
24
|
+
Path(r"01 Networking\DNS\Multilevel_DNS.pkt"),
|
|
25
|
+
Path(r"01 Networking\DHCP\dhcp_snooping_trusted_untrusted_gigabit_ports.pkt"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class CompatibilityDonorDetails:
|
|
31
|
+
target_version: str
|
|
32
|
+
resolved_path: Path | None
|
|
33
|
+
donor_version: str | None
|
|
34
|
+
donor_source: str | None
|
|
35
|
+
status: str
|
|
36
|
+
blocking_reason: str
|
|
37
|
+
candidate_paths: list[tuple[str, Path]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _existing_path(raw: str | None) -> Path | None:
|
|
41
|
+
if not raw:
|
|
42
|
+
return None
|
|
43
|
+
path = Path(raw).expanduser()
|
|
44
|
+
return path if path.exists() else None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_packet_tracer_root() -> Path | None:
|
|
48
|
+
env_root = _existing_path(os.getenv("PACKET_TRACER_ROOT"))
|
|
49
|
+
if env_root is not None:
|
|
50
|
+
return env_root
|
|
51
|
+
for candidate in DEFAULT_INSTALL_CANDIDATES:
|
|
52
|
+
if candidate.exists():
|
|
53
|
+
return candidate
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_packet_tracer_saves_root() -> Path | None:
|
|
58
|
+
env_saves = _existing_path(os.getenv("PACKET_TRACER_SAVES_ROOT"))
|
|
59
|
+
if env_saves is not None:
|
|
60
|
+
return env_saves
|
|
61
|
+
root = get_packet_tracer_root()
|
|
62
|
+
if root is None:
|
|
63
|
+
return None
|
|
64
|
+
saves = root / "saves"
|
|
65
|
+
return saves if saves.exists() else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_packet_tracer_exe() -> Path | None:
|
|
69
|
+
env_exe = _existing_path(os.getenv("PACKET_TRACER_EXE"))
|
|
70
|
+
if env_exe is not None:
|
|
71
|
+
return env_exe
|
|
72
|
+
root = get_packet_tracer_root()
|
|
73
|
+
if root is None:
|
|
74
|
+
return None
|
|
75
|
+
for candidate in [root / "bin" / "PacketTracer.exe", root / "PacketTracer.exe"]:
|
|
76
|
+
if candidate.exists():
|
|
77
|
+
return candidate
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def require_packet_tracer_saves_root() -> Path:
|
|
82
|
+
saves = get_packet_tracer_saves_root()
|
|
83
|
+
if saves is None:
|
|
84
|
+
raise FileNotFoundError(
|
|
85
|
+
"Packet Tracer sample saves were not found. Set PACKET_TRACER_SAVES_ROOT or PACKET_TRACER_ROOT."
|
|
86
|
+
)
|
|
87
|
+
return saves
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def require_packet_tracer_exe() -> Path:
|
|
91
|
+
exe = get_packet_tracer_exe()
|
|
92
|
+
if exe is None:
|
|
93
|
+
raise FileNotFoundError(
|
|
94
|
+
"Packet Tracer executable was not found. Set PACKET_TRACER_EXE or PACKET_TRACER_ROOT."
|
|
95
|
+
)
|
|
96
|
+
return exe
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve_sample_path(relative_path: str) -> Path:
|
|
100
|
+
return require_packet_tracer_saves_root() / relative_path
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_packet_tracer_target_version() -> str:
|
|
104
|
+
return os.getenv("PACKET_TRACER_TARGET_VERSION", DEFAULT_PACKET_TRACER_TARGET_VERSION)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _pkt_version(pkt_path: Path) -> str | None:
|
|
108
|
+
try:
|
|
109
|
+
from pkt_codec import decode_pkt_modern
|
|
110
|
+
|
|
111
|
+
root = ET.fromstring(decode_pkt_modern(pkt_path.read_bytes()))
|
|
112
|
+
except Exception:
|
|
113
|
+
return None
|
|
114
|
+
return root.findtext("./VERSION")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _candidate_pkt_files(directory: Path, source: str) -> list[tuple[str, Path]]:
|
|
118
|
+
if not directory.exists() or not directory.is_dir():
|
|
119
|
+
return []
|
|
120
|
+
candidates = sorted(
|
|
121
|
+
(path for path in directory.glob("*.pkt") if path.is_file()),
|
|
122
|
+
key=lambda path: (path.stat().st_mtime, path.name.lower()),
|
|
123
|
+
reverse=True,
|
|
124
|
+
)
|
|
125
|
+
return [(source, candidate) for candidate in candidates]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def list_packet_tracer_compatibility_donor_candidates() -> list[tuple[str, Path]]:
|
|
129
|
+
candidates: list[tuple[str, Path]] = []
|
|
130
|
+
seen: set[str] = set()
|
|
131
|
+
|
|
132
|
+
env_donor = os.getenv("PACKET_TRACER_COMPAT_DONOR")
|
|
133
|
+
if env_donor:
|
|
134
|
+
env_path = Path(env_donor).expanduser()
|
|
135
|
+
seen.add(str(env_path).lower())
|
|
136
|
+
candidates.append(("env", env_path))
|
|
137
|
+
|
|
138
|
+
for directory in DEFAULT_DONOR_FALLBACKS:
|
|
139
|
+
for source, candidate in _candidate_pkt_files(directory, f"auto:{directory.name.lower()}"):
|
|
140
|
+
key = str(candidate).lower()
|
|
141
|
+
if key in seen:
|
|
142
|
+
continue
|
|
143
|
+
seen.add(key)
|
|
144
|
+
candidates.append((source, candidate))
|
|
145
|
+
|
|
146
|
+
saves_root = get_packet_tracer_saves_root()
|
|
147
|
+
if saves_root is not None:
|
|
148
|
+
for relative_path in DEFAULT_SAMPLE_DONOR_FILES:
|
|
149
|
+
candidate = saves_root / relative_path
|
|
150
|
+
key = str(candidate).lower()
|
|
151
|
+
if key in seen:
|
|
152
|
+
continue
|
|
153
|
+
seen.add(key)
|
|
154
|
+
candidates.append(("auto:packet-tracer-saves", candidate))
|
|
155
|
+
|
|
156
|
+
return candidates
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def inspect_packet_tracer_compatibility_donor() -> CompatibilityDonorDetails:
|
|
160
|
+
target_version = get_packet_tracer_target_version()
|
|
161
|
+
candidates = list_packet_tracer_compatibility_donor_candidates()
|
|
162
|
+
env_donor = os.getenv("PACKET_TRACER_COMPAT_DONOR")
|
|
163
|
+
|
|
164
|
+
if env_donor:
|
|
165
|
+
env_path = Path(env_donor).expanduser()
|
|
166
|
+
if not env_path.exists():
|
|
167
|
+
return CompatibilityDonorDetails(
|
|
168
|
+
target_version=target_version,
|
|
169
|
+
resolved_path=None,
|
|
170
|
+
donor_version=None,
|
|
171
|
+
donor_source="env",
|
|
172
|
+
status="missing",
|
|
173
|
+
blocking_reason=f"set but missing: {env_path}",
|
|
174
|
+
candidate_paths=candidates,
|
|
175
|
+
)
|
|
176
|
+
if env_path.suffix.lower() != ".pkt":
|
|
177
|
+
return CompatibilityDonorDetails(
|
|
178
|
+
target_version=target_version,
|
|
179
|
+
resolved_path=None,
|
|
180
|
+
donor_version=None,
|
|
181
|
+
donor_source="env",
|
|
182
|
+
status="invalid_extension",
|
|
183
|
+
blocking_reason=f"compatibility donor must be a .pkt file: {env_path}",
|
|
184
|
+
candidate_paths=candidates,
|
|
185
|
+
)
|
|
186
|
+
donor_version = _pkt_version(env_path)
|
|
187
|
+
if donor_version is None:
|
|
188
|
+
return CompatibilityDonorDetails(
|
|
189
|
+
target_version=target_version,
|
|
190
|
+
resolved_path=None,
|
|
191
|
+
donor_version=None,
|
|
192
|
+
donor_source="env",
|
|
193
|
+
status="decode_error",
|
|
194
|
+
blocking_reason=f"could not decode donor version: {env_path}",
|
|
195
|
+
candidate_paths=candidates,
|
|
196
|
+
)
|
|
197
|
+
if donor_version != target_version:
|
|
198
|
+
return CompatibilityDonorDetails(
|
|
199
|
+
target_version=target_version,
|
|
200
|
+
resolved_path=None,
|
|
201
|
+
donor_version=donor_version,
|
|
202
|
+
donor_source="env",
|
|
203
|
+
status="version_mismatch",
|
|
204
|
+
blocking_reason=f"{env_path} is version {donor_version}; expected {target_version}",
|
|
205
|
+
candidate_paths=candidates,
|
|
206
|
+
)
|
|
207
|
+
return CompatibilityDonorDetails(
|
|
208
|
+
target_version=target_version,
|
|
209
|
+
resolved_path=env_path,
|
|
210
|
+
donor_version=donor_version,
|
|
211
|
+
donor_source="env",
|
|
212
|
+
status="ok",
|
|
213
|
+
blocking_reason="",
|
|
214
|
+
candidate_paths=candidates,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
decode_failures = 0
|
|
218
|
+
wrong_version_count = 0
|
|
219
|
+
for source, candidate in candidates:
|
|
220
|
+
if not candidate.exists() or candidate.suffix.lower() != ".pkt":
|
|
221
|
+
continue
|
|
222
|
+
donor_version = _pkt_version(candidate)
|
|
223
|
+
if donor_version is None:
|
|
224
|
+
decode_failures += 1
|
|
225
|
+
continue
|
|
226
|
+
if donor_version != target_version:
|
|
227
|
+
wrong_version_count += 1
|
|
228
|
+
continue
|
|
229
|
+
return CompatibilityDonorDetails(
|
|
230
|
+
target_version=target_version,
|
|
231
|
+
resolved_path=candidate,
|
|
232
|
+
donor_version=donor_version,
|
|
233
|
+
donor_source=source,
|
|
234
|
+
status="ok",
|
|
235
|
+
blocking_reason="",
|
|
236
|
+
candidate_paths=candidates,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if candidates:
|
|
240
|
+
if decode_failures == len(candidates):
|
|
241
|
+
reason = (
|
|
242
|
+
"donor candidates were found, but none could be decoded. "
|
|
243
|
+
"Check the local Twofish bridge and Python 3.14 runtime."
|
|
244
|
+
)
|
|
245
|
+
elif wrong_version_count > 0:
|
|
246
|
+
reason = f"no Packet Tracer {target_version} donor was found among the discovered local candidates"
|
|
247
|
+
else:
|
|
248
|
+
reason = f"no compatible Packet Tracer {target_version} donor was found"
|
|
249
|
+
else:
|
|
250
|
+
reason = (
|
|
251
|
+
"no donor candidates were discovered. Set PACKET_TRACER_COMPAT_DONOR "
|
|
252
|
+
"or place a working 9.0 donor lab in Downloads, Documents, Desktop, or Packet Tracer saves."
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return CompatibilityDonorDetails(
|
|
256
|
+
target_version=target_version,
|
|
257
|
+
resolved_path=None,
|
|
258
|
+
donor_version=None,
|
|
259
|
+
donor_source=None,
|
|
260
|
+
status="missing",
|
|
261
|
+
blocking_reason=reason,
|
|
262
|
+
candidate_paths=candidates,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def get_packet_tracer_compatibility_donor() -> Path | None:
|
|
267
|
+
details = inspect_packet_tracer_compatibility_donor()
|
|
268
|
+
return details.resolved_path if details.status == "ok" else None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def require_packet_tracer_compatibility_donor() -> Path:
|
|
272
|
+
details = inspect_packet_tracer_compatibility_donor()
|
|
273
|
+
if details.status != "ok" or details.resolved_path is None:
|
|
274
|
+
raise FileNotFoundError(
|
|
275
|
+
"Packet Tracer 9.0 compatibility donor was not found. "
|
|
276
|
+
f"{details.blocking_reason}"
|
|
277
|
+
)
|
|
278
|
+
return details.resolved_path
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pkt_transformer import transform_from_blueprint
|
|
4
|
+
from sample_catalog import load_catalog
|
|
5
|
+
from sample_selector import select_best_sample
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_packet_tracer_xml(blueprint: dict) -> bytes:
|
|
9
|
+
samples = load_catalog()
|
|
10
|
+
device_requirements: dict[str, int] = {}
|
|
11
|
+
for device in blueprint.get("devices", []):
|
|
12
|
+
device_type = str(device["type"])
|
|
13
|
+
device_requirements[device_type] = device_requirements.get(device_type, 0) + 1
|
|
14
|
+
selected = select_best_sample(samples, list(blueprint.get("capabilities", [])), device_requirements)
|
|
15
|
+
return transform_from_blueprint(blueprint, selected)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
import zlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from vendor.twofish import Twofish
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
BLOCK_SIZE = 16
|
|
13
|
+
TAG_LEN = 16
|
|
14
|
+
NEW_KEY = bytes([0x89]) * 16
|
|
15
|
+
NEW_IV = bytes([0x10]) * 16
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _twofish_cls() -> type["Twofish"]:
|
|
19
|
+
try:
|
|
20
|
+
from vendor.twofish import Twofish
|
|
21
|
+
except ImportError as exc:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"Packet Tracer modern codec requires a local Twofish bridge. "
|
|
24
|
+
"Set PKT_TWOFISH_LIBRARY or place a local _twofish binary next to scripts/vendor/twofish.py."
|
|
25
|
+
) from exc
|
|
26
|
+
return Twofish
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def qcompress(xml_bytes: bytes) -> bytes:
|
|
30
|
+
if not isinstance(xml_bytes, bytes):
|
|
31
|
+
raise TypeError("xml_bytes must be bytes")
|
|
32
|
+
return struct.pack(">I", len(xml_bytes)) + zlib.compress(xml_bytes, 9)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def quncompress(blob: bytes) -> bytes:
|
|
36
|
+
if len(blob) < 4:
|
|
37
|
+
raise ValueError("qCompress blob is too short")
|
|
38
|
+
size = struct.unpack(">I", blob[:4])[0]
|
|
39
|
+
out = zlib.decompress(blob[4:])
|
|
40
|
+
return out[:size]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def stage2_xor(data: bytes) -> bytes:
|
|
44
|
+
length = len(data)
|
|
45
|
+
return bytes(byte ^ ((length - index) & 0xFF) for index, byte in enumerate(data))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def stage1_obfuscate(clear: bytes) -> bytes:
|
|
49
|
+
length = len(clear)
|
|
50
|
+
out = bytearray(length)
|
|
51
|
+
for index, byte in enumerate(clear):
|
|
52
|
+
out[length - 1 - index] = byte ^ ((length - index * length) & 0xFF)
|
|
53
|
+
return bytes(out)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def stage1_deobfuscate(obfuscated: bytes) -> bytes:
|
|
57
|
+
length = len(obfuscated)
|
|
58
|
+
return bytes(
|
|
59
|
+
obfuscated[length - 1 - index] ^ ((length - index * length) & 0xFF)
|
|
60
|
+
for index in range(length)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _xor_bytes(left: bytes, right: bytes) -> bytes:
|
|
65
|
+
return bytes(a ^ b for a, b in zip(left, right))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _gf_double(block: bytes) -> bytes:
|
|
69
|
+
value = int.from_bytes(block, "big")
|
|
70
|
+
carry = (value >> 127) & 1
|
|
71
|
+
value = ((value << 1) & ((1 << 128) - 1))
|
|
72
|
+
if carry:
|
|
73
|
+
value ^= 0x87
|
|
74
|
+
return value.to_bytes(16, "big")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _pad_cmac(block: bytes) -> bytes:
|
|
78
|
+
return block + b"\x80" + b"\x00" * (BLOCK_SIZE - len(block) - 1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _iterate_blocks(data: bytes) -> list[bytes]:
|
|
82
|
+
return [data[index : index + BLOCK_SIZE] for index in range(0, len(data), BLOCK_SIZE)]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _cmac(cipher: "Twofish", data: bytes) -> bytes:
|
|
86
|
+
zero = b"\x00" * BLOCK_SIZE
|
|
87
|
+
l_val = cipher.encrypt(zero)
|
|
88
|
+
k1 = _gf_double(l_val)
|
|
89
|
+
k2 = _gf_double(k1)
|
|
90
|
+
|
|
91
|
+
blocks = _iterate_blocks(data)
|
|
92
|
+
if not blocks:
|
|
93
|
+
blocks = [b""]
|
|
94
|
+
|
|
95
|
+
if len(blocks[-1]) == BLOCK_SIZE:
|
|
96
|
+
last = _xor_bytes(blocks[-1], k1)
|
|
97
|
+
else:
|
|
98
|
+
last = _xor_bytes(_pad_cmac(blocks[-1]), k2)
|
|
99
|
+
blocks[-1] = last
|
|
100
|
+
|
|
101
|
+
state = zero
|
|
102
|
+
for block in blocks:
|
|
103
|
+
if len(block) != BLOCK_SIZE:
|
|
104
|
+
raise ValueError("CMAC internal block must be 16 bytes")
|
|
105
|
+
state = cipher.encrypt(_xor_bytes(state, block))
|
|
106
|
+
return state
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _omac(cipher: "Twofish", domain: int, data: bytes) -> bytes:
|
|
110
|
+
prefix = b"\x00" * 15 + bytes([domain & 0xFF])
|
|
111
|
+
return _cmac(cipher, prefix + data)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _ctr_crypt(cipher: "Twofish", initial_counter: bytes, data: bytes) -> bytes:
|
|
115
|
+
counter = int.from_bytes(initial_counter, "big")
|
|
116
|
+
out = bytearray()
|
|
117
|
+
for offset in range(0, len(data), BLOCK_SIZE):
|
|
118
|
+
block = data[offset : offset + BLOCK_SIZE]
|
|
119
|
+
keystream = cipher.encrypt(counter.to_bytes(16, "big"))
|
|
120
|
+
out.extend(bytes(a ^ b for a, b in zip(block, keystream)))
|
|
121
|
+
counter = (counter + 1) % (1 << 128)
|
|
122
|
+
return bytes(out)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def eax_twofish_encrypt(plaintext: bytes, nonce: bytes = NEW_IV, header: bytes = b"") -> tuple[bytes, bytes]:
|
|
126
|
+
cipher = _twofish_cls()(NEW_KEY)
|
|
127
|
+
nonce_mac = _omac(cipher, 0, nonce)
|
|
128
|
+
header_mac = _omac(cipher, 1, header)
|
|
129
|
+
ciphertext = _ctr_crypt(cipher, nonce_mac, plaintext)
|
|
130
|
+
body_mac = _omac(cipher, 2, ciphertext)
|
|
131
|
+
tag = _xor_bytes(_xor_bytes(nonce_mac, header_mac), body_mac)
|
|
132
|
+
return ciphertext, tag
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def eax_twofish_decrypt(ciphertext: bytes, tag: bytes, nonce: bytes = NEW_IV, header: bytes = b"") -> bytes:
|
|
136
|
+
if len(tag) != TAG_LEN:
|
|
137
|
+
raise ValueError("invalid EAX tag length")
|
|
138
|
+
cipher = _twofish_cls()(NEW_KEY)
|
|
139
|
+
nonce_mac = _omac(cipher, 0, nonce)
|
|
140
|
+
header_mac = _omac(cipher, 1, header)
|
|
141
|
+
body_mac = _omac(cipher, 2, ciphertext)
|
|
142
|
+
expected_tag = _xor_bytes(_xor_bytes(nonce_mac, header_mac), body_mac)
|
|
143
|
+
if expected_tag != tag:
|
|
144
|
+
raise ValueError("EAX authentication tag verification failed")
|
|
145
|
+
return _ctr_crypt(cipher, nonce_mac, ciphertext)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def encode_pkt_modern(xml_bytes: bytes) -> bytes:
|
|
149
|
+
payload = qcompress(xml_bytes)
|
|
150
|
+
stage2 = stage2_xor(payload)
|
|
151
|
+
ciphertext, tag = eax_twofish_encrypt(stage2)
|
|
152
|
+
return stage1_obfuscate(ciphertext + tag)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def decode_pkt_modern(pkt_bytes: bytes) -> bytes:
|
|
156
|
+
if len(pkt_bytes) < TAG_LEN:
|
|
157
|
+
raise ValueError("pkt blob is too short")
|
|
158
|
+
stage1 = stage1_deobfuscate(pkt_bytes)
|
|
159
|
+
ciphertext = stage1[:-TAG_LEN]
|
|
160
|
+
tag = stage1[-TAG_LEN:]
|
|
161
|
+
stage2 = eax_twofish_decrypt(ciphertext, tag)
|
|
162
|
+
payload = stage2_xor(stage2)
|
|
163
|
+
return quncompress(payload)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def encode_xml_file(xml_path: str | Path, output_path: str | Path) -> Path:
|
|
167
|
+
xml_path = Path(xml_path)
|
|
168
|
+
output_path = Path(output_path)
|
|
169
|
+
pkt_bytes = encode_pkt_modern(xml_path.read_bytes())
|
|
170
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
output_path.write_bytes(pkt_bytes)
|
|
172
|
+
return output_path
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def decode_pkt_file(pkt_path: str | Path, xml_out_path: str | Path) -> Path:
|
|
176
|
+
pkt_path = Path(pkt_path)
|
|
177
|
+
xml_out_path = Path(xml_out_path)
|
|
178
|
+
xml_bytes = decode_pkt_modern(pkt_path.read_bytes())
|
|
179
|
+
xml_out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
xml_out_path.write_bytes(xml_bytes)
|
|
181
|
+
return xml_out_path
|