chrome-tools 1.1.15 → 1.1.18
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/dist/chrome_abe_decrypt.exe +0 -0
- package/dist/cli.js +8837 -0
- package/dist/index.d.ts +629 -0
- package/dist/index.js +2532 -0
- package/package.json +62 -29
- package/bin/chrome_abe_decrypt.exe +0 -0
- package/dist/bin/chrome_abe_decrypt.exe +0 -0
- package/dist/bin/cli.d.ts +0 -9
- package/dist/bin/cli.js +0 -590
- package/dist/src/core/browsers.d.ts +0 -73
- package/dist/src/core/browsers.js +0 -186
- package/dist/src/core/chrome.d.ts +0 -124
- package/dist/src/core/chrome.js +0 -259
- package/dist/src/core/crypto.d.ts +0 -52
- package/dist/src/core/crypto.js +0 -152
- package/dist/src/core/db.d.ts +0 -22
- package/dist/src/core/db.js +0 -227
- package/dist/src/core/keychain.d.ts +0 -57
- package/dist/src/core/keychain.js +0 -191
- package/dist/src/core/limit.d.ts +0 -4
- package/dist/src/core/limit.js +0 -12
- package/dist/src/core/platform/darwin.d.ts +0 -12
- package/dist/src/core/platform/darwin.js +0 -31
- package/dist/src/core/platform/index.d.ts +0 -30
- package/dist/src/core/platform/index.js +0 -69
- package/dist/src/core/platform/linux.d.ts +0 -10
- package/dist/src/core/platform/linux.js +0 -64
- package/dist/src/core/platform/types.d.ts +0 -45
- package/dist/src/core/platform/types.js +0 -1
- package/dist/src/core/platform/win32.d.ts +0 -13
- package/dist/src/core/platform/win32.js +0 -329
- package/dist/src/core/sql.d.ts +0 -2
- package/dist/src/core/sql.js +0 -4
- package/dist/src/index.d.ts +0 -28
- package/dist/src/index.js +0 -34
- package/dist/src/modules/autofill.d.ts +0 -68
- package/dist/src/modules/autofill.js +0 -170
- package/dist/src/modules/bookmarks.d.ts +0 -22
- package/dist/src/modules/bookmarks.js +0 -83
- package/dist/src/modules/cookies.d.ts +0 -40
- package/dist/src/modules/cookies.js +0 -163
- package/dist/src/modules/downloads.d.ts +0 -27
- package/dist/src/modules/downloads.js +0 -80
- package/dist/src/modules/extensions.d.ts +0 -24
- package/dist/src/modules/extensions.js +0 -71
- package/dist/src/modules/history.d.ts +0 -34
- package/dist/src/modules/history.js +0 -83
- package/dist/src/modules/local-storage.d.ts +0 -40
- package/dist/src/modules/local-storage.js +0 -455
- package/dist/src/modules/passwords.d.ts +0 -50
- package/dist/src/modules/passwords.js +0 -198
- package/dist/src/modules/search-engines.d.ts +0 -21
- package/dist/src/modules/search-engines.js +0 -38
- package/dist/src/modules/top-sites.d.ts +0 -17
- package/dist/src/modules/top-sites.js +0 -33
- package/src/native/build.bat +0 -31
- package/src/native/chrome_abe_decrypt.c +0 -336
- package/src/native/chrome_abe_decrypt.exe +0 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2532 @@
|
|
|
1
|
+
// src/core/chrome.ts
|
|
2
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
3
|
+
import { basename, join as join4 } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/core/platform/darwin.ts
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/core/keychain.ts
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { pbkdf2Sync } from "node:crypto";
|
|
12
|
+
var PBKDF2_ITERATIONS = 1003;
|
|
13
|
+
var PBKDF2_KEY_LENGTH = 16;
|
|
14
|
+
var PBKDF2_SALT = Buffer.from("saltysalt");
|
|
15
|
+
var DEFAULT_SAFE_STORAGE_SERVICE = "Chrome Safe Storage";
|
|
16
|
+
var SECURITY_EXIT = {
|
|
17
|
+
ITEM_NOT_FOUND: 44,
|
|
18
|
+
// errSecItemNotFound
|
|
19
|
+
INTERACTION_NOT_ALLOWED: 36,
|
|
20
|
+
// errSecInteractionNotAllowed (SSH / headless / locked)
|
|
21
|
+
AUTH_FAILED: 45,
|
|
22
|
+
// errSecAuthFailed
|
|
23
|
+
USER_CANCELED: 128
|
|
24
|
+
// user clicked "Deny"/cancelled the GUI authorization prompt
|
|
25
|
+
};
|
|
26
|
+
var KeychainAccessError = class extends Error {
|
|
27
|
+
service;
|
|
28
|
+
kind;
|
|
29
|
+
constructor(message, service, kind) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "KeychainAccessError";
|
|
32
|
+
this.service = service;
|
|
33
|
+
this.kind = kind;
|
|
34
|
+
}
|
|
35
|
+
/** True when the failure means we couldn't get permission to read the key. */
|
|
36
|
+
get isAccessDenied() {
|
|
37
|
+
return this.kind === "access-denied";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var _cachedPasswords = /* @__PURE__ */ new Map();
|
|
41
|
+
var _cachedDerivedKeys = /* @__PURE__ */ new Map();
|
|
42
|
+
function isLikelyHeadless() {
|
|
43
|
+
return Boolean(process.env.SSH_CONNECTION || process.env.SSH_TTY) || !process.env.TERM_PROGRAM;
|
|
44
|
+
}
|
|
45
|
+
function parseSecurityError(err) {
|
|
46
|
+
const e = err;
|
|
47
|
+
return {
|
|
48
|
+
status: typeof e?.status === "number" ? e.status : null,
|
|
49
|
+
stderr: (e?.stderr ?? "").toString().trim()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function buildKeychainError(service, status, stderr) {
|
|
53
|
+
const docs = "See docs/remote-keychain-access.md for non-interactive / SSH setup.";
|
|
54
|
+
let kind;
|
|
55
|
+
let hint;
|
|
56
|
+
if (status === SECURITY_EXIT.ITEM_NOT_FOUND || /could not be found/i.test(stderr)) {
|
|
57
|
+
kind = "not-found";
|
|
58
|
+
hint = `No '${service}' item found in the Keychain.
|
|
59
|
+
Run this browser at least once on this machine so it creates the key.`;
|
|
60
|
+
} else if (status === SECURITY_EXIT.INTERACTION_NOT_ALLOWED || /interaction.*not allowed/i.test(stderr)) {
|
|
61
|
+
kind = "access-denied";
|
|
62
|
+
hint = "The Keychain needs a GUI authorization prompt that can't be shown here " + (isLikelyHeadless() ? "(you appear to be on a remote/SSH session).\n" : ".\n") + 'Authorize once from a GUI Terminal (click "Always Allow"), then unlock the\nkeychain in this session:\n security unlock-keychain ~/Library/Keychains/login.keychain-db\n' + docs;
|
|
63
|
+
} else if (status === SECURITY_EXIT.AUTH_FAILED) {
|
|
64
|
+
kind = "access-denied";
|
|
65
|
+
hint = `Keychain authorization failed (wrong password or denied).
|
|
66
|
+
Try: security unlock-keychain ~/Library/Keychains/login.keychain-db
|
|
67
|
+
${docs}`;
|
|
68
|
+
} else {
|
|
69
|
+
kind = "access-denied";
|
|
70
|
+
hint = `macOS showed a Keychain access prompt that was denied or dismissed.
|
|
71
|
+
Re-run the command and click "Allow" (or "Always Allow"), or authorize once with:
|
|
72
|
+
security find-generic-password -w -s "${service}"
|
|
73
|
+
${docs}`;
|
|
74
|
+
}
|
|
75
|
+
const detail = [
|
|
76
|
+
status !== null ? `exit ${status}` : null,
|
|
77
|
+
stderr || null
|
|
78
|
+
].filter(Boolean).join(": ");
|
|
79
|
+
return new KeychainAccessError(
|
|
80
|
+
`Could not retrieve ${service} key from macOS Keychain.
|
|
81
|
+
${hint}` + (detail ? `
|
|
82
|
+
(security: ${detail})` : ""),
|
|
83
|
+
service,
|
|
84
|
+
kind
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
var _passwordProvider = null;
|
|
88
|
+
function getKeychainPasswordProvider() {
|
|
89
|
+
return _passwordProvider;
|
|
90
|
+
}
|
|
91
|
+
function setKeychainPasswordProvider(provider) {
|
|
92
|
+
_passwordProvider = provider;
|
|
93
|
+
}
|
|
94
|
+
function getKeychainPassword(service = DEFAULT_SAFE_STORAGE_SERVICE) {
|
|
95
|
+
const cached = _cachedPasswords.get(service);
|
|
96
|
+
if (cached !== void 0) {
|
|
97
|
+
return cached;
|
|
98
|
+
}
|
|
99
|
+
let password;
|
|
100
|
+
if (_passwordProvider) {
|
|
101
|
+
password = _passwordProvider(service);
|
|
102
|
+
} else {
|
|
103
|
+
try {
|
|
104
|
+
password = execFileSync(
|
|
105
|
+
"security",
|
|
106
|
+
["find-generic-password", "-w", "-s", service],
|
|
107
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
108
|
+
).trim();
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const { status, stderr } = parseSecurityError(err);
|
|
111
|
+
throw buildKeychainError(service, status, stderr);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!password) {
|
|
115
|
+
throw new KeychainAccessError(
|
|
116
|
+
`Keychain returned an empty ${service} key.
|
|
117
|
+
The item may exist with no password. Re-run the browser to recreate it.`,
|
|
118
|
+
service,
|
|
119
|
+
"empty"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
_cachedPasswords.set(service, password);
|
|
123
|
+
return password;
|
|
124
|
+
}
|
|
125
|
+
function getChromeKeychainPassword() {
|
|
126
|
+
return getKeychainPassword(DEFAULT_SAFE_STORAGE_SERVICE);
|
|
127
|
+
}
|
|
128
|
+
function deriveEncryptionKey(service = DEFAULT_SAFE_STORAGE_SERVICE, keychainPassword) {
|
|
129
|
+
const cached = _cachedDerivedKeys.get(service);
|
|
130
|
+
if (cached !== void 0) {
|
|
131
|
+
return cached;
|
|
132
|
+
}
|
|
133
|
+
const password = keychainPassword || getKeychainPassword(service);
|
|
134
|
+
if (!password) {
|
|
135
|
+
throw new Error("Keychain password cannot be empty for key derivation.");
|
|
136
|
+
}
|
|
137
|
+
const key = pbkdf2Sync(
|
|
138
|
+
password,
|
|
139
|
+
PBKDF2_SALT,
|
|
140
|
+
PBKDF2_ITERATIONS,
|
|
141
|
+
PBKDF2_KEY_LENGTH,
|
|
142
|
+
"sha1"
|
|
143
|
+
);
|
|
144
|
+
_cachedDerivedKeys.set(service, key);
|
|
145
|
+
return key;
|
|
146
|
+
}
|
|
147
|
+
function clearKeychainCache() {
|
|
148
|
+
_cachedPasswords.clear();
|
|
149
|
+
_cachedDerivedKeys.clear();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/core/crypto.ts
|
|
153
|
+
import { createDecipheriv } from "node:crypto";
|
|
154
|
+
var AES_IV = Buffer.alloc(16, " ");
|
|
155
|
+
var GCM_NONCE_LENGTH = 12;
|
|
156
|
+
var GCM_TAG_LENGTH = 16;
|
|
157
|
+
function decryptValueBuffer(encryptedBlob, derivedKey) {
|
|
158
|
+
if (!encryptedBlob || encryptedBlob.length === 0) {
|
|
159
|
+
return Buffer.alloc(0);
|
|
160
|
+
}
|
|
161
|
+
const blob = Buffer.isBuffer(encryptedBlob) ? encryptedBlob : Buffer.from(encryptedBlob);
|
|
162
|
+
const prefix = blob.subarray(0, 3).toString("utf-8");
|
|
163
|
+
if (prefix !== "v10" && prefix !== "v11") {
|
|
164
|
+
return "<unsupported encryption format>";
|
|
165
|
+
}
|
|
166
|
+
const ciphertext = blob.subarray(3);
|
|
167
|
+
try {
|
|
168
|
+
const decipher = createDecipheriv("aes-128-cbc", derivedKey, AES_IV);
|
|
169
|
+
let decrypted = decipher.update(ciphertext);
|
|
170
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
171
|
+
return decrypted;
|
|
172
|
+
} catch {
|
|
173
|
+
return "<decryption failed>";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function decryptValueBufferGcm(encryptedBlob, key) {
|
|
177
|
+
if (!encryptedBlob || encryptedBlob.length === 0) {
|
|
178
|
+
return Buffer.alloc(0);
|
|
179
|
+
}
|
|
180
|
+
const blob = Buffer.isBuffer(encryptedBlob) ? encryptedBlob : Buffer.from(encryptedBlob);
|
|
181
|
+
const prefix = blob.subarray(0, 3).toString("utf-8");
|
|
182
|
+
if (prefix !== "v10" && prefix !== "v11") {
|
|
183
|
+
return "<unsupported encryption format>";
|
|
184
|
+
}
|
|
185
|
+
if (blob.length < 3 + GCM_NONCE_LENGTH + GCM_TAG_LENGTH) {
|
|
186
|
+
return "<decryption failed>";
|
|
187
|
+
}
|
|
188
|
+
const nonce = blob.subarray(3, 3 + GCM_NONCE_LENGTH);
|
|
189
|
+
const tag = blob.subarray(blob.length - GCM_TAG_LENGTH);
|
|
190
|
+
const ciphertext = blob.subarray(3 + GCM_NONCE_LENGTH, blob.length - GCM_TAG_LENGTH);
|
|
191
|
+
try {
|
|
192
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
193
|
+
decipher.setAuthTag(tag);
|
|
194
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
195
|
+
} catch {
|
|
196
|
+
return "<decryption failed>";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function decryptValueBufferGcmV20(encryptedBlob, key) {
|
|
200
|
+
if (!encryptedBlob || encryptedBlob.length === 0) {
|
|
201
|
+
return Buffer.alloc(0);
|
|
202
|
+
}
|
|
203
|
+
const blob = Buffer.isBuffer(encryptedBlob) ? encryptedBlob : Buffer.from(encryptedBlob);
|
|
204
|
+
const prefix = blob.subarray(0, 3).toString("utf-8");
|
|
205
|
+
if (prefix !== "v20") {
|
|
206
|
+
return "<unsupported encryption format>";
|
|
207
|
+
}
|
|
208
|
+
if (blob.length < 3 + GCM_NONCE_LENGTH + GCM_TAG_LENGTH) {
|
|
209
|
+
return "<decryption failed>";
|
|
210
|
+
}
|
|
211
|
+
const nonce = blob.subarray(3, 3 + GCM_NONCE_LENGTH);
|
|
212
|
+
const tag = blob.subarray(blob.length - GCM_TAG_LENGTH);
|
|
213
|
+
const ciphertext = blob.subarray(3 + GCM_NONCE_LENGTH, blob.length - GCM_TAG_LENGTH);
|
|
214
|
+
try {
|
|
215
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
216
|
+
decipher.setAuthTag(tag);
|
|
217
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
218
|
+
} catch {
|
|
219
|
+
return "<decryption failed>";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function chromeTimeToDate(chromeTimestamp) {
|
|
223
|
+
if (chromeTimestamp === null || chromeTimestamp === void 0) {
|
|
224
|
+
return "N/A";
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const timestampBigInt = BigInt(chromeTimestamp);
|
|
228
|
+
if (timestampBigInt === 0n) {
|
|
229
|
+
return "N/A";
|
|
230
|
+
}
|
|
231
|
+
const unixMicroseconds = timestampBigInt - 11644473600000000n;
|
|
232
|
+
const ms = Number(unixMicroseconds / 1000n);
|
|
233
|
+
const date = new Date(ms);
|
|
234
|
+
if (isNaN(date.getTime())) {
|
|
235
|
+
return "N/A";
|
|
236
|
+
}
|
|
237
|
+
return date.toLocaleString();
|
|
238
|
+
} catch {
|
|
239
|
+
return "N/A";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/core/browsers.ts
|
|
244
|
+
var BROWSERS = {
|
|
245
|
+
chrome: {
|
|
246
|
+
id: "chrome",
|
|
247
|
+
name: "Google Chrome",
|
|
248
|
+
aliases: ["google-chrome", "google chrome"],
|
|
249
|
+
safeStorageLabel: "Chrome Safe Storage",
|
|
250
|
+
linuxSecretApp: "chrome",
|
|
251
|
+
dirs: {
|
|
252
|
+
darwin: "Google/Chrome",
|
|
253
|
+
win32: "Google/Chrome/User Data",
|
|
254
|
+
linux: "google-chrome"
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
"chrome-beta": {
|
|
258
|
+
id: "chrome-beta",
|
|
259
|
+
name: "Google Chrome Beta",
|
|
260
|
+
safeStorageLabel: "Chrome Safe Storage",
|
|
261
|
+
linuxSecretApp: "chrome",
|
|
262
|
+
dirs: {
|
|
263
|
+
darwin: "Google/Chrome Beta",
|
|
264
|
+
win32: "Google/Chrome Beta/User Data",
|
|
265
|
+
linux: "google-chrome-beta"
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
"chrome-canary": {
|
|
269
|
+
id: "chrome-canary",
|
|
270
|
+
name: "Google Chrome Canary",
|
|
271
|
+
aliases: ["canary"],
|
|
272
|
+
safeStorageLabel: "Chrome Safe Storage",
|
|
273
|
+
linuxSecretApp: "chrome",
|
|
274
|
+
dirs: {
|
|
275
|
+
darwin: "Google/Chrome Canary",
|
|
276
|
+
win32: "Google/Chrome SxS/User Data",
|
|
277
|
+
linux: "google-chrome-unstable"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
chromium: {
|
|
281
|
+
id: "chromium",
|
|
282
|
+
name: "Chromium",
|
|
283
|
+
safeStorageLabel: "Chromium Safe Storage",
|
|
284
|
+
linuxSecretApp: "chromium",
|
|
285
|
+
dirs: {
|
|
286
|
+
darwin: "Chromium",
|
|
287
|
+
win32: "Chromium/User Data",
|
|
288
|
+
linux: "chromium"
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
edge: {
|
|
292
|
+
id: "edge",
|
|
293
|
+
name: "Microsoft Edge",
|
|
294
|
+
aliases: ["msedge", "microsoft-edge", "microsoft edge"],
|
|
295
|
+
safeStorageLabel: "Microsoft Edge Safe Storage",
|
|
296
|
+
linuxSecretApp: "chromium",
|
|
297
|
+
dirs: {
|
|
298
|
+
darwin: "Microsoft Edge",
|
|
299
|
+
win32: "Microsoft/Edge/User Data",
|
|
300
|
+
linux: "microsoft-edge"
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
brave: {
|
|
304
|
+
id: "brave",
|
|
305
|
+
name: "Brave",
|
|
306
|
+
aliases: ["brave-browser"],
|
|
307
|
+
safeStorageLabel: "Brave Safe Storage",
|
|
308
|
+
linuxSecretApp: "chrome",
|
|
309
|
+
dirs: {
|
|
310
|
+
darwin: "BraveSoftware/Brave-Browser",
|
|
311
|
+
win32: "BraveSoftware/Brave-Browser/User Data",
|
|
312
|
+
linux: "BraveSoftware/Brave-Browser"
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
comet: {
|
|
316
|
+
id: "comet",
|
|
317
|
+
name: "Comet",
|
|
318
|
+
aliases: ["perplexity", "perplexity-comet"],
|
|
319
|
+
safeStorageLabel: "Comet Safe Storage",
|
|
320
|
+
linuxSecretApp: "chromium",
|
|
321
|
+
// macOS dir verified (~/Library/Application Support/Comet); Win/Linux best-effort.
|
|
322
|
+
dirs: {
|
|
323
|
+
darwin: "Comet",
|
|
324
|
+
win32: "Perplexity/Comet/User Data",
|
|
325
|
+
linux: "comet"
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
vivaldi: {
|
|
329
|
+
id: "vivaldi",
|
|
330
|
+
name: "Vivaldi",
|
|
331
|
+
// Vivaldi reuses Chrome's Keychain item rather than minting its own.
|
|
332
|
+
safeStorageLabel: "Chrome Safe Storage",
|
|
333
|
+
linuxSecretApp: "chrome",
|
|
334
|
+
dirs: {
|
|
335
|
+
darwin: "Vivaldi",
|
|
336
|
+
win32: "Vivaldi/User Data",
|
|
337
|
+
linux: "vivaldi"
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
opera: {
|
|
341
|
+
id: "opera",
|
|
342
|
+
name: "Opera",
|
|
343
|
+
safeStorageLabel: "Opera Safe Storage",
|
|
344
|
+
linuxSecretApp: "chromium",
|
|
345
|
+
// Opera's product dir *is* the user-data root (no nested "User Data").
|
|
346
|
+
// Profiles are usually still under Default/; some layouts are flat at root —
|
|
347
|
+
// discoverProfiles handles both.
|
|
348
|
+
dirs: {
|
|
349
|
+
darwin: "com.operasoftware.Opera",
|
|
350
|
+
win32: "Opera Software/Opera Stable",
|
|
351
|
+
win32Base: "roaming",
|
|
352
|
+
linux: "opera"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var DEFAULT_BROWSER = "chrome";
|
|
357
|
+
function listBrowsers() {
|
|
358
|
+
return Object.values(BROWSERS);
|
|
359
|
+
}
|
|
360
|
+
function resolveBrowser(idOrName) {
|
|
361
|
+
if (!idOrName) return null;
|
|
362
|
+
const needle = idOrName.trim().toLowerCase();
|
|
363
|
+
for (const b of listBrowsers()) {
|
|
364
|
+
if (b.id.toLowerCase() === needle) return b;
|
|
365
|
+
if (b.name.toLowerCase() === needle) return b;
|
|
366
|
+
if (b.aliases?.some((a) => a.toLowerCase() === needle)) return b;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
function requireBrowser(idOrName) {
|
|
371
|
+
const resolved = resolveBrowser(idOrName);
|
|
372
|
+
if (resolved) return resolved;
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Unknown browser "${idOrName}". Supported: ${listBrowsers().map((b) => b.id).join(", ")}.`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
var _active = BROWSERS[DEFAULT_BROWSER];
|
|
378
|
+
function getActiveBrowser() {
|
|
379
|
+
return _active;
|
|
380
|
+
}
|
|
381
|
+
function setActiveBrowser(idOrName) {
|
|
382
|
+
if (!idOrName || !idOrName.trim()) {
|
|
383
|
+
_active = BROWSERS[DEFAULT_BROWSER];
|
|
384
|
+
return _active;
|
|
385
|
+
}
|
|
386
|
+
_active = requireBrowser(idOrName);
|
|
387
|
+
return _active;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/core/platform/darwin.ts
|
|
391
|
+
var darwin = {
|
|
392
|
+
name: "darwin",
|
|
393
|
+
getDefaultChromeDir(browser = getActiveBrowser()) {
|
|
394
|
+
return join(
|
|
395
|
+
homedir(),
|
|
396
|
+
"Library",
|
|
397
|
+
"Application Support",
|
|
398
|
+
...browser.dirs.darwin.split("/")
|
|
399
|
+
);
|
|
400
|
+
},
|
|
401
|
+
getDecryptionKey(_chromeDir, browser = getActiveBrowser()) {
|
|
402
|
+
return deriveEncryptionKey(browser.safeStorageLabel);
|
|
403
|
+
},
|
|
404
|
+
decrypt(blob, key) {
|
|
405
|
+
return decryptValueBuffer(blob, key);
|
|
406
|
+
},
|
|
407
|
+
clearCache() {
|
|
408
|
+
clearKeychainCache();
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// src/core/platform/win32.ts
|
|
413
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
414
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
415
|
+
import { dirname, join as join2 } from "node:path";
|
|
416
|
+
var DPAPI_PREFIX = Buffer.from("DPAPI", "ascii");
|
|
417
|
+
var APPB_PREFIX = Buffer.from("APPB", "ascii");
|
|
418
|
+
var _cachedKeys = /* @__PURE__ */ new Map();
|
|
419
|
+
var _cachedABEKeys = /* @__PURE__ */ new Map();
|
|
420
|
+
var _lastResolvedDir;
|
|
421
|
+
var _lastResolvedBrowser;
|
|
422
|
+
var ABE_BROWSER_MAP = {
|
|
423
|
+
chrome: "chrome",
|
|
424
|
+
"chrome-beta": "chrome-beta",
|
|
425
|
+
"chrome-canary": "chrome",
|
|
426
|
+
chromium: "chrome",
|
|
427
|
+
brave: "brave",
|
|
428
|
+
edge: "edge",
|
|
429
|
+
vivaldi: "chrome",
|
|
430
|
+
comet: "chrome",
|
|
431
|
+
opera: "chrome"
|
|
432
|
+
};
|
|
433
|
+
function dpapiDecrypt(encrypted) {
|
|
434
|
+
const script = [
|
|
435
|
+
"$ErrorActionPreference='Stop';",
|
|
436
|
+
"Add-Type -AssemblyName System.Security;",
|
|
437
|
+
"$b=[Console]::In.ReadToEnd().Trim();",
|
|
438
|
+
"$enc=[Convert]::FromBase64String($b);",
|
|
439
|
+
"$dec=[System.Security.Cryptography.ProtectedData]::Unprotect($enc,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser);",
|
|
440
|
+
"[Console]::Out.Write([Convert]::ToBase64String($dec));"
|
|
441
|
+
].join(" ");
|
|
442
|
+
let out;
|
|
443
|
+
try {
|
|
444
|
+
out = execFileSync2(
|
|
445
|
+
"powershell.exe",
|
|
446
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
447
|
+
{ input: encrypted.toString("base64"), encoding: "utf-8" }
|
|
448
|
+
);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`DPAPI decryption via PowerShell failed.
|
|
452
|
+
The encrypted_key can only be unprotected by the same Windows user that created it. Run as that user on the same machine.
|
|
453
|
+
(powershell: ${err?.message ?? err})`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return Buffer.from(out.trim(), "base64");
|
|
457
|
+
}
|
|
458
|
+
function resolveLocalStateDir(requestedDir) {
|
|
459
|
+
const parentDir = dirname(requestedDir);
|
|
460
|
+
return existsSync(join2(requestedDir, "Local State")) ? requestedDir : parentDir !== requestedDir && existsSync(join2(parentDir, "Local State")) ? parentDir : requestedDir;
|
|
461
|
+
}
|
|
462
|
+
function findABEHelper() {
|
|
463
|
+
const base = import.meta.dirname ?? "";
|
|
464
|
+
const candidates = [
|
|
465
|
+
join2(base, "chrome_abe_decrypt.exe"),
|
|
466
|
+
join2(base, "..", "..", "..", "bin", "chrome_abe_decrypt.exe"),
|
|
467
|
+
join2(base, "..", "..", "..", "..", "bin", "chrome_abe_decrypt.exe"),
|
|
468
|
+
join2(base, "..", "..", "..", "..", "src", "native", "chrome_abe_decrypt.exe")
|
|
469
|
+
];
|
|
470
|
+
for (const p of candidates) {
|
|
471
|
+
if (existsSync(p)) return p;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
execFileSync2("where", ["chrome_abe_decrypt.exe"], { encoding: "utf-8", stdio: "pipe" });
|
|
475
|
+
return "chrome_abe_decrypt.exe";
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function getBrowserTempDirs(browserId) {
|
|
481
|
+
const dirs = [];
|
|
482
|
+
const progFiles = process.env["ProgramFiles"] || "C:\\Program Files";
|
|
483
|
+
const progFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
484
|
+
const localAppData = process.env.LOCALAPPDATA || (process.env.USERPROFILE ? join2(process.env.USERPROFILE, "AppData", "Local") : "");
|
|
485
|
+
const subpaths = {
|
|
486
|
+
chrome: ["Google\\Chrome"],
|
|
487
|
+
"chrome-beta": ["Google\\Chrome Beta"],
|
|
488
|
+
"chrome-canary": ["Google\\Chrome SxS"],
|
|
489
|
+
chromium: ["Chromium"],
|
|
490
|
+
edge: ["Microsoft\\Edge"],
|
|
491
|
+
brave: ["BraveSoftware\\Brave-Browser"],
|
|
492
|
+
vivaldi: ["Vivaldi"]
|
|
493
|
+
};
|
|
494
|
+
const rels = subpaths[browserId] || ["Google\\Chrome"];
|
|
495
|
+
for (const rel of rels) {
|
|
496
|
+
dirs.push(join2(progFiles, rel, "Temp"));
|
|
497
|
+
dirs.push(join2(progFilesX86, rel, "Temp"));
|
|
498
|
+
if (localAppData) {
|
|
499
|
+
dirs.push(join2(localAppData, rel, "Temp"));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return dirs;
|
|
503
|
+
}
|
|
504
|
+
function findOrStageABEHelper(browserId) {
|
|
505
|
+
const sourceExe = findABEHelper();
|
|
506
|
+
if (!sourceExe) return null;
|
|
507
|
+
const targetDirs = getBrowserTempDirs(browserId);
|
|
508
|
+
for (const targetDir of targetDirs) {
|
|
509
|
+
try {
|
|
510
|
+
if (!existsSync(targetDir)) {
|
|
511
|
+
mkdirSync(targetDir, { recursive: true });
|
|
512
|
+
}
|
|
513
|
+
const stagedExe = join2(targetDir, "chrome_abe_decrypt.exe");
|
|
514
|
+
if (!existsSync(stagedExe) || statSync(stagedExe).size !== statSync(sourceExe).size) {
|
|
515
|
+
copyFileSync(sourceExe, stagedExe);
|
|
516
|
+
}
|
|
517
|
+
return stagedExe;
|
|
518
|
+
} catch {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return sourceExe;
|
|
523
|
+
}
|
|
524
|
+
var _abeWarningShown = false;
|
|
525
|
+
function abeDecrypt(rawEncryptedBytes, browserId) {
|
|
526
|
+
const helperExe = findOrStageABEHelper(browserId);
|
|
527
|
+
if (!helperExe) {
|
|
528
|
+
if (!_abeWarningShown) {
|
|
529
|
+
_abeWarningShown = true;
|
|
530
|
+
process.stderr.write(
|
|
531
|
+
"[chrome-tools] chrome_abe_decrypt.exe not found. Chrome 127+ v20-encrypted\ncookies/passwords cannot be decrypted.\n"
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
const comBrowser = ABE_BROWSER_MAP[browserId] || "chrome";
|
|
537
|
+
try {
|
|
538
|
+
const out = execFileSync2(
|
|
539
|
+
helperExe,
|
|
540
|
+
[comBrowser],
|
|
541
|
+
{
|
|
542
|
+
input: rawEncryptedBytes.toString("base64"),
|
|
543
|
+
encoding: "utf-8",
|
|
544
|
+
timeout: 3e4,
|
|
545
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
546
|
+
cwd: dirname(helperExe)
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
return Buffer.from(out.trim(), "base64");
|
|
550
|
+
} catch {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
var win32 = {
|
|
555
|
+
name: "win32",
|
|
556
|
+
getDefaultChromeDir(browser = getActiveBrowser()) {
|
|
557
|
+
const roaming = browser.dirs.win32Base === "roaming";
|
|
558
|
+
const appData = roaming ? process.env.APPDATA || (process.env.USERPROFILE ? join2(process.env.USERPROFILE, "AppData", "Roaming") : "") : process.env.LOCALAPPDATA || (process.env.USERPROFILE ? join2(process.env.USERPROFILE, "AppData", "Local") : "");
|
|
559
|
+
if (!appData) {
|
|
560
|
+
throw new Error("Could not resolve the Windows application-data directory.");
|
|
561
|
+
}
|
|
562
|
+
return join2(appData, ...browser.dirs.win32.split("/"));
|
|
563
|
+
},
|
|
564
|
+
getDecryptionKey(chromeDir, browser = getActiveBrowser()) {
|
|
565
|
+
const requestedDir = chromeDir || this.getDefaultChromeDir(browser);
|
|
566
|
+
const dir = resolveLocalStateDir(requestedDir);
|
|
567
|
+
_lastResolvedDir = dir;
|
|
568
|
+
_lastResolvedBrowser = browser;
|
|
569
|
+
const cached = _cachedKeys.get(dir);
|
|
570
|
+
if (cached !== void 0) {
|
|
571
|
+
if (!_cachedABEKeys.has(dir)) {
|
|
572
|
+
try {
|
|
573
|
+
this.getAppBoundKey?.(dir, browser);
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return cached;
|
|
578
|
+
}
|
|
579
|
+
const localStatePath = join2(dir, "Local State");
|
|
580
|
+
if (!existsSync(localStatePath)) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`Could not find Chrome's "Local State" at ${localStatePath}.
|
|
583
|
+
Run Google Chrome at least once so it creates the encryption key.`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
let encryptedKeyB64;
|
|
587
|
+
try {
|
|
588
|
+
const raw = readFileSync(localStatePath, "utf-8");
|
|
589
|
+
encryptedKeyB64 = JSON.parse(raw)?.os_crypt?.encrypted_key;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
throw new Error(`Failed to read os_crypt.encrypted_key from Local State: ${err.message}`);
|
|
592
|
+
}
|
|
593
|
+
if (!encryptedKeyB64) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
"No os_crypt.encrypted_key found in Local State.\nThis profile may predate Chrome's encrypted-key scheme."
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const encryptedKey = Buffer.from(encryptedKeyB64, "base64");
|
|
599
|
+
if (!encryptedKey.subarray(0, DPAPI_PREFIX.length).equals(DPAPI_PREFIX)) {
|
|
600
|
+
throw new Error('Unexpected encrypted_key format (missing "DPAPI" prefix).');
|
|
601
|
+
}
|
|
602
|
+
const key = dpapiDecrypt(encryptedKey.subarray(DPAPI_PREFIX.length));
|
|
603
|
+
_cachedKeys.set(dir, key);
|
|
604
|
+
try {
|
|
605
|
+
this.getAppBoundKey?.(dir, browser);
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
return key;
|
|
609
|
+
},
|
|
610
|
+
getAppBoundKey(chromeDir, browser = _lastResolvedBrowser ?? getActiveBrowser()) {
|
|
611
|
+
const requestedDir = chromeDir || _lastResolvedDir || this.getDefaultChromeDir(browser);
|
|
612
|
+
const dir = resolveLocalStateDir(requestedDir);
|
|
613
|
+
_lastResolvedDir = dir;
|
|
614
|
+
_lastResolvedBrowser = browser;
|
|
615
|
+
const cached = _cachedABEKeys.get(dir);
|
|
616
|
+
if (cached !== void 0) {
|
|
617
|
+
return cached;
|
|
618
|
+
}
|
|
619
|
+
const localStatePath = join2(dir, "Local State");
|
|
620
|
+
if (!existsSync(localStatePath)) return void 0;
|
|
621
|
+
let appBoundKeyB64;
|
|
622
|
+
try {
|
|
623
|
+
const raw = readFileSync(localStatePath, "utf-8");
|
|
624
|
+
appBoundKeyB64 = JSON.parse(raw)?.os_crypt?.app_bound_encrypted_key;
|
|
625
|
+
} catch {
|
|
626
|
+
return void 0;
|
|
627
|
+
}
|
|
628
|
+
if (!appBoundKeyB64) return void 0;
|
|
629
|
+
const appBoundKey = Buffer.from(appBoundKeyB64, "base64");
|
|
630
|
+
if (!appBoundKey.subarray(0, APPB_PREFIX.length).equals(APPB_PREFIX)) {
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
const abeKey = abeDecrypt(appBoundKey.subarray(APPB_PREFIX.length), browser.id);
|
|
634
|
+
if (!abeKey) return void 0;
|
|
635
|
+
const key = abeKey.length > 32 ? abeKey.subarray(abeKey.length - 32) : abeKey;
|
|
636
|
+
_cachedABEKeys.set(dir, key);
|
|
637
|
+
return key;
|
|
638
|
+
},
|
|
639
|
+
decrypt(blob, key) {
|
|
640
|
+
const prefix = blob.subarray(0, 3).toString("utf-8");
|
|
641
|
+
if (prefix === "v20") {
|
|
642
|
+
const abeKey = this.getAppBoundKey?.(_lastResolvedDir, _lastResolvedBrowser);
|
|
643
|
+
if (!abeKey) {
|
|
644
|
+
return "<v20 app-bound encryption - requires chrome_abe_decrypt.exe>";
|
|
645
|
+
}
|
|
646
|
+
return decryptValueBufferGcmV20(blob, abeKey);
|
|
647
|
+
}
|
|
648
|
+
return decryptValueBufferGcm(blob, key);
|
|
649
|
+
},
|
|
650
|
+
clearCache() {
|
|
651
|
+
_cachedKeys.clear();
|
|
652
|
+
_cachedABEKeys.clear();
|
|
653
|
+
_lastResolvedDir = void 0;
|
|
654
|
+
_lastResolvedBrowser = void 0;
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// src/core/platform/linux.ts
|
|
659
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
660
|
+
import { homedir as homedir2 } from "node:os";
|
|
661
|
+
import { join as join3 } from "node:path";
|
|
662
|
+
import { pbkdf2Sync as pbkdf2Sync2 } from "node:crypto";
|
|
663
|
+
var PBKDF2_ITERATIONS2 = 1;
|
|
664
|
+
var PBKDF2_KEY_LENGTH2 = 16;
|
|
665
|
+
var PBKDF2_SALT2 = Buffer.from("saltysalt");
|
|
666
|
+
var FALLBACK_PASSWORD = "peanuts";
|
|
667
|
+
var _cachedKeys2 = /* @__PURE__ */ new Map();
|
|
668
|
+
function getStoragePassword(application) {
|
|
669
|
+
const provider = getKeychainPasswordProvider();
|
|
670
|
+
if (provider) {
|
|
671
|
+
return provider(application);
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const out = execFileSync3(
|
|
675
|
+
"secret-tool",
|
|
676
|
+
["lookup", "application", application],
|
|
677
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
678
|
+
).trim();
|
|
679
|
+
if (out) return out;
|
|
680
|
+
} catch {
|
|
681
|
+
}
|
|
682
|
+
return FALLBACK_PASSWORD;
|
|
683
|
+
}
|
|
684
|
+
var linux = {
|
|
685
|
+
name: "linux",
|
|
686
|
+
getDefaultChromeDir(browser = getActiveBrowser()) {
|
|
687
|
+
const configHome = process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config");
|
|
688
|
+
return join3(configHome, ...browser.dirs.linux.split("/"));
|
|
689
|
+
},
|
|
690
|
+
getDecryptionKey(_chromeDir, browser = getActiveBrowser()) {
|
|
691
|
+
const application = browser.linuxSecretApp || "chrome";
|
|
692
|
+
const cached = _cachedKeys2.get(application);
|
|
693
|
+
if (cached !== void 0) {
|
|
694
|
+
return cached;
|
|
695
|
+
}
|
|
696
|
+
const key = pbkdf2Sync2(
|
|
697
|
+
getStoragePassword(application),
|
|
698
|
+
PBKDF2_SALT2,
|
|
699
|
+
PBKDF2_ITERATIONS2,
|
|
700
|
+
PBKDF2_KEY_LENGTH2,
|
|
701
|
+
"sha1"
|
|
702
|
+
);
|
|
703
|
+
_cachedKeys2.set(application, key);
|
|
704
|
+
return key;
|
|
705
|
+
},
|
|
706
|
+
decrypt(blob, key) {
|
|
707
|
+
return decryptValueBuffer(blob, key);
|
|
708
|
+
},
|
|
709
|
+
clearCache() {
|
|
710
|
+
_cachedKeys2.clear();
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/core/platform/index.ts
|
|
715
|
+
var _active2 = null;
|
|
716
|
+
function selectPlatform() {
|
|
717
|
+
if (_active2) return _active2;
|
|
718
|
+
switch (process.platform) {
|
|
719
|
+
case "darwin":
|
|
720
|
+
_active2 = darwin;
|
|
721
|
+
break;
|
|
722
|
+
case "win32":
|
|
723
|
+
_active2 = win32;
|
|
724
|
+
break;
|
|
725
|
+
case "linux":
|
|
726
|
+
_active2 = linux;
|
|
727
|
+
break;
|
|
728
|
+
default:
|
|
729
|
+
throw new Error(
|
|
730
|
+
`Unsupported platform: ${process.platform}. chrome-tools supports macOS, Windows, and Linux.`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return _active2;
|
|
734
|
+
}
|
|
735
|
+
function getPlatformChromeDir(browser) {
|
|
736
|
+
return selectPlatform().getDefaultChromeDir(resolveTargetBrowser(browser));
|
|
737
|
+
}
|
|
738
|
+
function deriveEncryptionKey2(chromeDir, browser) {
|
|
739
|
+
return selectPlatform().getDecryptionKey(chromeDir, resolveTargetBrowser(browser));
|
|
740
|
+
}
|
|
741
|
+
function decryptValueBuffer2(blob, key) {
|
|
742
|
+
if (!blob || blob.length === 0) {
|
|
743
|
+
return Buffer.alloc(0);
|
|
744
|
+
}
|
|
745
|
+
const buf = Buffer.isBuffer(blob) ? blob : Buffer.from(blob);
|
|
746
|
+
return selectPlatform().decrypt(buf, key);
|
|
747
|
+
}
|
|
748
|
+
function decryptValue(blob, key) {
|
|
749
|
+
const decrypted = decryptValueBuffer2(blob, key);
|
|
750
|
+
return Buffer.isBuffer(decrypted) ? decrypted.toString("utf-8") : decrypted;
|
|
751
|
+
}
|
|
752
|
+
function clearKeyCache() {
|
|
753
|
+
selectPlatform().clearCache();
|
|
754
|
+
}
|
|
755
|
+
function resolveTargetBrowser(browser) {
|
|
756
|
+
if (!browser) return getActiveBrowser();
|
|
757
|
+
return typeof browser === "string" ? requireBrowser(browser) : browser;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/core/chrome.ts
|
|
761
|
+
var PROFILE_INDICATORS = [
|
|
762
|
+
"Login Data",
|
|
763
|
+
"History",
|
|
764
|
+
"Bookmarks",
|
|
765
|
+
"Cookies",
|
|
766
|
+
"Web Data",
|
|
767
|
+
"Local Storage",
|
|
768
|
+
"Preferences"
|
|
769
|
+
];
|
|
770
|
+
function resolveCookiesDatabasePath(profilePath) {
|
|
771
|
+
const networkPath = join4(profilePath, "Network", "Cookies");
|
|
772
|
+
return existsSync2(networkPath) ? networkPath : join4(profilePath, "Cookies");
|
|
773
|
+
}
|
|
774
|
+
function isProfileDirectory(dir) {
|
|
775
|
+
return PROFILE_INDICATORS.some((file) => existsSync2(join4(dir, file))) || existsSync2(resolveCookiesDatabasePath(dir));
|
|
776
|
+
}
|
|
777
|
+
function getDefaultChromeDir(browser) {
|
|
778
|
+
return getPlatformChromeDir(browser);
|
|
779
|
+
}
|
|
780
|
+
function detectInstalledBrowsers({ includeMissing = false } = {}) {
|
|
781
|
+
const out = [];
|
|
782
|
+
for (const browser of listBrowsers()) {
|
|
783
|
+
const chromeDir = getDefaultChromeDir(browser);
|
|
784
|
+
const installed = existsSync2(chromeDir);
|
|
785
|
+
if (!installed && !includeMissing) continue;
|
|
786
|
+
out.push({
|
|
787
|
+
browser,
|
|
788
|
+
chromeDir,
|
|
789
|
+
installed,
|
|
790
|
+
profileCount: installed ? discoverProfiles(chromeDir).length : 0
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
return out;
|
|
794
|
+
}
|
|
795
|
+
function discoverProfiles(chromeDir) {
|
|
796
|
+
const dir = chromeDir || getDefaultChromeDir();
|
|
797
|
+
if (!dir || !existsSync2(dir)) {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
802
|
+
const profiles = [];
|
|
803
|
+
for (const entry of entries) {
|
|
804
|
+
if (!entry.isDirectory()) continue;
|
|
805
|
+
if (entry.name === "Network") continue;
|
|
806
|
+
const profilePath = join4(dir, entry.name);
|
|
807
|
+
if (isProfileDirectory(profilePath)) {
|
|
808
|
+
profiles.push({ name: entry.name, path: profilePath });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (profiles.length === 0 && isProfileDirectory(dir)) {
|
|
812
|
+
const name = basename(dir) || "Default";
|
|
813
|
+
profiles.push({ name, path: dir });
|
|
814
|
+
}
|
|
815
|
+
return profiles;
|
|
816
|
+
} catch {
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function getProfileMetadata(chromeDir) {
|
|
821
|
+
const dir = chromeDir || getDefaultChromeDir();
|
|
822
|
+
const localStatePath = join4(dir, "Local State");
|
|
823
|
+
if (!existsSync2(localStatePath)) {
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
const raw = readFileSync2(localStatePath, "utf-8");
|
|
828
|
+
const localState = JSON.parse(raw);
|
|
829
|
+
const infoCache = localState?.profile?.info_cache;
|
|
830
|
+
if (!infoCache || typeof infoCache !== "object") {
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
return Object.entries(infoCache).map(([dirName, infoAny]) => {
|
|
834
|
+
const info = infoAny;
|
|
835
|
+
return {
|
|
836
|
+
directory: dirName,
|
|
837
|
+
name: info.name || dirName,
|
|
838
|
+
gaiaName: info.gaia_name || "",
|
|
839
|
+
userName: info.user_name || "",
|
|
840
|
+
email: info.user_name || "",
|
|
841
|
+
avatarIcon: info.avatar_icon || "",
|
|
842
|
+
isUsingDefaultName: info.is_using_default_name ?? true,
|
|
843
|
+
isEphemeral: info.is_ephemeral ?? false,
|
|
844
|
+
activeTime: info.active_time || 0
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
} catch {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function getProfileStates(chromeDir) {
|
|
852
|
+
const dir = chromeDir || getDefaultChromeDir();
|
|
853
|
+
const discovered = discoverProfiles(dir);
|
|
854
|
+
const metadata = getProfileMetadata(dir);
|
|
855
|
+
const metaMap = /* @__PURE__ */ new Map();
|
|
856
|
+
for (const m of metadata) {
|
|
857
|
+
metaMap.set(m.directory, m);
|
|
858
|
+
}
|
|
859
|
+
return discovered.map((profile) => {
|
|
860
|
+
const meta = metaMap.get(profile.name);
|
|
861
|
+
const profilePath = profile.path;
|
|
862
|
+
const dataFiles = {};
|
|
863
|
+
for (const file of PROFILE_INDICATORS) {
|
|
864
|
+
dataFiles[file.replace(/\s+/g, "_").toLowerCase()] = existsSync2(
|
|
865
|
+
join4(profilePath, file)
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
dataFiles.cookies = existsSync2(resolveCookiesDatabasePath(profilePath));
|
|
869
|
+
dataFiles.top_sites = existsSync2(join4(profilePath, "Top Sites"));
|
|
870
|
+
dataFiles.extensions = existsSync2(join4(profilePath, "Extensions"));
|
|
871
|
+
return {
|
|
872
|
+
directory: profile.name,
|
|
873
|
+
path: profilePath,
|
|
874
|
+
displayName: meta?.name || profile.name,
|
|
875
|
+
gaiaName: meta?.gaiaName || "",
|
|
876
|
+
email: meta?.email || "",
|
|
877
|
+
avatarIcon: meta?.avatarIcon || "",
|
|
878
|
+
isEphemeral: meta?.isEphemeral ?? false,
|
|
879
|
+
activeTime: meta?.activeTime || 0,
|
|
880
|
+
dataFiles
|
|
881
|
+
};
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
function resolveProfilePath(chromeDir, profileName) {
|
|
885
|
+
const dir = chromeDir || getDefaultChromeDir();
|
|
886
|
+
const profilePath = join4(dir, profileName);
|
|
887
|
+
return existsSync2(profilePath) ? profilePath : null;
|
|
888
|
+
}
|
|
889
|
+
function resolveTargetProfiles({
|
|
890
|
+
profilePath,
|
|
891
|
+
chromeDir,
|
|
892
|
+
profile,
|
|
893
|
+
browser
|
|
894
|
+
} = {}) {
|
|
895
|
+
const targetBrowser = browser ? requireBrowser(browser) : void 0;
|
|
896
|
+
const dir = profilePath || chromeDir || getDefaultChromeDir(targetBrowser);
|
|
897
|
+
const allProfiles = discoverProfiles(dir);
|
|
898
|
+
if (profile) {
|
|
899
|
+
const match = allProfiles.filter((p) => p.name === profile);
|
|
900
|
+
if (match.length === 0) {
|
|
901
|
+
throw new Error(`Profile "${profile}" not found in ${dir}`);
|
|
902
|
+
}
|
|
903
|
+
return match;
|
|
904
|
+
}
|
|
905
|
+
return allProfiles;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/core/db.ts
|
|
909
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
910
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync3, mkdtempSync, rmSync } from "node:fs";
|
|
911
|
+
import { tmpdir } from "node:os";
|
|
912
|
+
import { join as join5, basename as basename2 } from "node:path";
|
|
913
|
+
import { DatabaseSync } from "node:sqlite";
|
|
914
|
+
var SQLITE_BUSY = 5;
|
|
915
|
+
var SQLITE_LOCKED = 6;
|
|
916
|
+
var SQLITE_CANTOPEN = 14;
|
|
917
|
+
function shouldCopyDatabase(err) {
|
|
918
|
+
const errcode = err?.errcode;
|
|
919
|
+
if (typeof errcode === "number") {
|
|
920
|
+
const primary = errcode & 255;
|
|
921
|
+
if (primary === SQLITE_BUSY || primary === SQLITE_LOCKED || primary === SQLITE_CANTOPEN) {
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
926
|
+
return /database is locked|database is busy|unable to open database file/i.test(message);
|
|
927
|
+
}
|
|
928
|
+
function toCallerTypes(rows) {
|
|
929
|
+
return rows.map((row) => {
|
|
930
|
+
const plain = {};
|
|
931
|
+
for (const key of Object.keys(row)) {
|
|
932
|
+
const value = row[key];
|
|
933
|
+
if (typeof value === "bigint") {
|
|
934
|
+
plain[key] = Number(value);
|
|
935
|
+
} else if (value instanceof Uint8Array && !Buffer.isBuffer(value)) {
|
|
936
|
+
plain[key] = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
937
|
+
} else {
|
|
938
|
+
plain[key] = value;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return plain;
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
function findLockedCopyHelper() {
|
|
945
|
+
const base = import.meta.dirname ?? "";
|
|
946
|
+
const candidates = [
|
|
947
|
+
join5(base, "chrome_abe_decrypt.exe"),
|
|
948
|
+
join5(base, "..", "bin", "chrome_abe_decrypt.exe"),
|
|
949
|
+
join5(base, "..", "..", "bin", "chrome_abe_decrypt.exe"),
|
|
950
|
+
join5(base, "..", "..", "..", "bin", "chrome_abe_decrypt.exe"),
|
|
951
|
+
join5(base, "..", "..", "src", "native", "chrome_abe_decrypt.exe")
|
|
952
|
+
];
|
|
953
|
+
for (const p of candidates) {
|
|
954
|
+
if (existsSync3(p)) return p;
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
function copyLockedFileWindows(src, dst) {
|
|
959
|
+
const helper = findLockedCopyHelper();
|
|
960
|
+
if (helper) {
|
|
961
|
+
try {
|
|
962
|
+
execFileSync4(helper, ["--copy-locked", src, dst], {
|
|
963
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
964
|
+
windowsHide: true,
|
|
965
|
+
timeout: 15e3
|
|
966
|
+
});
|
|
967
|
+
if (existsSync3(dst)) return;
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const psScript = `
|
|
972
|
+
$ErrorActionPreference = "Stop";
|
|
973
|
+
$in = [Console]::In.ReadToEnd() | ConvertFrom-Json;
|
|
974
|
+
$src = $in.src;
|
|
975
|
+
$dst = $in.dst;
|
|
976
|
+
$code = @"
|
|
977
|
+
using System;
|
|
978
|
+
using System.IO;
|
|
979
|
+
using System.Text;
|
|
980
|
+
using System.Collections.Generic;
|
|
981
|
+
using System.Runtime.InteropServices;
|
|
982
|
+
using Microsoft.Win32.SafeHandles;
|
|
983
|
+
|
|
984
|
+
public class LockedFileCopier {
|
|
985
|
+
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
|
|
986
|
+
static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey);
|
|
987
|
+
|
|
988
|
+
[DllImport("rstrtmgr.dll")]
|
|
989
|
+
static extern int RmEndSession(uint pSessionHandle);
|
|
990
|
+
|
|
991
|
+
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
|
|
992
|
+
static extern int RmRegisterResources(uint pSessionHandle, uint nFiles, string[] rgsFilenames,
|
|
993
|
+
uint nApplications, [In] uint[] rgApplications, uint nServices, string[] rgsServiceNames);
|
|
994
|
+
|
|
995
|
+
[DllImport("rstrtmgr.dll")]
|
|
996
|
+
static extern int RmGetList(uint pSessionHandle, out uint pnProcInfoNeeded,
|
|
997
|
+
ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[] rgAffectedApps, ref uint lpdwRebootReasons);
|
|
998
|
+
|
|
999
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
1000
|
+
struct RM_UNIQUE_PROCESS {
|
|
1001
|
+
public int dwProcessId;
|
|
1002
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
1006
|
+
struct RM_PROCESS_INFO {
|
|
1007
|
+
public RM_UNIQUE_PROCESS Process;
|
|
1008
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
|
1009
|
+
public string strAppName;
|
|
1010
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
|
1011
|
+
public string strServiceShortName;
|
|
1012
|
+
public int ApplicationType;
|
|
1013
|
+
public uint AppStatus;
|
|
1014
|
+
public uint TSSessionId;
|
|
1015
|
+
[MarshalAs(UnmanagedType.Bool)]
|
|
1016
|
+
public bool bRestartable;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
1020
|
+
public static extern IntPtr OpenProcess(int processAccess, bool bInheritHandle, int processId);
|
|
1021
|
+
|
|
1022
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
1023
|
+
public static extern bool DuplicateHandle(
|
|
1024
|
+
IntPtr hSourceProcessHandle,
|
|
1025
|
+
IntPtr hSourceHandle,
|
|
1026
|
+
IntPtr hTargetProcessHandle,
|
|
1027
|
+
out IntPtr lpTargetHandle,
|
|
1028
|
+
uint dwDesiredAccess,
|
|
1029
|
+
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
|
|
1030
|
+
uint dwOptions);
|
|
1031
|
+
|
|
1032
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
|
1033
|
+
public static extern uint GetFinalPathNameByHandle(IntPtr hFile, [Out] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags);
|
|
1034
|
+
|
|
1035
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
1036
|
+
public static extern bool CloseHandle(IntPtr hObject);
|
|
1037
|
+
|
|
1038
|
+
[DllImport("ntdll.dll")]
|
|
1039
|
+
public static extern int NtQuerySystemInformation(int SystemInformationClass, IntPtr SystemInformation, int SystemInformationLength, out int ReturnLength);
|
|
1040
|
+
|
|
1041
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
1042
|
+
public struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX {
|
|
1043
|
+
public IntPtr Object;
|
|
1044
|
+
public IntPtr UniqueProcessId;
|
|
1045
|
+
public IntPtr HandleValue;
|
|
1046
|
+
public uint GrantedAccess;
|
|
1047
|
+
public ushort CreatorBackTraceIndex;
|
|
1048
|
+
public ushort ObjectTypeIndex;
|
|
1049
|
+
public uint HandleAttributes;
|
|
1050
|
+
public uint Reserved;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
public static bool CopyLockedFile(string sourcePath, string destPath) {
|
|
1054
|
+
uint sessionHandle;
|
|
1055
|
+
string sessionKey = Guid.NewGuid().ToString();
|
|
1056
|
+
int res = RmStartSession(out sessionHandle, 0, sessionKey);
|
|
1057
|
+
if (res != 0) return false;
|
|
1058
|
+
|
|
1059
|
+
List<int> pids = new List<int>();
|
|
1060
|
+
try {
|
|
1061
|
+
string[] resources = new string[] { sourcePath };
|
|
1062
|
+
res = RmRegisterResources(sessionHandle, (uint)resources.Length, resources, 0, null, 0, null);
|
|
1063
|
+
if (res == 0) {
|
|
1064
|
+
uint nNeeded = 0;
|
|
1065
|
+
uint nProcInfo = 0;
|
|
1066
|
+
uint rebootReasons = 0;
|
|
1067
|
+
res = RmGetList(sessionHandle, out nNeeded, ref nProcInfo, null, ref rebootReasons);
|
|
1068
|
+
if (res == 234 && nNeeded > 0) {
|
|
1069
|
+
RM_PROCESS_INFO[] procInfo = new RM_PROCESS_INFO[nNeeded];
|
|
1070
|
+
nProcInfo = nNeeded;
|
|
1071
|
+
res = RmGetList(sessionHandle, out nNeeded, ref nProcInfo, procInfo, ref rebootReasons);
|
|
1072
|
+
if (res == 0) {
|
|
1073
|
+
for (int i = 0; i < nProcInfo; i++) {
|
|
1074
|
+
pids.Add(procInfo[i].Process.dwProcessId);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
} finally {
|
|
1080
|
+
RmEndSession(sessionHandle);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (pids.Count == 0) return false;
|
|
1084
|
+
|
|
1085
|
+
int infoLength = 0x10000;
|
|
1086
|
+
IntPtr infoPtr = Marshal.AllocHGlobal(infoLength);
|
|
1087
|
+
int retLen;
|
|
1088
|
+
while (NtQuerySystemInformation(64, infoPtr, infoLength, out retLen) == unchecked((int)0xc0000004)) {
|
|
1089
|
+
Marshal.FreeHGlobal(infoPtr);
|
|
1090
|
+
infoLength = retLen + 0x10000;
|
|
1091
|
+
infoPtr = Marshal.AllocHGlobal(infoLength);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
long handleCount = Marshal.ReadInt64(infoPtr);
|
|
1095
|
+
IntPtr currentPtr = new IntPtr(infoPtr.ToInt64() + 16);
|
|
1096
|
+
int entrySize = Marshal.SizeOf(typeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX));
|
|
1097
|
+
string normSource = Path.GetFullPath(sourcePath);
|
|
1098
|
+
|
|
1099
|
+
bool success = false;
|
|
1100
|
+
foreach (int pid in pids) {
|
|
1101
|
+
IntPtr hProcess = OpenProcess(0x0040 | 0x0400, false, pid);
|
|
1102
|
+
if (hProcess == IntPtr.Zero) continue;
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
IntPtr entryPtr = currentPtr;
|
|
1106
|
+
for (long i = 0; i < handleCount; i++) {
|
|
1107
|
+
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX entry = (SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX)Marshal.PtrToStructure(entryPtr, typeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX));
|
|
1108
|
+
entryPtr = new IntPtr(entryPtr.ToInt64() + entrySize);
|
|
1109
|
+
|
|
1110
|
+
if (entry.UniqueProcessId.ToInt64() == pid) {
|
|
1111
|
+
IntPtr dupHandle;
|
|
1112
|
+
if (DuplicateHandle(hProcess, entry.HandleValue, new IntPtr(-1), out dupHandle, 0, false, 2)) {
|
|
1113
|
+
StringBuilder sb = new StringBuilder(1024);
|
|
1114
|
+
uint len = GetFinalPathNameByHandle(dupHandle, sb, (uint)sb.Capacity, 0);
|
|
1115
|
+
if (len > 0) {
|
|
1116
|
+
string filePath = sb.ToString();
|
|
1117
|
+
if (filePath.StartsWith(@"\\\\?\\")) filePath = filePath.Substring(4);
|
|
1118
|
+
if (string.Equals(filePath, normSource, StringComparison.OrdinalIgnoreCase)) {
|
|
1119
|
+
try {
|
|
1120
|
+
using (FileStream fsIn = new FileStream(new SafeFileHandle(dupHandle, false), FileAccess.Read))
|
|
1121
|
+
using (FileStream fsOut = new FileStream(destPath, FileMode.Create, FileAccess.Write)) {
|
|
1122
|
+
fsIn.Position = 0;
|
|
1123
|
+
fsIn.CopyTo(fsOut);
|
|
1124
|
+
}
|
|
1125
|
+
success = true;
|
|
1126
|
+
CloseHandle(dupHandle);
|
|
1127
|
+
break;
|
|
1128
|
+
} catch {}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
CloseHandle(dupHandle);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
} finally {
|
|
1136
|
+
CloseHandle(hProcess);
|
|
1137
|
+
}
|
|
1138
|
+
if (success) break;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
Marshal.FreeHGlobal(infoPtr);
|
|
1142
|
+
return success;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
"@;
|
|
1146
|
+
Add-Type -TypeDefinition $code;
|
|
1147
|
+
$res = [LockedFileCopier]::CopyLockedFile($src, $dst);
|
|
1148
|
+
if (-not $res) { exit 1 }
|
|
1149
|
+
`;
|
|
1150
|
+
execFileSync4(
|
|
1151
|
+
"powershell.exe",
|
|
1152
|
+
["-NoProfile", "-NonInteractive", "-Command", psScript],
|
|
1153
|
+
{
|
|
1154
|
+
input: JSON.stringify({ src, dst }),
|
|
1155
|
+
encoding: "utf-8",
|
|
1156
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1157
|
+
windowsHide: true,
|
|
1158
|
+
timeout: 3e4
|
|
1159
|
+
}
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
function copySingleFileWithFallback(src, dst) {
|
|
1163
|
+
try {
|
|
1164
|
+
copyFileSync2(src, dst);
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
if (process.platform === "win32" && (err.code === "EBUSY" || err.code === "EPERM")) {
|
|
1167
|
+
copyLockedFileWindows(src, dst);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
throw err;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function copyLockedDatabase(dbFilePath, tmpDb) {
|
|
1174
|
+
copySingleFileWithFallback(dbFilePath, tmpDb);
|
|
1175
|
+
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
1176
|
+
const sidecar = `${dbFilePath}${suffix}`;
|
|
1177
|
+
if (existsSync3(sidecar)) {
|
|
1178
|
+
try {
|
|
1179
|
+
copySingleFileWithFallback(sidecar, `${tmpDb}${suffix}`);
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function queryDatabase(dbFilePath, sql, params = []) {
|
|
1186
|
+
if (!existsSync3(dbFilePath)) {
|
|
1187
|
+
throw new Error(`Database file not found: ${dbFilePath}`);
|
|
1188
|
+
}
|
|
1189
|
+
const tmpDir = mkdtempSync(join5(tmpdir(), "chrome-tools-"));
|
|
1190
|
+
const tmpDb = join5(tmpDir, basename2(dbFilePath));
|
|
1191
|
+
let sourceDb;
|
|
1192
|
+
try {
|
|
1193
|
+
sourceDb = new DatabaseSync(dbFilePath, {
|
|
1194
|
+
// `readOnly`, not `readonly`. An unrecognized option is *ignored* rather
|
|
1195
|
+
// than rejected, so the lowercase spelling — which is what better-sqlite3
|
|
1196
|
+
// called it, and what this line used to say — silently hands back a
|
|
1197
|
+
// writable handle onto the user's live browser profile.
|
|
1198
|
+
readOnly: true,
|
|
1199
|
+
// No `fileMustExist` equivalent, and none needed: a read-only open will
|
|
1200
|
+
// not create the file, and the `existsSync` guard above is the explicit
|
|
1201
|
+
// check that turns a missing store into a message worth reading.
|
|
1202
|
+
//
|
|
1203
|
+
// No wait. A running Chrome holds these stores in `locking_mode =
|
|
1204
|
+
// EXCLUSIVE` for as long as it is open, so a busy timeout here buys
|
|
1205
|
+
// nothing that waiting longer would win — the lock is not transient, and
|
|
1206
|
+
// the copy below is the answer to it either way. Five seconds *per store*
|
|
1207
|
+
// was the whole cost of reading a saved login while the browser was up:
|
|
1208
|
+
// two stores, ten seconds, spent to arrive at the fallback that then
|
|
1209
|
+
// succeeded instantly.
|
|
1210
|
+
timeout: 0
|
|
1211
|
+
});
|
|
1212
|
+
sourceDb.prepare("VACUUM INTO ?").run(tmpDb);
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
if (!shouldCopyDatabase(err)) {
|
|
1215
|
+
cleanupTmp(tmpDir);
|
|
1216
|
+
throw new Error(`Failed to snapshot database file '${basename2(dbFilePath)}': ${err.message}`);
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
copyLockedDatabase(dbFilePath, tmpDb);
|
|
1220
|
+
} catch (copyErr) {
|
|
1221
|
+
cleanupTmp(tmpDir);
|
|
1222
|
+
throw new Error(
|
|
1223
|
+
`Failed to snapshot database file '${basename2(dbFilePath)}': ${err.message} (copying it instead also failed: ${copyErr.message})`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
} finally {
|
|
1227
|
+
if (sourceDb) {
|
|
1228
|
+
try {
|
|
1229
|
+
sourceDb.close();
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
let db;
|
|
1235
|
+
try {
|
|
1236
|
+
db = new DatabaseSync(tmpDb, { readOnly: true, timeout: 5e3 });
|
|
1237
|
+
const stmt = db.prepare(sql);
|
|
1238
|
+
stmt.setReadBigInts(true);
|
|
1239
|
+
return toCallerTypes(
|
|
1240
|
+
params.length > 0 ? stmt.all(...params) : stmt.all()
|
|
1241
|
+
);
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
throw new Error(`Database query failed on '${basename2(dbFilePath)}': ${err.message}`);
|
|
1244
|
+
} finally {
|
|
1245
|
+
if (db) {
|
|
1246
|
+
try {
|
|
1247
|
+
db.close();
|
|
1248
|
+
} catch {
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
cleanupTmp(tmpDir);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function tableExists(dbFilePath, tableName) {
|
|
1255
|
+
try {
|
|
1256
|
+
const rows = queryDatabase(
|
|
1257
|
+
dbFilePath,
|
|
1258
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
|
1259
|
+
[tableName]
|
|
1260
|
+
);
|
|
1261
|
+
return rows.length > 0;
|
|
1262
|
+
} catch {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function cleanupTmp(tmpDir) {
|
|
1267
|
+
try {
|
|
1268
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/modules/passwords.ts
|
|
1274
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
1275
|
+
import { homedir as homedir3 } from "node:os";
|
|
1276
|
+
import { join as join6 } from "node:path";
|
|
1277
|
+
var PASSWORD_STORES = [
|
|
1278
|
+
{ file: "Login Data", source: "device" },
|
|
1279
|
+
{ file: "Login Data For Account", source: "account" }
|
|
1280
|
+
];
|
|
1281
|
+
var AUTOMATION_DIR = join6(homedir3(), ".chrome_cdp_profile");
|
|
1282
|
+
var AUTOMATION_CONFIG = join6(homedir3(), ".config", "chrome-cdp-manager", "config.json");
|
|
1283
|
+
var AUTOMATION_OFF = ["0", "off", "none", "false"];
|
|
1284
|
+
function automationChromeDir() {
|
|
1285
|
+
const override = process.env.CHROME_AUTOMATION_DIR?.trim();
|
|
1286
|
+
if (override && AUTOMATION_OFF.includes(override.toLowerCase())) return void 0;
|
|
1287
|
+
const dir = override || configuredAutomationDir() || AUTOMATION_DIR;
|
|
1288
|
+
return discoverProfiles(dir).length > 0 ? dir : void 0;
|
|
1289
|
+
}
|
|
1290
|
+
function configuredAutomationDir() {
|
|
1291
|
+
try {
|
|
1292
|
+
const stored = JSON.parse(readFileSync3(AUTOMATION_CONFIG, "utf-8"));
|
|
1293
|
+
return typeof stored.profileDir === "string" && stored.profileDir ? stored.profileDir : void 0;
|
|
1294
|
+
} catch {
|
|
1295
|
+
return void 0;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function passwordRoots(userDataDir, browser) {
|
|
1299
|
+
const roots = [];
|
|
1300
|
+
const automation = userDataDir || browser ? void 0 : automationChromeDir();
|
|
1301
|
+
if (automation) roots.push(automation);
|
|
1302
|
+
roots.push(userDataDir);
|
|
1303
|
+
return roots;
|
|
1304
|
+
}
|
|
1305
|
+
function describePasswordStores({
|
|
1306
|
+
chromeDir,
|
|
1307
|
+
profilePath,
|
|
1308
|
+
profile,
|
|
1309
|
+
browser
|
|
1310
|
+
} = {}) {
|
|
1311
|
+
const userDataDir = profilePath || chromeDir;
|
|
1312
|
+
const reports = [];
|
|
1313
|
+
for (const root of passwordRoots(userDataDir, browser)) {
|
|
1314
|
+
let profiles;
|
|
1315
|
+
try {
|
|
1316
|
+
profiles = resolveTargetProfiles({ chromeDir: root, profile, browser });
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
reports.push({
|
|
1319
|
+
root: root ?? "default",
|
|
1320
|
+
profile: profile ?? "*",
|
|
1321
|
+
file: "*",
|
|
1322
|
+
source: "device",
|
|
1323
|
+
exists: false,
|
|
1324
|
+
rows: null,
|
|
1325
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1326
|
+
});
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
for (const p of profiles) {
|
|
1330
|
+
for (const store of PASSWORD_STORES) {
|
|
1331
|
+
const dbPath = join6(p.path, store.file);
|
|
1332
|
+
const report = {
|
|
1333
|
+
root: root ?? "default",
|
|
1334
|
+
profile: p.name,
|
|
1335
|
+
file: store.file,
|
|
1336
|
+
source: store.source,
|
|
1337
|
+
exists: existsSync4(dbPath),
|
|
1338
|
+
rows: null
|
|
1339
|
+
};
|
|
1340
|
+
try {
|
|
1341
|
+
report.rows = queryDatabase(dbPath, "SELECT COUNT(*) AS n FROM logins")[0]?.n ?? 0;
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
report.error = error instanceof Error ? error.message : String(error);
|
|
1344
|
+
}
|
|
1345
|
+
reports.push(report);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return reports;
|
|
1350
|
+
}
|
|
1351
|
+
function getPasswords({
|
|
1352
|
+
chromeDir,
|
|
1353
|
+
profilePath,
|
|
1354
|
+
profile,
|
|
1355
|
+
search,
|
|
1356
|
+
browser,
|
|
1357
|
+
decryptionKey
|
|
1358
|
+
} = {}) {
|
|
1359
|
+
const userDataDir = profilePath || chromeDir;
|
|
1360
|
+
const roots = passwordRoots(userDataDir, browser);
|
|
1361
|
+
const results = [];
|
|
1362
|
+
const term = search?.toLowerCase();
|
|
1363
|
+
let refusal;
|
|
1364
|
+
let resolved = false;
|
|
1365
|
+
for (const root of roots) {
|
|
1366
|
+
let profiles;
|
|
1367
|
+
try {
|
|
1368
|
+
profiles = resolveTargetProfiles({ chromeDir: root, profile, browser });
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
refusal ??= error;
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
resolved = true;
|
|
1374
|
+
let derivedKey = decryptionKey;
|
|
1375
|
+
for (const p of profiles) {
|
|
1376
|
+
for (const store of PASSWORD_STORES) {
|
|
1377
|
+
const dbPath = join6(p.path, store.file);
|
|
1378
|
+
try {
|
|
1379
|
+
const rows = queryDatabase(
|
|
1380
|
+
dbPath,
|
|
1381
|
+
`SELECT origin_url, username_value, password_value, date_last_used
|
|
1382
|
+
FROM logins ORDER BY origin_url`
|
|
1383
|
+
);
|
|
1384
|
+
for (const row of rows) {
|
|
1385
|
+
if (term && !(row.origin_url && row.origin_url.toLowerCase().includes(term)) && !(row.username_value && row.username_value.toLowerCase().includes(term))) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
results.push({
|
|
1389
|
+
profile: p.name,
|
|
1390
|
+
source: store.source,
|
|
1391
|
+
url: row.origin_url,
|
|
1392
|
+
username: row.username_value,
|
|
1393
|
+
password: row.password_value?.length > 0 ? decryptValue(
|
|
1394
|
+
row.password_value,
|
|
1395
|
+
derivedKey ??= deriveEncryptionKey2(root, browser)
|
|
1396
|
+
) : "",
|
|
1397
|
+
lastUsed: chromeTimeToDate(row.date_last_used || 0)
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
} catch {
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (!resolved && refusal) throw refusal;
|
|
1406
|
+
return results;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/modules/bookmarks.ts
|
|
1410
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
1411
|
+
import { join as join7 } from "node:path";
|
|
1412
|
+
function flattenBookmarks(node, folderPath = "") {
|
|
1413
|
+
const results = [];
|
|
1414
|
+
if (!node) return results;
|
|
1415
|
+
const currentPath = folderPath ? `${folderPath}/${node.name || ""}` : node.name || "";
|
|
1416
|
+
if (node.type === "url") {
|
|
1417
|
+
results.push({
|
|
1418
|
+
title: node.name || "",
|
|
1419
|
+
url: node.url || "",
|
|
1420
|
+
folder: folderPath,
|
|
1421
|
+
dateAdded: chromeTimeToDate(node.date_added),
|
|
1422
|
+
dateLastUsed: chromeTimeToDate(node.date_last_used),
|
|
1423
|
+
id: node.id,
|
|
1424
|
+
guid: node.guid || ""
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
if (node.children && Array.isArray(node.children)) {
|
|
1428
|
+
for (const child of node.children) {
|
|
1429
|
+
results.push(...flattenBookmarks(child, currentPath));
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return results;
|
|
1433
|
+
}
|
|
1434
|
+
function getBookmarks({
|
|
1435
|
+
chromeDir,
|
|
1436
|
+
profilePath,
|
|
1437
|
+
profile,
|
|
1438
|
+
folder,
|
|
1439
|
+
search,
|
|
1440
|
+
browser
|
|
1441
|
+
} = {}) {
|
|
1442
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
1443
|
+
const results = [];
|
|
1444
|
+
for (const p of profiles) {
|
|
1445
|
+
const bookmarksPath = join7(p.path, "Bookmarks");
|
|
1446
|
+
if (!existsSync5(bookmarksPath)) continue;
|
|
1447
|
+
try {
|
|
1448
|
+
const raw = readFileSync4(bookmarksPath, "utf-8");
|
|
1449
|
+
const data = JSON.parse(raw);
|
|
1450
|
+
const roots = data.roots;
|
|
1451
|
+
if (!roots) continue;
|
|
1452
|
+
const rootNames = ["bookmark_bar", "other", "synced"];
|
|
1453
|
+
for (const rootName of rootNames) {
|
|
1454
|
+
if (roots[rootName]) {
|
|
1455
|
+
const bookmarks = flattenBookmarks(roots[rootName], "");
|
|
1456
|
+
for (const bm of bookmarks) {
|
|
1457
|
+
bm.profile = p.name;
|
|
1458
|
+
results.push(bm);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
let filtered = results;
|
|
1466
|
+
if (folder) {
|
|
1467
|
+
const term = folder.toLowerCase();
|
|
1468
|
+
filtered = filtered.filter((b) => b.folder.toLowerCase().includes(term));
|
|
1469
|
+
}
|
|
1470
|
+
if (search) {
|
|
1471
|
+
const term = search.toLowerCase();
|
|
1472
|
+
filtered = filtered.filter(
|
|
1473
|
+
(b) => b.title.toLowerCase().includes(term) || b.url.toLowerCase().includes(term)
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
return filtered;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/modules/history.ts
|
|
1480
|
+
import { join as join8 } from "node:path";
|
|
1481
|
+
|
|
1482
|
+
// src/core/limit.ts
|
|
1483
|
+
function normalizeLimit(limit) {
|
|
1484
|
+
if (limit === void 0) return void 0;
|
|
1485
|
+
const parsed = Math.trunc(Number(limit));
|
|
1486
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1487
|
+
}
|
|
1488
|
+
function limitResults(results, limit) {
|
|
1489
|
+
const normalized = normalizeLimit(limit);
|
|
1490
|
+
return normalized === void 0 ? results : results.slice(0, normalized);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/core/sql.ts
|
|
1494
|
+
function sqlLikePattern(term) {
|
|
1495
|
+
return `%${term.replace(/([%_\\])/g, "\\$1")}%`;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// src/modules/history.ts
|
|
1499
|
+
function getHistory({
|
|
1500
|
+
chromeDir,
|
|
1501
|
+
profilePath,
|
|
1502
|
+
profile,
|
|
1503
|
+
search,
|
|
1504
|
+
limit = 100,
|
|
1505
|
+
browser
|
|
1506
|
+
} = {}) {
|
|
1507
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
1508
|
+
const results = [];
|
|
1509
|
+
const rowLimit = normalizeLimit(limit);
|
|
1510
|
+
for (const p of profiles) {
|
|
1511
|
+
const dbPath = join8(p.path, "History");
|
|
1512
|
+
try {
|
|
1513
|
+
let sql = `SELECT url, title, visit_count, last_visit_time FROM urls`;
|
|
1514
|
+
const params = [];
|
|
1515
|
+
if (search) {
|
|
1516
|
+
sql += ` WHERE url LIKE ? ESCAPE '\\' OR IFNULL(title, '') LIKE ? ESCAPE '\\'`;
|
|
1517
|
+
const like = sqlLikePattern(search);
|
|
1518
|
+
params.push(like, like);
|
|
1519
|
+
}
|
|
1520
|
+
sql += ` ORDER BY last_visit_time DESC`;
|
|
1521
|
+
if (rowLimit) {
|
|
1522
|
+
sql += ` LIMIT ${rowLimit}`;
|
|
1523
|
+
}
|
|
1524
|
+
const rows = queryDatabase(dbPath, sql, params);
|
|
1525
|
+
for (const row of rows) {
|
|
1526
|
+
results.push({
|
|
1527
|
+
profile: p.name,
|
|
1528
|
+
url: row.url,
|
|
1529
|
+
title: row.title || "",
|
|
1530
|
+
visitCount: row.visit_count || 0,
|
|
1531
|
+
lastVisit: chromeTimeToDate(row.last_visit_time || 0)
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
} catch {
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return limitResults(results, limit);
|
|
1538
|
+
}
|
|
1539
|
+
function getSearchTerms({
|
|
1540
|
+
chromeDir,
|
|
1541
|
+
profilePath,
|
|
1542
|
+
profile,
|
|
1543
|
+
limit = 100,
|
|
1544
|
+
browser
|
|
1545
|
+
} = {}) {
|
|
1546
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
1547
|
+
const results = [];
|
|
1548
|
+
const rowLimit = normalizeLimit(limit);
|
|
1549
|
+
for (const p of profiles) {
|
|
1550
|
+
const dbPath = join8(p.path, "History");
|
|
1551
|
+
try {
|
|
1552
|
+
let sql = `SELECT kst.term, u.url
|
|
1553
|
+
FROM keyword_search_terms kst
|
|
1554
|
+
JOIN urls u ON kst.url_id = u.id
|
|
1555
|
+
ORDER BY u.last_visit_time DESC`;
|
|
1556
|
+
if (rowLimit) {
|
|
1557
|
+
sql += ` LIMIT ${rowLimit}`;
|
|
1558
|
+
}
|
|
1559
|
+
const rows = queryDatabase(dbPath, sql);
|
|
1560
|
+
for (const row of rows) {
|
|
1561
|
+
results.push({
|
|
1562
|
+
profile: p.name,
|
|
1563
|
+
term: row.term || "",
|
|
1564
|
+
url: row.url || ""
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
} catch {
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return limitResults(results, limit);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/modules/cookies.ts
|
|
1574
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1575
|
+
import { createHash } from "node:crypto";
|
|
1576
|
+
function decodeCookieValue(encryptedValue, derivedKey, hostKey) {
|
|
1577
|
+
const decrypted = decryptValueBuffer2(encryptedValue, derivedKey);
|
|
1578
|
+
if (!Buffer.isBuffer(decrypted)) {
|
|
1579
|
+
return decrypted;
|
|
1580
|
+
}
|
|
1581
|
+
const hostHash = createHash("sha256").update(hostKey).digest();
|
|
1582
|
+
const valueBytes = decrypted.length >= hostHash.length && decrypted.subarray(0, hostHash.length).equals(hostHash) ? decrypted.subarray(hostHash.length) : decrypted;
|
|
1583
|
+
return valueBytes.toString("utf-8");
|
|
1584
|
+
}
|
|
1585
|
+
function parseURL(urlStr) {
|
|
1586
|
+
if (!urlStr) return null;
|
|
1587
|
+
try {
|
|
1588
|
+
return new URL(urlStr);
|
|
1589
|
+
} catch {
|
|
1590
|
+
try {
|
|
1591
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(urlStr)) {
|
|
1592
|
+
return new URL("https://" + urlStr);
|
|
1593
|
+
}
|
|
1594
|
+
} catch {
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
function getPossibleHostKeys(hostname) {
|
|
1600
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1601
|
+
keys.add(hostname);
|
|
1602
|
+
keys.add("." + hostname);
|
|
1603
|
+
const parts = hostname.split(".");
|
|
1604
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1605
|
+
const suffix = parts.slice(i).join(".");
|
|
1606
|
+
keys.add(suffix);
|
|
1607
|
+
keys.add("." + suffix);
|
|
1608
|
+
}
|
|
1609
|
+
return Array.from(keys);
|
|
1610
|
+
}
|
|
1611
|
+
function domainMatch(hostname, hostKey) {
|
|
1612
|
+
if (hostname === hostKey) {
|
|
1613
|
+
return true;
|
|
1614
|
+
}
|
|
1615
|
+
if (hostKey.startsWith(".")) {
|
|
1616
|
+
if (hostname === hostKey.slice(1) || hostname.endsWith(hostKey)) {
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
function pathMatch(urlPath, cookiePath) {
|
|
1623
|
+
const uPath = urlPath || "/";
|
|
1624
|
+
const cPath = cookiePath || "/";
|
|
1625
|
+
if (cPath === "/") {
|
|
1626
|
+
return true;
|
|
1627
|
+
}
|
|
1628
|
+
if (uPath === cPath) {
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
if (uPath.startsWith(cPath)) {
|
|
1632
|
+
if (cPath.endsWith("/") || uPath[cPath.length] === "/") {
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
function getCookies({
|
|
1639
|
+
chromeDir,
|
|
1640
|
+
profilePath,
|
|
1641
|
+
profile,
|
|
1642
|
+
browser,
|
|
1643
|
+
domain,
|
|
1644
|
+
url,
|
|
1645
|
+
decrypt = true,
|
|
1646
|
+
limit = 100,
|
|
1647
|
+
decryptionKey
|
|
1648
|
+
} = {}) {
|
|
1649
|
+
const userDataDir = profilePath || chromeDir;
|
|
1650
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
1651
|
+
let derivedKey = decryptionKey;
|
|
1652
|
+
const results = [];
|
|
1653
|
+
const parsedUrl = parseURL(url);
|
|
1654
|
+
const rowLimit = normalizeLimit(limit);
|
|
1655
|
+
let firstReadError;
|
|
1656
|
+
for (const p of profiles) {
|
|
1657
|
+
const dbPath = resolveCookiesDatabasePath(p.path);
|
|
1658
|
+
if (!existsSync6(dbPath)) continue;
|
|
1659
|
+
try {
|
|
1660
|
+
let sql = `SELECT host_key, name, value, encrypted_value, path,
|
|
1661
|
+
expires_utc, is_secure, is_httponly, samesite,
|
|
1662
|
+
creation_utc, last_access_utc
|
|
1663
|
+
FROM cookies`;
|
|
1664
|
+
const conditions = [];
|
|
1665
|
+
const params = [];
|
|
1666
|
+
if (domain) {
|
|
1667
|
+
conditions.push(`host_key LIKE ?`);
|
|
1668
|
+
params.push(`%${domain}%`);
|
|
1669
|
+
} else if (parsedUrl) {
|
|
1670
|
+
const hostKeys = getPossibleHostKeys(parsedUrl.hostname);
|
|
1671
|
+
const placeholders = hostKeys.map(() => "?").join(", ");
|
|
1672
|
+
conditions.push(`host_key IN (${placeholders})`);
|
|
1673
|
+
params.push(...hostKeys);
|
|
1674
|
+
}
|
|
1675
|
+
if (conditions.length > 0) {
|
|
1676
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
1677
|
+
}
|
|
1678
|
+
sql += ` ORDER BY host_key, name`;
|
|
1679
|
+
if (rowLimit && !url) {
|
|
1680
|
+
sql += ` LIMIT ${rowLimit}`;
|
|
1681
|
+
}
|
|
1682
|
+
const rows = queryDatabase(dbPath, sql, params);
|
|
1683
|
+
for (const row of rows) {
|
|
1684
|
+
if (parsedUrl) {
|
|
1685
|
+
if (!domainMatch(parsedUrl.hostname, row.host_key || "")) continue;
|
|
1686
|
+
if (!pathMatch(parsedUrl.pathname || "/", row.path || "/")) continue;
|
|
1687
|
+
if (row.is_secure && parsedUrl.protocol !== "https:") continue;
|
|
1688
|
+
}
|
|
1689
|
+
let cookieValue = row.value || "";
|
|
1690
|
+
if (!cookieValue && row.encrypted_value?.length > 0 && decrypt) {
|
|
1691
|
+
cookieValue = decodeCookieValue(
|
|
1692
|
+
row.encrypted_value,
|
|
1693
|
+
derivedKey ??= deriveEncryptionKey2(userDataDir, browser),
|
|
1694
|
+
row.host_key || ""
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
results.push({
|
|
1698
|
+
profile: p.name,
|
|
1699
|
+
domain: row.host_key || "",
|
|
1700
|
+
name: row.name || "",
|
|
1701
|
+
value: cookieValue,
|
|
1702
|
+
path: row.path || "/",
|
|
1703
|
+
expires: chromeTimeToDate(row.expires_utc || 0),
|
|
1704
|
+
secure: Boolean(row.is_secure),
|
|
1705
|
+
httpOnly: Boolean(row.is_httponly),
|
|
1706
|
+
sameSite: ["unspecified", "lax", "strict", "none"][row.samesite] || "unspecified",
|
|
1707
|
+
created: chromeTimeToDate(row.creation_utc || 0),
|
|
1708
|
+
lastAccessed: chromeTimeToDate(row.last_access_utc || 0)
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
firstReadError ??= new Error(
|
|
1713
|
+
`Could not read cookies for profile "${p.name}" at ${dbPath}: ` + (error instanceof Error ? error.message : String(error))
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (results.length === 0 && firstReadError) throw firstReadError;
|
|
1718
|
+
return limitResults(results, limit);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// src/modules/local-storage.ts
|
|
1722
|
+
import { cpSync, existsSync as existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync2 } from "node:fs";
|
|
1723
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1724
|
+
import { basename as basename3, join as join9 } from "node:path";
|
|
1725
|
+
var TABLE_MAGIC = 0xdb4775248b80fb57n;
|
|
1726
|
+
var LOG_BLOCK_SIZE = 32768;
|
|
1727
|
+
var VALUE_TYPE_DELETION = 0;
|
|
1728
|
+
var VALUE_TYPE_VALUE = 1;
|
|
1729
|
+
function getLocalStorage({
|
|
1730
|
+
chromeDir,
|
|
1731
|
+
profilePath,
|
|
1732
|
+
profile,
|
|
1733
|
+
browser,
|
|
1734
|
+
origin,
|
|
1735
|
+
url,
|
|
1736
|
+
key,
|
|
1737
|
+
search,
|
|
1738
|
+
limit = 100
|
|
1739
|
+
} = {}) {
|
|
1740
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
1741
|
+
const results = [];
|
|
1742
|
+
const originFilter = normalizeOriginFilter(url || origin);
|
|
1743
|
+
const keyFilter = key?.toLowerCase();
|
|
1744
|
+
const searchFilter = search?.toLowerCase();
|
|
1745
|
+
for (const p of profiles) {
|
|
1746
|
+
const dbPath = join9(p.path, "Local Storage", "leveldb");
|
|
1747
|
+
if (!existsSync7(dbPath)) continue;
|
|
1748
|
+
let tmpPath = null;
|
|
1749
|
+
try {
|
|
1750
|
+
tmpPath = copyLevelDBToTemp(dbPath);
|
|
1751
|
+
const rawRecords = readLevelDBRecords(tmpPath);
|
|
1752
|
+
for (const record of rawRecords.values()) {
|
|
1753
|
+
if (record.deleted || !record.value) continue;
|
|
1754
|
+
const parsed = parseLocalStorageRecord(record.key, record.value);
|
|
1755
|
+
if (!parsed) continue;
|
|
1756
|
+
const entry = {
|
|
1757
|
+
profile: p.name,
|
|
1758
|
+
origin: parsed.origin,
|
|
1759
|
+
key: parsed.key,
|
|
1760
|
+
value: parsed.value
|
|
1761
|
+
};
|
|
1762
|
+
if (originFilter && !entry.origin.toLowerCase().includes(originFilter)) {
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
if (keyFilter && !entry.key.toLowerCase().includes(keyFilter)) {
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if (searchFilter) {
|
|
1769
|
+
const haystack = `${entry.origin}
|
|
1770
|
+
${entry.key}
|
|
1771
|
+
${entry.value}`.toLowerCase();
|
|
1772
|
+
if (!haystack.includes(searchFilter)) continue;
|
|
1773
|
+
}
|
|
1774
|
+
results.push(entry);
|
|
1775
|
+
}
|
|
1776
|
+
} catch {
|
|
1777
|
+
} finally {
|
|
1778
|
+
if (tmpPath) {
|
|
1779
|
+
rmSync2(tmpPath, { recursive: true, force: true });
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
results.sort(
|
|
1784
|
+
(a, b) => a.profile.localeCompare(b.profile) || a.origin.localeCompare(b.origin) || a.key.localeCompare(b.key)
|
|
1785
|
+
);
|
|
1786
|
+
return limitResults(results, limit);
|
|
1787
|
+
}
|
|
1788
|
+
function copyLevelDBToTemp(dbPath) {
|
|
1789
|
+
const tmpPath = mkdtempSync2(join9(tmpdir2(), "chrome-local-storage-"));
|
|
1790
|
+
cpSync(dbPath, tmpPath, {
|
|
1791
|
+
recursive: true,
|
|
1792
|
+
force: true,
|
|
1793
|
+
filter: (source) => basename3(source) !== "LOCK"
|
|
1794
|
+
});
|
|
1795
|
+
return tmpPath;
|
|
1796
|
+
}
|
|
1797
|
+
function readLevelDBRecords(dbPath) {
|
|
1798
|
+
const records = /* @__PURE__ */ new Map();
|
|
1799
|
+
const files = readdirSync2(dbPath).filter((file) => /\.(log|ldb|sst)$/i.test(file)).sort();
|
|
1800
|
+
for (const file of files) {
|
|
1801
|
+
const filePath = join9(dbPath, file);
|
|
1802
|
+
const data = readFileSync5(filePath);
|
|
1803
|
+
if (/\.(ldb|sst)$/i.test(file)) {
|
|
1804
|
+
for (const record of parseTableRecords(data)) {
|
|
1805
|
+
applyLevelDBRecord(records, record);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
for (const file of files) {
|
|
1810
|
+
if (!/\.log$/i.test(file)) continue;
|
|
1811
|
+
const filePath = join9(dbPath, file);
|
|
1812
|
+
const data = readFileSync5(filePath);
|
|
1813
|
+
for (const record of parseLogRecords(data)) {
|
|
1814
|
+
applyLevelDBRecord(records, record);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return records;
|
|
1818
|
+
}
|
|
1819
|
+
function applyLevelDBRecord(records, record) {
|
|
1820
|
+
const id = record.key.toString("hex");
|
|
1821
|
+
const existing = records.get(id);
|
|
1822
|
+
if (existing && existing.sequence > record.sequence) return;
|
|
1823
|
+
records.set(id, record);
|
|
1824
|
+
}
|
|
1825
|
+
function parseLocalStorageRecord(rawKey, rawValue) {
|
|
1826
|
+
if (rawKey.length < 3 || rawKey[0] !== 95) return null;
|
|
1827
|
+
const separator = rawKey.indexOf(0, 1);
|
|
1828
|
+
if (separator === -1) return null;
|
|
1829
|
+
const origin = rawKey.subarray(1, separator).toString("utf8");
|
|
1830
|
+
const key = decodeLocalStorageString(rawKey.subarray(separator + 1));
|
|
1831
|
+
const value = decodeLocalStorageString(rawValue);
|
|
1832
|
+
if (!origin || !key) return null;
|
|
1833
|
+
return { origin, key, value };
|
|
1834
|
+
}
|
|
1835
|
+
function decodeLocalStorageString(data) {
|
|
1836
|
+
if (data.length === 0) return "";
|
|
1837
|
+
const format = data[0];
|
|
1838
|
+
const body = data.subarray(1);
|
|
1839
|
+
if (format === 0 && body.length % 2 === 0) {
|
|
1840
|
+
return body.toString("utf16le");
|
|
1841
|
+
}
|
|
1842
|
+
if (format === 1) {
|
|
1843
|
+
return body.toString("latin1");
|
|
1844
|
+
}
|
|
1845
|
+
return data.toString("utf8");
|
|
1846
|
+
}
|
|
1847
|
+
function normalizeOriginFilter(input) {
|
|
1848
|
+
if (!input) return void 0;
|
|
1849
|
+
try {
|
|
1850
|
+
return new URL(input).origin.toLowerCase();
|
|
1851
|
+
} catch {
|
|
1852
|
+
return input.toLowerCase();
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
function parseLogRecords(data) {
|
|
1856
|
+
const records = [];
|
|
1857
|
+
let offset = 0;
|
|
1858
|
+
let fragments = [];
|
|
1859
|
+
while (offset + 7 <= data.length) {
|
|
1860
|
+
const blockRemaining = LOG_BLOCK_SIZE - offset % LOG_BLOCK_SIZE;
|
|
1861
|
+
if (blockRemaining < 7) {
|
|
1862
|
+
offset += blockRemaining;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
const length = data.readUInt16LE(offset + 4);
|
|
1866
|
+
const type = data[offset + 6];
|
|
1867
|
+
offset += 7;
|
|
1868
|
+
if (type === 0 && length === 0) {
|
|
1869
|
+
offset += length;
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (length > data.length - offset || length > blockRemaining - 7) {
|
|
1873
|
+
break;
|
|
1874
|
+
}
|
|
1875
|
+
const payload = data.subarray(offset, offset + length);
|
|
1876
|
+
offset += length;
|
|
1877
|
+
if (type === 1) {
|
|
1878
|
+
records.push(...parseWriteBatch(payload));
|
|
1879
|
+
fragments = [];
|
|
1880
|
+
} else if (type === 2) {
|
|
1881
|
+
fragments = [Buffer.from(payload)];
|
|
1882
|
+
} else if (type === 3 && fragments.length > 0) {
|
|
1883
|
+
fragments.push(Buffer.from(payload));
|
|
1884
|
+
} else if (type === 4 && fragments.length > 0) {
|
|
1885
|
+
fragments.push(Buffer.from(payload));
|
|
1886
|
+
records.push(...parseWriteBatch(Buffer.concat(fragments)));
|
|
1887
|
+
fragments = [];
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return records;
|
|
1891
|
+
}
|
|
1892
|
+
function parseWriteBatch(batch) {
|
|
1893
|
+
if (batch.length < 12) return [];
|
|
1894
|
+
const records = [];
|
|
1895
|
+
const startSequence = readUInt64LE(batch, 0);
|
|
1896
|
+
const count = batch.readUInt32LE(8);
|
|
1897
|
+
let offset = 12;
|
|
1898
|
+
let seen = 0;
|
|
1899
|
+
while (offset < batch.length && seen < count) {
|
|
1900
|
+
const tag = batch[offset++];
|
|
1901
|
+
const keyPart = readLengthPrefixedSlice(batch, offset);
|
|
1902
|
+
if (!keyPart) break;
|
|
1903
|
+
offset = keyPart.nextOffset;
|
|
1904
|
+
const sequence = startSequence + BigInt(seen);
|
|
1905
|
+
if (tag === VALUE_TYPE_VALUE) {
|
|
1906
|
+
const valuePart = readLengthPrefixedSlice(batch, offset);
|
|
1907
|
+
if (!valuePart) break;
|
|
1908
|
+
offset = valuePart.nextOffset;
|
|
1909
|
+
records.push({ key: keyPart.value, value: valuePart.value, sequence, deleted: false });
|
|
1910
|
+
} else if (tag === VALUE_TYPE_DELETION) {
|
|
1911
|
+
records.push({ key: keyPart.value, sequence, deleted: true });
|
|
1912
|
+
} else {
|
|
1913
|
+
break;
|
|
1914
|
+
}
|
|
1915
|
+
seen++;
|
|
1916
|
+
}
|
|
1917
|
+
return records;
|
|
1918
|
+
}
|
|
1919
|
+
function parseTableRecords(data) {
|
|
1920
|
+
const records = [];
|
|
1921
|
+
if (data.length < 48) return records;
|
|
1922
|
+
const magic = data.readBigUInt64LE(data.length - 8);
|
|
1923
|
+
if (magic !== TABLE_MAGIC) return records;
|
|
1924
|
+
const footer = data.subarray(data.length - 48, data.length - 8);
|
|
1925
|
+
const metaHandle = readBlockHandle(footer, 0);
|
|
1926
|
+
if (!metaHandle) return records;
|
|
1927
|
+
const indexHandle = readBlockHandle(footer, metaHandle.bytesRead);
|
|
1928
|
+
if (!indexHandle) return records;
|
|
1929
|
+
const indexBlock = readBlock(data, indexHandle);
|
|
1930
|
+
if (!indexBlock) return records;
|
|
1931
|
+
for (const indexEntry of parseBlockEntries(indexBlock)) {
|
|
1932
|
+
const dataHandle = readBlockHandle(indexEntry.value, 0);
|
|
1933
|
+
if (!dataHandle) continue;
|
|
1934
|
+
const dataBlock = readBlock(data, dataHandle);
|
|
1935
|
+
if (!dataBlock) continue;
|
|
1936
|
+
for (const entry of parseBlockEntries(dataBlock)) {
|
|
1937
|
+
if (entry.key.length < 8) continue;
|
|
1938
|
+
const internalKey = entry.key;
|
|
1939
|
+
const tagOffset = internalKey.length - 8;
|
|
1940
|
+
const type = internalKey[tagOffset];
|
|
1941
|
+
const sequence = readSequence(internalKey.subarray(tagOffset));
|
|
1942
|
+
const key = internalKey.subarray(0, tagOffset);
|
|
1943
|
+
if (type === VALUE_TYPE_VALUE) {
|
|
1944
|
+
records.push({ key, value: entry.value, sequence, deleted: false });
|
|
1945
|
+
} else if (type === VALUE_TYPE_DELETION) {
|
|
1946
|
+
records.push({ key, sequence, deleted: true });
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return records;
|
|
1951
|
+
}
|
|
1952
|
+
function readBlock(data, handle) {
|
|
1953
|
+
const trailerOffset = handle.offset + handle.size;
|
|
1954
|
+
if (handle.offset < 0 || handle.size < 0 || trailerOffset + 5 > data.length) {
|
|
1955
|
+
return null;
|
|
1956
|
+
}
|
|
1957
|
+
const block = data.subarray(handle.offset, trailerOffset);
|
|
1958
|
+
const compressionType = data[trailerOffset];
|
|
1959
|
+
if (compressionType === 0) {
|
|
1960
|
+
return block;
|
|
1961
|
+
}
|
|
1962
|
+
if (compressionType === 1) {
|
|
1963
|
+
try {
|
|
1964
|
+
return snappyUncompress(block);
|
|
1965
|
+
} catch {
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
function parseBlockEntries(block) {
|
|
1972
|
+
if (block.length < 4) return [];
|
|
1973
|
+
const restartCount = block.readUInt32LE(block.length - 4);
|
|
1974
|
+
const restartsSize = restartCount * 4 + 4;
|
|
1975
|
+
if (restartCount < 1 || restartsSize > block.length) return [];
|
|
1976
|
+
const entriesEnd = block.length - restartsSize;
|
|
1977
|
+
const entries = [];
|
|
1978
|
+
let offset = 0;
|
|
1979
|
+
let previousKey = Buffer.alloc(0);
|
|
1980
|
+
while (offset < entriesEnd) {
|
|
1981
|
+
const shared = readVarint32(block, offset);
|
|
1982
|
+
if (!shared) break;
|
|
1983
|
+
offset = shared.nextOffset;
|
|
1984
|
+
const nonShared = readVarint32(block, offset);
|
|
1985
|
+
if (!nonShared) break;
|
|
1986
|
+
offset = nonShared.nextOffset;
|
|
1987
|
+
const valueLength = readVarint32(block, offset);
|
|
1988
|
+
if (!valueLength) break;
|
|
1989
|
+
offset = valueLength.nextOffset;
|
|
1990
|
+
if (shared.value > previousKey.length || offset + nonShared.value + valueLength.value > entriesEnd) {
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
const key = Buffer.concat([
|
|
1994
|
+
previousKey.subarray(0, shared.value),
|
|
1995
|
+
block.subarray(offset, offset + nonShared.value)
|
|
1996
|
+
]);
|
|
1997
|
+
offset += nonShared.value;
|
|
1998
|
+
const value = Buffer.from(block.subarray(offset, offset + valueLength.value));
|
|
1999
|
+
offset += valueLength.value;
|
|
2000
|
+
entries.push({ key, value });
|
|
2001
|
+
previousKey = key;
|
|
2002
|
+
}
|
|
2003
|
+
return entries;
|
|
2004
|
+
}
|
|
2005
|
+
function readBlockHandle(data, offset) {
|
|
2006
|
+
const blockOffset = readVarint64(data, offset);
|
|
2007
|
+
if (!blockOffset) return null;
|
|
2008
|
+
const blockSize = readVarint64(data, blockOffset.nextOffset);
|
|
2009
|
+
if (!blockSize) return null;
|
|
2010
|
+
if (blockOffset.value > BigInt(Number.MAX_SAFE_INTEGER) || blockSize.value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
return {
|
|
2014
|
+
offset: Number(blockOffset.value),
|
|
2015
|
+
size: Number(blockSize.value),
|
|
2016
|
+
bytesRead: blockSize.nextOffset - offset
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function readLengthPrefixedSlice(data, offset) {
|
|
2020
|
+
const length = readVarint32(data, offset);
|
|
2021
|
+
if (!length) return null;
|
|
2022
|
+
const end = length.nextOffset + length.value;
|
|
2023
|
+
if (end > data.length) return null;
|
|
2024
|
+
return { value: Buffer.from(data.subarray(length.nextOffset, end)), nextOffset: end };
|
|
2025
|
+
}
|
|
2026
|
+
function readVarint32(data, offset) {
|
|
2027
|
+
let result = 0;
|
|
2028
|
+
let shift = 0;
|
|
2029
|
+
for (let i = offset; i < data.length && i < offset + 5; i++) {
|
|
2030
|
+
const byte = data[i];
|
|
2031
|
+
result |= (byte & 127) << shift;
|
|
2032
|
+
if ((byte & 128) === 0) {
|
|
2033
|
+
return { value: result >>> 0, nextOffset: i + 1 };
|
|
2034
|
+
}
|
|
2035
|
+
shift += 7;
|
|
2036
|
+
}
|
|
2037
|
+
return null;
|
|
2038
|
+
}
|
|
2039
|
+
function readVarint64(data, offset) {
|
|
2040
|
+
let result = 0n;
|
|
2041
|
+
let shift = 0n;
|
|
2042
|
+
for (let i = offset; i < data.length && i < offset + 10; i++) {
|
|
2043
|
+
const byte = data[i];
|
|
2044
|
+
result |= BigInt(byte & 127) << shift;
|
|
2045
|
+
if ((byte & 128) === 0) {
|
|
2046
|
+
return { value: result, nextOffset: i + 1 };
|
|
2047
|
+
}
|
|
2048
|
+
shift += 7n;
|
|
2049
|
+
}
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
function readUInt64LE(data, offset) {
|
|
2053
|
+
let value = 0n;
|
|
2054
|
+
for (let i = 0; i < 8; i++) {
|
|
2055
|
+
value |= BigInt(data[offset + i] || 0) << BigInt(i * 8);
|
|
2056
|
+
}
|
|
2057
|
+
return value;
|
|
2058
|
+
}
|
|
2059
|
+
function readSequence(tagBytes) {
|
|
2060
|
+
let sequence = 0n;
|
|
2061
|
+
for (let i = 1; i < 8; i++) {
|
|
2062
|
+
sequence |= BigInt(tagBytes[i] || 0) << BigInt((i - 1) * 8);
|
|
2063
|
+
}
|
|
2064
|
+
return sequence;
|
|
2065
|
+
}
|
|
2066
|
+
function snappyUncompress(input) {
|
|
2067
|
+
const expectedLength = readVarint32(input, 0);
|
|
2068
|
+
if (!expectedLength) throw new Error("Invalid Snappy length");
|
|
2069
|
+
const output = Buffer.alloc(expectedLength.value);
|
|
2070
|
+
let ip = expectedLength.nextOffset;
|
|
2071
|
+
let op = 0;
|
|
2072
|
+
while (ip < input.length && op < output.length) {
|
|
2073
|
+
const tag = input[ip++];
|
|
2074
|
+
const type = tag & 3;
|
|
2075
|
+
if (type === 0) {
|
|
2076
|
+
let literalLength = tag >> 2;
|
|
2077
|
+
if (literalLength < 60) {
|
|
2078
|
+
literalLength += 1;
|
|
2079
|
+
} else {
|
|
2080
|
+
const bytesToRead = literalLength - 59;
|
|
2081
|
+
if (ip + bytesToRead > input.length) throw new Error("Invalid Snappy literal");
|
|
2082
|
+
literalLength = 1;
|
|
2083
|
+
for (let i = 0; i < bytesToRead; i++) {
|
|
2084
|
+
literalLength += input[ip++] << i * 8;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (ip + literalLength > input.length || op + literalLength > output.length) {
|
|
2088
|
+
throw new Error("Invalid Snappy literal length");
|
|
2089
|
+
}
|
|
2090
|
+
input.copy(output, op, ip, ip + literalLength);
|
|
2091
|
+
ip += literalLength;
|
|
2092
|
+
op += literalLength;
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
let copyLength;
|
|
2096
|
+
let copyOffset;
|
|
2097
|
+
if (type === 1) {
|
|
2098
|
+
if (ip >= input.length) throw new Error("Invalid Snappy copy");
|
|
2099
|
+
copyLength = (tag >> 2 & 7) + 4;
|
|
2100
|
+
copyOffset = (tag & 224) << 3 | input[ip++];
|
|
2101
|
+
} else if (type === 2) {
|
|
2102
|
+
if (ip + 2 > input.length) throw new Error("Invalid Snappy copy");
|
|
2103
|
+
copyLength = (tag >> 2) + 1;
|
|
2104
|
+
copyOffset = input.readUInt16LE(ip);
|
|
2105
|
+
ip += 2;
|
|
2106
|
+
} else {
|
|
2107
|
+
if (ip + 4 > input.length) throw new Error("Invalid Snappy copy");
|
|
2108
|
+
copyLength = (tag >> 2) + 1;
|
|
2109
|
+
copyOffset = input.readUInt32LE(ip);
|
|
2110
|
+
ip += 4;
|
|
2111
|
+
}
|
|
2112
|
+
if (copyOffset <= 0 || copyOffset > op || op + copyLength > output.length) {
|
|
2113
|
+
throw new Error("Invalid Snappy offset");
|
|
2114
|
+
}
|
|
2115
|
+
for (let i = 0; i < copyLength; i++) {
|
|
2116
|
+
output[op] = output[op - copyOffset];
|
|
2117
|
+
op++;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (op !== output.length) throw new Error("Truncated Snappy stream");
|
|
2121
|
+
return output;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/modules/autofill.ts
|
|
2125
|
+
import { join as join10 } from "node:path";
|
|
2126
|
+
function getAutofillEntries({
|
|
2127
|
+
chromeDir,
|
|
2128
|
+
profilePath,
|
|
2129
|
+
profile,
|
|
2130
|
+
browser,
|
|
2131
|
+
search,
|
|
2132
|
+
limit = 100
|
|
2133
|
+
} = {}) {
|
|
2134
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
2135
|
+
const results = [];
|
|
2136
|
+
const rowLimit = normalizeLimit(limit);
|
|
2137
|
+
for (const p of profiles) {
|
|
2138
|
+
const dbPath = join10(p.path, "Web Data");
|
|
2139
|
+
try {
|
|
2140
|
+
let sql = `SELECT name, value, date_created, date_last_used, count FROM autofill`;
|
|
2141
|
+
const params = [];
|
|
2142
|
+
if (search) {
|
|
2143
|
+
sql += ` WHERE IFNULL(name, '') LIKE ? ESCAPE '\\' OR IFNULL(value, '') LIKE ? ESCAPE '\\'`;
|
|
2144
|
+
const like = sqlLikePattern(search);
|
|
2145
|
+
params.push(like, like);
|
|
2146
|
+
}
|
|
2147
|
+
sql += ` ORDER BY date_last_used DESC`;
|
|
2148
|
+
if (rowLimit) {
|
|
2149
|
+
sql += ` LIMIT ${rowLimit}`;
|
|
2150
|
+
}
|
|
2151
|
+
const rows = queryDatabase(dbPath, sql, params);
|
|
2152
|
+
for (const row of rows) {
|
|
2153
|
+
results.push({
|
|
2154
|
+
profile: p.name,
|
|
2155
|
+
name: row.name || "",
|
|
2156
|
+
value: row.value || "",
|
|
2157
|
+
dateCreated: chromeTimeToDate(row.date_created ? BigInt(row.date_created) * 1000000n + 11644473600000000n : 0),
|
|
2158
|
+
dateLastUsed: chromeTimeToDate(row.date_last_used ? BigInt(row.date_last_used) * 1000000n + 11644473600000000n : 0),
|
|
2159
|
+
count: row.count || 0
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
} catch {
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return limitResults(results, limit);
|
|
2166
|
+
}
|
|
2167
|
+
function getCreditCards({
|
|
2168
|
+
chromeDir,
|
|
2169
|
+
profilePath,
|
|
2170
|
+
profile,
|
|
2171
|
+
browser,
|
|
2172
|
+
decryptionKey
|
|
2173
|
+
} = {}) {
|
|
2174
|
+
const userDataDir = profilePath || chromeDir;
|
|
2175
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
2176
|
+
let derivedKey = decryptionKey;
|
|
2177
|
+
const results = [];
|
|
2178
|
+
for (const p of profiles) {
|
|
2179
|
+
const dbPath = join10(p.path, "Web Data");
|
|
2180
|
+
try {
|
|
2181
|
+
const rows = queryDatabase(
|
|
2182
|
+
dbPath,
|
|
2183
|
+
`SELECT guid, name_on_card, expiration_month, expiration_year,
|
|
2184
|
+
card_number_encrypted, date_modified, origin, use_count,
|
|
2185
|
+
nickname
|
|
2186
|
+
FROM credit_cards ORDER BY use_date DESC`
|
|
2187
|
+
);
|
|
2188
|
+
for (const row of rows) {
|
|
2189
|
+
let cardNumber = "";
|
|
2190
|
+
if (row.card_number_encrypted?.length > 0) {
|
|
2191
|
+
cardNumber = decryptValue(
|
|
2192
|
+
row.card_number_encrypted,
|
|
2193
|
+
derivedKey ??= deriveEncryptionKey2(userDataDir, browser)
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
results.push({
|
|
2197
|
+
profile: p.name,
|
|
2198
|
+
guid: row.guid || "",
|
|
2199
|
+
nameOnCard: row.name_on_card || "",
|
|
2200
|
+
expirationMonth: row.expiration_month || 0,
|
|
2201
|
+
expirationYear: row.expiration_year || 0,
|
|
2202
|
+
cardNumber,
|
|
2203
|
+
nickname: row.nickname || "",
|
|
2204
|
+
origin: row.origin || "",
|
|
2205
|
+
useCount: row.use_count || 0,
|
|
2206
|
+
dateModified: chromeTimeToDate(row.date_modified ? BigInt(row.date_modified) * 1000000n + 11644473600000000n : 0)
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
} catch {
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return results;
|
|
2213
|
+
}
|
|
2214
|
+
function getAddresses({ chromeDir, profilePath, profile, browser } = {}) {
|
|
2215
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
2216
|
+
const results = [];
|
|
2217
|
+
for (const p of profiles) {
|
|
2218
|
+
const dbPath = join10(p.path, "Web Data");
|
|
2219
|
+
try {
|
|
2220
|
+
const profileRows = queryDatabase(
|
|
2221
|
+
dbPath,
|
|
2222
|
+
`SELECT guid, company_name, street_address,
|
|
2223
|
+
city, state, zipcode, country_code, date_modified,
|
|
2224
|
+
origin, use_count
|
|
2225
|
+
FROM autofill_profiles ORDER BY use_date DESC`
|
|
2226
|
+
);
|
|
2227
|
+
let nameRows = [];
|
|
2228
|
+
let emailRows = [];
|
|
2229
|
+
let phoneRows = [];
|
|
2230
|
+
try {
|
|
2231
|
+
nameRows = queryDatabase(
|
|
2232
|
+
dbPath,
|
|
2233
|
+
`SELECT guid, first_name, middle_name, last_name, full_name FROM autofill_profile_names`
|
|
2234
|
+
);
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
try {
|
|
2238
|
+
emailRows = queryDatabase(
|
|
2239
|
+
dbPath,
|
|
2240
|
+
`SELECT guid, email FROM autofill_profile_emails`
|
|
2241
|
+
);
|
|
2242
|
+
} catch {
|
|
2243
|
+
}
|
|
2244
|
+
try {
|
|
2245
|
+
phoneRows = queryDatabase(
|
|
2246
|
+
dbPath,
|
|
2247
|
+
`SELECT guid, number FROM autofill_profile_phones`
|
|
2248
|
+
);
|
|
2249
|
+
} catch {
|
|
2250
|
+
}
|
|
2251
|
+
const nameMap = /* @__PURE__ */ new Map();
|
|
2252
|
+
for (const n of nameRows) {
|
|
2253
|
+
nameMap.set(n.guid, n.full_name || [n.first_name, n.middle_name, n.last_name].filter(Boolean).join(" "));
|
|
2254
|
+
}
|
|
2255
|
+
const emailMap = /* @__PURE__ */ new Map();
|
|
2256
|
+
for (const e of emailRows) {
|
|
2257
|
+
emailMap.set(e.guid, e.email || "");
|
|
2258
|
+
}
|
|
2259
|
+
const phoneMap = /* @__PURE__ */ new Map();
|
|
2260
|
+
for (const ph of phoneRows) {
|
|
2261
|
+
phoneMap.set(ph.guid, ph.number || "");
|
|
2262
|
+
}
|
|
2263
|
+
for (const row of profileRows) {
|
|
2264
|
+
results.push({
|
|
2265
|
+
profile: p.name,
|
|
2266
|
+
guid: row.guid || "",
|
|
2267
|
+
fullName: nameMap.get(row.guid) || "",
|
|
2268
|
+
email: emailMap.get(row.guid) || "",
|
|
2269
|
+
phone: phoneMap.get(row.guid) || "",
|
|
2270
|
+
companyName: row.company_name || "",
|
|
2271
|
+
streetAddress: row.street_address || "",
|
|
2272
|
+
city: row.city || "",
|
|
2273
|
+
state: row.state || "",
|
|
2274
|
+
zipcode: row.zipcode || "",
|
|
2275
|
+
countryCode: row.country_code || "",
|
|
2276
|
+
origin: row.origin || "",
|
|
2277
|
+
useCount: row.use_count || 0,
|
|
2278
|
+
dateModified: chromeTimeToDate(row.date_modified ? BigInt(row.date_modified) * 1000000n + 11644473600000000n : 0)
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
} catch {
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return results;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// src/modules/downloads.ts
|
|
2288
|
+
import { join as join11 } from "node:path";
|
|
2289
|
+
var DOWNLOAD_STATES = {
|
|
2290
|
+
0: "in_progress",
|
|
2291
|
+
1: "complete",
|
|
2292
|
+
2: "cancelled",
|
|
2293
|
+
3: "interrupted"
|
|
2294
|
+
};
|
|
2295
|
+
var DANGER_TYPES = {
|
|
2296
|
+
0: "not_dangerous",
|
|
2297
|
+
1: "dangerous_file",
|
|
2298
|
+
2: "dangerous_url",
|
|
2299
|
+
3: "dangerous_content",
|
|
2300
|
+
4: "maybe_dangerous_content",
|
|
2301
|
+
5: "uncommon_content",
|
|
2302
|
+
6: "user_validated",
|
|
2303
|
+
7: "dangerous_host",
|
|
2304
|
+
8: "potentially_unwanted",
|
|
2305
|
+
9: "allowlisted_by_policy"
|
|
2306
|
+
};
|
|
2307
|
+
function getDownloads({
|
|
2308
|
+
chromeDir,
|
|
2309
|
+
profilePath,
|
|
2310
|
+
profile,
|
|
2311
|
+
browser,
|
|
2312
|
+
search,
|
|
2313
|
+
limit = 100
|
|
2314
|
+
} = {}) {
|
|
2315
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
2316
|
+
const results = [];
|
|
2317
|
+
const rowLimit = normalizeLimit(limit);
|
|
2318
|
+
for (const p of profiles) {
|
|
2319
|
+
const dbPath = join11(p.path, "History");
|
|
2320
|
+
try {
|
|
2321
|
+
let sql = `SELECT target_path, tab_url, total_bytes, received_bytes,
|
|
2322
|
+
start_time, end_time, mime_type, original_mime_type,
|
|
2323
|
+
state, danger_type, opened, referrer, site_url
|
|
2324
|
+
FROM downloads`;
|
|
2325
|
+
const params = [];
|
|
2326
|
+
if (search) {
|
|
2327
|
+
sql += ` WHERE IFNULL(target_path, '') LIKE ? ESCAPE '\\'
|
|
2328
|
+
OR IFNULL(tab_url, '') LIKE ? ESCAPE '\\'`;
|
|
2329
|
+
const like = sqlLikePattern(search);
|
|
2330
|
+
params.push(like, like);
|
|
2331
|
+
}
|
|
2332
|
+
sql += ` ORDER BY start_time DESC`;
|
|
2333
|
+
if (rowLimit) {
|
|
2334
|
+
sql += ` LIMIT ${rowLimit}`;
|
|
2335
|
+
}
|
|
2336
|
+
const rows = queryDatabase(dbPath, sql, params);
|
|
2337
|
+
for (const row of rows) {
|
|
2338
|
+
results.push({
|
|
2339
|
+
profile: p.name,
|
|
2340
|
+
targetPath: row.target_path || "",
|
|
2341
|
+
url: row.tab_url || "",
|
|
2342
|
+
siteUrl: row.site_url || "",
|
|
2343
|
+
referrer: row.referrer || "",
|
|
2344
|
+
totalBytes: row.total_bytes || 0,
|
|
2345
|
+
receivedBytes: row.received_bytes || 0,
|
|
2346
|
+
mimeType: row.mime_type || row.original_mime_type || "",
|
|
2347
|
+
state: DOWNLOAD_STATES[row.state] || `unknown(${row.state})`,
|
|
2348
|
+
dangerType: DANGER_TYPES[row.danger_type] || `unknown(${row.danger_type})`,
|
|
2349
|
+
opened: Boolean(row.opened),
|
|
2350
|
+
startTime: chromeTimeToDate(row.start_time || 0),
|
|
2351
|
+
endTime: chromeTimeToDate(row.end_time || 0)
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
} catch {
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return limitResults(results, limit);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// src/modules/extensions.ts
|
|
2361
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
2362
|
+
import { join as join12 } from "node:path";
|
|
2363
|
+
function getExtensions({
|
|
2364
|
+
chromeDir,
|
|
2365
|
+
profilePath,
|
|
2366
|
+
profile,
|
|
2367
|
+
search,
|
|
2368
|
+
browser
|
|
2369
|
+
} = {}) {
|
|
2370
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
2371
|
+
const results = [];
|
|
2372
|
+
for (const p of profiles) {
|
|
2373
|
+
let settings = null;
|
|
2374
|
+
for (const filename of ["Secure Preferences", "Preferences"]) {
|
|
2375
|
+
const prefsPath = join12(p.path, filename);
|
|
2376
|
+
if (existsSync8(prefsPath)) {
|
|
2377
|
+
try {
|
|
2378
|
+
const prefs = JSON.parse(readFileSync6(prefsPath, "utf-8"));
|
|
2379
|
+
const candidate = prefs?.extensions?.settings;
|
|
2380
|
+
if (candidate && typeof candidate === "object") {
|
|
2381
|
+
settings = candidate;
|
|
2382
|
+
break;
|
|
2383
|
+
}
|
|
2384
|
+
} catch {
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
if (!settings) continue;
|
|
2390
|
+
for (const [extId, extDataAny] of Object.entries(settings)) {
|
|
2391
|
+
const extData = extDataAny;
|
|
2392
|
+
if (extData.location === 5 || extData.location === 10) continue;
|
|
2393
|
+
const manifest = extData.manifest || {};
|
|
2394
|
+
const entry = {
|
|
2395
|
+
profile: p.name,
|
|
2396
|
+
id: extId,
|
|
2397
|
+
name: manifest.name || extData.name || extId,
|
|
2398
|
+
version: manifest.version || extData.version || "unknown",
|
|
2399
|
+
description: manifest.description || "",
|
|
2400
|
+
enabled: extData.state === 1,
|
|
2401
|
+
homepageUrl: manifest.homepage_url || "",
|
|
2402
|
+
updateUrl: manifest.update_url || "",
|
|
2403
|
+
installTime: extData.install_time || "",
|
|
2404
|
+
fromWebstore: extData.from_webstore || false,
|
|
2405
|
+
location: extData.location || 0
|
|
2406
|
+
};
|
|
2407
|
+
if (entry.name.startsWith("__MSG_")) {
|
|
2408
|
+
entry.name = extId;
|
|
2409
|
+
}
|
|
2410
|
+
if (entry.description.startsWith("__MSG_")) {
|
|
2411
|
+
entry.description = "";
|
|
2412
|
+
}
|
|
2413
|
+
if (search) {
|
|
2414
|
+
const term = search.toLowerCase();
|
|
2415
|
+
if (!(entry.name && entry.name.toLowerCase().includes(term)) && !(entry.id && entry.id.toLowerCase().includes(term))) {
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
results.push(entry);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return results;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// src/modules/search-engines.ts
|
|
2426
|
+
import { join as join13 } from "node:path";
|
|
2427
|
+
function getSearchEngines({
|
|
2428
|
+
chromeDir,
|
|
2429
|
+
profilePath,
|
|
2430
|
+
profile,
|
|
2431
|
+
browser
|
|
2432
|
+
} = {}) {
|
|
2433
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
2434
|
+
const results = [];
|
|
2435
|
+
for (const p of profiles) {
|
|
2436
|
+
const dbPath = join13(p.path, "Web Data");
|
|
2437
|
+
try {
|
|
2438
|
+
const rows = queryDatabase(
|
|
2439
|
+
dbPath,
|
|
2440
|
+
`SELECT id, short_name, keyword, url, suggestions_url,
|
|
2441
|
+
favicon_url, prepopulate_id, is_active
|
|
2442
|
+
FROM keywords ORDER BY short_name`
|
|
2443
|
+
);
|
|
2444
|
+
for (const row of rows) {
|
|
2445
|
+
results.push({
|
|
2446
|
+
profile: p.name,
|
|
2447
|
+
id: row.id,
|
|
2448
|
+
name: row.short_name || "",
|
|
2449
|
+
keyword: row.keyword || "",
|
|
2450
|
+
url: row.url || "",
|
|
2451
|
+
suggestionsUrl: row.suggestions_url || "",
|
|
2452
|
+
faviconUrl: row.favicon_url || "",
|
|
2453
|
+
isDefault: row.prepopulate_id === 1,
|
|
2454
|
+
isActive: row.is_active === 1
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
} catch {
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
return results;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/modules/top-sites.ts
|
|
2464
|
+
import { join as join14 } from "node:path";
|
|
2465
|
+
function getTopSites({ chromeDir, profilePath, profile, browser } = {}) {
|
|
2466
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
2467
|
+
const results = [];
|
|
2468
|
+
for (const p of profiles) {
|
|
2469
|
+
const dbPath = join14(p.path, "Top Sites");
|
|
2470
|
+
try {
|
|
2471
|
+
const rows = queryDatabase(
|
|
2472
|
+
dbPath,
|
|
2473
|
+
`SELECT url, url_rank, title, redirects
|
|
2474
|
+
FROM top_sites ORDER BY url_rank ASC`
|
|
2475
|
+
);
|
|
2476
|
+
for (const row of rows) {
|
|
2477
|
+
results.push({
|
|
2478
|
+
profile: p.name,
|
|
2479
|
+
url: row.url || "",
|
|
2480
|
+
title: row.title || "",
|
|
2481
|
+
rank: row.url_rank ?? 0,
|
|
2482
|
+
redirects: row.redirects || ""
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return results;
|
|
2489
|
+
}
|
|
2490
|
+
export {
|
|
2491
|
+
BROWSERS,
|
|
2492
|
+
chromeTimeToDate,
|
|
2493
|
+
clearKeyCache,
|
|
2494
|
+
clearKeychainCache,
|
|
2495
|
+
decryptValue,
|
|
2496
|
+
decryptValueBuffer2 as decryptValueBuffer,
|
|
2497
|
+
deriveEncryptionKey2 as deriveEncryptionKey,
|
|
2498
|
+
describePasswordStores,
|
|
2499
|
+
detectInstalledBrowsers,
|
|
2500
|
+
discoverProfiles,
|
|
2501
|
+
getActiveBrowser,
|
|
2502
|
+
getAddresses,
|
|
2503
|
+
getAutofillEntries,
|
|
2504
|
+
getBookmarks,
|
|
2505
|
+
getChromeKeychainPassword,
|
|
2506
|
+
getCookies,
|
|
2507
|
+
getCreditCards,
|
|
2508
|
+
getDefaultChromeDir,
|
|
2509
|
+
getDownloads,
|
|
2510
|
+
getExtensions,
|
|
2511
|
+
getHistory,
|
|
2512
|
+
getKeychainPassword,
|
|
2513
|
+
getKeychainPasswordProvider,
|
|
2514
|
+
getLocalStorage,
|
|
2515
|
+
getPasswords,
|
|
2516
|
+
getPlatformChromeDir,
|
|
2517
|
+
getProfileMetadata,
|
|
2518
|
+
getProfileStates,
|
|
2519
|
+
getSearchEngines,
|
|
2520
|
+
getSearchTerms,
|
|
2521
|
+
getTopSites,
|
|
2522
|
+
listBrowsers,
|
|
2523
|
+
queryDatabase,
|
|
2524
|
+
requireBrowser,
|
|
2525
|
+
resolveBrowser,
|
|
2526
|
+
resolveProfilePath,
|
|
2527
|
+
resolveTargetProfiles,
|
|
2528
|
+
selectPlatform,
|
|
2529
|
+
setActiveBrowser,
|
|
2530
|
+
setKeychainPasswordProvider,
|
|
2531
|
+
tableExists
|
|
2532
|
+
};
|