feat: BSL license + encrypted export/import for cross-browser transfer
License: Changed from MIT to BSL 1.1. Free for personal use, commercial use requires a paid license. Auto-converts to MIT on March 26, 2030. Export/Import: Options page now has "Transfer Data" section: - Export All (plain) — JSON file with all identities, mappings, settings - Export Encrypted — AES-256-GCM with PBKDF2 password derivation, saved as .ssbackup file - Import — handles both plain and encrypted backups, prompts for password if encrypted Crypto uses Web Crypto API (browser-native, no dependencies): 100k PBKDF2 iterations, random salt + IV per export. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
@@ -66,13 +66,24 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Transfer Data</h2>
|
||||
<p class="section-desc">Export all your identities, mappings, and settings to move between browsers. Encrypted exports require a password to decrypt.</p>
|
||||
<div class="bulk-actions">
|
||||
<button class="btn" id="btnExportAll">Export All (plain)</button>
|
||||
<button class="btn" id="btnExportEncrypted">Export Encrypted</button>
|
||||
<button class="btn" id="btnImportAll">Import</button>
|
||||
<input type="file" id="fileImportAll" accept=".json,.ssbackup" hidden>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Mappings</h2>
|
||||
<p class="section-desc">Manage all your substitution rules. Longer matches take priority.</p>
|
||||
|
||||
<div class="bulk-actions">
|
||||
<button class="btn" id="btnExport">Export JSON</button>
|
||||
<button class="btn" id="btnImport">Import JSON</button>
|
||||
<button class="btn" id="btnExport">Export Mappings</button>
|
||||
<button class="btn" id="btnImport">Import Mappings</button>
|
||||
<input type="file" id="fileImport" accept=".json" hidden>
|
||||
<button class="btn btn-danger" id="btnClearAll">Clear All</button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Storage from '../lib/storage.js';
|
||||
import SilentSendCrypto from '../lib/crypto.js';
|
||||
import api from '../lib/browser-polyfill.js';
|
||||
|
||||
let mappings = [];
|
||||
@@ -19,6 +20,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
renderDomains();
|
||||
renderLog();
|
||||
|
||||
// Transfer data
|
||||
$('#btnExportAll').addEventListener('click', exportAllPlain);
|
||||
$('#btnExportEncrypted').addEventListener('click', exportAllEncrypted);
|
||||
$('#btnImportAll').addEventListener('click', () => $('#fileImportAll').click());
|
||||
$('#fileImportAll').addEventListener('change', importAll);
|
||||
|
||||
// Custom domains
|
||||
$('#btnAddDomain').addEventListener('click', addDomain);
|
||||
$('#newDomain').addEventListener('keydown', (e) => {
|
||||
@@ -271,6 +278,106 @@ function renderDomains() {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Transfer Data (Export/Import All) ---
|
||||
|
||||
async function getAllData() {
|
||||
const result = await api.storage.local.get(null); // get everything
|
||||
return {
|
||||
version: '1',
|
||||
exportedAt: new Date().toISOString(),
|
||||
identity: result.ss_identity || {},
|
||||
mappings: result.ss_mappings || [],
|
||||
settings: result.ss_settings || {},
|
||||
};
|
||||
}
|
||||
|
||||
function downloadFile(content, filename) {
|
||||
const blob = new Blob([content], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function exportAllPlain() {
|
||||
const data = await getAllData();
|
||||
downloadFile(JSON.stringify(data, null, 2), 'silent-send-backup.json');
|
||||
}
|
||||
|
||||
async function exportAllEncrypted() {
|
||||
const password = prompt('Set a password for this backup:');
|
||||
if (!password) return;
|
||||
const confirm = prompt('Confirm password:');
|
||||
if (password !== confirm) {
|
||||
alert('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await getAllData();
|
||||
try {
|
||||
const encrypted = await SilentSendCrypto.encrypt(data, password);
|
||||
const wrapper = JSON.stringify({ encrypted: true, data: encrypted });
|
||||
downloadFile(wrapper, 'silent-send-backup.ssbackup');
|
||||
alert('Encrypted backup saved. You will need the password to import it.');
|
||||
} catch (e) {
|
||||
alert('Encryption failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function importAll(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const parsed = JSON.parse(text);
|
||||
let data;
|
||||
|
||||
if (parsed.encrypted) {
|
||||
// Encrypted backup
|
||||
const password = prompt('Enter the password for this backup:');
|
||||
if (!password) return;
|
||||
try {
|
||||
data = await SilentSendCrypto.decrypt(parsed.data, password);
|
||||
} catch (err) {
|
||||
alert('Wrong password or corrupted file.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Plain backup
|
||||
data = parsed;
|
||||
}
|
||||
|
||||
if (!data.version) {
|
||||
alert('Not a valid Silent Send backup file.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('This will replace all your current data. Continue?')) return;
|
||||
|
||||
// Restore
|
||||
if (data.identity) await api.storage.local.set({ ss_identity: data.identity });
|
||||
if (data.mappings) await api.storage.local.set({ ss_mappings: data.mappings });
|
||||
if (data.settings) await api.storage.local.set({ ss_settings: data.settings });
|
||||
|
||||
// Refresh UI
|
||||
mappings = await Storage.getMappings();
|
||||
settings = await Storage.getSettings();
|
||||
renderMappings();
|
||||
renderDomains();
|
||||
renderLog();
|
||||
|
||||
alert('Import complete. Reload the extension for changes to take effect.');
|
||||
} catch (err) {
|
||||
alert('Failed to import: ' + err.message);
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
|
||||
Reference in New Issue
Block a user