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