chrome-tools 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -246
- package/dist/bin/cli.d.ts +8 -0
- package/dist/bin/cli.js +2 -0
- package/dist/src/core/chrome.d.ts +77 -0
- package/dist/src/core/chrome.js +1 -0
- package/dist/src/core/crypto.d.ts +26 -0
- package/dist/src/core/crypto.js +1 -0
- package/dist/src/core/db.d.ts +19 -0
- package/dist/src/core/db.js +1 -0
- package/dist/src/core/keychain.d.ts +20 -0
- package/dist/src/core/keychain.js +1 -0
- package/{src/index.js → dist/src/index.d.ts} +5 -29
- package/dist/src/index.js +1 -0
- package/dist/src/modules/autofill.d.ts +72 -0
- package/dist/src/modules/autofill.js +1 -0
- package/dist/src/modules/bookmarks.d.ts +23 -0
- package/dist/src/modules/bookmarks.js +1 -0
- package/dist/src/modules/cookies.d.ts +40 -0
- package/dist/src/modules/cookies.js +1 -0
- package/dist/src/modules/downloads.d.ts +28 -0
- package/dist/src/modules/downloads.js +1 -0
- package/dist/src/modules/extensions.d.ts +25 -0
- package/dist/src/modules/extensions.js +1 -0
- package/dist/src/modules/history.d.ts +37 -0
- package/dist/src/modules/history.js +1 -0
- package/dist/src/modules/passwords.d.ts +20 -0
- package/dist/src/modules/passwords.js +1 -0
- package/dist/src/modules/search-engines.d.ts +22 -0
- package/dist/src/modules/search-engines.js +1 -0
- package/dist/src/modules/top-sites.d.ts +18 -0
- package/dist/src/modules/top-sites.js +1 -0
- package/package.json +26 -19
- package/bin/cli.js +0 -629
- package/src/core/chrome.js +0 -178
- 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
package/src/core/chrome.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
/** Known database/data files that indicate a valid Chrome profile directory. */
|
|
6
|
-
const PROFILE_INDICATORS = [
|
|
7
|
-
"Login Data",
|
|
8
|
-
"History",
|
|
9
|
-
"Bookmarks",
|
|
10
|
-
"Cookies",
|
|
11
|
-
"Web Data",
|
|
12
|
-
"Preferences",
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Returns the default Chrome User Data directory for macOS.
|
|
17
|
-
* @returns {string}
|
|
18
|
-
*/
|
|
19
|
-
export function getDefaultChromeDir() {
|
|
20
|
-
return join(homedir(), "Library", "Application Support", "Google", "Chrome");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Scans the Chrome User Data directory for profile subdirectories.
|
|
25
|
-
* A directory is considered a profile if it contains at least one known data file.
|
|
26
|
-
*
|
|
27
|
-
* @param {string} [chromeDir] - Chrome User Data path. Defaults to standard macOS location.
|
|
28
|
-
* @returns {Array<{name: string, path: string}>} Discovered profiles with name and absolute path.
|
|
29
|
-
*/
|
|
30
|
-
export function discoverProfiles(chromeDir) {
|
|
31
|
-
const dir = chromeDir || getDefaultChromeDir();
|
|
32
|
-
if (!dir || !existsSync(dir)) {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
38
|
-
const profiles = [];
|
|
39
|
-
|
|
40
|
-
for (const entry of entries) {
|
|
41
|
-
if (!entry.isDirectory()) continue;
|
|
42
|
-
const profilePath = join(dir, entry.name);
|
|
43
|
-
const hasIndicator = PROFILE_INDICATORS.some((file) =>
|
|
44
|
-
existsSync(join(profilePath, file))
|
|
45
|
-
);
|
|
46
|
-
if (hasIndicator) {
|
|
47
|
-
profiles.push({ name: entry.name, path: profilePath });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return profiles;
|
|
51
|
-
} catch {
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Parses the Chrome "Local State" JSON file to extract profile metadata.
|
|
58
|
-
* Returns enriched profile info including display name, email, avatar, etc.
|
|
59
|
-
*
|
|
60
|
-
* @param {string} [chromeDir] - Chrome User Data path.
|
|
61
|
-
* @returns {Array<Object>} Profile metadata objects.
|
|
62
|
-
*/
|
|
63
|
-
export function getProfileMetadata(chromeDir) {
|
|
64
|
-
const dir = chromeDir || getDefaultChromeDir();
|
|
65
|
-
const localStatePath = join(dir, "Local State");
|
|
66
|
-
|
|
67
|
-
if (!existsSync(localStatePath)) {
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const raw = readFileSync(localStatePath, "utf-8");
|
|
73
|
-
const localState = JSON.parse(raw);
|
|
74
|
-
const infoCache = localState?.profile?.info_cache;
|
|
75
|
-
|
|
76
|
-
if (!infoCache || typeof infoCache !== "object") {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return Object.entries(infoCache).map(([dirName, info]) => ({
|
|
81
|
-
directory: dirName,
|
|
82
|
-
name: info.name || dirName,
|
|
83
|
-
gaiaName: info.gaia_name || "",
|
|
84
|
-
userName: info.user_name || "",
|
|
85
|
-
email: info.user_name || "",
|
|
86
|
-
avatarIcon: info.avatar_icon || "",
|
|
87
|
-
isUsingDefaultName: info.is_using_default_name ?? true,
|
|
88
|
-
isEphemeral: info.is_ephemeral ?? false,
|
|
89
|
-
activeTime: info.active_time || 0,
|
|
90
|
-
}));
|
|
91
|
-
} catch {
|
|
92
|
-
return [];
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Combines filesystem profile discovery with Local State metadata
|
|
98
|
-
* to provide a comprehensive view of each profile's state.
|
|
99
|
-
*
|
|
100
|
-
* @param {string} [chromeDir] - Chrome User Data path.
|
|
101
|
-
* @returns {Array<Object>} Enriched profile state objects.
|
|
102
|
-
*/
|
|
103
|
-
export function getProfileStates(chromeDir) {
|
|
104
|
-
const dir = chromeDir || getDefaultChromeDir();
|
|
105
|
-
const discovered = discoverProfiles(dir);
|
|
106
|
-
const metadata = getProfileMetadata(dir);
|
|
107
|
-
|
|
108
|
-
const metaMap = new Map();
|
|
109
|
-
for (const m of metadata) {
|
|
110
|
-
metaMap.set(m.directory, m);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return discovered.map((profile) => {
|
|
114
|
-
const meta = metaMap.get(profile.name) || {};
|
|
115
|
-
const profilePath = profile.path;
|
|
116
|
-
|
|
117
|
-
// Check which data files exist
|
|
118
|
-
const dataFiles = {};
|
|
119
|
-
for (const file of PROFILE_INDICATORS) {
|
|
120
|
-
dataFiles[file.replace(/\s+/g, "_").toLowerCase()] = existsSync(
|
|
121
|
-
join(profilePath, file)
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
// Additional files
|
|
125
|
-
dataFiles.top_sites = existsSync(join(profilePath, "Top Sites"));
|
|
126
|
-
dataFiles.extensions = existsSync(join(profilePath, "Extensions"));
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
directory: profile.name,
|
|
130
|
-
path: profilePath,
|
|
131
|
-
displayName: meta.name || profile.name,
|
|
132
|
-
gaiaName: meta.gaiaName || "",
|
|
133
|
-
email: meta.email || "",
|
|
134
|
-
avatarIcon: meta.avatarIcon || "",
|
|
135
|
-
isEphemeral: meta.isEphemeral ?? false,
|
|
136
|
-
activeTime: meta.activeTime || 0,
|
|
137
|
-
dataFiles,
|
|
138
|
-
};
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Resolves a profile name to its directory path.
|
|
144
|
-
*
|
|
145
|
-
* @param {string} chromeDir - Chrome User Data path.
|
|
146
|
-
* @param {string} profileName - Profile directory name (e.g., "Default", "Profile 1").
|
|
147
|
-
* @returns {string|null} Absolute path to profile directory, or null if not found.
|
|
148
|
-
*/
|
|
149
|
-
export function resolveProfilePath(chromeDir, profileName) {
|
|
150
|
-
const dir = chromeDir || getDefaultChromeDir();
|
|
151
|
-
const profilePath = join(dir, profileName);
|
|
152
|
-
return existsSync(profilePath) ? profilePath : null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Helper to get the profiles to operate on based on options.
|
|
157
|
-
* If a specific profile is specified, returns only that one.
|
|
158
|
-
* Otherwise returns all discovered profiles.
|
|
159
|
-
*
|
|
160
|
-
* @param {Object} options
|
|
161
|
-
* @param {string} [options.profilePath] - Custom Chrome User Data/profile directory.
|
|
162
|
-
* @param {string} [options.chromeDir] - Chrome User Data path (alias for profilePath).
|
|
163
|
-
* @param {string} [options.profile] - Specific profile name to target.
|
|
164
|
-
* @returns {Array<{name: string, path: string}>}
|
|
165
|
-
*/
|
|
166
|
-
export function resolveTargetProfiles({ profilePath, chromeDir, profile } = {}) {
|
|
167
|
-
const dir = profilePath || chromeDir || getDefaultChromeDir();
|
|
168
|
-
const allProfiles = discoverProfiles(dir);
|
|
169
|
-
|
|
170
|
-
if (profile) {
|
|
171
|
-
const match = allProfiles.filter((p) => p.name === profile);
|
|
172
|
-
if (match.length === 0) {
|
|
173
|
-
throw new Error(`Profile "${profile}" not found in ${dir}`);
|
|
174
|
-
}
|
|
175
|
-
return match;
|
|
176
|
-
}
|
|
177
|
-
return allProfiles;
|
|
178
|
-
}
|
package/src/core/crypto.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { createDecipheriv } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
const AES_IV = Buffer.alloc(16, " "); // 16 spaces (0x20)
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Decrypts a password/value buffer encrypted by Chrome on macOS.
|
|
7
|
-
* Chrome v10+ on macOS prefixes the encrypted blob with "v10" or "v11",
|
|
8
|
-
* followed by AES-128-CBC ciphertext.
|
|
9
|
-
*
|
|
10
|
-
* @param {Buffer|Uint8Array} encryptedBlob - The encrypted blob from the database.
|
|
11
|
-
* @param {Buffer} derivedKey - The derived 16-byte key.
|
|
12
|
-
* @returns {string} The decrypted plaintext, or an error placeholder.
|
|
13
|
-
*/
|
|
14
|
-
export function decryptValue(encryptedBlob, derivedKey) {
|
|
15
|
-
const decrypted = decryptValueBuffer(encryptedBlob, derivedKey);
|
|
16
|
-
return Buffer.isBuffer(decrypted) ? decrypted.toString("utf-8") : decrypted;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Decrypts a Chrome encrypted blob and returns the raw plaintext bytes.
|
|
21
|
-
*
|
|
22
|
-
* @param {Buffer|Uint8Array} encryptedBlob - The encrypted blob from the database.
|
|
23
|
-
* @param {Buffer} derivedKey - The derived 16-byte key.
|
|
24
|
-
* @returns {Buffer|string} The decrypted plaintext bytes, or an error placeholder.
|
|
25
|
-
*/
|
|
26
|
-
export function decryptValueBuffer(encryptedBlob, derivedKey) {
|
|
27
|
-
if (!encryptedBlob || encryptedBlob.length === 0) {
|
|
28
|
-
return Buffer.alloc(0);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const blob = Buffer.isBuffer(encryptedBlob)
|
|
32
|
-
? encryptedBlob
|
|
33
|
-
: Buffer.from(encryptedBlob);
|
|
34
|
-
|
|
35
|
-
const prefix = blob.subarray(0, 3).toString("utf-8");
|
|
36
|
-
if (prefix !== "v10" && prefix !== "v11") {
|
|
37
|
-
return "<unsupported encryption format>";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const ciphertext = blob.subarray(3);
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const decipher = createDecipheriv("aes-128-cbc", derivedKey, AES_IV);
|
|
44
|
-
let decrypted = decipher.update(ciphertext);
|
|
45
|
-
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
46
|
-
return decrypted;
|
|
47
|
-
} catch {
|
|
48
|
-
return "<decryption failed>";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Converts a Chrome/Webkit timestamp (microseconds since 1601-01-01)
|
|
54
|
-
* to a readable local Date string.
|
|
55
|
-
*
|
|
56
|
-
* @param {bigint|number} chromeTimestamp - Timestamp in microseconds.
|
|
57
|
-
* @returns {string} Human-readable date-time string, or "N/A".
|
|
58
|
-
*/
|
|
59
|
-
export function chromeTimeToDate(chromeTimestamp) {
|
|
60
|
-
if (!chromeTimestamp || chromeTimestamp === 0 || chromeTimestamp === 0n) {
|
|
61
|
-
return "N/A";
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
const timestampBigInt = BigInt(chromeTimestamp);
|
|
65
|
-
const unixMicroseconds = timestampBigInt - 11644473600000000n;
|
|
66
|
-
const ms = Number(unixMicroseconds / 1000n);
|
|
67
|
-
const date = new Date(ms);
|
|
68
|
-
if (isNaN(date.getTime())) {
|
|
69
|
-
return "N/A";
|
|
70
|
-
}
|
|
71
|
-
return date.toLocaleString();
|
|
72
|
-
} catch {
|
|
73
|
-
return "N/A";
|
|
74
|
-
}
|
|
75
|
-
}
|
package/src/core/db.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { existsSync, copyFileSync, mkdtempSync, unlinkSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join, basename } from "node:path";
|
|
4
|
-
import Database from "better-sqlite3";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Copies a Chrome SQLite database to a temporary location, executes a query,
|
|
8
|
-
* and cleans up. This avoids SQLite locking issues when Chrome is running.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} dbFilePath - Absolute path to the Chrome database file.
|
|
11
|
-
* @param {string} sql - SQL query to execute.
|
|
12
|
-
* @param {Array} [params=[]] - Bound parameters for the query.
|
|
13
|
-
* @returns {Array<Object>} Query result rows.
|
|
14
|
-
* @throws {Error} If the file doesn't exist or query fails.
|
|
15
|
-
*/
|
|
16
|
-
export function queryDatabase(dbFilePath, sql, params = []) {
|
|
17
|
-
if (!existsSync(dbFilePath)) {
|
|
18
|
-
throw new Error(`Database file not found: ${dbFilePath}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "chrome-tools-"));
|
|
22
|
-
const tmpDb = join(tmpDir, basename(dbFilePath));
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
copyFileSync(dbFilePath, tmpDb);
|
|
26
|
-
} catch (err) {
|
|
27
|
-
cleanupTmp(tmpDb, tmpDir);
|
|
28
|
-
throw new Error(`Failed to copy database file '${basename(dbFilePath)}': ${err.message}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let db;
|
|
32
|
-
try {
|
|
33
|
-
db = new Database(tmpDb, { readonly: true, timeout: 5000 });
|
|
34
|
-
const stmt = db.prepare(sql);
|
|
35
|
-
return params.length > 0 ? stmt.all(...params) : stmt.all();
|
|
36
|
-
} catch (err) {
|
|
37
|
-
throw new Error(`Database query failed on '${basename(dbFilePath)}': ${err.message}`);
|
|
38
|
-
} finally {
|
|
39
|
-
if (db) {
|
|
40
|
-
try { db.close(); } catch { /* ignore */ }
|
|
41
|
-
}
|
|
42
|
-
cleanupTmp(tmpDb, tmpDir);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Checks whether a specific table exists in the given database file.
|
|
48
|
-
*
|
|
49
|
-
* @param {string} dbFilePath - Absolute path to the database file.
|
|
50
|
-
* @param {string} tableName - Name of the table to check.
|
|
51
|
-
* @returns {boolean} True if the table exists.
|
|
52
|
-
*/
|
|
53
|
-
export function tableExists(dbFilePath, tableName) {
|
|
54
|
-
try {
|
|
55
|
-
const rows = queryDatabase(
|
|
56
|
-
dbFilePath,
|
|
57
|
-
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
|
58
|
-
[tableName]
|
|
59
|
-
);
|
|
60
|
-
return rows.length > 0;
|
|
61
|
-
} catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function cleanupTmp(tmpDb, tmpDir) {
|
|
67
|
-
try { unlinkSync(tmpDb); } catch { /* ignore */ }
|
|
68
|
-
try { rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
69
|
-
}
|
package/src/core/keychain.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { pbkdf2Sync } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
const PBKDF2_ITERATIONS = 1003;
|
|
5
|
-
const PBKDF2_KEY_LENGTH = 16;
|
|
6
|
-
const PBKDF2_SALT = Buffer.from("saltysalt");
|
|
7
|
-
|
|
8
|
-
let _cachedKeychainPassword = null;
|
|
9
|
-
let _cachedDerivedKey = null;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Retrieves the "Chrome Safe Storage" password from the macOS Keychain.
|
|
13
|
-
* Caches the result for subsequent calls within the same process.
|
|
14
|
-
*
|
|
15
|
-
* @returns {string} The raw keychain password.
|
|
16
|
-
* @throws {Error} If the password could not be retrieved.
|
|
17
|
-
*/
|
|
18
|
-
export function getChromeKeychainPassword() {
|
|
19
|
-
if (_cachedKeychainPassword !== null) {
|
|
20
|
-
return _cachedKeychainPassword;
|
|
21
|
-
}
|
|
22
|
-
try {
|
|
23
|
-
const password = execSync(
|
|
24
|
-
'security find-generic-password -w -s "Chrome Safe Storage"',
|
|
25
|
-
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
26
|
-
).trim();
|
|
27
|
-
_cachedKeychainPassword = password;
|
|
28
|
-
return password;
|
|
29
|
-
} catch {
|
|
30
|
-
throw new Error(
|
|
31
|
-
"Could not retrieve Chrome Safe Storage key from macOS Keychain.\n" +
|
|
32
|
-
"Ensure Google Chrome has been run at least once and you have granted Keychain access."
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Derives the AES-128-CBC key from the Keychain password using PBKDF2.
|
|
39
|
-
* Caches the result for subsequent calls.
|
|
40
|
-
*
|
|
41
|
-
* @param {string} [keychainPassword] - The raw password. If omitted, fetched from Keychain.
|
|
42
|
-
* @returns {Buffer} The derived 16-byte key.
|
|
43
|
-
*/
|
|
44
|
-
export function deriveEncryptionKey(keychainPassword) {
|
|
45
|
-
if (_cachedDerivedKey !== null) {
|
|
46
|
-
return _cachedDerivedKey;
|
|
47
|
-
}
|
|
48
|
-
const password = keychainPassword || getChromeKeychainPassword();
|
|
49
|
-
if (!password) {
|
|
50
|
-
throw new Error("Keychain password cannot be empty for key derivation.");
|
|
51
|
-
}
|
|
52
|
-
_cachedDerivedKey = pbkdf2Sync(
|
|
53
|
-
password,
|
|
54
|
-
PBKDF2_SALT,
|
|
55
|
-
PBKDF2_ITERATIONS,
|
|
56
|
-
PBKDF2_KEY_LENGTH,
|
|
57
|
-
"sha1"
|
|
58
|
-
);
|
|
59
|
-
return _cachedDerivedKey;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Clears cached keychain data. Useful for testing or re-authentication.
|
|
64
|
-
*/
|
|
65
|
-
export function clearKeychainCache() {
|
|
66
|
-
_cachedKeychainPassword = null;
|
|
67
|
-
_cachedDerivedKey = null;
|
|
68
|
-
}
|
package/src/modules/autofill.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
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
|
-
* Retrieves autofill entries from Chrome's Web 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 field name or value (case-insensitive).
|
|
14
|
-
* @param {number} [options.limit=100] - Maximum results.
|
|
15
|
-
* @returns {Array<Object>}
|
|
16
|
-
*/
|
|
17
|
-
export function getAutofillEntries({ chromeDir, profile, search, limit = 100 } = {}) {
|
|
18
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
19
|
-
const results = [];
|
|
20
|
-
|
|
21
|
-
for (const p of profiles) {
|
|
22
|
-
const dbPath = join(p.path, "Web Data");
|
|
23
|
-
try {
|
|
24
|
-
if (!tableExists(dbPath, "autofill")) continue;
|
|
25
|
-
|
|
26
|
-
let sql = `SELECT name, value, date_created, date_last_used, count
|
|
27
|
-
FROM autofill ORDER BY date_last_used DESC`;
|
|
28
|
-
if (limit) {
|
|
29
|
-
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const rows = queryDatabase(dbPath, sql);
|
|
33
|
-
|
|
34
|
-
for (const row of rows) {
|
|
35
|
-
const entry = {
|
|
36
|
-
profile: p.name,
|
|
37
|
-
name: row.name || "",
|
|
38
|
-
value: row.value || "",
|
|
39
|
-
dateCreated: chromeTimeToDate(row.date_created ? BigInt(row.date_created) * 1000000n + 11644473600000000n : 0),
|
|
40
|
-
dateLastUsed: chromeTimeToDate(row.date_last_used ? BigInt(row.date_last_used) * 1000000n + 11644473600000000n : 0),
|
|
41
|
-
count: row.count || 0,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
if (search) {
|
|
45
|
-
const term = search.toLowerCase();
|
|
46
|
-
if (
|
|
47
|
-
!entry.name.toLowerCase().includes(term) &&
|
|
48
|
-
!entry.value.toLowerCase().includes(term)
|
|
49
|
-
) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
results.push(entry);
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
// Skip profiles with errors
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return results;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Retrieves saved credit cards from Chrome's Web Data database.
|
|
65
|
-
* Card numbers are encrypted and will be decrypted.
|
|
66
|
-
*
|
|
67
|
-
* @param {Object} [options]
|
|
68
|
-
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
69
|
-
* @param {string} [options.profile] - Specific profile to target.
|
|
70
|
-
* @returns {Array<Object>}
|
|
71
|
-
*/
|
|
72
|
-
export function getCreditCards({ chromeDir, profile } = {}) {
|
|
73
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
74
|
-
const derivedKey = deriveEncryptionKey();
|
|
75
|
-
const results = [];
|
|
76
|
-
|
|
77
|
-
for (const p of profiles) {
|
|
78
|
-
const dbPath = join(p.path, "Web Data");
|
|
79
|
-
try {
|
|
80
|
-
if (!tableExists(dbPath, "credit_cards")) continue;
|
|
81
|
-
|
|
82
|
-
const rows = queryDatabase(
|
|
83
|
-
dbPath,
|
|
84
|
-
`SELECT guid, name_on_card, expiration_month, expiration_year,
|
|
85
|
-
card_number_encrypted, date_modified, origin, use_count,
|
|
86
|
-
use_date, nickname
|
|
87
|
-
FROM credit_cards ORDER BY use_date DESC`
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
for (const row of rows) {
|
|
91
|
-
let cardNumber = "";
|
|
92
|
-
if (row.card_number_encrypted) {
|
|
93
|
-
cardNumber = decryptValue(row.card_number_encrypted, derivedKey);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
results.push({
|
|
97
|
-
profile: p.name,
|
|
98
|
-
guid: row.guid || "",
|
|
99
|
-
nameOnCard: row.name_on_card || "",
|
|
100
|
-
expirationMonth: row.expiration_month || 0,
|
|
101
|
-
expirationYear: row.expiration_year || 0,
|
|
102
|
-
cardNumber,
|
|
103
|
-
nickname: row.nickname || "",
|
|
104
|
-
origin: row.origin || "",
|
|
105
|
-
useCount: row.use_count || 0,
|
|
106
|
-
dateModified: chromeTimeToDate(row.date_modified ? BigInt(row.date_modified) * 1000000n + 11644473600000000n : 0),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
// Skip profiles with errors
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return results;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Retrieves saved addresses from Chrome's Web Data database.
|
|
119
|
-
*
|
|
120
|
-
* @param {Object} [options]
|
|
121
|
-
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
122
|
-
* @param {string} [options.profile] - Specific profile to target.
|
|
123
|
-
* @returns {Array<Object>}
|
|
124
|
-
*/
|
|
125
|
-
export function getAddresses({ chromeDir, profile } = {}) {
|
|
126
|
-
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
127
|
-
const results = [];
|
|
128
|
-
|
|
129
|
-
for (const p of profiles) {
|
|
130
|
-
const dbPath = join(p.path, "Web Data");
|
|
131
|
-
try {
|
|
132
|
-
if (!tableExists(dbPath, "autofill_profiles")) continue;
|
|
133
|
-
|
|
134
|
-
const profileRows = queryDatabase(
|
|
135
|
-
dbPath,
|
|
136
|
-
`SELECT guid, company_name, street_address, dependent_locality,
|
|
137
|
-
city, state, zipcode, country_code, date_modified,
|
|
138
|
-
origin, use_count, use_date, language_code
|
|
139
|
-
FROM autofill_profiles ORDER BY use_date DESC`
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
// Try to join with names, emails, and phones
|
|
143
|
-
let nameRows = [];
|
|
144
|
-
let emailRows = [];
|
|
145
|
-
let phoneRows = [];
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
if (tableExists(dbPath, "autofill_profile_names")) {
|
|
149
|
-
nameRows = queryDatabase(
|
|
150
|
-
dbPath,
|
|
151
|
-
`SELECT guid, first_name, middle_name, last_name, full_name FROM autofill_profile_names`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
} catch { /* ignore */ }
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
if (tableExists(dbPath, "autofill_profile_emails")) {
|
|
158
|
-
emailRows = queryDatabase(
|
|
159
|
-
dbPath,
|
|
160
|
-
`SELECT guid, email FROM autofill_profile_emails`
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
} catch { /* ignore */ }
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
if (tableExists(dbPath, "autofill_profile_phones")) {
|
|
167
|
-
phoneRows = queryDatabase(
|
|
168
|
-
dbPath,
|
|
169
|
-
`SELECT guid, number FROM autofill_profile_phones`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
} catch { /* ignore */ }
|
|
173
|
-
|
|
174
|
-
const nameMap = new Map();
|
|
175
|
-
for (const n of nameRows) {
|
|
176
|
-
nameMap.set(n.guid, n.full_name || [n.first_name, n.middle_name, n.last_name].filter(Boolean).join(" "));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const emailMap = new Map();
|
|
180
|
-
for (const e of emailRows) {
|
|
181
|
-
emailMap.set(e.guid, e.email || "");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const phoneMap = new Map();
|
|
185
|
-
for (const ph of phoneRows) {
|
|
186
|
-
phoneMap.set(ph.guid, ph.number || "");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
for (const row of profileRows) {
|
|
190
|
-
results.push({
|
|
191
|
-
profile: p.name,
|
|
192
|
-
guid: row.guid || "",
|
|
193
|
-
fullName: nameMap.get(row.guid) || "",
|
|
194
|
-
email: emailMap.get(row.guid) || "",
|
|
195
|
-
phone: phoneMap.get(row.guid) || "",
|
|
196
|
-
companyName: row.company_name || "",
|
|
197
|
-
streetAddress: row.street_address || "",
|
|
198
|
-
city: row.city || "",
|
|
199
|
-
state: row.state || "",
|
|
200
|
-
zipcode: row.zipcode || "",
|
|
201
|
-
countryCode: row.country_code || "",
|
|
202
|
-
origin: row.origin || "",
|
|
203
|
-
useCount: row.use_count || 0,
|
|
204
|
-
dateModified: chromeTimeToDate(row.date_modified ? BigInt(row.date_modified) * 1000000n + 11644473600000000n : 0),
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
} catch {
|
|
208
|
-
// Skip profiles with errors
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return results;
|
|
213
|
-
}
|