chrome-tools 1.1.14 → 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.
- package/dist/cli.js +8602 -0
- package/dist/index.d.ts +629 -0
- package/dist/index.js +2297 -0
- package/package.json +62 -29
- package/dist/bin/chrome_abe_decrypt.exe +0 -0
- package/dist/bin/cli.d.ts +0 -9
- package/dist/bin/cli.js +0 -590
- package/dist/src/core/browsers.d.ts +0 -73
- package/dist/src/core/browsers.js +0 -186
- package/dist/src/core/chrome.d.ts +0 -124
- package/dist/src/core/chrome.js +0 -259
- package/dist/src/core/crypto.d.ts +0 -52
- package/dist/src/core/crypto.js +0 -152
- package/dist/src/core/db.d.ts +0 -22
- package/dist/src/core/db.js +0 -227
- package/dist/src/core/keychain.d.ts +0 -57
- package/dist/src/core/keychain.js +0 -191
- package/dist/src/core/limit.d.ts +0 -4
- package/dist/src/core/limit.js +0 -12
- package/dist/src/core/platform/darwin.d.ts +0 -12
- package/dist/src/core/platform/darwin.js +0 -31
- package/dist/src/core/platform/index.d.ts +0 -30
- package/dist/src/core/platform/index.js +0 -69
- package/dist/src/core/platform/linux.d.ts +0 -10
- package/dist/src/core/platform/linux.js +0 -64
- package/dist/src/core/platform/types.d.ts +0 -45
- package/dist/src/core/platform/types.js +0 -1
- package/dist/src/core/platform/win32.d.ts +0 -13
- package/dist/src/core/platform/win32.js +0 -249
- package/dist/src/core/sql.d.ts +0 -2
- package/dist/src/core/sql.js +0 -4
- package/dist/src/index.d.ts +0 -28
- package/dist/src/index.js +0 -34
- package/dist/src/modules/autofill.d.ts +0 -68
- package/dist/src/modules/autofill.js +0 -170
- package/dist/src/modules/bookmarks.d.ts +0 -22
- package/dist/src/modules/bookmarks.js +0 -83
- package/dist/src/modules/cookies.d.ts +0 -40
- package/dist/src/modules/cookies.js +0 -163
- package/dist/src/modules/downloads.d.ts +0 -27
- package/dist/src/modules/downloads.js +0 -80
- package/dist/src/modules/extensions.d.ts +0 -24
- package/dist/src/modules/extensions.js +0 -71
- package/dist/src/modules/history.d.ts +0 -34
- package/dist/src/modules/history.js +0 -83
- package/dist/src/modules/local-storage.d.ts +0 -40
- package/dist/src/modules/local-storage.js +0 -455
- package/dist/src/modules/passwords.d.ts +0 -50
- package/dist/src/modules/passwords.js +0 -198
- package/dist/src/modules/search-engines.d.ts +0 -21
- package/dist/src/modules/search-engines.js +0 -38
- package/dist/src/modules/top-sites.d.ts +0 -17
- package/dist/src/modules/top-sites.js +0 -33
- package/src/native/build.bat +0 -31
- package/src/native/chrome_abe_decrypt.c +0 -336
- package/src/native/chrome_abe_decrypt.exe +0 -0
- /package/{bin → dist}/chrome_abe_decrypt.exe +0 -0
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { type ResolveProfilesOptions } from "../core/chrome.js";
|
|
2
|
-
export interface HistoryOptions extends ResolveProfilesOptions {
|
|
3
|
-
search?: string;
|
|
4
|
-
limit?: number;
|
|
5
|
-
}
|
|
6
|
-
export interface HistoryEntry {
|
|
7
|
-
profile: string;
|
|
8
|
-
url: string;
|
|
9
|
-
title: string;
|
|
10
|
-
visitCount: number;
|
|
11
|
-
lastVisit: string;
|
|
12
|
-
}
|
|
13
|
-
export interface SearchTermOptions extends ResolveProfilesOptions {
|
|
14
|
-
limit?: number;
|
|
15
|
-
}
|
|
16
|
-
export interface SearchTermEntry {
|
|
17
|
-
profile: string;
|
|
18
|
-
term: string;
|
|
19
|
-
url: string;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Retrieves browsing history from Chrome's History database.
|
|
23
|
-
*
|
|
24
|
-
* @param options - Filtering and targeting options.
|
|
25
|
-
* @returns Array of history entries.
|
|
26
|
-
*/
|
|
27
|
-
export declare function getHistory({ chromeDir, profilePath, profile, search, limit, browser, }?: HistoryOptions): HistoryEntry[];
|
|
28
|
-
/**
|
|
29
|
-
* Retrieves search terms from Chrome's History database.
|
|
30
|
-
*
|
|
31
|
-
* @param options - Targeting and limit options.
|
|
32
|
-
* @returns Array of search term entries.
|
|
33
|
-
*/
|
|
34
|
-
export declare function getSearchTerms({ chromeDir, profilePath, profile, limit, browser, }?: SearchTermOptions): SearchTermEntry[];
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
-
import { chromeTimeToDate } from "../core/crypto.js";
|
|
4
|
-
import { queryDatabase } from "../core/db.js";
|
|
5
|
-
import { limitResults, normalizeLimit } from "../core/limit.js";
|
|
6
|
-
import { sqlLikePattern } from "../core/sql.js";
|
|
7
|
-
/**
|
|
8
|
-
* Retrieves browsing history from Chrome's History database.
|
|
9
|
-
*
|
|
10
|
-
* @param options - Filtering and targeting options.
|
|
11
|
-
* @returns Array of history entries.
|
|
12
|
-
*/
|
|
13
|
-
export function getHistory({ chromeDir, profilePath, profile, search, limit = 100, browser, } = {}) {
|
|
14
|
-
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
15
|
-
const results = [];
|
|
16
|
-
const rowLimit = normalizeLimit(limit);
|
|
17
|
-
for (const p of profiles) {
|
|
18
|
-
const dbPath = join(p.path, "History");
|
|
19
|
-
try {
|
|
20
|
-
// Apply search in SQL before LIMIT so older matches are not truncated away.
|
|
21
|
-
let sql = `SELECT url, title, visit_count, last_visit_time FROM urls`;
|
|
22
|
-
const params = [];
|
|
23
|
-
if (search) {
|
|
24
|
-
sql += ` WHERE url LIKE ? ESCAPE '\\' OR IFNULL(title, '') LIKE ? ESCAPE '\\'`;
|
|
25
|
-
const like = sqlLikePattern(search);
|
|
26
|
-
params.push(like, like);
|
|
27
|
-
}
|
|
28
|
-
sql += ` ORDER BY last_visit_time DESC`;
|
|
29
|
-
if (rowLimit) {
|
|
30
|
-
sql += ` LIMIT ${rowLimit}`;
|
|
31
|
-
}
|
|
32
|
-
const rows = queryDatabase(dbPath, sql, params);
|
|
33
|
-
for (const row of rows) {
|
|
34
|
-
results.push({
|
|
35
|
-
profile: p.name,
|
|
36
|
-
url: row.url,
|
|
37
|
-
title: row.title || "",
|
|
38
|
-
visitCount: row.visit_count || 0,
|
|
39
|
-
lastVisit: chromeTimeToDate(row.last_visit_time || 0),
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Skip profiles with errors
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return limitResults(results, limit);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Retrieves search terms from Chrome's History database.
|
|
51
|
-
*
|
|
52
|
-
* @param options - Targeting and limit options.
|
|
53
|
-
* @returns Array of search term entries.
|
|
54
|
-
*/
|
|
55
|
-
export function getSearchTerms({ chromeDir, profilePath, profile, limit = 100, browser, } = {}) {
|
|
56
|
-
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
57
|
-
const results = [];
|
|
58
|
-
const rowLimit = normalizeLimit(limit);
|
|
59
|
-
for (const p of profiles) {
|
|
60
|
-
const dbPath = join(p.path, "History");
|
|
61
|
-
try {
|
|
62
|
-
let sql = `SELECT kst.term, u.url
|
|
63
|
-
FROM keyword_search_terms kst
|
|
64
|
-
JOIN urls u ON kst.url_id = u.id
|
|
65
|
-
ORDER BY u.last_visit_time DESC`;
|
|
66
|
-
if (rowLimit) {
|
|
67
|
-
sql += ` LIMIT ${rowLimit}`;
|
|
68
|
-
}
|
|
69
|
-
const rows = queryDatabase(dbPath, sql);
|
|
70
|
-
for (const row of rows) {
|
|
71
|
-
results.push({
|
|
72
|
-
profile: p.name,
|
|
73
|
-
term: row.term || "",
|
|
74
|
-
url: row.url || "",
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Skip profiles with errors
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return limitResults(results, limit);
|
|
83
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { type ResolveProfilesOptions } from "../core/chrome.js";
|
|
2
|
-
export interface LocalStorageOptions extends ResolveProfilesOptions {
|
|
3
|
-
origin?: string;
|
|
4
|
-
url?: string;
|
|
5
|
-
key?: string;
|
|
6
|
-
search?: string;
|
|
7
|
-
limit?: number;
|
|
8
|
-
}
|
|
9
|
-
export interface LocalStorageEntry {
|
|
10
|
-
profile: string;
|
|
11
|
-
origin: string;
|
|
12
|
-
key: string;
|
|
13
|
-
value: string;
|
|
14
|
-
}
|
|
15
|
-
interface RawLevelDBRecord {
|
|
16
|
-
key: Buffer;
|
|
17
|
-
value?: Buffer;
|
|
18
|
-
sequence: bigint;
|
|
19
|
-
deleted: boolean;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Reads localStorage key/value pairs from Chrome's Local Storage LevelDB store.
|
|
23
|
-
*
|
|
24
|
-
* @param options - Targeting and filtering options.
|
|
25
|
-
* @returns Array of localStorage entries.
|
|
26
|
-
*/
|
|
27
|
-
export declare function getLocalStorage({ chromeDir, profilePath, profile, browser, origin, url, key, search, limit, }?: LocalStorageOptions): LocalStorageEntry[];
|
|
28
|
-
declare function parseLocalStorageRecord(rawKey: Buffer, rawValue: Buffer): {
|
|
29
|
-
origin: string;
|
|
30
|
-
key: string;
|
|
31
|
-
value: string;
|
|
32
|
-
} | null;
|
|
33
|
-
declare function decodeLocalStorageString(data: Buffer): string;
|
|
34
|
-
declare function parseLogRecords(data: Buffer): RawLevelDBRecord[];
|
|
35
|
-
export declare const _test: {
|
|
36
|
-
decodeLocalStorageString: typeof decodeLocalStorageString;
|
|
37
|
-
parseLocalStorageRecord: typeof parseLocalStorageRecord;
|
|
38
|
-
parseLogRecords: typeof parseLogRecords;
|
|
39
|
-
};
|
|
40
|
-
export {};
|
|
@@ -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[];
|