chrome-tools 1.1.15 → 1.1.16

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.
Files changed (57) hide show
  1. package/dist/cli.js +8602 -0
  2. package/dist/index.d.ts +629 -0
  3. package/dist/index.js +2297 -0
  4. package/package.json +62 -29
  5. package/dist/bin/chrome_abe_decrypt.exe +0 -0
  6. package/dist/bin/cli.d.ts +0 -9
  7. package/dist/bin/cli.js +0 -590
  8. package/dist/src/core/browsers.d.ts +0 -73
  9. package/dist/src/core/browsers.js +0 -186
  10. package/dist/src/core/chrome.d.ts +0 -124
  11. package/dist/src/core/chrome.js +0 -259
  12. package/dist/src/core/crypto.d.ts +0 -52
  13. package/dist/src/core/crypto.js +0 -152
  14. package/dist/src/core/db.d.ts +0 -22
  15. package/dist/src/core/db.js +0 -227
  16. package/dist/src/core/keychain.d.ts +0 -57
  17. package/dist/src/core/keychain.js +0 -191
  18. package/dist/src/core/limit.d.ts +0 -4
  19. package/dist/src/core/limit.js +0 -12
  20. package/dist/src/core/platform/darwin.d.ts +0 -12
  21. package/dist/src/core/platform/darwin.js +0 -31
  22. package/dist/src/core/platform/index.d.ts +0 -30
  23. package/dist/src/core/platform/index.js +0 -69
  24. package/dist/src/core/platform/linux.d.ts +0 -10
  25. package/dist/src/core/platform/linux.js +0 -64
  26. package/dist/src/core/platform/types.d.ts +0 -45
  27. package/dist/src/core/platform/types.js +0 -1
  28. package/dist/src/core/platform/win32.d.ts +0 -13
  29. package/dist/src/core/platform/win32.js +0 -329
  30. package/dist/src/core/sql.d.ts +0 -2
  31. package/dist/src/core/sql.js +0 -4
  32. package/dist/src/index.d.ts +0 -28
  33. package/dist/src/index.js +0 -34
  34. package/dist/src/modules/autofill.d.ts +0 -68
  35. package/dist/src/modules/autofill.js +0 -170
  36. package/dist/src/modules/bookmarks.d.ts +0 -22
  37. package/dist/src/modules/bookmarks.js +0 -83
  38. package/dist/src/modules/cookies.d.ts +0 -40
  39. package/dist/src/modules/cookies.js +0 -163
  40. package/dist/src/modules/downloads.d.ts +0 -27
  41. package/dist/src/modules/downloads.js +0 -80
  42. package/dist/src/modules/extensions.d.ts +0 -24
  43. package/dist/src/modules/extensions.js +0 -71
  44. package/dist/src/modules/history.d.ts +0 -34
  45. package/dist/src/modules/history.js +0 -83
  46. package/dist/src/modules/local-storage.d.ts +0 -40
  47. package/dist/src/modules/local-storage.js +0 -455
  48. package/dist/src/modules/passwords.d.ts +0 -50
  49. package/dist/src/modules/passwords.js +0 -198
  50. package/dist/src/modules/search-engines.d.ts +0 -21
  51. package/dist/src/modules/search-engines.js +0 -38
  52. package/dist/src/modules/top-sites.d.ts +0 -17
  53. package/dist/src/modules/top-sites.js +0 -33
  54. package/src/native/build.bat +0 -31
  55. package/src/native/chrome_abe_decrypt.c +0 -336
  56. package/src/native/chrome_abe_decrypt.exe +0 -0
  57. /package/{bin → dist}/chrome_abe_decrypt.exe +0 -0
@@ -1,455 +0,0 @@
1
- import { cpSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { basename, join } from "node:path";
4
- import { resolveTargetProfiles } from "../core/chrome.js";
5
- import { limitResults } from "../core/limit.js";
6
- const TABLE_MAGIC = 0xdb4775248b80fb57n;
7
- const LOG_BLOCK_SIZE = 32768;
8
- const VALUE_TYPE_DELETION = 0;
9
- const VALUE_TYPE_VALUE = 1;
10
- /**
11
- * Reads localStorage key/value pairs from Chrome's Local Storage LevelDB store.
12
- *
13
- * @param options - Targeting and filtering options.
14
- * @returns Array of localStorage entries.
15
- */
16
- export function getLocalStorage({ chromeDir, profilePath, profile, browser, origin, url, key, search, limit = 100, } = {}) {
17
- const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
18
- const results = [];
19
- const originFilter = normalizeOriginFilter(url || origin);
20
- const keyFilter = key?.toLowerCase();
21
- const searchFilter = search?.toLowerCase();
22
- for (const p of profiles) {
23
- const dbPath = join(p.path, "Local Storage", "leveldb");
24
- if (!existsSync(dbPath))
25
- continue;
26
- let tmpPath = null;
27
- try {
28
- tmpPath = copyLevelDBToTemp(dbPath);
29
- const rawRecords = readLevelDBRecords(tmpPath);
30
- for (const record of rawRecords.values()) {
31
- if (record.deleted || !record.value)
32
- continue;
33
- const parsed = parseLocalStorageRecord(record.key, record.value);
34
- if (!parsed)
35
- continue;
36
- const entry = {
37
- profile: p.name,
38
- origin: parsed.origin,
39
- key: parsed.key,
40
- value: parsed.value,
41
- };
42
- if (originFilter && !entry.origin.toLowerCase().includes(originFilter)) {
43
- continue;
44
- }
45
- if (keyFilter && !entry.key.toLowerCase().includes(keyFilter)) {
46
- continue;
47
- }
48
- if (searchFilter) {
49
- const haystack = `${entry.origin}\n${entry.key}\n${entry.value}`.toLowerCase();
50
- if (!haystack.includes(searchFilter))
51
- continue;
52
- }
53
- results.push(entry);
54
- }
55
- }
56
- catch {
57
- // Skip profiles with unreadable or partially-corrupt LevelDB files.
58
- }
59
- finally {
60
- if (tmpPath) {
61
- rmSync(tmpPath, { recursive: true, force: true });
62
- }
63
- }
64
- }
65
- results.sort((a, b) => a.profile.localeCompare(b.profile) ||
66
- a.origin.localeCompare(b.origin) ||
67
- a.key.localeCompare(b.key));
68
- return limitResults(results, limit);
69
- }
70
- function copyLevelDBToTemp(dbPath) {
71
- const tmpPath = mkdtempSync(join(tmpdir(), "chrome-local-storage-"));
72
- cpSync(dbPath, tmpPath, {
73
- recursive: true,
74
- force: true,
75
- filter: (source) => basename(source) !== "LOCK",
76
- });
77
- return tmpPath;
78
- }
79
- function readLevelDBRecords(dbPath) {
80
- const records = new Map();
81
- const files = readdirSync(dbPath)
82
- .filter((file) => /\.(log|ldb|sst)$/i.test(file))
83
- .sort();
84
- for (const file of files) {
85
- const filePath = join(dbPath, file);
86
- const data = readFileSync(filePath);
87
- if (/\.(ldb|sst)$/i.test(file)) {
88
- for (const record of parseTableRecords(data)) {
89
- applyLevelDBRecord(records, record);
90
- }
91
- }
92
- }
93
- for (const file of files) {
94
- if (!/\.log$/i.test(file))
95
- continue;
96
- const filePath = join(dbPath, file);
97
- const data = readFileSync(filePath);
98
- for (const record of parseLogRecords(data)) {
99
- applyLevelDBRecord(records, record);
100
- }
101
- }
102
- return records;
103
- }
104
- function applyLevelDBRecord(records, record) {
105
- const id = record.key.toString("hex");
106
- const existing = records.get(id);
107
- if (existing && existing.sequence > record.sequence)
108
- return;
109
- records.set(id, record);
110
- }
111
- function parseLocalStorageRecord(rawKey, rawValue) {
112
- if (rawKey.length < 3 || rawKey[0] !== 0x5f)
113
- return null;
114
- const separator = rawKey.indexOf(0x00, 1);
115
- if (separator === -1)
116
- return null;
117
- const origin = rawKey.subarray(1, separator).toString("utf8");
118
- const key = decodeLocalStorageString(rawKey.subarray(separator + 1));
119
- const value = decodeLocalStorageString(rawValue);
120
- if (!origin || !key)
121
- return null;
122
- return { origin, key, value };
123
- }
124
- function decodeLocalStorageString(data) {
125
- if (data.length === 0)
126
- return "";
127
- const format = data[0];
128
- const body = data.subarray(1);
129
- if (format === 0x00 && body.length % 2 === 0) {
130
- return body.toString("utf16le");
131
- }
132
- if (format === 0x01) {
133
- return body.toString("latin1");
134
- }
135
- return data.toString("utf8");
136
- }
137
- function normalizeOriginFilter(input) {
138
- if (!input)
139
- return undefined;
140
- try {
141
- return new URL(input).origin.toLowerCase();
142
- }
143
- catch {
144
- return input.toLowerCase();
145
- }
146
- }
147
- function parseLogRecords(data) {
148
- const records = [];
149
- let offset = 0;
150
- let fragments = [];
151
- while (offset + 7 <= data.length) {
152
- const blockRemaining = LOG_BLOCK_SIZE - (offset % LOG_BLOCK_SIZE);
153
- if (blockRemaining < 7) {
154
- offset += blockRemaining;
155
- continue;
156
- }
157
- const length = data.readUInt16LE(offset + 4);
158
- const type = data[offset + 6];
159
- offset += 7;
160
- if (type === 0 && length === 0) {
161
- offset += length;
162
- continue;
163
- }
164
- if (length > data.length - offset || length > blockRemaining - 7) {
165
- break;
166
- }
167
- const payload = data.subarray(offset, offset + length);
168
- offset += length;
169
- if (type === 1) {
170
- records.push(...parseWriteBatch(payload));
171
- fragments = [];
172
- }
173
- else if (type === 2) {
174
- fragments = [Buffer.from(payload)];
175
- }
176
- else if (type === 3 && fragments.length > 0) {
177
- fragments.push(Buffer.from(payload));
178
- }
179
- else if (type === 4 && fragments.length > 0) {
180
- fragments.push(Buffer.from(payload));
181
- records.push(...parseWriteBatch(Buffer.concat(fragments)));
182
- fragments = [];
183
- }
184
- }
185
- return records;
186
- }
187
- function parseWriteBatch(batch) {
188
- if (batch.length < 12)
189
- return [];
190
- const records = [];
191
- const startSequence = readUInt64LE(batch, 0);
192
- const count = batch.readUInt32LE(8);
193
- let offset = 12;
194
- let seen = 0;
195
- while (offset < batch.length && seen < count) {
196
- const tag = batch[offset++];
197
- const keyPart = readLengthPrefixedSlice(batch, offset);
198
- if (!keyPart)
199
- break;
200
- offset = keyPart.nextOffset;
201
- const sequence = startSequence + BigInt(seen);
202
- if (tag === VALUE_TYPE_VALUE) {
203
- const valuePart = readLengthPrefixedSlice(batch, offset);
204
- if (!valuePart)
205
- break;
206
- offset = valuePart.nextOffset;
207
- records.push({ key: keyPart.value, value: valuePart.value, sequence, deleted: false });
208
- }
209
- else if (tag === VALUE_TYPE_DELETION) {
210
- records.push({ key: keyPart.value, sequence, deleted: true });
211
- }
212
- else {
213
- break;
214
- }
215
- seen++;
216
- }
217
- return records;
218
- }
219
- function parseTableRecords(data) {
220
- const records = [];
221
- if (data.length < 48)
222
- return records;
223
- const magic = data.readBigUInt64LE(data.length - 8);
224
- if (magic !== TABLE_MAGIC)
225
- return records;
226
- const footer = data.subarray(data.length - 48, data.length - 8);
227
- const metaHandle = readBlockHandle(footer, 0);
228
- if (!metaHandle)
229
- return records;
230
- const indexHandle = readBlockHandle(footer, metaHandle.bytesRead);
231
- if (!indexHandle)
232
- return records;
233
- const indexBlock = readBlock(data, indexHandle);
234
- if (!indexBlock)
235
- return records;
236
- for (const indexEntry of parseBlockEntries(indexBlock)) {
237
- const dataHandle = readBlockHandle(indexEntry.value, 0);
238
- if (!dataHandle)
239
- continue;
240
- const dataBlock = readBlock(data, dataHandle);
241
- if (!dataBlock)
242
- continue;
243
- for (const entry of parseBlockEntries(dataBlock)) {
244
- if (entry.key.length < 8)
245
- continue;
246
- const internalKey = entry.key;
247
- const tagOffset = internalKey.length - 8;
248
- const type = internalKey[tagOffset];
249
- const sequence = readSequence(internalKey.subarray(tagOffset));
250
- const key = internalKey.subarray(0, tagOffset);
251
- if (type === VALUE_TYPE_VALUE) {
252
- records.push({ key, value: entry.value, sequence, deleted: false });
253
- }
254
- else if (type === VALUE_TYPE_DELETION) {
255
- records.push({ key, sequence, deleted: true });
256
- }
257
- }
258
- }
259
- return records;
260
- }
261
- function readBlock(data, handle) {
262
- const trailerOffset = handle.offset + handle.size;
263
- if (handle.offset < 0 || handle.size < 0 || trailerOffset + 5 > data.length) {
264
- return null;
265
- }
266
- const block = data.subarray(handle.offset, trailerOffset);
267
- const compressionType = data[trailerOffset];
268
- if (compressionType === 0) {
269
- return block;
270
- }
271
- if (compressionType === 1) {
272
- try {
273
- return snappyUncompress(block);
274
- }
275
- catch {
276
- return null;
277
- }
278
- }
279
- return null;
280
- }
281
- function parseBlockEntries(block) {
282
- if (block.length < 4)
283
- return [];
284
- const restartCount = block.readUInt32LE(block.length - 4);
285
- const restartsSize = restartCount * 4 + 4;
286
- if (restartCount < 1 || restartsSize > block.length)
287
- return [];
288
- const entriesEnd = block.length - restartsSize;
289
- const entries = [];
290
- let offset = 0;
291
- let previousKey = Buffer.alloc(0);
292
- while (offset < entriesEnd) {
293
- const shared = readVarint32(block, offset);
294
- if (!shared)
295
- break;
296
- offset = shared.nextOffset;
297
- const nonShared = readVarint32(block, offset);
298
- if (!nonShared)
299
- break;
300
- offset = nonShared.nextOffset;
301
- const valueLength = readVarint32(block, offset);
302
- if (!valueLength)
303
- break;
304
- offset = valueLength.nextOffset;
305
- if (shared.value > previousKey.length || offset + nonShared.value + valueLength.value > entriesEnd) {
306
- break;
307
- }
308
- const key = Buffer.concat([
309
- previousKey.subarray(0, shared.value),
310
- block.subarray(offset, offset + nonShared.value),
311
- ]);
312
- offset += nonShared.value;
313
- const value = Buffer.from(block.subarray(offset, offset + valueLength.value));
314
- offset += valueLength.value;
315
- entries.push({ key, value });
316
- previousKey = key;
317
- }
318
- return entries;
319
- }
320
- function readBlockHandle(data, offset) {
321
- const blockOffset = readVarint64(data, offset);
322
- if (!blockOffset)
323
- return null;
324
- const blockSize = readVarint64(data, blockOffset.nextOffset);
325
- if (!blockSize)
326
- return null;
327
- if (blockOffset.value > BigInt(Number.MAX_SAFE_INTEGER) || blockSize.value > BigInt(Number.MAX_SAFE_INTEGER)) {
328
- return null;
329
- }
330
- return {
331
- offset: Number(blockOffset.value),
332
- size: Number(blockSize.value),
333
- bytesRead: blockSize.nextOffset - offset,
334
- };
335
- }
336
- function readLengthPrefixedSlice(data, offset) {
337
- const length = readVarint32(data, offset);
338
- if (!length)
339
- return null;
340
- const end = length.nextOffset + length.value;
341
- if (end > data.length)
342
- return null;
343
- return { value: Buffer.from(data.subarray(length.nextOffset, end)), nextOffset: end };
344
- }
345
- function readVarint32(data, offset) {
346
- let result = 0;
347
- let shift = 0;
348
- for (let i = offset; i < data.length && i < offset + 5; i++) {
349
- const byte = data[i];
350
- result |= (byte & 0x7f) << shift;
351
- if ((byte & 0x80) === 0) {
352
- return { value: result >>> 0, nextOffset: i + 1 };
353
- }
354
- shift += 7;
355
- }
356
- return null;
357
- }
358
- function readVarint64(data, offset) {
359
- let result = 0n;
360
- let shift = 0n;
361
- for (let i = offset; i < data.length && i < offset + 10; i++) {
362
- const byte = data[i];
363
- result |= BigInt(byte & 0x7f) << shift;
364
- if ((byte & 0x80) === 0) {
365
- return { value: result, nextOffset: i + 1 };
366
- }
367
- shift += 7n;
368
- }
369
- return null;
370
- }
371
- function readUInt64LE(data, offset) {
372
- let value = 0n;
373
- for (let i = 0; i < 8; i++) {
374
- value |= BigInt(data[offset + i] || 0) << BigInt(i * 8);
375
- }
376
- return value;
377
- }
378
- function readSequence(tagBytes) {
379
- let sequence = 0n;
380
- for (let i = 1; i < 8; i++) {
381
- sequence |= BigInt(tagBytes[i] || 0) << BigInt((i - 1) * 8);
382
- }
383
- return sequence;
384
- }
385
- function snappyUncompress(input) {
386
- const expectedLength = readVarint32(input, 0);
387
- if (!expectedLength)
388
- throw new Error("Invalid Snappy length");
389
- const output = Buffer.alloc(expectedLength.value);
390
- let ip = expectedLength.nextOffset;
391
- let op = 0;
392
- while (ip < input.length && op < output.length) {
393
- const tag = input[ip++];
394
- const type = tag & 0x03;
395
- if (type === 0) {
396
- let literalLength = tag >> 2;
397
- if (literalLength < 60) {
398
- literalLength += 1;
399
- }
400
- else {
401
- const bytesToRead = literalLength - 59;
402
- if (ip + bytesToRead > input.length)
403
- throw new Error("Invalid Snappy literal");
404
- literalLength = 1;
405
- for (let i = 0; i < bytesToRead; i++) {
406
- literalLength += input[ip++] << (i * 8);
407
- }
408
- }
409
- if (ip + literalLength > input.length || op + literalLength > output.length) {
410
- throw new Error("Invalid Snappy literal length");
411
- }
412
- input.copy(output, op, ip, ip + literalLength);
413
- ip += literalLength;
414
- op += literalLength;
415
- continue;
416
- }
417
- let copyLength;
418
- let copyOffset;
419
- if (type === 1) {
420
- if (ip >= input.length)
421
- throw new Error("Invalid Snappy copy");
422
- copyLength = ((tag >> 2) & 0x07) + 4;
423
- copyOffset = ((tag & 0xe0) << 3) | input[ip++];
424
- }
425
- else if (type === 2) {
426
- if (ip + 2 > input.length)
427
- throw new Error("Invalid Snappy copy");
428
- copyLength = (tag >> 2) + 1;
429
- copyOffset = input.readUInt16LE(ip);
430
- ip += 2;
431
- }
432
- else {
433
- if (ip + 4 > input.length)
434
- throw new Error("Invalid Snappy copy");
435
- copyLength = (tag >> 2) + 1;
436
- copyOffset = input.readUInt32LE(ip);
437
- ip += 4;
438
- }
439
- if (copyOffset <= 0 || copyOffset > op || op + copyLength > output.length) {
440
- throw new Error("Invalid Snappy offset");
441
- }
442
- for (let i = 0; i < copyLength; i++) {
443
- output[op] = output[op - copyOffset];
444
- op++;
445
- }
446
- }
447
- if (op !== output.length)
448
- throw new Error("Truncated Snappy stream");
449
- return output;
450
- }
451
- export const _test = {
452
- decodeLocalStorageString,
453
- parseLocalStorageRecord,
454
- parseLogRecords,
455
- };
@@ -1,50 +0,0 @@
1
- import { type ResolveProfilesOptions } from "../core/chrome.js";
2
- export interface PasswordOptions extends ResolveProfilesOptions {
3
- search?: string;
4
- decryptionKey?: Buffer;
5
- }
6
- /** Which on-disk store a password came from. */
7
- export type PasswordSource = "device" | "account";
8
- export interface PasswordEntry {
9
- profile: string;
10
- /** "device" = local "Login Data"; "account" = synced "Login Data For Account". */
11
- source?: PasswordSource;
12
- url?: string;
13
- username?: string;
14
- password?: string;
15
- lastUsed?: string;
16
- error?: string;
17
- }
18
- /** What a single password store looked like when it was read. */
19
- export interface PasswordStoreReport {
20
- /** The User Data directory searched; "default" when the platform picked it. */
21
- root: string;
22
- profile: string;
23
- /** "Login Data" (device) or "Login Data For Account" (synced). */
24
- file: string;
25
- source: PasswordSource;
26
- exists: boolean;
27
- /** Rows in `logins`, before any search filter. Null when the read failed. */
28
- rows: number | null;
29
- /** Why the read failed, when it did. No credential value is ever included. */
30
- error?: string;
31
- }
32
- /**
33
- * Reports what a `getPasswords` call would read, and what it could not.
34
- *
35
- * `getPasswords` skips a store it cannot open — a missing file, a schema it does
36
- * not know, a database another process is holding — and an empty result cannot
37
- * tell "nothing saved here" from "could not look". This runs the same profile
38
- * resolution and opens the same databases, and answers with row counts and
39
- * errors only. It decrypts nothing, so no password value can escape through it.
40
- */
41
- export declare function describePasswordStores({ chromeDir, profilePath, profile, browser, }?: PasswordOptions): PasswordStoreReport[];
42
- /**
43
- * Extracts and decrypts saved passwords from Chrome's password databases.
44
- * Reads both the device-local and account-synced stores, labelling each entry
45
- * with its `source` (no de-duplication across the two).
46
- *
47
- * @param options - Options for profile selection and filtering.
48
- * @returns Array of decrypted password entries.
49
- */
50
- export declare function getPasswords({ chromeDir, profilePath, profile, search, browser, decryptionKey, }?: PasswordOptions): PasswordEntry[];
@@ -1,198 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { discoverProfiles, resolveTargetProfiles, } from "../core/chrome.js";
5
- import { deriveEncryptionKey, decryptValue } from "../core/platform/index.js";
6
- import { chromeTimeToDate } from "../core/crypto.js";
7
- import { queryDatabase } from "../core/db.js";
8
- /**
9
- * Chrome stores saved passwords in two SQLite files per profile:
10
- * - "Login Data" — passwords kept on this device only.
11
- * - "Login Data For Account" — passwords synced to the signed-in Google Account.
12
- * Both share the same `logins` schema and v10 encryption, so we read both and
13
- * tag each entry with its source.
14
- */
15
- const PASSWORD_STORES = [
16
- { file: "Login Data", source: "device" },
17
- { file: "Login Data For Account", source: "account" },
18
- ];
19
- /** Where a CDP launcher keeps the User Data directory it drives Chrome with. */
20
- const AUTOMATION_DIR = join(homedir(), ".chrome_cdp_profile");
21
- /** …and where it records a relocated one. */
22
- const AUTOMATION_CONFIG = join(homedir(), ".config", "chrome-cdp-manager", "config.json");
23
- /** `CHROME_AUTOMATION_DIR` values meaning "do not look there at all". */
24
- const AUTOMATION_OFF = ["0", "off", "none", "false"];
25
- /**
26
- * The automation profile tree, on a machine that has one.
27
- *
28
- * Most machines do not: no CDP launcher is installed, the directory was never
29
- * created, or it exists but holds no profile yet. All three answer undefined,
30
- * which leaves the search exactly as it was before this existed — the everyday
31
- * profile, alone. Nothing here is required for a plain Chrome install.
32
- *
33
- * `CHROME_AUTOMATION_DIR` names the tree explicitly, and `off` (or `0`, `none`,
34
- * `false`) skips it even where one exists.
35
- */
36
- function automationChromeDir() {
37
- const override = process.env.CHROME_AUTOMATION_DIR?.trim();
38
- if (override && AUTOMATION_OFF.includes(override.toLowerCase()))
39
- return undefined;
40
- const dir = override || configuredAutomationDir() || AUTOMATION_DIR;
41
- // Holding a profile is what makes it a tree worth reading — an absent or
42
- // empty directory would otherwise add a root every lookup searches for
43
- // nothing, and turn a missing `--profile` into a spurious refusal.
44
- return discoverProfiles(dir).length > 0 ? dir : undefined;
45
- }
46
- /** The profile directory a CDP launcher was configured with, if it says. */
47
- function configuredAutomationDir() {
48
- try {
49
- const stored = JSON.parse(readFileSync(AUTOMATION_CONFIG, "utf-8"));
50
- return typeof stored.profileDir === "string" && stored.profileDir ? stored.profileDir : undefined;
51
- }
52
- catch {
53
- // No launcher config, or an unreadable one: fall back to its default path.
54
- return undefined;
55
- }
56
- }
57
- /**
58
- * The User Data directories a lookup reads, in order.
59
- *
60
- * The automation tree first, then the everyday one. A Chrome driven over CDP
61
- * keeps its own profile tree, so a login saved while signing in to that window
62
- * is stored there and nowhere else — looking only at the everyday tree could
63
- * not find it. Naming a directory or a browser answers "which tree?" outright,
64
- * and is used as given.
65
- */
66
- function passwordRoots(userDataDir, browser) {
67
- const roots = [];
68
- const automation = userDataDir || browser ? undefined : automationChromeDir();
69
- if (automation)
70
- roots.push(automation);
71
- roots.push(userDataDir);
72
- return roots;
73
- }
74
- /**
75
- * Reports what a `getPasswords` call would read, and what it could not.
76
- *
77
- * `getPasswords` skips a store it cannot open — a missing file, a schema it does
78
- * not know, a database another process is holding — and an empty result cannot
79
- * tell "nothing saved here" from "could not look". This runs the same profile
80
- * resolution and opens the same databases, and answers with row counts and
81
- * errors only. It decrypts nothing, so no password value can escape through it.
82
- */
83
- export function describePasswordStores({ chromeDir, profilePath, profile, browser, } = {}) {
84
- const userDataDir = profilePath || chromeDir;
85
- const reports = [];
86
- for (const root of passwordRoots(userDataDir, browser)) {
87
- let profiles;
88
- try {
89
- profiles = resolveTargetProfiles({ chromeDir: root, profile, browser });
90
- }
91
- catch (error) {
92
- reports.push({
93
- root: root ?? "default",
94
- profile: profile ?? "*",
95
- file: "*",
96
- source: "device",
97
- exists: false,
98
- rows: null,
99
- error: error instanceof Error ? error.message : String(error),
100
- });
101
- continue;
102
- }
103
- for (const p of profiles) {
104
- for (const store of PASSWORD_STORES) {
105
- const dbPath = join(p.path, store.file);
106
- const report = {
107
- root: root ?? "default",
108
- profile: p.name,
109
- file: store.file,
110
- source: store.source,
111
- exists: existsSync(dbPath),
112
- rows: null,
113
- };
114
- try {
115
- report.rows = queryDatabase(dbPath, "SELECT COUNT(*) AS n FROM logins")[0]?.n ?? 0;
116
- }
117
- catch (error) {
118
- report.error = error instanceof Error ? error.message : String(error);
119
- }
120
- reports.push(report);
121
- }
122
- }
123
- }
124
- return reports;
125
- }
126
- /**
127
- * Extracts and decrypts saved passwords from Chrome's password databases.
128
- * Reads both the device-local and account-synced stores, labelling each entry
129
- * with its `source` (no de-duplication across the two).
130
- *
131
- * @param options - Options for profile selection and filtering.
132
- * @returns Array of decrypted password entries.
133
- */
134
- export function getPasswords({ chromeDir, profilePath, profile, search, browser, decryptionKey, } = {}) {
135
- const userDataDir = profilePath || chromeDir;
136
- const roots = passwordRoots(userDataDir, browser);
137
- const results = [];
138
- const term = search?.toLowerCase();
139
- // A named profile missing from one tree is not a failure while another is
140
- // still to come — but a profile in none of them is still an error, so the
141
- // first refusal is kept and raised if no tree resolved at all.
142
- let refusal;
143
- let resolved = false;
144
- for (const root of roots) {
145
- let profiles;
146
- try {
147
- profiles = resolveTargetProfiles({ chromeDir: root, profile, browser });
148
- }
149
- catch (error) {
150
- refusal ??= error;
151
- continue;
152
- }
153
- resolved = true;
154
- // Per root: on Windows the key comes from that tree's own Local State.
155
- let derivedKey = decryptionKey;
156
- for (const p of profiles) {
157
- for (const store of PASSWORD_STORES) {
158
- const dbPath = join(p.path, store.file);
159
- try {
160
- const rows = queryDatabase(dbPath, `SELECT origin_url, username_value, password_value, date_last_used
161
- FROM logins ORDER BY origin_url`);
162
- for (const row of rows) {
163
- // Matched before anything is decrypted. Both columns the search
164
- // looks at — the origin and the username — are stored in the clear,
165
- // so a row that cannot match never needs its password read at all.
166
- //
167
- // Decrypting first cost one platform decryption per stored login on
168
- // the way to finding one: a store with a few hundred entries spent
169
- // upwards of ten seconds answering "what is the password for this
170
- // one site", and every one of those decryptions was thrown away by
171
- // the filter immediately below.
172
- if (term &&
173
- !(row.origin_url && row.origin_url.toLowerCase().includes(term)) &&
174
- !(row.username_value && row.username_value.toLowerCase().includes(term))) {
175
- continue;
176
- }
177
- results.push({
178
- profile: p.name,
179
- source: store.source,
180
- url: row.origin_url,
181
- username: row.username_value,
182
- password: row.password_value?.length > 0
183
- ? decryptValue(row.password_value, derivedKey ??= deriveEncryptionKey(root, browser))
184
- : "",
185
- lastUsed: chromeTimeToDate(row.date_last_used || 0),
186
- });
187
- }
188
- }
189
- catch {
190
- // Skip missing or unreadable stores and continue with the others.
191
- }
192
- }
193
- }
194
- }
195
- if (!resolved && refusal)
196
- throw refusal;
197
- return results;
198
- }