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,1248 @@
|
|
|
1
|
+
const CRYPT_KEYSTORE_DB_NAME = "packetsnitch-crypt-keystore";
|
|
2
|
+
const CRYPT_KEYSTORE_DB_VERSION = 1;
|
|
3
|
+
const CRYPT_KEYSTORE_STORE_NAME = "entries";
|
|
4
|
+
const CRYPT_KEYSTORE_RECORD_KEY = "default";
|
|
5
|
+
const CRYPT_KEYSTORE_SCHEMA_VERSION = 2;
|
|
6
|
+
const CRYPT_KEYSTORE_MODE_SESSION = "session";
|
|
7
|
+
const CRYPT_KEYSTORE_MODE_PERSISTENT = "persistent";
|
|
8
|
+
const SESSION_KEYCHAIN_LABEL = "session keychain";
|
|
9
|
+
const SESSION_SECRET_KEY_HINTS = [
|
|
10
|
+
"password",
|
|
11
|
+
"passwd",
|
|
12
|
+
"passphrase",
|
|
13
|
+
"secret",
|
|
14
|
+
"credential",
|
|
15
|
+
"token",
|
|
16
|
+
"authorization",
|
|
17
|
+
"auth",
|
|
18
|
+
"username",
|
|
19
|
+
"user",
|
|
20
|
+
"login",
|
|
21
|
+
"apikey",
|
|
22
|
+
"api_key",
|
|
23
|
+
"api-key",
|
|
24
|
+
"cookie",
|
|
25
|
+
"session",
|
|
26
|
+
"sessionid",
|
|
27
|
+
"set_cookie",
|
|
28
|
+
"set-cookie",
|
|
29
|
+
];
|
|
30
|
+
const SESSION_SECRET_IGNORE_KEY_HINTS = [
|
|
31
|
+
"encrypted",
|
|
32
|
+
"length",
|
|
33
|
+
"checksum",
|
|
34
|
+
"version",
|
|
35
|
+
"port",
|
|
36
|
+
"ip",
|
|
37
|
+
"mac",
|
|
38
|
+
"ttl",
|
|
39
|
+
"window",
|
|
40
|
+
"sequence",
|
|
41
|
+
"ack",
|
|
42
|
+
"timestamp",
|
|
43
|
+
"frame",
|
|
44
|
+
"packet",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function createKeystorePanel({
|
|
48
|
+
statusUpdate,
|
|
49
|
+
writeLogEntry,
|
|
50
|
+
doError,
|
|
51
|
+
logErrorEntry,
|
|
52
|
+
getCapturedPackets,
|
|
53
|
+
getJsonCapture,
|
|
54
|
+
setActiveMainTab,
|
|
55
|
+
MAIN_TAB_KEYSTORE,
|
|
56
|
+
parseDataToolsInput,
|
|
57
|
+
decodeHttpFromBytes,
|
|
58
|
+
extractCookieJarEntriesFromHttpFields,
|
|
59
|
+
getTrimmedSelectionText,
|
|
60
|
+
hideConvertContextMenu,
|
|
61
|
+
getActiveContextConversionText,
|
|
62
|
+
getApplyCryptCertificateText,
|
|
63
|
+
getApplyCryptPrivateKeyText,
|
|
64
|
+
openExternalUrl,
|
|
65
|
+
}) {
|
|
66
|
+
let cryptPersistentKeystoreEntries = [];
|
|
67
|
+
let cryptSessionKeystoreEntries = [];
|
|
68
|
+
let cryptActiveKeystoreMode = CRYPT_KEYSTORE_MODE_SESSION;
|
|
69
|
+
let cryptKeystoreUnlockKeyMaterial = null;
|
|
70
|
+
let cryptKeystoreUnlockDialogResolver = null;
|
|
71
|
+
let cryptKeystoreUnlockDialogMode = "unlock";
|
|
72
|
+
let cryptManualUriDialogResolver = null;
|
|
73
|
+
let cryptManualUriDialogMode = CRYPT_KEYSTORE_MODE_SESSION;
|
|
74
|
+
|
|
75
|
+
function generateCryptEntryId() {
|
|
76
|
+
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
77
|
+
return window.crypto.randomUUID();
|
|
78
|
+
}
|
|
79
|
+
if (window.crypto && typeof window.crypto.getRandomValues === "function") {
|
|
80
|
+
const bytes = window.crypto.getRandomValues(new Uint8Array(16));
|
|
81
|
+
const hex = Array.from(bytes, (byte) =>
|
|
82
|
+
byte.toString(16).padStart(2, "0"),
|
|
83
|
+
).join("");
|
|
84
|
+
return `${Date.now()}-${hex}`;
|
|
85
|
+
}
|
|
86
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toBase64(bytes) {
|
|
90
|
+
return window.btoa(
|
|
91
|
+
Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fromBase64(base64) {
|
|
96
|
+
const binary = window.atob(base64);
|
|
97
|
+
const bytes = new Uint8Array(binary.length);
|
|
98
|
+
for (let index = 0; index < binary.length; index++) {
|
|
99
|
+
bytes[index] = binary.charCodeAt(index);
|
|
100
|
+
}
|
|
101
|
+
return bytes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function importCryptKeyMaterial(passphrase) {
|
|
105
|
+
return window.crypto.subtle.importKey(
|
|
106
|
+
"raw",
|
|
107
|
+
new TextEncoder().encode(passphrase),
|
|
108
|
+
{ name: "PBKDF2" },
|
|
109
|
+
false,
|
|
110
|
+
["deriveKey"],
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function deriveCryptKey(passphraseOrKeyMaterial, saltBytes, usage) {
|
|
115
|
+
const keyMaterial =
|
|
116
|
+
typeof passphraseOrKeyMaterial === "string"
|
|
117
|
+
? await importCryptKeyMaterial(passphraseOrKeyMaterial)
|
|
118
|
+
: passphraseOrKeyMaterial;
|
|
119
|
+
return window.crypto.subtle.deriveKey(
|
|
120
|
+
{
|
|
121
|
+
name: "PBKDF2",
|
|
122
|
+
salt: saltBytes,
|
|
123
|
+
iterations: 600000,
|
|
124
|
+
hash: "SHA-256",
|
|
125
|
+
},
|
|
126
|
+
keyMaterial,
|
|
127
|
+
{ name: "AES-GCM", length: 256 },
|
|
128
|
+
false,
|
|
129
|
+
[usage],
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function encryptCryptContent(content, passphrase) {
|
|
134
|
+
const salt = window.crypto.getRandomValues(new Uint8Array(16));
|
|
135
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
136
|
+
const key = await deriveCryptKey(passphrase, salt, "encrypt");
|
|
137
|
+
const ciphertext = await window.crypto.subtle.encrypt(
|
|
138
|
+
{ name: "AES-GCM", iv },
|
|
139
|
+
key,
|
|
140
|
+
new TextEncoder().encode(content),
|
|
141
|
+
);
|
|
142
|
+
return {
|
|
143
|
+
encryptedContent: toBase64(new Uint8Array(ciphertext)),
|
|
144
|
+
salt: toBase64(salt),
|
|
145
|
+
iv: toBase64(iv),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function decryptCryptContent(entry, passphrase) {
|
|
150
|
+
const key = await deriveCryptKey(
|
|
151
|
+
passphrase,
|
|
152
|
+
fromBase64(entry.salt),
|
|
153
|
+
"decrypt",
|
|
154
|
+
);
|
|
155
|
+
const decrypted = await window.crypto.subtle.decrypt(
|
|
156
|
+
{ name: "AES-GCM", iv: fromBase64(entry.iv) },
|
|
157
|
+
key,
|
|
158
|
+
fromBase64(entry.encryptedContent),
|
|
159
|
+
);
|
|
160
|
+
return new TextDecoder().decode(new Uint8Array(decrypted));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function openCryptKeystoreDb() {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const request = window.indexedDB.open(
|
|
166
|
+
CRYPT_KEYSTORE_DB_NAME,
|
|
167
|
+
CRYPT_KEYSTORE_DB_VERSION,
|
|
168
|
+
);
|
|
169
|
+
request.onerror = () => reject(request.error);
|
|
170
|
+
request.onupgradeneeded = () => {
|
|
171
|
+
const db = request.result;
|
|
172
|
+
if (!db.objectStoreNames.contains(CRYPT_KEYSTORE_STORE_NAME)) {
|
|
173
|
+
db.createObjectStore(CRYPT_KEYSTORE_STORE_NAME);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
request.onsuccess = () => resolve(request.result);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function runIdbRequest(request) {
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
request.onerror = () => reject(request.error);
|
|
183
|
+
request.onsuccess = () => resolve(request.result);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function waitForIdbTransaction(transaction) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
transaction.oncomplete = () => resolve();
|
|
190
|
+
transaction.onerror = () => reject(transaction.error);
|
|
191
|
+
transaction.onabort = () => reject(transaction.error);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function loadCryptKeystore() {
|
|
196
|
+
try {
|
|
197
|
+
const db = await openCryptKeystoreDb();
|
|
198
|
+
const transaction = db.transaction(CRYPT_KEYSTORE_STORE_NAME, "readonly");
|
|
199
|
+
const store = transaction.objectStore(CRYPT_KEYSTORE_STORE_NAME);
|
|
200
|
+
const storedRecord = await runIdbRequest(
|
|
201
|
+
store.get(CRYPT_KEYSTORE_RECORD_KEY),
|
|
202
|
+
);
|
|
203
|
+
db.close();
|
|
204
|
+
return storedRecord || null;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logErrorEntry("crypt-keystore-load", error);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function saveCryptKeystoreRecord(storedRecord) {
|
|
212
|
+
try {
|
|
213
|
+
const db = await openCryptKeystoreDb();
|
|
214
|
+
const transaction = db.transaction(
|
|
215
|
+
CRYPT_KEYSTORE_STORE_NAME,
|
|
216
|
+
"readwrite",
|
|
217
|
+
);
|
|
218
|
+
const store = transaction.objectStore(CRYPT_KEYSTORE_STORE_NAME);
|
|
219
|
+
store.put(storedRecord, CRYPT_KEYSTORE_RECORD_KEY);
|
|
220
|
+
await waitForIdbTransaction(transaction);
|
|
221
|
+
db.close();
|
|
222
|
+
} catch (error) {
|
|
223
|
+
logErrorEntry("crypt-keystore-save", error);
|
|
224
|
+
doError("Could not save the persistent local keystore.");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function sanitizePersistentEntry(entry) {
|
|
229
|
+
if (!entry || typeof entry !== "object") return null;
|
|
230
|
+
const content =
|
|
231
|
+
typeof entry.content === "string"
|
|
232
|
+
? entry.content
|
|
233
|
+
: entry.encryptedContent && entry.salt && entry.iv
|
|
234
|
+
? null
|
|
235
|
+
: "";
|
|
236
|
+
return {
|
|
237
|
+
id: entry.id || generateCryptEntryId(),
|
|
238
|
+
type: String(entry.type || "secret"),
|
|
239
|
+
label: String(entry.label || "Untitled"),
|
|
240
|
+
source: String(entry.source || "manual"),
|
|
241
|
+
content,
|
|
242
|
+
encryptedContent: entry.encryptedContent
|
|
243
|
+
? String(entry.encryptedContent)
|
|
244
|
+
: "",
|
|
245
|
+
salt: entry.salt ? String(entry.salt) : "",
|
|
246
|
+
iv: entry.iv ? String(entry.iv) : "",
|
|
247
|
+
summary: String(entry.summary || ""),
|
|
248
|
+
createdAt: String(entry.createdAt || new Date().toISOString()),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function loadPersistentCryptKeystoreEntries(
|
|
253
|
+
passphrase,
|
|
254
|
+
existingRecord,
|
|
255
|
+
) {
|
|
256
|
+
const storedRecord =
|
|
257
|
+
existingRecord === undefined ? await loadCryptKeystore() : existingRecord;
|
|
258
|
+
if (!storedRecord) return [];
|
|
259
|
+
|
|
260
|
+
if (
|
|
261
|
+
storedRecord?.schemaVersion === CRYPT_KEYSTORE_SCHEMA_VERSION &&
|
|
262
|
+
storedRecord?.encryptedPayload &&
|
|
263
|
+
storedRecord?.salt &&
|
|
264
|
+
storedRecord?.iv
|
|
265
|
+
) {
|
|
266
|
+
const decryptedJson = await decryptCryptContent(
|
|
267
|
+
{
|
|
268
|
+
encryptedContent: String(storedRecord.encryptedPayload),
|
|
269
|
+
salt: String(storedRecord.salt),
|
|
270
|
+
iv: String(storedRecord.iv),
|
|
271
|
+
},
|
|
272
|
+
passphrase,
|
|
273
|
+
);
|
|
274
|
+
const parsed = JSON.parse(decryptedJson);
|
|
275
|
+
if (!Array.isArray(parsed)) return [];
|
|
276
|
+
return parsed.map(sanitizePersistentEntry).filter(Boolean);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const legacyEntries = Array.isArray(storedRecord?.entries)
|
|
280
|
+
? storedRecord.entries.map(sanitizePersistentEntry).filter(Boolean)
|
|
281
|
+
: [];
|
|
282
|
+
await savePersistentCryptKeystoreEntries(legacyEntries, passphrase);
|
|
283
|
+
return legacyEntries;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function savePersistentCryptKeystoreEntries(entries, passphrase) {
|
|
287
|
+
const encryptedPayload = await encryptCryptContent(
|
|
288
|
+
JSON.stringify(entries),
|
|
289
|
+
passphrase,
|
|
290
|
+
);
|
|
291
|
+
await saveCryptKeystoreRecord({
|
|
292
|
+
schemaVersion: CRYPT_KEYSTORE_SCHEMA_VERSION,
|
|
293
|
+
encryptedPayload: encryptedPayload.encryptedContent,
|
|
294
|
+
salt: encryptedPayload.salt,
|
|
295
|
+
iv: encryptedPayload.iv,
|
|
296
|
+
updatedAt: new Date().toISOString(),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function getActiveCryptKeystoreEntries() {
|
|
301
|
+
return cryptActiveKeystoreMode === CRYPT_KEYSTORE_MODE_SESSION
|
|
302
|
+
? cryptSessionKeystoreEntries
|
|
303
|
+
: cryptPersistentKeystoreEntries;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function getActiveKeystoreLabel() {
|
|
307
|
+
return cryptActiveKeystoreMode === CRYPT_KEYSTORE_MODE_SESSION
|
|
308
|
+
? SESSION_KEYCHAIN_LABEL
|
|
309
|
+
: "persistent keychain";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeOpenableLink(value) {
|
|
313
|
+
const normalized = normalizeSessionSecretValue(value);
|
|
314
|
+
if (!normalized) return "";
|
|
315
|
+
try {
|
|
316
|
+
const parsed = new URL(normalized);
|
|
317
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
318
|
+
return parsed.href;
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
return "";
|
|
322
|
+
}
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function canEntryOpenInBrowser(entry) {
|
|
327
|
+
return !!normalizeOpenableLink(entry?.content);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function updateCryptKeystoreWorkspaceState(activeEntry = null) {
|
|
331
|
+
const isPersistentMode =
|
|
332
|
+
cryptActiveKeystoreMode === CRYPT_KEYSTORE_MODE_PERSISTENT;
|
|
333
|
+
const saveCertBtn = document.getElementById("crypt-save-cert-keystore-btn");
|
|
334
|
+
const saveKeyBtn = document.getElementById("crypt-save-key-keystore-btn");
|
|
335
|
+
const saveSecretBtn = document.getElementById(
|
|
336
|
+
"crypt-save-secret-keystore-btn",
|
|
337
|
+
);
|
|
338
|
+
const sendToPersistentBtn = document.getElementById(
|
|
339
|
+
"crypt-send-to-persistent-btn",
|
|
340
|
+
);
|
|
341
|
+
const deleteBtn = document.getElementById(
|
|
342
|
+
"crypt-delete-keystore-entry-btn",
|
|
343
|
+
);
|
|
344
|
+
const openLinkBtn = document.getElementById("crypt-open-link-btn");
|
|
345
|
+
saveCertBtn.disabled = !isPersistentMode;
|
|
346
|
+
saveKeyBtn.disabled = !isPersistentMode;
|
|
347
|
+
saveSecretBtn.disabled = !isPersistentMode;
|
|
348
|
+
sendToPersistentBtn.disabled = isPersistentMode;
|
|
349
|
+
deleteBtn.disabled = !isPersistentMode;
|
|
350
|
+
if (openLinkBtn) {
|
|
351
|
+
openLinkBtn.disabled = !canEntryOpenInBrowser(activeEntry);
|
|
352
|
+
}
|
|
353
|
+
const unlockStatusEl = document.getElementById(
|
|
354
|
+
"crypt-keystore-unlock-status",
|
|
355
|
+
);
|
|
356
|
+
unlockStatusEl.textContent = isPersistentMode
|
|
357
|
+
? "Persistent keychain is unlocked for this app session."
|
|
358
|
+
: "Session keychain is auto-populated from decodable packet secrets and cert-tab imports.";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function renderCryptKeystoreDetails(entry) {
|
|
362
|
+
const detailsEl = document.getElementById("crypt-keystore-details");
|
|
363
|
+
if (!entry) {
|
|
364
|
+
detailsEl.textContent = `No entries available in ${getActiveKeystoreLabel()}.`;
|
|
365
|
+
updateCryptKeystoreWorkspaceState(null);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
detailsEl.textContent = [
|
|
369
|
+
`Keychain: ${getActiveKeystoreLabel()}`,
|
|
370
|
+
`Type: ${entry.type}`,
|
|
371
|
+
`Label: ${entry.label}`,
|
|
372
|
+
`Source: ${entry.source}`,
|
|
373
|
+
entry.packetIndex !== undefined ? `Packet: ${entry.packetIndex}` : null,
|
|
374
|
+
`Saved: ${entry.createdAt}`,
|
|
375
|
+
entry.summary ? `Summary: ${entry.summary}` : "Summary: n/a",
|
|
376
|
+
]
|
|
377
|
+
.filter(Boolean)
|
|
378
|
+
.join("\n");
|
|
379
|
+
updateCryptKeystoreWorkspaceState(entry);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function renderCryptKeystoreList() {
|
|
383
|
+
const listEl = document.getElementById("crypt-keystore-list");
|
|
384
|
+
const activeEntries = getActiveCryptKeystoreEntries();
|
|
385
|
+
listEl.replaceChildren();
|
|
386
|
+
if (!activeEntries.length) {
|
|
387
|
+
const option = document.createElement("option");
|
|
388
|
+
option.textContent = `No entries in ${getActiveKeystoreLabel()}.`;
|
|
389
|
+
option.disabled = true;
|
|
390
|
+
listEl.appendChild(option);
|
|
391
|
+
renderCryptKeystoreDetails(null);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
activeEntries.forEach((entry, index) => {
|
|
396
|
+
const option = document.createElement("option");
|
|
397
|
+
option.value = String(index);
|
|
398
|
+
option.textContent = `[${entry.type}] ${entry.label}`;
|
|
399
|
+
listEl.appendChild(option);
|
|
400
|
+
});
|
|
401
|
+
listEl.selectedIndex = 0;
|
|
402
|
+
renderCryptKeystoreDetails(activeEntries[0]);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function normalizeSessionSecretValue(value) {
|
|
406
|
+
if (value === null || value === undefined) return "";
|
|
407
|
+
const normalized =
|
|
408
|
+
typeof value === "string"
|
|
409
|
+
? value
|
|
410
|
+
: typeof value === "number"
|
|
411
|
+
? String(value)
|
|
412
|
+
: "";
|
|
413
|
+
return normalized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "").trim();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function decodeHttpBasicAuth(rawValue) {
|
|
417
|
+
if (!rawValue || !/^basic\s+/i.test(rawValue)) return "";
|
|
418
|
+
const encoded = rawValue.replace(/^basic\s+/i, "").trim();
|
|
419
|
+
if (!encoded) return "";
|
|
420
|
+
try {
|
|
421
|
+
const decoded = window.atob(encoded);
|
|
422
|
+
return decoded.includes(":") ? decoded.trim() : "";
|
|
423
|
+
} catch (error) {
|
|
424
|
+
logErrorEntry("crypt-keystore-basic-auth-decode", error);
|
|
425
|
+
return "";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function hashContentForDeduplication(content) {
|
|
430
|
+
let hash = 2166136261;
|
|
431
|
+
for (let index = 0; index < content.length; index++) {
|
|
432
|
+
hash ^= content.charCodeAt(index);
|
|
433
|
+
hash = Math.imul(hash, 16777619);
|
|
434
|
+
}
|
|
435
|
+
return (hash >>> 0).toString(16);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function normalizeUriCandidate(uri) {
|
|
439
|
+
return String(uri || "")
|
|
440
|
+
.trim()
|
|
441
|
+
.replace(/[),.;!?]+$/g, "");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function extractUriCandidatesFromText(rawText) {
|
|
445
|
+
const sourceText = normalizeSessionSecretValue(rawText);
|
|
446
|
+
if (!sourceText) return [];
|
|
447
|
+
const candidatePattern = /\b[a-z][a-z0-9+.-]*:[^\s<>"']+/gi;
|
|
448
|
+
const discovered = new Set();
|
|
449
|
+
let match;
|
|
450
|
+
while ((match = candidatePattern.exec(sourceText)) !== null) {
|
|
451
|
+
const normalized = normalizeUriCandidate(match[0]);
|
|
452
|
+
if (!normalized) continue;
|
|
453
|
+
try {
|
|
454
|
+
const parsed = new URL(normalized);
|
|
455
|
+
if (parsed.protocol) {
|
|
456
|
+
discovered.add(parsed.href);
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return Array.from(discovered);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function shouldIncludeSessionSecretKey(pathKey) {
|
|
466
|
+
if (!pathKey) return false;
|
|
467
|
+
const lower = pathKey.toLowerCase();
|
|
468
|
+
if (SESSION_SECRET_IGNORE_KEY_HINTS.some((hint) => lower.includes(hint))) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
return SESSION_SECRET_KEY_HINTS.some((hint) => lower.includes(hint));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function inferSessionEntryType(pathKey) {
|
|
475
|
+
const lower = String(pathKey || "").toLowerCase();
|
|
476
|
+
if (lower.includes("cert")) return "certificate";
|
|
477
|
+
if (lower.includes("private") && lower.includes("key"))
|
|
478
|
+
return "private-key";
|
|
479
|
+
if (lower.includes("key")) return "private-key";
|
|
480
|
+
return "secret";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function collectSessionSecretCandidates(source, visit, parentPath = "") {
|
|
484
|
+
if (!source || typeof source !== "object") return;
|
|
485
|
+
for (const [key, value] of Object.entries(source)) {
|
|
486
|
+
const nextPath = parentPath ? `${parentPath}.${key}` : key;
|
|
487
|
+
if (value && typeof value === "object") {
|
|
488
|
+
collectSessionSecretCandidates(value, visit, nextPath);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
visit(nextPath, value);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function buildSessionAutoKeystoreEntries() {
|
|
496
|
+
const generatedEntries = [];
|
|
497
|
+
const dedupe = new Set();
|
|
498
|
+
const hosts = getCapturedPackets()?.Host;
|
|
499
|
+
if (!hosts || typeof hosts !== "object") return generatedEntries;
|
|
500
|
+
|
|
501
|
+
const pushSessionEntry = ({
|
|
502
|
+
type = "secret",
|
|
503
|
+
label,
|
|
504
|
+
source,
|
|
505
|
+
content,
|
|
506
|
+
summary,
|
|
507
|
+
packetIndex,
|
|
508
|
+
protocol,
|
|
509
|
+
}) => {
|
|
510
|
+
const normalizedContent = normalizeSessionSecretValue(content);
|
|
511
|
+
if (!normalizedContent) return;
|
|
512
|
+
const fingerprint = `${type}|${label}|${hashContentForDeduplication(normalizedContent)}`;
|
|
513
|
+
if (dedupe.has(fingerprint)) return;
|
|
514
|
+
dedupe.add(fingerprint);
|
|
515
|
+
generatedEntries.push({
|
|
516
|
+
id: generateCryptEntryId(),
|
|
517
|
+
type,
|
|
518
|
+
label: label || `${type}-${new Date().toISOString()}`,
|
|
519
|
+
source: source || "session-auto",
|
|
520
|
+
content: normalizedContent,
|
|
521
|
+
summary: summary || "",
|
|
522
|
+
packetIndex: packetIndex ?? "?",
|
|
523
|
+
protocol: protocol || "Unknown",
|
|
524
|
+
createdAt: new Date().toISOString(),
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
Object.entries(hosts).forEach(([host, packets]) => {
|
|
529
|
+
if (!Array.isArray(packets)) return;
|
|
530
|
+
packets.forEach((packet) => {
|
|
531
|
+
const packetInfo = packet?.["Packet Info"] || {};
|
|
532
|
+
const protocol = packetInfo?.["Protocol"] ?? "Unknown";
|
|
533
|
+
const transportData =
|
|
534
|
+
packetInfo?.["Transport Layer"] || packetInfo?.[protocol] || {};
|
|
535
|
+
const extraInfo = packet?.["Extra Info"] || {};
|
|
536
|
+
const packetIndex = packetInfo?.["Index"] ?? "?";
|
|
537
|
+
[transportData, extraInfo].forEach((candidateRoot) => {
|
|
538
|
+
collectSessionSecretCandidates(candidateRoot, (pathKey, rawValue) => {
|
|
539
|
+
const rawText = normalizeSessionSecretValue(rawValue);
|
|
540
|
+
if (rawText) {
|
|
541
|
+
extractUriCandidatesFromText(rawText).forEach((uriValue) => {
|
|
542
|
+
const uriType = /^https?:\/\//i.test(uriValue) ? "url" : "uri";
|
|
543
|
+
pushSessionEntry({
|
|
544
|
+
type: uriType,
|
|
545
|
+
label: `${uriType.toUpperCase()} ${uriValue}`,
|
|
546
|
+
source: "session-auto-uri",
|
|
547
|
+
content: uriValue,
|
|
548
|
+
summary: `Host ${host} packet #${packetIndex} ${pathKey}`,
|
|
549
|
+
packetIndex,
|
|
550
|
+
protocol,
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (!shouldIncludeSessionSecretKey(pathKey)) return;
|
|
555
|
+
if (!rawText) return;
|
|
556
|
+
const decodedBasic = decodeHttpBasicAuth(rawText);
|
|
557
|
+
const contentToSave = decodedBasic || rawText;
|
|
558
|
+
pushSessionEntry({
|
|
559
|
+
type: inferSessionEntryType(pathKey),
|
|
560
|
+
label: `${protocol} ${pathKey}`,
|
|
561
|
+
source: "session-auto-decoded",
|
|
562
|
+
content: contentToSave,
|
|
563
|
+
summary: `Host ${host} packet #${packetIndex}`,
|
|
564
|
+
packetIndex,
|
|
565
|
+
protocol,
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const payloadHex =
|
|
571
|
+
packetInfo?.["Raw data"]?.["Payload"]?.["Hex Encoded"];
|
|
572
|
+
if (typeof payloadHex === "string" && payloadHex.trim()) {
|
|
573
|
+
try {
|
|
574
|
+
const payloadBytes = parseDataToolsInput("hex", payloadHex);
|
|
575
|
+
const decodedHttp = decodeHttpFromBytes(payloadBytes);
|
|
576
|
+
if (decodedHttp?.protocol === "HTTP") {
|
|
577
|
+
const cookieEntries = extractCookieJarEntriesFromHttpFields(
|
|
578
|
+
decodedHttp.fields,
|
|
579
|
+
);
|
|
580
|
+
cookieEntries.forEach((cookieEntry) => {
|
|
581
|
+
const separatorIndex = cookieEntry.indexOf("=");
|
|
582
|
+
const cookieName =
|
|
583
|
+
separatorIndex >= 0
|
|
584
|
+
? cookieEntry.slice(0, separatorIndex).trim()
|
|
585
|
+
: "";
|
|
586
|
+
const cookieLabelSuffix =
|
|
587
|
+
cookieName ||
|
|
588
|
+
`packet-${packetIndex}-${hashContentForDeduplication(cookieEntry)}`;
|
|
589
|
+
pushSessionEntry({
|
|
590
|
+
type: "cookie",
|
|
591
|
+
label: `HTTP Cookie ${cookieLabelSuffix}`,
|
|
592
|
+
source: "session-auto-cookie-jar",
|
|
593
|
+
content: cookieEntry,
|
|
594
|
+
summary: `Host ${host} packet #${packetIndex}`,
|
|
595
|
+
packetIndex,
|
|
596
|
+
protocol: "HTTP",
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
logErrorEntry("crypt-keystore-cookie-auto", error);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
return generatedEntries.sort((a, b) => {
|
|
608
|
+
const aPacketNumber = Number(a.packetIndex);
|
|
609
|
+
const bPacketNumber = Number(b.packetIndex);
|
|
610
|
+
if (Number.isFinite(aPacketNumber) && Number.isFinite(bPacketNumber)) {
|
|
611
|
+
return aPacketNumber - bPacketNumber;
|
|
612
|
+
}
|
|
613
|
+
return String(a.packetIndex).localeCompare(String(b.packetIndex));
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function addSessionKeystoreEntry({
|
|
618
|
+
type,
|
|
619
|
+
label,
|
|
620
|
+
source,
|
|
621
|
+
content,
|
|
622
|
+
summary,
|
|
623
|
+
packetIndex,
|
|
624
|
+
}) {
|
|
625
|
+
const normalizedContent = normalizeSessionSecretValue(content);
|
|
626
|
+
if (!normalizedContent) return;
|
|
627
|
+
const exists = cryptSessionKeystoreEntries.some(
|
|
628
|
+
(entry) =>
|
|
629
|
+
entry.type === type &&
|
|
630
|
+
entry.label === label &&
|
|
631
|
+
normalizeSessionSecretValue(entry.content) === normalizedContent,
|
|
632
|
+
);
|
|
633
|
+
if (exists) return;
|
|
634
|
+
cryptSessionKeystoreEntries.unshift({
|
|
635
|
+
id: generateCryptEntryId(),
|
|
636
|
+
type,
|
|
637
|
+
label: label?.trim()
|
|
638
|
+
? label.trim()
|
|
639
|
+
: `${type}-${new Date().toISOString()}`,
|
|
640
|
+
source: source || "session-auto",
|
|
641
|
+
content: normalizedContent,
|
|
642
|
+
summary: summary || "",
|
|
643
|
+
packetIndex: packetIndex ?? "?",
|
|
644
|
+
createdAt: new Date().toISOString(),
|
|
645
|
+
});
|
|
646
|
+
if (cryptActiveKeystoreMode === CRYPT_KEYSTORE_MODE_SESSION) {
|
|
647
|
+
renderCryptKeystoreList();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function addCryptKeystoreEntry(
|
|
652
|
+
{ type, label, source, content, summary },
|
|
653
|
+
{ force = false } = {},
|
|
654
|
+
) {
|
|
655
|
+
if (!force && cryptActiveKeystoreMode !== CRYPT_KEYSTORE_MODE_PERSISTENT) {
|
|
656
|
+
statusUpdate("Status: Switch to persistent keychain to save entries");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (!cryptKeystoreUnlockKeyMaterial) {
|
|
660
|
+
doError(
|
|
661
|
+
"Persistent keychain is locked. Reopen the keychain tab with password.",
|
|
662
|
+
);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const normalizedContent = (content || "").trim();
|
|
666
|
+
if (!normalizedContent) {
|
|
667
|
+
statusUpdate("Status: No content available to save");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const entry = {
|
|
672
|
+
id: generateCryptEntryId(),
|
|
673
|
+
type,
|
|
674
|
+
label: label?.trim()
|
|
675
|
+
? label.trim()
|
|
676
|
+
: `${type}-${new Date().toISOString()}`,
|
|
677
|
+
source,
|
|
678
|
+
content: normalizedContent,
|
|
679
|
+
summary: summary || "",
|
|
680
|
+
createdAt: new Date().toISOString(),
|
|
681
|
+
};
|
|
682
|
+
cryptPersistentKeystoreEntries.unshift(entry);
|
|
683
|
+
try {
|
|
684
|
+
await savePersistentCryptKeystoreEntries(
|
|
685
|
+
cryptPersistentKeystoreEntries,
|
|
686
|
+
cryptKeystoreUnlockKeyMaterial,
|
|
687
|
+
);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
logErrorEntry("crypt-keystore-save", error);
|
|
690
|
+
doError("Could not save the persistent keychain.");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
renderCryptKeystoreList();
|
|
694
|
+
statusUpdate(`Status: Saved ${type} in persistent keychain`);
|
|
695
|
+
writeLogEntry(
|
|
696
|
+
`Crypt keystore entry added type=${type} label="${entry.label}"`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function loadSelectedCryptKeystoreEntry() {
|
|
701
|
+
const listEl = document.getElementById("crypt-keystore-list");
|
|
702
|
+
const selectedIndex = Number(listEl.value);
|
|
703
|
+
const activeEntries = getActiveCryptKeystoreEntries();
|
|
704
|
+
if (!Number.isFinite(selectedIndex) || !activeEntries[selectedIndex]) {
|
|
705
|
+
statusUpdate("Status: Select a keystore entry first");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const selectedEntry = activeEntries[selectedIndex];
|
|
710
|
+
let loadedContent = normalizeSessionSecretValue(selectedEntry.content);
|
|
711
|
+
if (
|
|
712
|
+
!loadedContent &&
|
|
713
|
+
selectedEntry.encryptedContent &&
|
|
714
|
+
selectedEntry.salt &&
|
|
715
|
+
selectedEntry.iv
|
|
716
|
+
) {
|
|
717
|
+
if (!cryptKeystoreUnlockKeyMaterial) {
|
|
718
|
+
doError(
|
|
719
|
+
"Persistent keychain is locked. Reopen keychain with password.",
|
|
720
|
+
);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
loadedContent = await decryptCryptContent(
|
|
725
|
+
selectedEntry,
|
|
726
|
+
cryptKeystoreUnlockKeyMaterial,
|
|
727
|
+
);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
logErrorEntry("crypt-keystore-decrypt", error);
|
|
730
|
+
doError("Could not decrypt keystore entry with current password.");
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (!loadedContent) {
|
|
735
|
+
statusUpdate("Status: Selected entry has no decodable content");
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
renderCryptKeystoreDetails(selectedEntry);
|
|
739
|
+
document.getElementById("crypt-keystore-label").value = selectedEntry.label;
|
|
740
|
+
if (selectedEntry.type === "certificate") {
|
|
741
|
+
getApplyCryptCertificateText()(loadedContent, getActiveKeystoreLabel());
|
|
742
|
+
} else if (selectedEntry.type === "private-key") {
|
|
743
|
+
getApplyCryptPrivateKeyText()(loadedContent, getActiveKeystoreLabel());
|
|
744
|
+
} else {
|
|
745
|
+
document.getElementById("crypt-credential-input").value = loadedContent;
|
|
746
|
+
}
|
|
747
|
+
statusUpdate(`Status: Loaded keystore entry "${selectedEntry.label}"`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function deleteSelectedCryptKeystoreEntry() {
|
|
751
|
+
if (cryptActiveKeystoreMode !== CRYPT_KEYSTORE_MODE_PERSISTENT) {
|
|
752
|
+
statusUpdate("Status: Session keychain entries are auto-managed");
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const listEl = document.getElementById("crypt-keystore-list");
|
|
756
|
+
const selectedIndex = Number(listEl.value);
|
|
757
|
+
if (
|
|
758
|
+
!Number.isFinite(selectedIndex) ||
|
|
759
|
+
!cryptPersistentKeystoreEntries[selectedIndex]
|
|
760
|
+
) {
|
|
761
|
+
statusUpdate("Status: Select a keystore entry first");
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (!cryptKeystoreUnlockKeyMaterial) {
|
|
765
|
+
doError("Persistent keychain is locked. Reopen keychain with password.");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const [removedEntry] = cryptPersistentKeystoreEntries.splice(
|
|
769
|
+
selectedIndex,
|
|
770
|
+
1,
|
|
771
|
+
);
|
|
772
|
+
try {
|
|
773
|
+
await savePersistentCryptKeystoreEntries(
|
|
774
|
+
cryptPersistentKeystoreEntries,
|
|
775
|
+
cryptKeystoreUnlockKeyMaterial,
|
|
776
|
+
);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
logErrorEntry("crypt-keystore-save", error);
|
|
779
|
+
doError("Could not save the persistent keychain.");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
renderCryptKeystoreList();
|
|
783
|
+
statusUpdate(`Status: Deleted keystore entry "${removedEntry.label}"`);
|
|
784
|
+
writeLogEntry(
|
|
785
|
+
`Crypt keystore entry deleted type=${removedEntry.type} label="${removedEntry.label}"`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function sendSelectedSessionEntryToPersistent() {
|
|
790
|
+
if (cryptActiveKeystoreMode !== CRYPT_KEYSTORE_MODE_SESSION) {
|
|
791
|
+
statusUpdate(
|
|
792
|
+
"Status: Switch to session keychain to send temporary entries",
|
|
793
|
+
);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (!cryptKeystoreUnlockKeyMaterial) {
|
|
797
|
+
doError("Persistent keychain is locked. Reopen keychain with password.");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const listEl = document.getElementById("crypt-keystore-list");
|
|
801
|
+
const selectedIndex = Number(listEl.value);
|
|
802
|
+
if (
|
|
803
|
+
!Number.isFinite(selectedIndex) ||
|
|
804
|
+
!cryptSessionKeystoreEntries[selectedIndex]
|
|
805
|
+
) {
|
|
806
|
+
statusUpdate("Status: Select a session keychain entry first");
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const selectedEntry = cryptSessionKeystoreEntries[selectedIndex];
|
|
811
|
+
const normalizedContent = normalizeSessionSecretValue(
|
|
812
|
+
selectedEntry.content,
|
|
813
|
+
);
|
|
814
|
+
if (!normalizedContent) {
|
|
815
|
+
statusUpdate("Status: Selected session entry has no content to persist");
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const alreadyStored = cryptPersistentKeystoreEntries.some(
|
|
820
|
+
(entry) =>
|
|
821
|
+
entry.type === selectedEntry.type &&
|
|
822
|
+
entry.label === selectedEntry.label &&
|
|
823
|
+
normalizeSessionSecretValue(entry.content) === normalizedContent,
|
|
824
|
+
);
|
|
825
|
+
if (alreadyStored) {
|
|
826
|
+
statusUpdate("Status: Entry is already stored in persistent keychain");
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
cryptPersistentKeystoreEntries.unshift({
|
|
831
|
+
id: generateCryptEntryId(),
|
|
832
|
+
type: selectedEntry.type,
|
|
833
|
+
label: selectedEntry.label,
|
|
834
|
+
source: `bookmarked from ${selectedEntry.source || "session-auto"}`,
|
|
835
|
+
content: normalizedContent,
|
|
836
|
+
summary: selectedEntry.summary || "Bookmarked from session keychain",
|
|
837
|
+
createdAt: new Date().toISOString(),
|
|
838
|
+
});
|
|
839
|
+
try {
|
|
840
|
+
await savePersistentCryptKeystoreEntries(
|
|
841
|
+
cryptPersistentKeystoreEntries,
|
|
842
|
+
cryptKeystoreUnlockKeyMaterial,
|
|
843
|
+
);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
logErrorEntry("crypt-keystore-save", error);
|
|
846
|
+
doError("Could not save selected entry to persistent keychain.");
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
statusUpdate(
|
|
850
|
+
`Status: Sent "${selectedEntry.label}" to persistent keychain`,
|
|
851
|
+
);
|
|
852
|
+
writeLogEntry(
|
|
853
|
+
`Session keychain entry persisted label="${selectedEntry.label}"`,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function configureKeystoreUnlockDialog(mode) {
|
|
858
|
+
cryptKeystoreUnlockDialogMode = mode === "setup" ? "setup" : "unlock";
|
|
859
|
+
const isSetup = cryptKeystoreUnlockDialogMode === "setup";
|
|
860
|
+
const titleEl = document.getElementById("crypt-keystore-unlock-title");
|
|
861
|
+
const descriptionEl = document.getElementById(
|
|
862
|
+
"crypt-keystore-unlock-description",
|
|
863
|
+
);
|
|
864
|
+
const passwordEl = document.getElementById(
|
|
865
|
+
"crypt-keystore-unlock-password",
|
|
866
|
+
);
|
|
867
|
+
const confirmEl = document.getElementById(
|
|
868
|
+
"crypt-keystore-unlock-password-confirm",
|
|
869
|
+
);
|
|
870
|
+
const confirmBtn = document.getElementById(
|
|
871
|
+
"crypt-keystore-unlock-confirm-btn",
|
|
872
|
+
);
|
|
873
|
+
if (titleEl) {
|
|
874
|
+
titleEl.textContent = isSetup
|
|
875
|
+
? "Set Keychain Password"
|
|
876
|
+
: "Unlock Keychain";
|
|
877
|
+
}
|
|
878
|
+
if (descriptionEl) {
|
|
879
|
+
descriptionEl.textContent = isSetup
|
|
880
|
+
? "Create the initial password for the persistent keychain (minimum 8 characters). You will only be asked when selecting the keychain tab."
|
|
881
|
+
: "Enter password to unlock the persistent keychain.";
|
|
882
|
+
}
|
|
883
|
+
if (passwordEl) {
|
|
884
|
+
passwordEl.placeholder = isSetup
|
|
885
|
+
? "Create keychain password"
|
|
886
|
+
: "Enter keychain password";
|
|
887
|
+
}
|
|
888
|
+
if (confirmEl) {
|
|
889
|
+
confirmEl.hidden = !isSetup;
|
|
890
|
+
confirmEl.placeholder = "Confirm keychain password";
|
|
891
|
+
}
|
|
892
|
+
if (confirmBtn) {
|
|
893
|
+
confirmBtn.textContent = isSetup ? "Set password" : "Unlock";
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function requestKeystoreUnlockPassword(mode = "unlock") {
|
|
898
|
+
const dialogEl = document.getElementById("crypt-keystore-unlock-dialog");
|
|
899
|
+
const inputEl = document.getElementById("crypt-keystore-unlock-password");
|
|
900
|
+
const confirmEl = document.getElementById(
|
|
901
|
+
"crypt-keystore-unlock-password-confirm",
|
|
902
|
+
);
|
|
903
|
+
if (!dialogEl || !inputEl || !confirmEl) return Promise.resolve(null);
|
|
904
|
+
configureKeystoreUnlockDialog(mode);
|
|
905
|
+
dialogEl.hidden = false;
|
|
906
|
+
inputEl.value = "";
|
|
907
|
+
confirmEl.value = "";
|
|
908
|
+
inputEl.focus();
|
|
909
|
+
return new Promise((resolve) => {
|
|
910
|
+
cryptKeystoreUnlockDialogResolver = resolve;
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function resolveKeystoreUnlockPassword(value) {
|
|
915
|
+
const dialogEl = document.getElementById("crypt-keystore-unlock-dialog");
|
|
916
|
+
const inputEl = document.getElementById("crypt-keystore-unlock-password");
|
|
917
|
+
const confirmEl = document.getElementById(
|
|
918
|
+
"crypt-keystore-unlock-password-confirm",
|
|
919
|
+
);
|
|
920
|
+
if (dialogEl) dialogEl.hidden = true;
|
|
921
|
+
if (inputEl) inputEl.value = "";
|
|
922
|
+
if (confirmEl) confirmEl.value = "";
|
|
923
|
+
if (!cryptKeystoreUnlockDialogResolver) return;
|
|
924
|
+
const resolve = cryptKeystoreUnlockDialogResolver;
|
|
925
|
+
cryptKeystoreUnlockDialogResolver = null;
|
|
926
|
+
resolve(value);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function submitKeystoreUnlockDialog() {
|
|
930
|
+
const inputEl = document.getElementById("crypt-keystore-unlock-password");
|
|
931
|
+
const confirmEl = document.getElementById(
|
|
932
|
+
"crypt-keystore-unlock-password-confirm",
|
|
933
|
+
);
|
|
934
|
+
resolveKeystoreUnlockPassword({
|
|
935
|
+
password: inputEl?.value || "",
|
|
936
|
+
confirmPassword: confirmEl?.value || "",
|
|
937
|
+
mode: cryptKeystoreUnlockDialogMode,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function requestManualUriFromContextMenuDialog(keystoreMode) {
|
|
942
|
+
const dialogEl = document.getElementById("crypt-keystore-manual-uri-dialog");
|
|
943
|
+
const descriptionEl = document.getElementById(
|
|
944
|
+
"crypt-keystore-manual-uri-description",
|
|
945
|
+
);
|
|
946
|
+
const inputEl = document.getElementById("crypt-keystore-manual-uri-input");
|
|
947
|
+
if (!dialogEl || !descriptionEl || !inputEl) return Promise.resolve(null);
|
|
948
|
+
if (cryptManualUriDialogResolver) {
|
|
949
|
+
const resolve = cryptManualUriDialogResolver;
|
|
950
|
+
cryptManualUriDialogResolver = null;
|
|
951
|
+
resolve(null);
|
|
952
|
+
}
|
|
953
|
+
cryptManualUriDialogMode =
|
|
954
|
+
keystoreMode === CRYPT_KEYSTORE_MODE_PERSISTENT
|
|
955
|
+
? CRYPT_KEYSTORE_MODE_PERSISTENT
|
|
956
|
+
: CRYPT_KEYSTORE_MODE_SESSION;
|
|
957
|
+
const modeLabel =
|
|
958
|
+
cryptManualUriDialogMode === CRYPT_KEYSTORE_MODE_PERSISTENT
|
|
959
|
+
? "persistent keychain"
|
|
960
|
+
: "session keychain";
|
|
961
|
+
descriptionEl.textContent = `Enter URI/URL to add to the ${modeLabel}.`;
|
|
962
|
+
dialogEl.hidden = false;
|
|
963
|
+
inputEl.value = "";
|
|
964
|
+
inputEl.focus();
|
|
965
|
+
return new Promise((resolve) => {
|
|
966
|
+
cryptManualUriDialogResolver = resolve;
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function resolveManualUriFromContextMenuDialog(value) {
|
|
971
|
+
const dialogEl = document.getElementById("crypt-keystore-manual-uri-dialog");
|
|
972
|
+
const inputEl = document.getElementById("crypt-keystore-manual-uri-input");
|
|
973
|
+
if (dialogEl) dialogEl.hidden = true;
|
|
974
|
+
if (inputEl) inputEl.value = "";
|
|
975
|
+
if (!cryptManualUriDialogResolver) return;
|
|
976
|
+
const resolve = cryptManualUriDialogResolver;
|
|
977
|
+
cryptManualUriDialogResolver = null;
|
|
978
|
+
resolve({
|
|
979
|
+
value,
|
|
980
|
+
mode: cryptManualUriDialogMode,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function submitManualUriFromContextMenuDialog() {
|
|
985
|
+
const inputEl = document.getElementById("crypt-keystore-manual-uri-input");
|
|
986
|
+
resolveManualUriFromContextMenuDialog(inputEl?.value || "");
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function unlockPersistentKeystoreAndLoad() {
|
|
990
|
+
if (!(window.crypto && window.crypto.subtle)) {
|
|
991
|
+
doError("WebCrypto API is unavailable; cannot unlock keychain.");
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
if (cryptKeystoreUnlockKeyMaterial) return true;
|
|
995
|
+
|
|
996
|
+
const storedRecord = await loadCryptKeystore();
|
|
997
|
+
const isInitialSetup = !storedRecord;
|
|
998
|
+
const dialogResult = await requestKeystoreUnlockPassword(
|
|
999
|
+
isInitialSetup ? "setup" : "unlock",
|
|
1000
|
+
);
|
|
1001
|
+
const normalizedPassword = (dialogResult?.password || "").trim();
|
|
1002
|
+
if (!normalizedPassword) {
|
|
1003
|
+
statusUpdate("Status: Keychain remains locked");
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
if (normalizedPassword.length < 8) {
|
|
1007
|
+
doError("Keychain password must be at least 8 characters.");
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
if (
|
|
1011
|
+
isInitialSetup &&
|
|
1012
|
+
normalizedPassword !== String(dialogResult?.confirmPassword || "").trim()
|
|
1013
|
+
) {
|
|
1014
|
+
doError("Keychain password confirmation does not match.");
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
const keyMaterial = await importCryptKeyMaterial(normalizedPassword);
|
|
1019
|
+
if (isInitialSetup) {
|
|
1020
|
+
cryptPersistentKeystoreEntries = [];
|
|
1021
|
+
await savePersistentCryptKeystoreEntries([], keyMaterial);
|
|
1022
|
+
statusUpdate("Status: Keychain password set");
|
|
1023
|
+
writeLogEntry("Persistent keychain password initialized");
|
|
1024
|
+
} else {
|
|
1025
|
+
cryptPersistentKeystoreEntries =
|
|
1026
|
+
await loadPersistentCryptKeystoreEntries(keyMaterial, storedRecord);
|
|
1027
|
+
statusUpdate("Status: Keychain unlocked");
|
|
1028
|
+
writeLogEntry("Persistent keychain unlocked");
|
|
1029
|
+
}
|
|
1030
|
+
cryptKeystoreUnlockKeyMaterial = keyMaterial;
|
|
1031
|
+
return true;
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
logErrorEntry("crypt-keystore-unlock", error);
|
|
1034
|
+
doError(
|
|
1035
|
+
isInitialSetup
|
|
1036
|
+
? "Could not initialize persistent keychain."
|
|
1037
|
+
: "Could not unlock persistent keychain. Verify password.",
|
|
1038
|
+
);
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function showKeystoreWorkspace() {
|
|
1044
|
+
setActiveMainTab(MAIN_TAB_KEYSTORE);
|
|
1045
|
+
if (getJsonCapture() === "") {
|
|
1046
|
+
statusUpdate("Status: No JSON file loaded, please upload a file first");
|
|
1047
|
+
doError("Please upload a JSON file before accessing the keystore.");
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (!cryptKeystoreUnlockKeyMaterial) {
|
|
1052
|
+
doError("Please unlock the keychain with password first.");
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
statusUpdate("Status: Displaying keychain manager");
|
|
1056
|
+
writeLogEntry("User opened keystore workspace");
|
|
1057
|
+
document.getElementById("prev-btn").style.display = "none";
|
|
1058
|
+
document.getElementById("next-btn").style.display = "none";
|
|
1059
|
+
document.getElementById("packetInfoPane").style.display = "none";
|
|
1060
|
+
document.getElementById("packetPayloadPane").style.display = "none";
|
|
1061
|
+
document.getElementById("summary_box").style.display = "none";
|
|
1062
|
+
document.getElementById("stats_box").style.display = "none";
|
|
1063
|
+
document.getElementById("data_tools_box").style.display = "none";
|
|
1064
|
+
document.getElementById("list_box").style.display = "none";
|
|
1065
|
+
document.getElementById("notes_box").style.display = "none";
|
|
1066
|
+
document.getElementById("crypt_box").style.display = "none";
|
|
1067
|
+
document.getElementById("rightside").style.display = "none";
|
|
1068
|
+
const keystoreBoxEl = document.getElementById("keystore_box");
|
|
1069
|
+
keystoreBoxEl.style.display = "flex";
|
|
1070
|
+
const modeEl = document.getElementById("crypt-keystore-mode");
|
|
1071
|
+
modeEl.value = cryptActiveKeystoreMode;
|
|
1072
|
+
renderCryptKeystoreList();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async function addToKeystoreFromContextMenu(type, keystoreMode) {
|
|
1076
|
+
const text = (
|
|
1077
|
+
getTrimmedSelectionText() || getActiveContextConversionText()
|
|
1078
|
+
).trim();
|
|
1079
|
+
hideConvertContextMenu();
|
|
1080
|
+
if (!text) {
|
|
1081
|
+
statusUpdate("Status: No text to add to keystore");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (keystoreMode === CRYPT_KEYSTORE_MODE_SESSION) {
|
|
1085
|
+
addSessionKeystoreEntry({
|
|
1086
|
+
type,
|
|
1087
|
+
label: "",
|
|
1088
|
+
source: "context-menu",
|
|
1089
|
+
content: text,
|
|
1090
|
+
summary: "",
|
|
1091
|
+
});
|
|
1092
|
+
statusUpdate(`Status: Saved ${type} in session keychain`);
|
|
1093
|
+
writeLogEntry(
|
|
1094
|
+
`Context menu keystore entry added type=${type} mode=session`,
|
|
1095
|
+
);
|
|
1096
|
+
} else {
|
|
1097
|
+
await addCryptKeystoreEntry(
|
|
1098
|
+
{ type, label: "", source: "context-menu", content: text, summary: "" },
|
|
1099
|
+
{ force: true },
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async function addManualUriToKeystoreFromContextMenu(keystoreMode) {
|
|
1105
|
+
hideConvertContextMenu();
|
|
1106
|
+
const dialogResult = await requestManualUriFromContextMenuDialog(
|
|
1107
|
+
keystoreMode,
|
|
1108
|
+
);
|
|
1109
|
+
if (!dialogResult) return;
|
|
1110
|
+
const normalized = normalizeUriCandidate(dialogResult.value);
|
|
1111
|
+
if (!normalized) {
|
|
1112
|
+
statusUpdate("Status: No URI/URL provided");
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
let parsedUrl;
|
|
1117
|
+
try {
|
|
1118
|
+
parsedUrl = new URL(normalized);
|
|
1119
|
+
} catch {
|
|
1120
|
+
statusUpdate("Status: Invalid URI/URL");
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const normalizedUri = parsedUrl.href;
|
|
1125
|
+
const uriType = /^https?:$/i.test(parsedUrl.protocol) ? "url" : "uri";
|
|
1126
|
+
const entry = {
|
|
1127
|
+
type: uriType,
|
|
1128
|
+
label: `${uriType.toUpperCase()} ${normalizedUri}`,
|
|
1129
|
+
source: "context-menu-manual-uri",
|
|
1130
|
+
content: normalizedUri,
|
|
1131
|
+
summary: "Manual URI/URL entry from context menu",
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
if (dialogResult.mode === CRYPT_KEYSTORE_MODE_SESSION) {
|
|
1135
|
+
addSessionKeystoreEntry(entry);
|
|
1136
|
+
statusUpdate(`Status: Saved ${uriType} in session keychain`);
|
|
1137
|
+
writeLogEntry(`Manual context URI saved mode=session type=${uriType}`);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
await addCryptKeystoreEntry(entry, { force: true });
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function openSelectedKeystoreLinkInBrowser() {
|
|
1145
|
+
const listEl = document.getElementById("crypt-keystore-list");
|
|
1146
|
+
const selectedIndex = Number(listEl.value);
|
|
1147
|
+
const activeEntries = getActiveCryptKeystoreEntries();
|
|
1148
|
+
if (!Number.isFinite(selectedIndex) || !activeEntries[selectedIndex]) {
|
|
1149
|
+
statusUpdate("Status: Select a keystore entry first");
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const selectedEntry = activeEntries[selectedIndex];
|
|
1154
|
+
const openableLink = normalizeOpenableLink(selectedEntry.content);
|
|
1155
|
+
if (!openableLink) {
|
|
1156
|
+
statusUpdate("Status: Selected entry is not an openable web link");
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (typeof openExternalUrl !== "function") {
|
|
1160
|
+
doError("External browser opening is unavailable in this environment.");
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const result = await openExternalUrl(openableLink);
|
|
1165
|
+
if (result?.success) {
|
|
1166
|
+
statusUpdate("Status: Opened link in external browser");
|
|
1167
|
+
writeLogEntry(
|
|
1168
|
+
`Keystore link opened in browser label="${selectedEntry.label}"`,
|
|
1169
|
+
);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const errorMessage =
|
|
1174
|
+
result && typeof result === "object" && "error" in result
|
|
1175
|
+
? result.error
|
|
1176
|
+
: "unknown";
|
|
1177
|
+
doError("Could not open the selected link in browser.");
|
|
1178
|
+
logErrorEntry("crypt-keystore-open-link", errorMessage || "unknown");
|
|
1179
|
+
statusUpdate(
|
|
1180
|
+
"Status: Could not open selected link – " + (errorMessage || "unknown"),
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return {
|
|
1185
|
+
addSessionKeystoreEntry,
|
|
1186
|
+
addCryptKeystoreEntry,
|
|
1187
|
+
loadSelectedCryptKeystoreEntry,
|
|
1188
|
+
deleteSelectedCryptKeystoreEntry,
|
|
1189
|
+
sendSelectedSessionEntryToPersistent,
|
|
1190
|
+
showKeystoreWorkspace,
|
|
1191
|
+
renderCryptKeystoreList,
|
|
1192
|
+
renderCryptKeystoreDetails,
|
|
1193
|
+
addToKeystoreFromContextMenu,
|
|
1194
|
+
addManualUriToKeystoreFromContextMenu,
|
|
1195
|
+
openSelectedKeystoreLinkInBrowser,
|
|
1196
|
+
unlockPersistentKeystoreAndLoad,
|
|
1197
|
+
submitKeystoreUnlockDialog,
|
|
1198
|
+
resolveKeystoreUnlockPassword,
|
|
1199
|
+
submitManualUriFromContextMenuDialog,
|
|
1200
|
+
resolveManualUriFromContextMenuDialog,
|
|
1201
|
+
getActiveCryptKeystoreEntries,
|
|
1202
|
+
setActiveMode(mode) {
|
|
1203
|
+
cryptActiveKeystoreMode = mode;
|
|
1204
|
+
renderCryptKeystoreList();
|
|
1205
|
+
},
|
|
1206
|
+
getSessionKeychainEntries() {
|
|
1207
|
+
return cryptSessionKeystoreEntries;
|
|
1208
|
+
},
|
|
1209
|
+
getKeystoreMode() {
|
|
1210
|
+
return cryptActiveKeystoreMode;
|
|
1211
|
+
},
|
|
1212
|
+
isUnlocked() {
|
|
1213
|
+
return !!cryptKeystoreUnlockKeyMaterial;
|
|
1214
|
+
},
|
|
1215
|
+
restoreSessionState(sessionKeychainEntries, keystoreMode) {
|
|
1216
|
+
cryptSessionKeystoreEntries = sessionKeychainEntries;
|
|
1217
|
+
if (
|
|
1218
|
+
keystoreMode === CRYPT_KEYSTORE_MODE_SESSION ||
|
|
1219
|
+
keystoreMode === CRYPT_KEYSTORE_MODE_PERSISTENT
|
|
1220
|
+
) {
|
|
1221
|
+
cryptActiveKeystoreMode = keystoreMode;
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
rebuildSessionEntries() {
|
|
1225
|
+
cryptSessionKeystoreEntries = buildSessionAutoKeystoreEntries();
|
|
1226
|
+
return cryptSessionKeystoreEntries.length;
|
|
1227
|
+
},
|
|
1228
|
+
resetKeystoreState() {
|
|
1229
|
+
cryptActiveKeystoreMode = CRYPT_KEYSTORE_MODE_SESSION;
|
|
1230
|
+
cryptSessionKeystoreEntries = [];
|
|
1231
|
+
cryptPersistentKeystoreEntries = [];
|
|
1232
|
+
cryptKeystoreUnlockKeyMaterial = null;
|
|
1233
|
+
cryptKeystoreUnlockDialogResolver = null;
|
|
1234
|
+
cryptKeystoreUnlockDialogMode = "unlock";
|
|
1235
|
+
cryptManualUriDialogResolver = null;
|
|
1236
|
+
cryptManualUriDialogMode = CRYPT_KEYSTORE_MODE_SESSION;
|
|
1237
|
+
renderCryptKeystoreList();
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
module.exports = {
|
|
1243
|
+
id: "keystore",
|
|
1244
|
+
createKeystorePanel,
|
|
1245
|
+
CRYPT_KEYSTORE_MODE_SESSION,
|
|
1246
|
+
CRYPT_KEYSTORE_MODE_PERSISTENT,
|
|
1247
|
+
SESSION_KEYCHAIN_LABEL,
|
|
1248
|
+
};
|