Skip to content

Instantly share code, notes, and snippets.

@wavedevgit
Created June 7, 2026 11:30
Show Gist options
  • Select an option

  • Save wavedevgit/529888b91fdba8ae8d12e655e13b0746 to your computer and use it in GitHub Desktop.

Select an option

Save wavedevgit/529888b91fdba8ae8d12e655e13b0746 to your computer and use it in GitHub Desktop.
Migrate from browser A to browser B

Migrate ALL browser data from browser A to browser B

  1. Modify the browser paths in the file
  2. Run the command:
npm init -y
npm i better-sqlite3 dbus-next
node main.js
  • This script only works for chromium based browsers. you can modify it to make it support firefox too if you want.
  • This script currently only works for browsers that store the encryption key in kwallet, I made it so I can switch from Brave Browser to Helium Browser. but if you want to help, you can send me better version that supports all types of key stores.
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');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment