chrome-tools 1.1.6 → 1.1.8
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/bin/cli.js +556 -1
- package/dist/src/core/browsers.d.ts +7 -3
- package/dist/src/core/browsers.js +186 -1
- package/dist/src/core/chrome.d.ts +6 -4
- package/dist/src/core/chrome.js +240 -1
- package/dist/src/core/crypto.d.ts +1 -1
- package/dist/src/core/crypto.js +113 -1
- package/dist/src/core/db.d.ts +4 -2
- package/dist/src/core/db.js +90 -1
- package/dist/src/core/keychain.js +191 -1
- package/dist/src/core/limit.d.ts +4 -0
- package/dist/src/core/limit.js +12 -0
- package/dist/src/core/platform/darwin.js +31 -1
- package/dist/src/core/platform/index.d.ts +4 -3
- package/dist/src/core/platform/index.js +69 -1
- package/dist/src/core/platform/linux.js +64 -1
- package/dist/src/core/platform/types.d.ts +3 -2
- package/dist/src/core/platform/types.js +1 -1
- package/dist/src/core/platform/win32.js +114 -1
- package/dist/src/core/sql.d.ts +2 -0
- package/dist/src/core/sql.js +4 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +34 -1
- package/dist/src/modules/autofill.d.ts +7 -15
- package/dist/src/modules/autofill.js +170 -1
- package/dist/src/modules/bookmarks.d.ts +3 -5
- package/dist/src/modules/bookmarks.js +83 -1
- package/dist/src/modules/cookies.d.ts +3 -5
- package/dist/src/modules/cookies.js +157 -1
- package/dist/src/modules/downloads.d.ts +3 -5
- package/dist/src/modules/downloads.js +80 -1
- package/dist/src/modules/extensions.d.ts +3 -5
- package/dist/src/modules/extensions.js +71 -1
- package/dist/src/modules/history.d.ts +5 -10
- package/dist/src/modules/history.js +83 -1
- package/dist/src/modules/local-storage.d.ts +3 -5
- package/dist/src/modules/local-storage.js +455 -1
- package/dist/src/modules/passwords.d.ts +3 -5
- package/dist/src/modules/passwords.js +131 -1
- package/dist/src/modules/search-engines.d.ts +3 -5
- package/dist/src/modules/search-engines.js +38 -1
- package/dist/src/modules/top-sites.d.ts +3 -5
- package/dist/src/modules/top-sites.js +33 -1
- package/package.json +14 -12
|
@@ -1 +1,455 @@
|
|
|
1
|
-
import{cpSync
|
|
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,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
profile?: string;
|
|
4
|
-
browser?: string;
|
|
1
|
+
import { type ResolveProfilesOptions } from "../core/chrome.js";
|
|
2
|
+
export interface PasswordOptions extends ResolveProfilesOptions {
|
|
5
3
|
search?: string;
|
|
6
4
|
decryptionKey?: Buffer;
|
|
7
5
|
}
|
|
@@ -25,4 +23,4 @@ export interface PasswordEntry {
|
|
|
25
23
|
* @param options - Options for profile selection and filtering.
|
|
26
24
|
* @returns Array of decrypted password entries.
|
|
27
25
|
*/
|
|
28
|
-
export declare function getPasswords({ chromeDir, profile, search, browser, decryptionKey }?: PasswordOptions): PasswordEntry[];
|
|
26
|
+
export declare function getPasswords({ chromeDir, profilePath, profile, search, browser, decryptionKey, }?: PasswordOptions): PasswordEntry[];
|
|
@@ -1 +1,131 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { 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
|
+
* Extracts and decrypts saved passwords from Chrome's password databases.
|
|
59
|
+
* Reads both the device-local and account-synced stores, labelling each entry
|
|
60
|
+
* with its `source` (no de-duplication across the two).
|
|
61
|
+
*
|
|
62
|
+
* @param options - Options for profile selection and filtering.
|
|
63
|
+
* @returns Array of decrypted password entries.
|
|
64
|
+
*/
|
|
65
|
+
export function getPasswords({ chromeDir, profilePath, profile, search, browser, decryptionKey, } = {}) {
|
|
66
|
+
const userDataDir = profilePath || chromeDir;
|
|
67
|
+
// The automation User Data directory first, then the everyday one. A Chrome
|
|
68
|
+
// driven over CDP keeps its own profile tree, so a login saved while signing
|
|
69
|
+
// in to that window is stored there and nowhere else — looking only at the
|
|
70
|
+
// everyday tree could not find it. Naming a directory or a browser answers
|
|
71
|
+
// "which tree?" outright, and is used as given.
|
|
72
|
+
const roots = [];
|
|
73
|
+
const automation = userDataDir || browser ? undefined : automationChromeDir();
|
|
74
|
+
if (automation)
|
|
75
|
+
roots.push(automation);
|
|
76
|
+
roots.push(userDataDir);
|
|
77
|
+
const results = [];
|
|
78
|
+
const term = search?.toLowerCase();
|
|
79
|
+
// A named profile missing from one tree is not a failure while another is
|
|
80
|
+
// still to come — but a profile in none of them is still an error, so the
|
|
81
|
+
// first refusal is kept and raised if no tree resolved at all.
|
|
82
|
+
let refusal;
|
|
83
|
+
let resolved = false;
|
|
84
|
+
for (const root of roots) {
|
|
85
|
+
let profiles;
|
|
86
|
+
try {
|
|
87
|
+
profiles = resolveTargetProfiles({ chromeDir: root, profile, browser });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
refusal ??= error;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
resolved = true;
|
|
94
|
+
// Per root: on Windows the key comes from that tree's own Local State.
|
|
95
|
+
let derivedKey = decryptionKey;
|
|
96
|
+
for (const p of profiles) {
|
|
97
|
+
for (const store of PASSWORD_STORES) {
|
|
98
|
+
const dbPath = join(p.path, store.file);
|
|
99
|
+
try {
|
|
100
|
+
const rows = queryDatabase(dbPath, `SELECT origin_url, username_value, password_value, date_last_used
|
|
101
|
+
FROM logins ORDER BY origin_url`);
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const entry = {
|
|
104
|
+
profile: p.name,
|
|
105
|
+
source: store.source,
|
|
106
|
+
url: row.origin_url,
|
|
107
|
+
username: row.username_value,
|
|
108
|
+
password: row.password_value?.length > 0
|
|
109
|
+
? decryptValue(row.password_value, derivedKey ??= deriveEncryptionKey(root, browser))
|
|
110
|
+
: "",
|
|
111
|
+
lastUsed: chromeTimeToDate(row.date_last_used || 0),
|
|
112
|
+
};
|
|
113
|
+
if (term) {
|
|
114
|
+
if (!(entry.url && entry.url.toLowerCase().includes(term)) &&
|
|
115
|
+
!(entry.username && entry.username.toLowerCase().includes(term))) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
results.push(entry);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Skip missing or unreadable stores and continue with the others.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!resolved && refusal)
|
|
129
|
+
throw refusal;
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
profile?: string;
|
|
4
|
-
browser?: string;
|
|
1
|
+
import { type ResolveProfilesOptions } from "../core/chrome.js";
|
|
2
|
+
export interface SearchEngineOptions extends ResolveProfilesOptions {
|
|
5
3
|
}
|
|
6
4
|
export interface SearchEngineEntry {
|
|
7
5
|
profile: string;
|
|
@@ -20,4 +18,4 @@ export interface SearchEngineEntry {
|
|
|
20
18
|
* @param options - Targeting options.
|
|
21
19
|
* @returns Array of search engine entries.
|
|
22
20
|
*/
|
|
23
|
-
export declare function getSearchEngines({ chromeDir, profile, browser }?: SearchEngineOptions): SearchEngineEntry[];
|
|
21
|
+
export declare function getSearchEngines({ chromeDir, profilePath, profile, browser, }?: SearchEngineOptions): SearchEngineEntry[];
|
|
@@ -1 +1,38 @@
|
|
|
1
|
-
import{join
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
+
import { queryDatabase } from "../core/db.js";
|
|
4
|
+
/**
|
|
5
|
+
* Retrieves search engines configured in Chrome from the Web Data database.
|
|
6
|
+
*
|
|
7
|
+
* @param options - Targeting options.
|
|
8
|
+
* @returns Array of search engine entries.
|
|
9
|
+
*/
|
|
10
|
+
export function getSearchEngines({ chromeDir, profilePath, profile, browser, } = {}) {
|
|
11
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
12
|
+
const results = [];
|
|
13
|
+
for (const p of profiles) {
|
|
14
|
+
const dbPath = join(p.path, "Web Data");
|
|
15
|
+
try {
|
|
16
|
+
const rows = queryDatabase(dbPath, `SELECT id, short_name, keyword, url, suggestions_url,
|
|
17
|
+
favicon_url, prepopulate_id, is_active
|
|
18
|
+
FROM keywords ORDER BY short_name`);
|
|
19
|
+
for (const row of rows) {
|
|
20
|
+
results.push({
|
|
21
|
+
profile: p.name,
|
|
22
|
+
id: row.id,
|
|
23
|
+
name: row.short_name || "",
|
|
24
|
+
keyword: row.keyword || "",
|
|
25
|
+
url: row.url || "",
|
|
26
|
+
suggestionsUrl: row.suggestions_url || "",
|
|
27
|
+
faviconUrl: row.favicon_url || "",
|
|
28
|
+
isDefault: row.prepopulate_id === 1,
|
|
29
|
+
isActive: row.is_active === 1,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Skip profiles with errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return results;
|
|
38
|
+
}
|