|
import os from 'os'; |
|
import fs from 'fs'; |
|
import path from 'path'; |
|
import crypto from 'crypto'; |
|
import dbus from 'dbus-next'; |
|
import sqlite3pkg from 'better-sqlite3'; |
|
|
|
// change this bro |
|
const sourceBrowserPath = |
|
'~/.var/app/com.brave.Browser/config/BraveSoftware/Brave-Browser/'.replaceAll( |
|
'~', |
|
os.homedir(), |
|
); |
|
const targetBrowserPath = '~/.config/net.imput.helium/'.replaceAll( |
|
'~', |
|
os.homedir(), |
|
);const sourceProfilePath = path.join(sourceBrowserPath, 'Default'); |
|
const targetProfilePath = path.join(targetBrowserPath, 'Default'); |
|
|
|
const unencryptedFiles = [ |
|
'Bookmarks', |
|
'Bookmarks.bak', |
|
|
|
'History', |
|
'History-journal', |
|
'History-wal', |
|
|
|
'Favicons', |
|
'Favicons-journal', |
|
'Favicons-wal', |
|
|
|
'Top Sites', |
|
'Shortcuts', |
|
|
|
'Preferences', |
|
|
|
'Extensions/', |
|
'Extension Rules/', |
|
'Extension State/', |
|
|
|
'Local Storage/', |
|
'IndexedDB/', |
|
'Session Storage/', |
|
'Storage/', |
|
'WebStorage/', |
|
'Service Worker/', |
|
|
|
'Visited Links', |
|
|
|
'GPUCache/', |
|
'Code Cache/', |
|
'Cache/', |
|
]; |
|
|
|
function copyFile(src, dest) { |
|
fs.mkdirSync(path.dirname(dest), { recursive: true }); |
|
fs.copyFileSync(src, dest); |
|
} |
|
|
|
function copyDir(src, dest) { |
|
fs.mkdirSync(dest, { recursive: true }); |
|
|
|
const entries = fs.readdirSync(src, { withFileTypes: true }); |
|
|
|
for (const entry of entries) { |
|
const srcPath = path.join(src, entry.name); |
|
const destPath = path.join(dest, entry.name); |
|
|
|
if (entry.isDirectory()) { |
|
copyDir(srcPath, destPath); |
|
} else if (entry.isFile()) { |
|
copyFile(srcPath, destPath); |
|
} |
|
} |
|
} |
|
|
|
console.log('➡️ handling non encrypted files first...'); |
|
for (let file of unencryptedFiles) { |
|
if (file.includes('-wal')) continue; |
|
const type = file.endsWith('/') ? 'dir' : 'file'; |
|
const src = path.join(sourceProfilePath, file); |
|
const dest = path.join(targetProfilePath, file); |
|
|
|
console.log('✨ copying ' + file); |
|
|
|
if (fs.existsSync(src)) { |
|
(type === 'dir' ? copyDir : copyFile)(src, dest); |
|
|
|
console.log('✅ success'); |
|
continue; |
|
} |
|
console.log(file, '(file doesnt exist)'); |
|
} |
|
|
|
const KWALLET_KEYS = { |
|
brave: { |
|
folder: 'Brave Keys', |
|
key: 'Brave Safe Storage', |
|
app: 'kdewallet', |
|
}, |
|
chrome: { |
|
folder: 'Chrome Keys', |
|
key: 'Chrome Safe Storage', |
|
app: 'kdewallet', |
|
}, |
|
chromium: { |
|
folder: 'Chromium Keys', |
|
key: 'Chromium Safe Storage', |
|
app: 'kdewallet', |
|
}, |
|
edge: { |
|
folder: 'Chrome Keys', |
|
key: 'Microsoft Edge Safe Storage', |
|
app: 'kdewallet', |
|
}, |
|
vivaldi: { |
|
folder: 'Chrome Keys', |
|
key: 'Vivaldi Safe Storage', |
|
app: 'kdewallet', |
|
}, |
|
helium: { |
|
folder: 'Chromium Keys', |
|
key: 'Chromium Safe Storage', |
|
app: 'kdewallet', |
|
}, |
|
}; |
|
|
|
async function getKWalletKey(browserName) { |
|
const name = browserName.toLowerCase(); |
|
if (!KWALLET_KEYS[name]) throw new Error(`Unknown browser: ${name}`); |
|
|
|
const bus = dbus.sessionBus(); |
|
try { |
|
const obj = await bus |
|
.getProxyObject('org.kde.kwalletd5', '/modules/kwalletd5') |
|
.catch(() => |
|
bus.getProxyObject('org.kde.kwalletd6', '/modules/kwalletd6'), |
|
); |
|
|
|
const wallet = obj.getInterface('org.kde.KWallet'); |
|
const { folder, key, app } = KWALLET_KEYS[name]; |
|
|
|
const actualWallet = await wallet.networkWallet(); |
|
const handle = await wallet.open(actualWallet, -1 >>> 0, app); |
|
|
|
if (handle === -1) throw new Error('KWallet locked'); |
|
|
|
if (!(await wallet.hasEntry(handle, folder, key, app))) |
|
throw new Error(`No KWallet entry for ${name} (wallet: ${actualWallet}, folder: ${folder}, key: ${key})`); |
|
|
|
return await wallet.readPassword(handle, folder, key, app); |
|
} finally { |
|
bus.disconnect(); |
|
} |
|
} |
|
|
|
const FIXED_IV = Buffer.alloc(16, 0x20); // 16 space bytes |
|
|
|
const SALT = 'saltysalt'; |
|
|
|
function deriveKey(key64) { |
|
return crypto.pbkdf2Sync(Buffer.from(key64, 'utf-8'), SALT, 1, 16, 'sha1'); |
|
} |
|
|
|
function decryptValue(encryptedValue, key) { |
|
const buf = Buffer.isBuffer(encryptedValue) |
|
? encryptedValue |
|
: Buffer.from(encryptedValue); |
|
if (!buf.slice(0, 3).equals(Buffer.from('v11'))) return null; |
|
const decipher = crypto.createDecipheriv('aes-128-cbc', deriveKey(key), FIXED_IV); |
|
try { |
|
return Buffer.concat([decipher.update(buf.slice(3)), decipher.final()]); |
|
} catch { |
|
return null; |
|
} |
|
} |
|
|
|
function encryptValue(plaintext, key) { |
|
const cipher = crypto.createCipheriv('aes-128-cbc', deriveKey(key), FIXED_IV); |
|
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); |
|
return Buffer.concat([Buffer.from('v11'), encrypted]); |
|
} |
|
|
|
export function migrateCookies(srcProfile, destProfile, srcKeyB64, destKeyB64) { |
|
const srcDb = path.join(srcProfile, 'Cookies'); |
|
const destDb = path.join(destProfile, 'Cookies'); |
|
|
|
// copy file first so schema is intact |
|
fs.mkdirSync(destProfile, { recursive: true }); |
|
fs.copyFileSync(srcDb, destDb); |
|
|
|
const db = new sqlite3pkg(destDb); |
|
|
|
const rows = db.prepare('SELECT rowid, encrypted_value FROM cookies').all(); |
|
|
|
const update = db.prepare( |
|
'UPDATE cookies SET encrypted_value = ? WHERE rowid = ?', |
|
); |
|
|
|
const migrate = db.transaction(() => { |
|
for (const row of rows) { |
|
const buf = Buffer.from(row.encrypted_value); |
|
if (!buf.slice(0, 3).equals(Buffer.from('v11'))) continue; |
|
const plain = decryptValue(buf, srcKeyB64); |
|
if (!plain) continue; |
|
const reencrypted = encryptValue(plain, destKeyB64); |
|
update.run(reencrypted, row.rowid); |
|
} |
|
}); |
|
|
|
migrate(); |
|
db.close(); |
|
} |
|
|
|
export function migrateLoginData( |
|
srcProfile, |
|
destProfile, |
|
srcKeyB64, |
|
destKeyB64, |
|
) { |
|
const srcDb = path.join(srcProfile, 'Login Data'); |
|
const destDb = path.join(destProfile, 'Login Data'); |
|
|
|
fs.mkdirSync(destProfile, { recursive: true }); |
|
fs.copyFileSync(srcDb, destDb); |
|
|
|
const db = new sqlite3pkg(destDb); |
|
|
|
const rows = db.prepare('SELECT rowid AS _rowid, password_value FROM logins').all(); |
|
const update = db.prepare( |
|
'UPDATE logins SET password_value = ? WHERE id = ?', |
|
); |
|
|
|
const migrate = db.transaction(() => { |
|
for (const row of rows) { |
|
const buf = Buffer.from(row.password_value); |
|
if (!buf.slice(0, 3).equals(Buffer.from('v11'))) continue; |
|
const plain = decryptValue(buf, srcKeyB64); |
|
if (!plain) continue; |
|
update.run(encryptValue(plain, destKeyB64), row._rowid); |
|
} |
|
}); |
|
|
|
migrate(); |
|
db.close(); |
|
} |
|
|
|
console.log('➡️ handling encrypted files now'); |
|
console.log('➡️ checking encryption key location'); |
|
|
|
const keyBrave = await getKWalletKey('brave'); |
|
|
|
let keyHelium; |
|
try { |
|
keyHelium = await getKWalletKey('helium'); |
|
} catch (e) { |
|
console.log(`✖ Helium key missing: ${e.message}`); |
|
} |
|
|
|
console.log('Brave key:', keyBrave); |
|
console.log('Helium key:', keyHelium); |
|
|
|
if (keyBrave && keyHelium) { |
|
console.log('\n--- Encryption round-trip tests ---\n'); |
|
|
|
const testPlain = 'hello_migration_test_123'; |
|
|
|
const enc = encryptValue(Buffer.from(testPlain, 'utf-8'), keyHelium); |
|
const dec = decryptValue(enc, keyHelium); |
|
const heliumOk = dec && dec.toString('utf-8') === testPlain; |
|
console.log(` ${heliumOk ? '✓' : '✖'} Helium key: encrypt/decrypt`); |
|
|
|
const braveEnc = encryptValue(Buffer.from(testPlain, 'utf-8'), keyBrave); |
|
const braveDec = decryptValue(braveEnc, keyBrave); |
|
const braveSelfOk = braveDec && braveDec.toString('utf-8') === testPlain; |
|
console.log(` ${braveSelfOk ? '✓' : '✖'} Brave key: encrypt/decrypt self-test`); |
|
|
|
const cookiesDb = path.join(sourceProfilePath, 'Cookies'); |
|
let bravePlain = null; |
|
let braveOk = 0, v11count = 0; |
|
if (fs.existsSync(cookiesDb)) { |
|
const db = new sqlite3pkg(cookiesDb); |
|
const rows = db.prepare("SELECT rowid, encrypted_value FROM cookies WHERE encrypted_value IS NOT NULL AND typeof(encrypted_value) = 'blob' LIMIT 10").all(); |
|
db.close(); |
|
|
|
for (const row of rows) { |
|
const buf = Buffer.from(row.encrypted_value); |
|
if (!buf.slice(0, 3).equals(Buffer.from('v11'))) continue; |
|
v11count++; |
|
const dec = decryptValue(buf, keyBrave); |
|
if (dec) { braveOk++; if (!bravePlain) bravePlain = dec; } |
|
} |
|
console.log(` ${braveOk}/${v11count} v11 cookies decrypt with Brave key`); |
|
if (bravePlain) { |
|
const str = bravePlain.toString('utf-8').replace(/[\x00-\x1f\x7f-\xff]/g, '.'); |
|
console.log(` sample: ${str.slice(-50)}`); |
|
} |
|
} else { |
|
console.log(' - Cookies DB not found at', cookiesDb); |
|
} |
|
|
|
if (bravePlain) { |
|
const reenc = encryptValue(bravePlain, keyHelium); |
|
const redec = decryptValue(reenc, keyHelium); |
|
const migrateOk = redec && redec.equals(bravePlain); |
|
console.log(` ${migrateOk ? '✓' : '✖'} Brave > Helium re-encrypt round-trip`); |
|
} |
|
|
|
console.log(''); |
|
} else if (!keyBrave) { |
|
console.log('\nCannot run tests: Brave key missing'); |
|
} else { |
|
console.log('\nCannot run tests: Helium key missing'); |
|
} |
|
|
|
if (keyBrave && keyHelium) { |
|
console.log('➡️ migrating cookies...'); |
|
migrateCookies(sourceProfilePath, targetProfilePath, keyBrave, keyHelium); |
|
console.log('✅ cookies done'); |
|
|
|
console.log('➡️ migrating login data...'); |
|
migrateLoginData(sourceProfilePath, targetProfilePath, keyBrave, keyHelium); |
|
console.log('✅ login data done'); |
|
} |
|
|
|
console.log('done'); |