chrome-tools 1.0.4 → 1.0.6
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/README.md +24 -23
- package/dist/bin/cli.d.ts +8 -0
- package/dist/bin/cli.js +543 -0
- package/dist/src/core/chrome.d.ts +77 -0
- package/dist/src/core/chrome.js +159 -0
- package/dist/src/core/crypto.d.ts +26 -0
- package/dist/src/core/crypto.js +69 -0
- package/dist/src/core/db.d.ts +19 -0
- package/dist/src/core/db.js +78 -0
- package/dist/src/core/keychain.d.ts +20 -0
- package/dist/src/core/keychain.js +53 -0
- package/dist/src/index.d.ts +25 -0
- package/{src → dist/src}/index.js +4 -26
- package/dist/src/modules/autofill.d.ts +72 -0
- package/dist/src/modules/autofill.js +178 -0
- package/dist/src/modules/bookmarks.d.ts +23 -0
- package/dist/src/modules/bookmarks.js +92 -0
- package/dist/src/modules/cookies.d.ts +40 -0
- package/dist/src/modules/cookies.js +159 -0
- package/dist/src/modules/downloads.d.ts +28 -0
- package/dist/src/modules/downloads.js +78 -0
- package/dist/src/modules/extensions.d.ts +25 -0
- package/dist/src/modules/extensions.js +70 -0
- package/dist/src/modules/history.d.ts +37 -0
- package/dist/src/modules/history.js +84 -0
- package/dist/src/modules/passwords.d.ts +20 -0
- package/dist/src/modules/passwords.js +50 -0
- package/dist/src/modules/search-engines.d.ts +22 -0
- package/dist/src/modules/search-engines.js +40 -0
- package/dist/src/modules/top-sites.d.ts +18 -0
- package/dist/src/modules/top-sites.js +35 -0
- package/package.json +24 -18
- package/bin/cli.js +0 -595
- package/src/core/chrome.js +0 -177
- package/src/core/crypto.js +0 -75
- package/src/core/db.js +0 -69
- package/src/core/keychain.js +0 -68
- package/src/modules/autofill.js +0 -213
- package/src/modules/bookmarks.js +0 -109
- package/src/modules/cookies.js +0 -178
- package/src/modules/downloads.js +0 -91
- package/src/modules/extensions.js +0 -80
- package/src/modules/history.js +0 -102
- package/src/modules/passwords.js +0 -61
- package/src/modules/search-engines.js +0 -48
- package/src/modules/top-sites.js +0 -43
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
+
import { deriveEncryptionKey } from "../core/keychain.js";
|
|
4
|
+
import { decryptValue, chromeTimeToDate } from "../core/crypto.js";
|
|
5
|
+
import { queryDatabase, tableExists } from "../core/db.js";
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves autofill entries from Chrome's Web Data database.
|
|
8
|
+
*
|
|
9
|
+
* @param options - Filtering and targeting options.
|
|
10
|
+
* @returns Array of autofill entries.
|
|
11
|
+
*/
|
|
12
|
+
export function getAutofillEntries({ chromeDir, profile, search, limit = 100, } = {}) {
|
|
13
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
14
|
+
const results = [];
|
|
15
|
+
for (const p of profiles) {
|
|
16
|
+
const dbPath = join(p.path, "Web Data");
|
|
17
|
+
try {
|
|
18
|
+
if (!tableExists(dbPath, "autofill"))
|
|
19
|
+
continue;
|
|
20
|
+
let sql = `SELECT name, value, date_created, date_last_used, count
|
|
21
|
+
FROM autofill ORDER BY date_last_used DESC`;
|
|
22
|
+
if (limit) {
|
|
23
|
+
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
24
|
+
}
|
|
25
|
+
const rows = queryDatabase(dbPath, sql);
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
const entry = {
|
|
28
|
+
profile: p.name,
|
|
29
|
+
name: row.name || "",
|
|
30
|
+
value: row.value || "",
|
|
31
|
+
dateCreated: chromeTimeToDate(row.date_created ? BigInt(row.date_created) * 1000000n + 11644473600000000n : 0),
|
|
32
|
+
dateLastUsed: chromeTimeToDate(row.date_last_used ? BigInt(row.date_last_used) * 1000000n + 11644473600000000n : 0),
|
|
33
|
+
count: row.count || 0,
|
|
34
|
+
};
|
|
35
|
+
if (search) {
|
|
36
|
+
const term = search.toLowerCase();
|
|
37
|
+
if (!(entry.name && entry.name.toLowerCase().includes(term)) &&
|
|
38
|
+
!(entry.value && entry.value.toLowerCase().includes(term))) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
results.push(entry);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Skip profiles with errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Retrieves saved credit cards from Chrome's Web Data database.
|
|
53
|
+
* Card numbers are encrypted and will be decrypted.
|
|
54
|
+
*
|
|
55
|
+
* @param options - Targeting options.
|
|
56
|
+
* @returns Array of credit card entries.
|
|
57
|
+
*/
|
|
58
|
+
export function getCreditCards({ chromeDir, profile } = {}) {
|
|
59
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
60
|
+
const derivedKey = deriveEncryptionKey();
|
|
61
|
+
const results = [];
|
|
62
|
+
for (const p of profiles) {
|
|
63
|
+
const dbPath = join(p.path, "Web Data");
|
|
64
|
+
try {
|
|
65
|
+
if (!tableExists(dbPath, "credit_cards"))
|
|
66
|
+
continue;
|
|
67
|
+
const rows = queryDatabase(dbPath, `SELECT guid, name_on_card, expiration_month, expiration_year,
|
|
68
|
+
card_number_encrypted, date_modified, origin, use_count,
|
|
69
|
+
use_date, nickname
|
|
70
|
+
FROM credit_cards ORDER BY use_date DESC`);
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
let cardNumber = "";
|
|
73
|
+
if (row.card_number_encrypted) {
|
|
74
|
+
cardNumber = decryptValue(row.card_number_encrypted, derivedKey);
|
|
75
|
+
}
|
|
76
|
+
results.push({
|
|
77
|
+
profile: p.name,
|
|
78
|
+
guid: row.guid || "",
|
|
79
|
+
nameOnCard: row.name_on_card || "",
|
|
80
|
+
expirationMonth: row.expiration_month || 0,
|
|
81
|
+
expirationYear: row.expiration_year || 0,
|
|
82
|
+
cardNumber,
|
|
83
|
+
nickname: row.nickname || "",
|
|
84
|
+
origin: row.origin || "",
|
|
85
|
+
useCount: row.use_count || 0,
|
|
86
|
+
dateModified: chromeTimeToDate(row.date_modified ? BigInt(row.date_modified) * 1000000n + 11644473600000000n : 0),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Skip profiles with errors
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Retrieves saved addresses from Chrome's Web Data database.
|
|
98
|
+
*
|
|
99
|
+
* @param options - Targeting options.
|
|
100
|
+
* @returns Array of address entries.
|
|
101
|
+
*/
|
|
102
|
+
export function getAddresses({ chromeDir, profile } = {}) {
|
|
103
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
104
|
+
const results = [];
|
|
105
|
+
for (const p of profiles) {
|
|
106
|
+
const dbPath = join(p.path, "Web Data");
|
|
107
|
+
try {
|
|
108
|
+
if (!tableExists(dbPath, "autofill_profiles"))
|
|
109
|
+
continue;
|
|
110
|
+
const profileRows = queryDatabase(dbPath, `SELECT guid, company_name, street_address, dependent_locality,
|
|
111
|
+
city, state, zipcode, country_code, date_modified,
|
|
112
|
+
origin, use_count, use_date, language_code
|
|
113
|
+
FROM autofill_profiles ORDER BY use_date DESC`);
|
|
114
|
+
// Try to join with names, emails, and phones
|
|
115
|
+
let nameRows = [];
|
|
116
|
+
let emailRows = [];
|
|
117
|
+
let phoneRows = [];
|
|
118
|
+
try {
|
|
119
|
+
if (tableExists(dbPath, "autofill_profile_names")) {
|
|
120
|
+
nameRows = queryDatabase(dbPath, `SELECT guid, first_name, middle_name, last_name, full_name FROM autofill_profile_names`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
if (tableExists(dbPath, "autofill_profile_emails")) {
|
|
128
|
+
emailRows = queryDatabase(dbPath, `SELECT guid, email FROM autofill_profile_emails`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* ignore */
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if (tableExists(dbPath, "autofill_profile_phones")) {
|
|
136
|
+
phoneRows = queryDatabase(dbPath, `SELECT guid, number FROM autofill_profile_phones`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* ignore */
|
|
141
|
+
}
|
|
142
|
+
const nameMap = new Map();
|
|
143
|
+
for (const n of nameRows) {
|
|
144
|
+
nameMap.set(n.guid, n.full_name || [n.first_name, n.middle_name, n.last_name].filter(Boolean).join(" "));
|
|
145
|
+
}
|
|
146
|
+
const emailMap = new Map();
|
|
147
|
+
for (const e of emailRows) {
|
|
148
|
+
emailMap.set(e.guid, e.email || "");
|
|
149
|
+
}
|
|
150
|
+
const phoneMap = new Map();
|
|
151
|
+
for (const ph of phoneRows) {
|
|
152
|
+
phoneMap.set(ph.guid, ph.number || "");
|
|
153
|
+
}
|
|
154
|
+
for (const row of profileRows) {
|
|
155
|
+
results.push({
|
|
156
|
+
profile: p.name,
|
|
157
|
+
guid: row.guid || "",
|
|
158
|
+
fullName: nameMap.get(row.guid) || "",
|
|
159
|
+
email: emailMap.get(row.guid) || "",
|
|
160
|
+
phone: phoneMap.get(row.guid) || "",
|
|
161
|
+
companyName: row.company_name || "",
|
|
162
|
+
streetAddress: row.street_address || "",
|
|
163
|
+
city: row.city || "",
|
|
164
|
+
state: row.state || "",
|
|
165
|
+
zipcode: row.zipcode || "",
|
|
166
|
+
countryCode: row.country_code || "",
|
|
167
|
+
origin: row.origin || "",
|
|
168
|
+
useCount: row.use_count || 0,
|
|
169
|
+
dateModified: chromeTimeToDate(row.date_modified ? BigInt(row.date_modified) * 1000000n + 11644473600000000n : 0),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Skip profiles with errors
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BookmarkOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
folder?: string;
|
|
5
|
+
search?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BookmarkEntry {
|
|
8
|
+
profile?: string;
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
folder: string;
|
|
12
|
+
dateAdded: string;
|
|
13
|
+
dateLastUsed: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
guid?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads and parses bookmarks from Chrome's Bookmarks JSON file.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Options for filtering and targeting.
|
|
21
|
+
* @returns Filtered bookmark entries.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getBookmarks({ chromeDir, profile, folder, search }?: BookmarkOptions): BookmarkEntry[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
4
|
+
import { chromeTimeToDate } from "../core/crypto.js";
|
|
5
|
+
/**
|
|
6
|
+
* Converts a Chrome bookmark timestamp (microseconds since 1601-01-01) to a date string.
|
|
7
|
+
* Bookmark timestamps use the same epoch as Chrome timestamps but are stored as strings.
|
|
8
|
+
*/
|
|
9
|
+
function bookmarkTimeToDate(timestamp) {
|
|
10
|
+
if (!timestamp || timestamp === "0")
|
|
11
|
+
return "N/A";
|
|
12
|
+
return chromeTimeToDate(BigInt(timestamp));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Recursively flattens the bookmark tree into a flat array.
|
|
16
|
+
*
|
|
17
|
+
* @param node - Bookmark tree node.
|
|
18
|
+
* @param folderPath - Current folder path for context.
|
|
19
|
+
* @returns Array of flattened bookmark entries.
|
|
20
|
+
*/
|
|
21
|
+
function flattenBookmarks(node, folderPath = "") {
|
|
22
|
+
const results = [];
|
|
23
|
+
if (!node)
|
|
24
|
+
return results;
|
|
25
|
+
const currentPath = folderPath
|
|
26
|
+
? `${folderPath}/${node.name || ""}`
|
|
27
|
+
: node.name || "";
|
|
28
|
+
if (node.type === "url") {
|
|
29
|
+
results.push({
|
|
30
|
+
title: node.name || "",
|
|
31
|
+
url: node.url || "",
|
|
32
|
+
folder: folderPath,
|
|
33
|
+
dateAdded: bookmarkTimeToDate(node.date_added),
|
|
34
|
+
dateLastUsed: bookmarkTimeToDate(node.date_last_used),
|
|
35
|
+
id: node.id,
|
|
36
|
+
guid: node.guid || "",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (node.children && Array.isArray(node.children)) {
|
|
40
|
+
for (const child of node.children) {
|
|
41
|
+
results.push(...flattenBookmarks(child, currentPath));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reads and parses bookmarks from Chrome's Bookmarks JSON file.
|
|
48
|
+
*
|
|
49
|
+
* @param options - Options for filtering and targeting.
|
|
50
|
+
* @returns Filtered bookmark entries.
|
|
51
|
+
*/
|
|
52
|
+
export function getBookmarks({ chromeDir, profile, folder, search } = {}) {
|
|
53
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
54
|
+
const results = [];
|
|
55
|
+
for (const p of profiles) {
|
|
56
|
+
const bookmarksPath = join(p.path, "Bookmarks");
|
|
57
|
+
if (!existsSync(bookmarksPath))
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const raw = readFileSync(bookmarksPath, "utf-8");
|
|
61
|
+
const data = JSON.parse(raw);
|
|
62
|
+
const roots = data.roots;
|
|
63
|
+
if (!roots)
|
|
64
|
+
continue;
|
|
65
|
+
// Flatten all root bookmark folders
|
|
66
|
+
const rootNames = ["bookmark_bar", "other", "synced"];
|
|
67
|
+
for (const rootName of rootNames) {
|
|
68
|
+
if (roots[rootName]) {
|
|
69
|
+
const bookmarks = flattenBookmarks(roots[rootName], "");
|
|
70
|
+
for (const bm of bookmarks) {
|
|
71
|
+
bm.profile = p.name;
|
|
72
|
+
results.push(bm);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Skip profiles with parse errors
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let filtered = results;
|
|
82
|
+
if (folder) {
|
|
83
|
+
const term = folder.toLowerCase();
|
|
84
|
+
filtered = filtered.filter((b) => b.folder.toLowerCase().includes(term));
|
|
85
|
+
}
|
|
86
|
+
if (search) {
|
|
87
|
+
const term = search.toLowerCase();
|
|
88
|
+
filtered = filtered.filter((b) => b.title.toLowerCase().includes(term) ||
|
|
89
|
+
b.url.toLowerCase().includes(term));
|
|
90
|
+
}
|
|
91
|
+
return filtered;
|
|
92
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface CookieOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
domain?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
decrypt?: boolean;
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CookieEntry {
|
|
10
|
+
profile: string;
|
|
11
|
+
domain: string;
|
|
12
|
+
name: string;
|
|
13
|
+
value: string;
|
|
14
|
+
path: string;
|
|
15
|
+
expires: string;
|
|
16
|
+
secure: boolean;
|
|
17
|
+
httpOnly: boolean;
|
|
18
|
+
sameSite: string;
|
|
19
|
+
created: string;
|
|
20
|
+
lastAccessed: string;
|
|
21
|
+
}
|
|
22
|
+
declare function parseURL(urlStr?: string): URL | null;
|
|
23
|
+
declare function getPossibleHostKeys(hostname: string): string[];
|
|
24
|
+
declare function domainMatch(hostname: string, hostKey: string): boolean;
|
|
25
|
+
declare function pathMatch(urlPath: string, cookiePath: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Extracts cookies from Chrome's Cookies database.
|
|
28
|
+
* Encrypted values are decrypted using the same mechanism as passwords.
|
|
29
|
+
*
|
|
30
|
+
* @param options - Targeting and filtering options.
|
|
31
|
+
* @returns Array of cookie entries.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getCookies({ chromeDir, profile, domain, url, decrypt, limit, }?: CookieOptions): CookieEntry[];
|
|
34
|
+
export declare const _test: {
|
|
35
|
+
parseURL: typeof parseURL;
|
|
36
|
+
getPossibleHostKeys: typeof getPossibleHostKeys;
|
|
37
|
+
domainMatch: typeof domainMatch;
|
|
38
|
+
pathMatch: typeof pathMatch;
|
|
39
|
+
};
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
4
|
+
import { deriveEncryptionKey } from "../core/keychain.js";
|
|
5
|
+
import { decryptValueBuffer, chromeTimeToDate } from "../core/crypto.js";
|
|
6
|
+
import { queryDatabase, tableExists } from "../core/db.js";
|
|
7
|
+
function decodeCookieValue(encryptedValue, derivedKey, hostKey) {
|
|
8
|
+
const decrypted = decryptValueBuffer(encryptedValue, derivedKey);
|
|
9
|
+
if (!Buffer.isBuffer(decrypted)) {
|
|
10
|
+
return decrypted;
|
|
11
|
+
}
|
|
12
|
+
const hostHash = createHash("sha256").update(hostKey).digest();
|
|
13
|
+
const valueBytes = decrypted.length >= hostHash.length && decrypted.subarray(0, hostHash.length).equals(hostHash)
|
|
14
|
+
? decrypted.subarray(hostHash.length)
|
|
15
|
+
: decrypted;
|
|
16
|
+
return valueBytes.toString("utf-8");
|
|
17
|
+
}
|
|
18
|
+
function parseURL(urlStr) {
|
|
19
|
+
if (!urlStr)
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
return new URL(urlStr);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
try {
|
|
26
|
+
// If it doesn't have a protocol, prepend https://
|
|
27
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(urlStr)) {
|
|
28
|
+
return new URL("https://" + urlStr);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function getPossibleHostKeys(hostname) {
|
|
38
|
+
const keys = new Set();
|
|
39
|
+
keys.add(hostname);
|
|
40
|
+
keys.add("." + hostname);
|
|
41
|
+
const parts = hostname.split(".");
|
|
42
|
+
for (let i = 1; i < parts.length; i++) {
|
|
43
|
+
const suffix = parts.slice(i).join(".");
|
|
44
|
+
keys.add(suffix);
|
|
45
|
+
keys.add("." + suffix);
|
|
46
|
+
}
|
|
47
|
+
return Array.from(keys);
|
|
48
|
+
}
|
|
49
|
+
function domainMatch(hostname, hostKey) {
|
|
50
|
+
if (hostname === hostKey) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (hostKey.startsWith(".")) {
|
|
54
|
+
if (hostname === hostKey.slice(1) || hostname.endsWith(hostKey)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function pathMatch(urlPath, cookiePath) {
|
|
61
|
+
const uPath = urlPath || "/";
|
|
62
|
+
const cPath = cookiePath || "/";
|
|
63
|
+
if (cPath === "/") {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (uPath === cPath) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (uPath.startsWith(cPath)) {
|
|
70
|
+
if (cPath.endsWith("/") || uPath[cPath.length] === "/") {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extracts cookies from Chrome's Cookies database.
|
|
78
|
+
* Encrypted values are decrypted using the same mechanism as passwords.
|
|
79
|
+
*
|
|
80
|
+
* @param options - Targeting and filtering options.
|
|
81
|
+
* @returns Array of cookie entries.
|
|
82
|
+
*/
|
|
83
|
+
export function getCookies({ chromeDir, profile, domain, url, decrypt = true, limit = 100, } = {}) {
|
|
84
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
85
|
+
const derivedKey = decrypt ? deriveEncryptionKey() : null;
|
|
86
|
+
const results = [];
|
|
87
|
+
const parsedUrl = parseURL(url);
|
|
88
|
+
for (const p of profiles) {
|
|
89
|
+
const dbPath = join(p.path, "Cookies");
|
|
90
|
+
try {
|
|
91
|
+
if (!tableExists(dbPath, "cookies"))
|
|
92
|
+
continue;
|
|
93
|
+
let sql = `SELECT host_key, name, value, encrypted_value, path,
|
|
94
|
+
expires_utc, is_secure, is_httponly, samesite,
|
|
95
|
+
creation_utc, last_access_utc
|
|
96
|
+
FROM cookies`;
|
|
97
|
+
const conditions = [];
|
|
98
|
+
const params = [];
|
|
99
|
+
if (domain) {
|
|
100
|
+
conditions.push(`host_key LIKE ?`);
|
|
101
|
+
params.push(`%${domain}%`);
|
|
102
|
+
}
|
|
103
|
+
else if (parsedUrl) {
|
|
104
|
+
const hostKeys = getPossibleHostKeys(parsedUrl.hostname);
|
|
105
|
+
const placeholders = hostKeys.map(() => "?").join(", ");
|
|
106
|
+
conditions.push(`host_key IN (${placeholders})`);
|
|
107
|
+
params.push(...hostKeys);
|
|
108
|
+
}
|
|
109
|
+
if (conditions.length > 0) {
|
|
110
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
111
|
+
}
|
|
112
|
+
sql += ` ORDER BY host_key, name`;
|
|
113
|
+
if (limit && !url) {
|
|
114
|
+
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
115
|
+
}
|
|
116
|
+
const rows = queryDatabase(dbPath, sql, params);
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
if (parsedUrl) {
|
|
119
|
+
if (!domainMatch(parsedUrl.hostname, row.host_key || ""))
|
|
120
|
+
continue;
|
|
121
|
+
if (!pathMatch(parsedUrl.pathname || "/", row.path || "/"))
|
|
122
|
+
continue;
|
|
123
|
+
if (row.is_secure && parsedUrl.protocol !== "https:")
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
let cookieValue = row.value || "";
|
|
127
|
+
if (!cookieValue && row.encrypted_value && derivedKey) {
|
|
128
|
+
cookieValue = decodeCookieValue(row.encrypted_value, derivedKey, row.host_key || "");
|
|
129
|
+
}
|
|
130
|
+
results.push({
|
|
131
|
+
profile: p.name,
|
|
132
|
+
domain: row.host_key || "",
|
|
133
|
+
name: row.name || "",
|
|
134
|
+
value: cookieValue,
|
|
135
|
+
path: row.path || "/",
|
|
136
|
+
expires: chromeTimeToDate(row.expires_utc || 0),
|
|
137
|
+
secure: Boolean(row.is_secure),
|
|
138
|
+
httpOnly: Boolean(row.is_httponly),
|
|
139
|
+
sameSite: ["unspecified", "lax", "strict", "none"][row.samesite] || "unspecified",
|
|
140
|
+
created: chromeTimeToDate(row.creation_utc || 0),
|
|
141
|
+
lastAccessed: chromeTimeToDate(row.last_access_utc || 0),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Skip profiles with errors
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (limit && url) {
|
|
150
|
+
return results.slice(0, parseInt(limit, 10));
|
|
151
|
+
}
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
export const _test = {
|
|
155
|
+
parseURL,
|
|
156
|
+
getPossibleHostKeys,
|
|
157
|
+
domainMatch,
|
|
158
|
+
pathMatch,
|
|
159
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface DownloadOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
search?: string;
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DownloadEntry {
|
|
8
|
+
profile: string;
|
|
9
|
+
targetPath: string;
|
|
10
|
+
url: string;
|
|
11
|
+
siteUrl: string;
|
|
12
|
+
referrer: string;
|
|
13
|
+
totalBytes: number;
|
|
14
|
+
receivedBytes: number;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
state: string;
|
|
17
|
+
dangerType: string;
|
|
18
|
+
opened: boolean;
|
|
19
|
+
startTime: string;
|
|
20
|
+
endTime: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Retrieves download history from Chrome's History database.
|
|
24
|
+
*
|
|
25
|
+
* @param options - Targeting, filtering, and limit options.
|
|
26
|
+
* @returns Array of download entries.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getDownloads({ chromeDir, profile, search, limit, }?: DownloadOptions): DownloadEntry[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
+
import { chromeTimeToDate } from "../core/crypto.js";
|
|
4
|
+
import { queryDatabase, tableExists } from "../core/db.js";
|
|
5
|
+
/** Maps Chrome download state integers to human-readable strings. */
|
|
6
|
+
const DOWNLOAD_STATES = {
|
|
7
|
+
0: "in_progress",
|
|
8
|
+
1: "complete",
|
|
9
|
+
2: "cancelled",
|
|
10
|
+
3: "interrupted",
|
|
11
|
+
};
|
|
12
|
+
/** Maps Chrome download danger type integers to strings. */
|
|
13
|
+
const DANGER_TYPES = {
|
|
14
|
+
0: "not_dangerous",
|
|
15
|
+
1: "dangerous_file",
|
|
16
|
+
2: "dangerous_url",
|
|
17
|
+
3: "dangerous_content",
|
|
18
|
+
4: "maybe_dangerous_content",
|
|
19
|
+
5: "uncommon_content",
|
|
20
|
+
6: "user_validated",
|
|
21
|
+
7: "dangerous_host",
|
|
22
|
+
8: "potentially_unwanted",
|
|
23
|
+
9: "allowlisted_by_policy",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Retrieves download history from Chrome's History database.
|
|
27
|
+
*
|
|
28
|
+
* @param options - Targeting, filtering, and limit options.
|
|
29
|
+
* @returns Array of download entries.
|
|
30
|
+
*/
|
|
31
|
+
export function getDownloads({ chromeDir, profile, search, limit = 100, } = {}) {
|
|
32
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const p of profiles) {
|
|
35
|
+
const dbPath = join(p.path, "History");
|
|
36
|
+
try {
|
|
37
|
+
if (!tableExists(dbPath, "downloads"))
|
|
38
|
+
continue;
|
|
39
|
+
let sql = `SELECT id, target_path, tab_url, total_bytes, received_bytes,
|
|
40
|
+
start_time, end_time, mime_type, original_mime_type,
|
|
41
|
+
state, danger_type, opened, referrer, site_url
|
|
42
|
+
FROM downloads ORDER BY start_time DESC`;
|
|
43
|
+
if (limit) {
|
|
44
|
+
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
45
|
+
}
|
|
46
|
+
const rows = queryDatabase(dbPath, sql);
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
const entry = {
|
|
49
|
+
profile: p.name,
|
|
50
|
+
targetPath: row.target_path || "",
|
|
51
|
+
url: row.tab_url || "",
|
|
52
|
+
siteUrl: row.site_url || "",
|
|
53
|
+
referrer: row.referrer || "",
|
|
54
|
+
totalBytes: row.total_bytes || 0,
|
|
55
|
+
receivedBytes: row.received_bytes || 0,
|
|
56
|
+
mimeType: row.mime_type || row.original_mime_type || "",
|
|
57
|
+
state: DOWNLOAD_STATES[row.state] || `unknown(${row.state})`,
|
|
58
|
+
dangerType: DANGER_TYPES[row.danger_type] || `unknown(${row.danger_type})`,
|
|
59
|
+
opened: Boolean(row.opened),
|
|
60
|
+
startTime: chromeTimeToDate(row.start_time || 0),
|
|
61
|
+
endTime: chromeTimeToDate(row.end_time || 0),
|
|
62
|
+
};
|
|
63
|
+
if (search) {
|
|
64
|
+
const term = search.toLowerCase();
|
|
65
|
+
if (!(entry.targetPath && entry.targetPath.toLowerCase().includes(term)) &&
|
|
66
|
+
!(entry.url && entry.url.toLowerCase().includes(term))) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
results.push(entry);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Skip profiles with errors
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ExtensionOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
search?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ExtensionEntry {
|
|
7
|
+
profile: string;
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
description: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
homepageUrl: string;
|
|
14
|
+
updateUrl: string;
|
|
15
|
+
installTime: string;
|
|
16
|
+
fromWebstore: boolean;
|
|
17
|
+
location: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves installed Chrome extensions from the Preferences JSON file.
|
|
21
|
+
*
|
|
22
|
+
* @param options - Targeting and filtering options.
|
|
23
|
+
* @returns Array of extension entries.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getExtensions({ chromeDir, profile, search }?: ExtensionOptions): ExtensionEntry[];
|