packetsnitch 1.5.599
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/.eslintrc.json +28 -0
- package/.webpack/x64/main/index.js +2 -0
- package/.webpack/x64/main/index.js.map +1 -0
- package/.webpack/x64/renderer/assets/css/rubikglitch.woff2 +0 -0
- package/.webpack/x64/renderer/assets/css/style.css +1916 -0
- package/.webpack/x64/renderer/assets/images/loading.gif +0 -0
- package/.webpack/x64/renderer/assets/images/logo.webp +0 -0
- package/.webpack/x64/renderer/assets/images/packet-snitch-tag.webp +0 -0
- package/.webpack/x64/renderer/main_window/index.html +3 -0
- package/.webpack/x64/renderer/main_window/index.js +3 -0
- package/.webpack/x64/renderer/main_window/index.js.LICENSE.txt +36 -0
- package/.webpack/x64/renderer/main_window/index.js.map +1 -0
- package/.webpack/x64/renderer/main_window/preload.js +2 -0
- package/.webpack/x64/renderer/main_window/preload.js.map +1 -0
- package/backend/common/GeoLite2-City.mmdb +0 -0
- package/backend/common/mac-vendors-export.csv +56923 -0
- package/backend/common/service-names-port-numbers.csv +15368 -0
- package/backend/requirements.txt +14 -0
- package/backend/snitch.py +3611 -0
- package/forge.config.js +80 -0
- package/package.json +102 -0
- package/ps-icon.ico +0 -0
- package/snitch.spec +44 -0
- package/src/assets/css/rubikglitch.woff2 +0 -0
- package/src/assets/css/style.css +1916 -0
- package/src/assets/images/loading.gif +0 -0
- package/src/assets/images/logo.webp +0 -0
- package/src/assets/images/packet-snitch-tag.webp +0 -0
- package/src/back-comm.js +70 -0
- package/src/decoders.js +579 -0
- package/src/filter.js +461 -0
- package/src/front.js +10 -0
- package/src/index.html +1036 -0
- package/src/logging.js +150 -0
- package/src/main.js +571 -0
- package/src/preload.js +73 -0
- package/src/renderer.js +30 -0
- package/src/ui/common-frontend.js +13 -0
- package/src/ui/context-menu.js +88 -0
- package/src/ui/decoders.js +1 -0
- package/src/ui/main-frontend.js +4957 -0
- package/src/ui/panels/crypt-panel.js +565 -0
- package/src/ui/panels/data-panel.js +151 -0
- package/src/ui/panels/data-tools-panel.js +939 -0
- package/src/ui/panels/install-screen.js +59 -0
- package/src/ui/panels/keystore-panel.js +1248 -0
- package/src/ui/panels/list-panel.js +403 -0
- package/src/ui/panels/stats-panel.js +351 -0
- package/src/ui/panels/summary-panel.js +63 -0
- package/webpack.main.config.js +11 -0
- package/webpack.plugins.js +13 -0
- package/webpack.preload.config.js +7 -0
- package/webpack.renderer.config.js +30 -0
- package/webpack.rules.js +35 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
const crypto = require("crypto-browserify");
|
|
2
|
+
const TLS_CONTENT_TYPE_MIN = 20;
|
|
3
|
+
const TLS_CONTENT_TYPE_MAX = 23;
|
|
4
|
+
const TLS_HANDSHAKE_TYPE_CLIENT_KEY_EXCHANGE = 16;
|
|
5
|
+
const PRINTABLE_UTF8_PREVIEW_REGEX = /^[\x09\x0A\x0D\x20-\x7E]*$/;
|
|
6
|
+
const MAX_ASCII_PREVIEW_LENGTH = 1024;
|
|
7
|
+
const MAX_DECRYPT_FAILURE_MESSAGES = 8;
|
|
8
|
+
|
|
9
|
+
function createCryptPanel({
|
|
10
|
+
constants,
|
|
11
|
+
getCapturedPackets,
|
|
12
|
+
getJsonCapture,
|
|
13
|
+
setActiveMainTab,
|
|
14
|
+
setActiveCryptSubtab,
|
|
15
|
+
statusUpdate,
|
|
16
|
+
writeLogEntry,
|
|
17
|
+
doError,
|
|
18
|
+
logErrorEntry,
|
|
19
|
+
filterInputEl,
|
|
20
|
+
syncFilterHighlight,
|
|
21
|
+
runFilterQuery,
|
|
22
|
+
addSessionKeystoreEntry,
|
|
23
|
+
getFirstLineOrFallback,
|
|
24
|
+
sendDecryptedToConv,
|
|
25
|
+
}) {
|
|
26
|
+
const {
|
|
27
|
+
MAIN_TAB_CRYPT,
|
|
28
|
+
CRYPT_SSL_SUBTAB,
|
|
29
|
+
CRYPT_PGP_SUBTAB,
|
|
30
|
+
CRYPT_OPENSSH_SUBTAB,
|
|
31
|
+
SESSION_KEYCHAIN_LABEL,
|
|
32
|
+
STRICT_IPV4_REGEX,
|
|
33
|
+
} = constants;
|
|
34
|
+
|
|
35
|
+
let cryptEncounteredEntries = [];
|
|
36
|
+
let cryptActiveEntryIndex = -1;
|
|
37
|
+
let cryptLastDecryptedPayload = null;
|
|
38
|
+
|
|
39
|
+
function formatCryptSummary(rawText, label, sourceLabel, expectedRegex) {
|
|
40
|
+
const normalized = (rawText || "").trim();
|
|
41
|
+
if (!normalized) {
|
|
42
|
+
return `No ${label.toLowerCase()} loaded.`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const lines = normalized.split(/\r?\n/);
|
|
46
|
+
const beginMatch = normalized.match(/-----BEGIN ([^-]+)-----/);
|
|
47
|
+
const endMatch = normalized.match(/-----END ([^-]+)-----/);
|
|
48
|
+
const blockType = beginMatch ? beginMatch[1] : "Plain text";
|
|
49
|
+
const looksExpected =
|
|
50
|
+
!expectedRegex || (beginMatch && expectedRegex.test(blockType));
|
|
51
|
+
|
|
52
|
+
return [
|
|
53
|
+
`${label} loaded from ${sourceLabel}.`,
|
|
54
|
+
`Bytes: ${new TextEncoder().encode(normalized).length}`,
|
|
55
|
+
`Lines: ${lines.length}`,
|
|
56
|
+
`Detected block type: ${blockType}`,
|
|
57
|
+
beginMatch && endMatch
|
|
58
|
+
? `PEM boundaries: ${beginMatch[1]} ... ${endMatch[1]}`
|
|
59
|
+
: "PEM boundaries not detected",
|
|
60
|
+
looksExpected
|
|
61
|
+
? "Format check: looks valid for this input type"
|
|
62
|
+
: "Format check: unexpected block type for this input",
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getCryptEncounteredEntries() {
|
|
67
|
+
const entries = [];
|
|
68
|
+
const capturedPackets = getCapturedPackets();
|
|
69
|
+
if (
|
|
70
|
+
!capturedPackets ||
|
|
71
|
+
typeof capturedPackets !== "object" ||
|
|
72
|
+
!capturedPackets["Host"]
|
|
73
|
+
) {
|
|
74
|
+
return entries;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const host of Object.keys(capturedPackets["Host"])) {
|
|
78
|
+
const packets = capturedPackets["Host"][host];
|
|
79
|
+
if (!Array.isArray(packets)) continue;
|
|
80
|
+
|
|
81
|
+
packets.forEach((packet) => {
|
|
82
|
+
const packetInfo = packet?.["Packet Info"];
|
|
83
|
+
const extraInfo = packet?.["Extra Info"];
|
|
84
|
+
const serverInfo = extraInfo?.["Traits"]?.["Server Info"];
|
|
85
|
+
const encryptionData = serverInfo?.["Encryption Data"];
|
|
86
|
+
if (!packetInfo || !serverInfo || !encryptionData || encryptionData === "N/A")
|
|
87
|
+
return;
|
|
88
|
+
|
|
89
|
+
const protocol = packetInfo["Protocol"] || "Unknown";
|
|
90
|
+
const transportData = packetInfo[protocol] || {};
|
|
91
|
+
const encryptedWithRaw = encryptionData["Encrypted With"];
|
|
92
|
+
const encryptedWith = Array.isArray(encryptedWithRaw)
|
|
93
|
+
? encryptedWithRaw.filter(Boolean)
|
|
94
|
+
: encryptedWithRaw
|
|
95
|
+
? [String(encryptedWithRaw)]
|
|
96
|
+
: [];
|
|
97
|
+
entries.push({
|
|
98
|
+
host,
|
|
99
|
+
packetIndex: packetInfo["Index"] ?? "?",
|
|
100
|
+
protocol,
|
|
101
|
+
srcIp: packetInfo?.["IP"]?.["Source IP"] ?? "N/A",
|
|
102
|
+
dstIp: packetInfo?.["IP"]?.["Destination IP"] ?? "N/A",
|
|
103
|
+
srcPort: transportData?.["Source port"] ?? "N/A",
|
|
104
|
+
dstPort: transportData?.["Destination port"] ?? "N/A",
|
|
105
|
+
encrypted: serverInfo["Encrypted"] ?? "Unknown",
|
|
106
|
+
sslVersion: encryptionData["SSL Version"] ?? "Unknown",
|
|
107
|
+
sslCert: encryptionData["SSL Cert"] ?? "",
|
|
108
|
+
encryptedWith,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return entries.sort((a, b) => {
|
|
114
|
+
const aIdx = Number(a.packetIndex);
|
|
115
|
+
const bIdx = Number(b.packetIndex);
|
|
116
|
+
if (Number.isFinite(aIdx) && Number.isFinite(bIdx)) return aIdx - bIdx;
|
|
117
|
+
return String(a.packetIndex).localeCompare(String(b.packetIndex));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderCryptEncounteredDetails(entry) {
|
|
122
|
+
const detailsEl = document.getElementById("crypt-encountered-details");
|
|
123
|
+
if (!entry) {
|
|
124
|
+
detailsEl.textContent = "Select an encountered SSL/TLS item to inspect.";
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const algoText = entry.encryptedWith.length
|
|
128
|
+
? entry.encryptedWith.join(", ")
|
|
129
|
+
: "Unavailable";
|
|
130
|
+
detailsEl.textContent = [
|
|
131
|
+
`Host: ${entry.host}`,
|
|
132
|
+
`Packet: ${entry.packetIndex}`,
|
|
133
|
+
`Protocol: ${entry.protocol}`,
|
|
134
|
+
`Path: ${entry.srcIp}:${entry.srcPort} -> ${entry.dstIp}:${entry.dstPort}`,
|
|
135
|
+
`Encrypted: ${entry.encrypted}`,
|
|
136
|
+
`SSL/TLS Version: ${entry.sslVersion}`,
|
|
137
|
+
`Algorithms: ${algoText}`,
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function refreshCryptEncounteredEntries() {
|
|
142
|
+
const listEl = document.getElementById("crypt-encountered-list");
|
|
143
|
+
cryptEncounteredEntries = getCryptEncounteredEntries();
|
|
144
|
+
listEl.replaceChildren();
|
|
145
|
+
cryptActiveEntryIndex = -1;
|
|
146
|
+
|
|
147
|
+
if (cryptEncounteredEntries.length === 0) {
|
|
148
|
+
const option = document.createElement("option");
|
|
149
|
+
option.textContent = "No SSL/TLS encryption encountered in loaded capture.";
|
|
150
|
+
option.disabled = true;
|
|
151
|
+
listEl.appendChild(option);
|
|
152
|
+
renderCryptEncounteredDetails(null);
|
|
153
|
+
clearCryptDecryptionOutput();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
cryptEncounteredEntries.forEach((entry, entryIndex) => {
|
|
158
|
+
const option = document.createElement("option");
|
|
159
|
+
option.value = String(entryIndex);
|
|
160
|
+
const algoPreview = entry.encryptedWith.length
|
|
161
|
+
? entry.encryptedWith[0]
|
|
162
|
+
: "Unknown cipher";
|
|
163
|
+
option.textContent = `#${entry.packetIndex} ${entry.sslVersion} ${entry.srcIp}:${entry.srcPort} -> ${entry.dstIp}:${entry.dstPort} (${algoPreview})`;
|
|
164
|
+
listEl.appendChild(option);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
listEl.selectedIndex = 0;
|
|
168
|
+
cryptActiveEntryIndex = 0;
|
|
169
|
+
renderCryptEncounteredDetails(cryptEncounteredEntries[0]);
|
|
170
|
+
clearCryptDecryptionOutput();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function setDecryptSendEnabled(isEnabled) {
|
|
174
|
+
const sendBtnEl = document.getElementById("crypt-send-decrypted-conv-btn");
|
|
175
|
+
if (sendBtnEl) {
|
|
176
|
+
sendBtnEl.disabled = !isEnabled;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function clearCryptDecryptionOutput() {
|
|
181
|
+
const decryptPreviewEl = document.getElementById("crypt-decrypt-preview");
|
|
182
|
+
if (decryptPreviewEl) {
|
|
183
|
+
decryptPreviewEl.textContent = "No decrypted TLS/SSL output yet.";
|
|
184
|
+
}
|
|
185
|
+
cryptLastDecryptedPayload = null;
|
|
186
|
+
setDecryptSendEnabled(false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function findPayloadHexForEncounteredEntry(entry) {
|
|
190
|
+
const packets = getCapturedPackets()?.["Host"]?.[entry.host];
|
|
191
|
+
if (!Array.isArray(packets)) return "";
|
|
192
|
+
const matchedPacket = packets.find((packet) => {
|
|
193
|
+
const packetIndex = packet?.["Packet Info"]?.["Index"];
|
|
194
|
+
return String(packetIndex) === String(entry.packetIndex);
|
|
195
|
+
});
|
|
196
|
+
return String(
|
|
197
|
+
matchedPacket?.["Packet Info"]?.["Raw data"]?.["Payload"]?.["Hex Encoded"] || "",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function extractDecryptCandidates(cipherBytes) {
|
|
202
|
+
const candidates = [cipherBytes];
|
|
203
|
+
if (
|
|
204
|
+
cipherBytes.length > 5 &&
|
|
205
|
+
cipherBytes[0] >= TLS_CONTENT_TYPE_MIN &&
|
|
206
|
+
cipherBytes[0] <= TLS_CONTENT_TYPE_MAX
|
|
207
|
+
) {
|
|
208
|
+
const recordLength = (cipherBytes[3] << 8) | cipherBytes[4];
|
|
209
|
+
const recordEnd = 5 + recordLength;
|
|
210
|
+
if (recordLength > 0 && recordEnd <= cipherBytes.length) {
|
|
211
|
+
const recordPayload = cipherBytes.subarray(5, recordEnd);
|
|
212
|
+
candidates.push(recordPayload);
|
|
213
|
+
if (
|
|
214
|
+
recordPayload.length > 6 &&
|
|
215
|
+
recordPayload[0] === TLS_HANDSHAKE_TYPE_CLIENT_KEY_EXCHANGE
|
|
216
|
+
) {
|
|
217
|
+
const handshakeBody = recordPayload.subarray(4);
|
|
218
|
+
candidates.push(handshakeBody);
|
|
219
|
+
if (handshakeBody.length > 2) {
|
|
220
|
+
const encryptedLen = (handshakeBody[0] << 8) | handshakeBody[1];
|
|
221
|
+
if (encryptedLen > 0 && encryptedLen + 2 <= handshakeBody.length) {
|
|
222
|
+
candidates.push(handshakeBody.subarray(2, 2 + encryptedLen));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return candidates;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function decryptTlsCipherBytes(cipherBytes, privateKeyPem) {
|
|
232
|
+
const candidates = extractDecryptCandidates(cipherBytes);
|
|
233
|
+
const decryptVariants = [
|
|
234
|
+
{ name: "RSA-OAEP-SHA256", options: { padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" } },
|
|
235
|
+
{ name: "RSA-PKCS1-v1_5", options: { padding: crypto.constants.RSA_PKCS1_PADDING } },
|
|
236
|
+
];
|
|
237
|
+
const failures = [];
|
|
238
|
+
for (const candidate of candidates) {
|
|
239
|
+
for (const variant of decryptVariants) {
|
|
240
|
+
try {
|
|
241
|
+
const decrypted = crypto.privateDecrypt(
|
|
242
|
+
{
|
|
243
|
+
key: privateKeyPem,
|
|
244
|
+
...variant.options,
|
|
245
|
+
},
|
|
246
|
+
candidate,
|
|
247
|
+
);
|
|
248
|
+
return decrypted;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
failures.push(`${variant.name}: ${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const failurePreview = [...new Set(failures)]
|
|
255
|
+
.slice(0, MAX_DECRYPT_FAILURE_MESSAGES)
|
|
256
|
+
.join("; ");
|
|
257
|
+
throw new Error(
|
|
258
|
+
`No TLS decrypt attempt succeeded with the loaded key (${failurePreview})`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function certMatchesPrivateKey(certificatePem, privateKeyPem) {
|
|
263
|
+
const normalizedCert = String(certificatePem || "").trim();
|
|
264
|
+
if (!normalizedCert) return { matched: true };
|
|
265
|
+
if (
|
|
266
|
+
typeof crypto.X509Certificate !== "function" ||
|
|
267
|
+
typeof crypto.createPublicKey !== "function"
|
|
268
|
+
) {
|
|
269
|
+
return {
|
|
270
|
+
matched: null,
|
|
271
|
+
reason: "Certificate/key pair validation is unavailable in this runtime.",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const certPublicKeyPem = crypto
|
|
276
|
+
.createPublicKey(new crypto.X509Certificate(normalizedCert).publicKey)
|
|
277
|
+
.export({ type: "spki", format: "pem" })
|
|
278
|
+
.toString();
|
|
279
|
+
const privateKeyPublicPem = crypto
|
|
280
|
+
.createPublicKey(privateKeyPem)
|
|
281
|
+
.export({ type: "spki", format: "pem" })
|
|
282
|
+
.toString();
|
|
283
|
+
return { matched: certPublicKeyPem === privateKeyPublicPem };
|
|
284
|
+
} catch (error) {
|
|
285
|
+
logErrorEntry("crypt-cert-key-check", error);
|
|
286
|
+
return {
|
|
287
|
+
matched: null,
|
|
288
|
+
reason: "Certificate/key pair validation failed and was skipped.",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function renderDecryptedPayload(entry, decryptedBytes) {
|
|
294
|
+
const decryptPreviewEl = document.getElementById("crypt-decrypt-preview");
|
|
295
|
+
const decryptedHex = decryptedBytes.toString("hex");
|
|
296
|
+
const decryptedUtf8 = decryptedBytes.toString("utf8");
|
|
297
|
+
const looksPrintable = PRINTABLE_UTF8_PREVIEW_REGEX.test(decryptedUtf8);
|
|
298
|
+
const asciiSummary = looksPrintable
|
|
299
|
+
? decryptedUtf8.slice(0, MAX_ASCII_PREVIEW_LENGTH)
|
|
300
|
+
: decryptedUtf8
|
|
301
|
+
.slice(0, MAX_ASCII_PREVIEW_LENGTH)
|
|
302
|
+
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, ".");
|
|
303
|
+
decryptPreviewEl.textContent = [
|
|
304
|
+
`Decrypted payload for packet #${entry.packetIndex}`,
|
|
305
|
+
`Bytes: ${decryptedBytes.length}`,
|
|
306
|
+
"",
|
|
307
|
+
"ASCII / UTF-8 preview:",
|
|
308
|
+
asciiSummary || "(no printable output)",
|
|
309
|
+
"",
|
|
310
|
+
"Hex:",
|
|
311
|
+
decryptedHex || "(empty)",
|
|
312
|
+
].join("\n");
|
|
313
|
+
cryptLastDecryptedPayload = {
|
|
314
|
+
sourceLabel: `packet #${entry.packetIndex}`,
|
|
315
|
+
hexValue: decryptedHex,
|
|
316
|
+
utf8Value: decryptedUtf8,
|
|
317
|
+
};
|
|
318
|
+
setDecryptSendEnabled(true);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function setCryptSubtab(tabName) {
|
|
322
|
+
setActiveCryptSubtab(tabName);
|
|
323
|
+
const sslActive = tabName === CRYPT_SSL_SUBTAB;
|
|
324
|
+
const pgpActive = tabName === CRYPT_PGP_SUBTAB;
|
|
325
|
+
const opensshActive = tabName === CRYPT_OPENSSH_SUBTAB;
|
|
326
|
+
document.getElementById("crypt-subtab-ssl").classList.toggle("active", sslActive);
|
|
327
|
+
document.getElementById("crypt-subtab-pgp").classList.toggle("active", pgpActive);
|
|
328
|
+
document
|
|
329
|
+
.getElementById("crypt-subtab-openssh")
|
|
330
|
+
.classList.toggle("active", opensshActive);
|
|
331
|
+
document.getElementById("crypt-ssl-panel").hidden = !sslActive;
|
|
332
|
+
document.getElementById("crypt-pgp-panel").hidden = !pgpActive;
|
|
333
|
+
document.getElementById("crypt-openssh-panel").hidden = !opensshActive;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function applyCryptCertificateText(rawText, sourceLabel) {
|
|
337
|
+
const certInputEl = document.getElementById("crypt-cert-input");
|
|
338
|
+
const certPreviewEl = document.getElementById("crypt-cert-preview");
|
|
339
|
+
const normalized = (rawText || "").trim();
|
|
340
|
+
certInputEl.value = normalized;
|
|
341
|
+
certPreviewEl.textContent = formatCryptSummary(
|
|
342
|
+
normalized,
|
|
343
|
+
"Certificate",
|
|
344
|
+
sourceLabel,
|
|
345
|
+
/CERTIFICATE/i,
|
|
346
|
+
);
|
|
347
|
+
if (normalized) {
|
|
348
|
+
statusUpdate(`Status: Certificate loaded from ${sourceLabel}`);
|
|
349
|
+
writeLogEntry(`Crypt certificate loaded source="${sourceLabel}"`);
|
|
350
|
+
if (sourceLabel !== SESSION_KEYCHAIN_LABEL) {
|
|
351
|
+
addSessionKeystoreEntry({
|
|
352
|
+
type: "certificate",
|
|
353
|
+
label: getFirstLineOrFallback(
|
|
354
|
+
"crypt-cert-preview",
|
|
355
|
+
`Certificate-${new Date().toISOString()}`,
|
|
356
|
+
),
|
|
357
|
+
source: `cert-tab ${sourceLabel}`,
|
|
358
|
+
content: normalized,
|
|
359
|
+
summary: "Imported into cert tab",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function applyCryptPrivateKeyText(rawText, sourceLabel) {
|
|
366
|
+
const keyInputEl = document.getElementById("crypt-key-input");
|
|
367
|
+
const keyPreviewEl = document.getElementById("crypt-key-preview");
|
|
368
|
+
const normalized = (rawText || "").trim();
|
|
369
|
+
keyInputEl.value = normalized;
|
|
370
|
+
keyPreviewEl.textContent = formatCryptSummary(
|
|
371
|
+
normalized,
|
|
372
|
+
"Private key",
|
|
373
|
+
sourceLabel,
|
|
374
|
+
/(PRIVATE KEY|OPENSSH)/i,
|
|
375
|
+
);
|
|
376
|
+
if (normalized) {
|
|
377
|
+
statusUpdate(`Status: Private key loaded from ${sourceLabel}`);
|
|
378
|
+
writeLogEntry(`Crypt private key loaded source="${sourceLabel}"`);
|
|
379
|
+
if (sourceLabel !== SESSION_KEYCHAIN_LABEL) {
|
|
380
|
+
addSessionKeystoreEntry({
|
|
381
|
+
type: "private-key",
|
|
382
|
+
label: getFirstLineOrFallback(
|
|
383
|
+
"crypt-key-preview",
|
|
384
|
+
`Private-key-${new Date().toISOString()}`,
|
|
385
|
+
),
|
|
386
|
+
source: `cert-tab ${sourceLabel}`,
|
|
387
|
+
content: normalized,
|
|
388
|
+
summary: "Imported into cert tab",
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readCryptTextFile(fileInputEl, onLoad) {
|
|
395
|
+
const file = fileInputEl.files?.[0];
|
|
396
|
+
if (!file) return;
|
|
397
|
+
const reader = new FileReader();
|
|
398
|
+
reader.onload = () => onLoad(String(reader.result || ""), `file ${file.name}`);
|
|
399
|
+
reader.onerror = (error) => {
|
|
400
|
+
logErrorEntry("crypt-file-read", error);
|
|
401
|
+
doError("Could not read selected crypt file.");
|
|
402
|
+
};
|
|
403
|
+
reader.readAsText(file);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function applyCryptFilterForActiveEntry() {
|
|
407
|
+
if (cryptActiveEntryIndex < 0 || !cryptEncounteredEntries[cryptActiveEntryIndex]) {
|
|
408
|
+
statusUpdate("Status: Select an encountered SSL/TLS entry first");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const activeEntry = cryptEncounteredEntries[cryptActiveEntryIndex];
|
|
412
|
+
if (
|
|
413
|
+
!STRICT_IPV4_REGEX.test(String(activeEntry.srcIp || "")) ||
|
|
414
|
+
!STRICT_IPV4_REGEX.test(String(activeEntry.dstIp || ""))
|
|
415
|
+
) {
|
|
416
|
+
statusUpdate("Status: Cannot build filter query for non-IPv4 packet endpoints");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const query = `ip.src.addr: ${activeEntry.srcIp} && ip.dst.addr: ${activeEntry.dstIp}`;
|
|
420
|
+
filterInputEl.value = query;
|
|
421
|
+
syncFilterHighlight();
|
|
422
|
+
runFilterQuery(query);
|
|
423
|
+
writeLogEntry(`Crypt filter applied query="${query}"`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function loadEncounteredCertificateIntoCrypt() {
|
|
427
|
+
if (cryptActiveEntryIndex < 0 || !cryptEncounteredEntries[cryptActiveEntryIndex]) {
|
|
428
|
+
statusUpdate("Status: Select an encountered SSL/TLS entry first");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const activeEntry = cryptEncounteredEntries[cryptActiveEntryIndex];
|
|
432
|
+
if (!activeEntry.sslCert || activeEntry.sslCert === "Not available") {
|
|
433
|
+
statusUpdate("Status: No certificate text available for selected entry");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
applyCryptCertificateText(
|
|
437
|
+
String(activeEntry.sslCert),
|
|
438
|
+
`encountered packet #${activeEntry.packetIndex}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function selectEncounteredEntry(selectedIndex) {
|
|
443
|
+
if (!Number.isFinite(selectedIndex) || !cryptEncounteredEntries[selectedIndex]) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
cryptActiveEntryIndex = selectedIndex;
|
|
447
|
+
renderCryptEncounteredDetails(cryptEncounteredEntries[selectedIndex]);
|
|
448
|
+
clearCryptDecryptionOutput();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function decryptActiveEntryWithLoadedKey() {
|
|
452
|
+
if (cryptActiveEntryIndex < 0 || !cryptEncounteredEntries[cryptActiveEntryIndex]) {
|
|
453
|
+
statusUpdate("Status: Select an encountered SSL/TLS entry first");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const privateKeyPem = String(
|
|
457
|
+
document.getElementById("crypt-key-input")?.value || "",
|
|
458
|
+
).trim();
|
|
459
|
+
if (!privateKeyPem) {
|
|
460
|
+
statusUpdate("Status: Load a private key from keychain or file first");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const certificatePem = String(
|
|
464
|
+
document.getElementById("crypt-cert-input")?.value || "",
|
|
465
|
+
).trim();
|
|
466
|
+
const certKeyCheck = certMatchesPrivateKey(certificatePem, privateKeyPem);
|
|
467
|
+
if (certificatePem && certKeyCheck.matched === false) {
|
|
468
|
+
statusUpdate(
|
|
469
|
+
"Status: Loaded certificate does not match private key (continuing with key)",
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (certificatePem && certKeyCheck.matched === null && certKeyCheck.reason) {
|
|
473
|
+
writeLogEntry(`Crypt cert/key check skipped: ${certKeyCheck.reason}`);
|
|
474
|
+
}
|
|
475
|
+
const activeEntry = cryptEncounteredEntries[cryptActiveEntryIndex];
|
|
476
|
+
const payloadHex = findPayloadHexForEncounteredEntry(activeEntry).replace(
|
|
477
|
+
/[^0-9A-Fa-f]/g,
|
|
478
|
+
"",
|
|
479
|
+
);
|
|
480
|
+
if (!payloadHex) {
|
|
481
|
+
statusUpdate("Status: Selected packet has no payload to decrypt");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (payloadHex.length % 2 !== 0) {
|
|
485
|
+
doError("Selected payload is not valid hex data.");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const decryptedBytes = decryptTlsCipherBytes(
|
|
490
|
+
Buffer.from(payloadHex, "hex"),
|
|
491
|
+
privateKeyPem,
|
|
492
|
+
);
|
|
493
|
+
renderDecryptedPayload(activeEntry, decryptedBytes);
|
|
494
|
+
statusUpdate(`Status: Decrypted TLS/SSL payload for packet #${activeEntry.packetIndex}`);
|
|
495
|
+
writeLogEntry(`Crypt decrypted payload packet_index=${activeEntry.packetIndex}`);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
clearCryptDecryptionOutput();
|
|
498
|
+
logErrorEntry("crypt-tls-decrypt", error);
|
|
499
|
+
doError(
|
|
500
|
+
"Could not decrypt selected TLS/SSL payload with the loaded private key.",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function sendDecryptedPayloadToConvTab() {
|
|
506
|
+
if (!cryptLastDecryptedPayload) {
|
|
507
|
+
statusUpdate("Status: Decrypt data first before sending to Conv");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
sendDecryptedToConv(cryptLastDecryptedPayload);
|
|
511
|
+
statusUpdate(
|
|
512
|
+
`Status: Sent decrypted payload from ${cryptLastDecryptedPayload.sourceLabel} to Conv`,
|
|
513
|
+
);
|
|
514
|
+
writeLogEntry(
|
|
515
|
+
`Crypt decrypted payload sent to Conv source="${cryptLastDecryptedPayload.sourceLabel}"`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function showCryptWorkspace(tabName = CRYPT_SSL_SUBTAB) {
|
|
520
|
+
setActiveMainTab(MAIN_TAB_CRYPT);
|
|
521
|
+
if (getJsonCapture() === "") {
|
|
522
|
+
statusUpdate("Status: No JSON file loaded, please upload a file first");
|
|
523
|
+
doError("Please upload a JSON file before accessing crypt tools.");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
statusUpdate("Status: Displaying crypt workspace");
|
|
528
|
+
writeLogEntry("User opened crypt workspace");
|
|
529
|
+
document.getElementById("prev-btn").style.display = "none";
|
|
530
|
+
document.getElementById("next-btn").style.display = "none";
|
|
531
|
+
document.getElementById("packetInfoPane").style.display = "none";
|
|
532
|
+
document.getElementById("packetPayloadPane").style.display = "none";
|
|
533
|
+
document.getElementById("summary_box").style.display = "none";
|
|
534
|
+
document.getElementById("stats_box").style.display = "none";
|
|
535
|
+
document.getElementById("data_tools_box").style.display = "none";
|
|
536
|
+
document.getElementById("list_box").style.display = "none";
|
|
537
|
+
document.getElementById("notes_box").style.display = "none";
|
|
538
|
+
document.getElementById("keystore_box").style.display = "none";
|
|
539
|
+
document.getElementById("rightside").style.display = "none";
|
|
540
|
+
const cryptBoxEl = document.getElementById("crypt_box");
|
|
541
|
+
cryptBoxEl.style.display = "flex";
|
|
542
|
+
setCryptSubtab(tabName);
|
|
543
|
+
refreshCryptEncounteredEntries();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
setCryptSubtab,
|
|
548
|
+
showCryptWorkspace,
|
|
549
|
+
refreshCryptEncounteredEntries,
|
|
550
|
+
readCryptTextFile,
|
|
551
|
+
applyCryptCertificateText,
|
|
552
|
+
applyCryptPrivateKeyText,
|
|
553
|
+
applyCryptFilterForActiveEntry,
|
|
554
|
+
loadEncounteredCertificateIntoCrypt,
|
|
555
|
+
selectEncounteredEntry,
|
|
556
|
+
decryptActiveEntryWithLoadedKey,
|
|
557
|
+
sendDecryptedPayloadToConvTab,
|
|
558
|
+
clearCryptDecryptionOutput,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
module.exports = {
|
|
563
|
+
id: "crypt",
|
|
564
|
+
createCryptPanel,
|
|
565
|
+
};
|