chrome-tools 1.1.5 → 1.1.7
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.d.ts +10 -0
- 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 +2 -2
- package/dist/src/index.js +34 -1
- package/dist/src/modules/autofill.d.ts +8 -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 +4 -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 +4 -5
- package/dist/src/modules/passwords.js +64 -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,8 +1,7 @@
|
|
|
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;
|
|
4
|
+
decryptionKey?: Buffer;
|
|
6
5
|
}
|
|
7
6
|
/** Which on-disk store a password came from. */
|
|
8
7
|
export type PasswordSource = "device" | "account";
|
|
@@ -24,4 +23,4 @@ export interface PasswordEntry {
|
|
|
24
23
|
* @param options - Options for profile selection and filtering.
|
|
25
24
|
* @returns Array of decrypted password entries.
|
|
26
25
|
*/
|
|
27
|
-
export declare function getPasswords({ chromeDir, profile, search, browser }?: PasswordOptions): PasswordEntry[];
|
|
26
|
+
export declare function getPasswords({ chromeDir, profilePath, profile, search, browser, decryptionKey, }?: PasswordOptions): PasswordEntry[];
|
|
@@ -1 +1,64 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
+
import { deriveEncryptionKey, decryptValue } from "../core/platform/index.js";
|
|
4
|
+
import { chromeTimeToDate } from "../core/crypto.js";
|
|
5
|
+
import { queryDatabase } from "../core/db.js";
|
|
6
|
+
/**
|
|
7
|
+
* Chrome stores saved passwords in two SQLite files per profile:
|
|
8
|
+
* - "Login Data" — passwords kept on this device only.
|
|
9
|
+
* - "Login Data For Account" — passwords synced to the signed-in Google Account.
|
|
10
|
+
* Both share the same `logins` schema and v10 encryption, so we read both and
|
|
11
|
+
* tag each entry with its source.
|
|
12
|
+
*/
|
|
13
|
+
const PASSWORD_STORES = [
|
|
14
|
+
{ file: "Login Data", source: "device" },
|
|
15
|
+
{ file: "Login Data For Account", source: "account" },
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Extracts and decrypts saved passwords from Chrome's password databases.
|
|
19
|
+
* Reads both the device-local and account-synced stores, labelling each entry
|
|
20
|
+
* with its `source` (no de-duplication across the two).
|
|
21
|
+
*
|
|
22
|
+
* @param options - Options for profile selection and filtering.
|
|
23
|
+
* @returns Array of decrypted password entries.
|
|
24
|
+
*/
|
|
25
|
+
export function getPasswords({ chromeDir, profilePath, profile, search, browser, decryptionKey, } = {}) {
|
|
26
|
+
const userDataDir = profilePath || chromeDir;
|
|
27
|
+
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
28
|
+
// Pass user-data dir so Windows reads Local State from the same tree as the profiles.
|
|
29
|
+
let derivedKey = decryptionKey;
|
|
30
|
+
const results = [];
|
|
31
|
+
const term = search?.toLowerCase();
|
|
32
|
+
for (const p of profiles) {
|
|
33
|
+
for (const store of PASSWORD_STORES) {
|
|
34
|
+
const dbPath = join(p.path, store.file);
|
|
35
|
+
try {
|
|
36
|
+
const rows = queryDatabase(dbPath, `SELECT origin_url, username_value, password_value, date_last_used
|
|
37
|
+
FROM logins ORDER BY origin_url`);
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
const entry = {
|
|
40
|
+
profile: p.name,
|
|
41
|
+
source: store.source,
|
|
42
|
+
url: row.origin_url,
|
|
43
|
+
username: row.username_value,
|
|
44
|
+
password: row.password_value?.length > 0
|
|
45
|
+
? decryptValue(row.password_value, derivedKey ??= deriveEncryptionKey(userDataDir, browser))
|
|
46
|
+
: "",
|
|
47
|
+
lastUsed: chromeTimeToDate(row.date_last_used || 0),
|
|
48
|
+
};
|
|
49
|
+
if (term) {
|
|
50
|
+
if (!(entry.url && entry.url.toLowerCase().includes(term)) &&
|
|
51
|
+
!(entry.username && entry.username.toLowerCase().includes(term))) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
results.push(entry);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Skip missing or unreadable stores and continue with the others.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
profile?: string;
|
|
4
|
-
browser?: string;
|
|
1
|
+
import { type ResolveProfilesOptions } from "../core/chrome.js";
|
|
2
|
+
export interface TopSiteOptions extends ResolveProfilesOptions {
|
|
5
3
|
}
|
|
6
4
|
export interface TopSiteEntry {
|
|
7
5
|
profile: string;
|
|
@@ -16,4 +14,4 @@ export interface TopSiteEntry {
|
|
|
16
14
|
* @param options - Targeting options.
|
|
17
15
|
* @returns Array of top site entries.
|
|
18
16
|
*/
|
|
19
|
-
export declare function getTopSites({ chromeDir, profile, browser }?: TopSiteOptions): TopSiteEntry[];
|
|
17
|
+
export declare function getTopSites({ chromeDir, profilePath, profile, browser }?: TopSiteOptions): TopSiteEntry[];
|
|
@@ -1 +1,33 @@
|
|
|
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 top sites from Chrome's Top Sites database.
|
|
6
|
+
*
|
|
7
|
+
* @param options - Targeting options.
|
|
8
|
+
* @returns Array of top site entries.
|
|
9
|
+
*/
|
|
10
|
+
export function getTopSites({ 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, "Top Sites");
|
|
15
|
+
try {
|
|
16
|
+
const rows = queryDatabase(dbPath, `SELECT url, url_rank, title, redirects
|
|
17
|
+
FROM top_sites ORDER BY url_rank ASC`);
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
results.push({
|
|
20
|
+
profile: p.name,
|
|
21
|
+
url: row.url || "",
|
|
22
|
+
title: row.title || "",
|
|
23
|
+
rank: row.url_rank ?? 0,
|
|
24
|
+
redirects: row.redirects || "",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Skip profiles with errors
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|