diff --git a/lib/backup-crypto.js b/lib/backup-crypto.js index 48d3a47..d6f76af 100644 --- a/lib/backup-crypto.js +++ b/lib/backup-crypto.js @@ -9,20 +9,24 @@ const ITERATIONS = 100_000; const DIGEST = 'sha512'; const ALGO = 'aes-256-gcm'; -function deriveKey(password, salt) { - return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST); +// Fixed app-internal passphrase — backups are opaque without the app, which is +// enough protection for API keys stored locally. We keep the AES-GCM envelope +// (with random salt/iv) so each export is still distinct and authenticated. +const APP_PASSPHRASE = 'multi-hoster-upload::backup::v1'; + +function deriveKey(salt) { + return crypto.pbkdf2Sync(APP_PASSPHRASE, salt, ITERATIONS, KEY_LEN, DIGEST); } /** - * Encrypt a config object with a password. + * Encrypt a config object. * Returns a Buffer: MHU1 | salt(16) | iv(12) | tag(16) | ciphertext */ -function encrypt(config, password) { - if (!password || typeof password !== 'string') throw new Error('Passwort darf nicht leer sein'); +function encrypt(config) { const plaintext = Buffer.from(JSON.stringify(config), 'utf-8'); const salt = crypto.randomBytes(SALT_LEN); const iv = crypto.randomBytes(IV_LEN); - const key = deriveKey(password, salt); + const key = deriveKey(salt); const cipher = crypto.createCipheriv(ALGO, key, iv); const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); @@ -34,11 +38,10 @@ function encrypt(config, password) { } /** - * Decrypt a .mhu buffer with a password. - * Returns the config object or throws on invalid password/data. + * Decrypt a .mhu buffer. + * Returns the config object or throws on invalid data. */ -function decrypt(buffer, password) { - if (!password || typeof password !== 'string') throw new Error('Passwort darf nicht leer sein'); +function decrypt(buffer) { if (buffer.length < MAGIC.length + SALT_LEN + IV_LEN + TAG_LEN + 1) { throw new Error('Ungültiges Backup-Format'); } @@ -54,7 +57,7 @@ function decrypt(buffer, password) { const tag = buffer.subarray(offset, offset += TAG_LEN); const ciphertext = buffer.subarray(offset); - const key = deriveKey(password, salt); + const key = deriveKey(salt); const decipher = crypto.createDecipheriv(ALGO, key, iv); decipher.setAuthTag(tag); @@ -62,7 +65,7 @@ function decrypt(buffer, password) { try { decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); } catch { - throw new Error('Falsches Passwort oder beschädigte Datei'); + throw new Error('Backup-Datei beschädigt oder aus einer inkompatiblen Version'); } try { diff --git a/main.js b/main.js index c70215a..a5c746f 100644 --- a/main.js +++ b/main.js @@ -966,20 +966,20 @@ ipcMain.handle('clear-history', async () => { }); // --- Backup export / import --- -ipcMain.handle('export-backup', async (_event, password) => { - const config = configStore.load(); - const encrypted = backupCrypto.encrypt(config, password); +ipcMain.handle('export-backup', async () => { const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, { title: 'Backup exportieren', defaultPath: `multi-hoster-backup-${new Date().toISOString().slice(0, 10)}.mhu`, filters: [{ name: 'Multi-Hoster Backup', extensions: ['mhu'] }] }); if (canceled || !filePath) return { ok: false, canceled: true }; + const config = configStore.load(); + const encrypted = backupCrypto.encrypt(config); fs.writeFileSync(filePath, encrypted); return { ok: true, path: filePath }; }); -ipcMain.handle('import-backup', async (_event, password) => { +ipcMain.handle('import-backup', async () => { const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { title: 'Backup importieren', filters: [{ name: 'Multi-Hoster Backup', extensions: ['mhu'] }], @@ -987,7 +987,7 @@ ipcMain.handle('import-backup', async (_event, password) => { }); if (canceled || !filePaths.length) return { ok: false, canceled: true }; const buffer = fs.readFileSync(filePaths[0]); - const imported = backupCrypto.decrypt(buffer, password); + const imported = backupCrypto.decrypt(buffer); // Validate imported data has required structure if (!imported || typeof imported !== 'object' || !imported.hosters || !imported.hosterSettings || !imported.globalSettings) { return { ok: false, error: 'Backup-Datei hat ungültige Struktur (hosters, hosterSettings oder globalSettings fehlt).' }; diff --git a/preload.js b/preload.js index 6affa90..f3cc1ec 100644 --- a/preload.js +++ b/preload.js @@ -59,8 +59,8 @@ contextBridge.exposeInMainWorld('api', { }, // Backup - exportBackup: (password) => ipcRenderer.invoke('export-backup', password), - importBackup: (password) => ipcRenderer.invoke('import-backup', password), + exportBackup: () => ipcRenderer.invoke('export-backup'), + importBackup: () => ipcRenderer.invoke('import-backup'), // Folder Monitor folderMonitorStart: (settings) => ipcRenderer.invoke('folder-monitor:start', settings), diff --git a/renderer/app.js b/renderer/app.js index d37be48..d4a448a 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -1130,77 +1130,36 @@ function copySelectedRecentLinks() { if (links.length) { window.api.copyToClipboard(links.join('\n')); showCopyToast(`${links.length} Links kopiert`); } } -// --- Backup modal --- -let _backupMode = null; // 'export' | 'import' - -function openBackupModal(mode) { - _backupMode = mode; - const modal = document.getElementById('backupPasswordModal'); - const title = document.getElementById('backupModalTitle'); - const confirmRow = document.getElementById('backupConfirmRow'); - const status = document.getElementById('backupModalStatus'); - document.getElementById('backupPassword').value = ''; - document.getElementById('backupPasswordConfirm').value = ''; - status.textContent = ''; - if (mode === 'export') { - title.textContent = 'Backup verschlüsseln'; - confirmRow.style.display = ''; - document.getElementById('confirmBackupBtn').textContent = 'Exportieren'; - } else { - title.textContent = 'Backup entschlüsseln'; - confirmRow.style.display = 'none'; - document.getElementById('confirmBackupBtn').textContent = 'Importieren'; +// --- Backup export / import --- +async function doBackupExport() { + try { + const result = await window.api.exportBackup(); + if (result && result.ok) showCopyToast('Backup exportiert'); + } catch (err) { + alert('Export fehlgeschlagen: ' + (err.message || err)); } - modal.style.display = 'flex'; - document.getElementById('backupPassword').focus(); } -function closeBackupModal() { - document.getElementById('backupPasswordModal').style.display = 'none'; - _backupMode = null; -} - -async function confirmBackupAction() { - const pw = document.getElementById('backupPassword').value; - const status = document.getElementById('backupModalStatus'); - if (!pw) { status.textContent = 'Bitte Passwort eingeben.'; return; } - - if (_backupMode === 'export') { - const pw2 = document.getElementById('backupPasswordConfirm').value; - if (pw !== pw2) { status.textContent = 'Passwörter stimmen nicht überein.'; return; } - status.textContent = 'Exportiere...'; - try { - const result = await window.api.exportBackup(pw); - if (result.canceled) { status.textContent = ''; return; } - if (result.ok) { - closeBackupModal(); - showCopyToast('Backup exportiert'); - } - } catch (err) { - status.textContent = err.message || 'Export fehlgeschlagen'; - } - } else { - status.textContent = 'Importiere...'; - try { - const result = await window.api.importBackup(pw); - if (result.canceled) { status.textContent = ''; return; } - if (result.ok) { - config = result.config; - hosterSettings = config.hosterSettings || {}; - // Refresh global settings state (always-on-top, etc.) - alwaysOnTopState = !!(config.globalSettings && config.globalSettings.alwaysOnTop); - window.api.setAlwaysOnTop(alwaysOnTopState); - closeBackupModal(); - renderSettings(); - renderAccounts(); - renderHosterSummary(); - renderHosterModal(); - loadHistory(); - showCopyToast('Backup importiert'); - } - } catch (err) { - status.textContent = err.message || 'Import fehlgeschlagen'; +async function doBackupImport() { + try { + const result = await window.api.importBackup(); + if (!result || result.canceled) return; + if (result.ok) { + config = result.config; + hosterSettings = config.hosterSettings || {}; + alwaysOnTopState = !!(config.globalSettings && config.globalSettings.alwaysOnTop); + window.api.setAlwaysOnTop(alwaysOnTopState); + renderSettings(); + renderAccounts(); + renderHosterSummary(); + renderHosterModal(); + loadHistory(); + showCopyToast('Backup importiert'); + } else if (result.error) { + alert('Import fehlgeschlagen: ' + result.error); } + } catch (err) { + alert('Import fehlgeschlagen: ' + (err.message || err)); } } @@ -2288,8 +2247,8 @@ function renderSettings() { arrow.innerHTML = isOpen ? '▶' : '▼'; }); - document.getElementById('exportBackupBtn').addEventListener('click', () => openBackupModal('export')); - document.getElementById('importBackupBtn').addEventListener('click', () => openBackupModal('import')); + document.getElementById('exportBackupBtn').addEventListener('click', doBackupExport); + document.getElementById('importBackupBtn').addEventListener('click', doBackupImport); // --- Separator before hoster panels --- const separator = document.createElement('div'); @@ -3239,20 +3198,6 @@ function setupListeners() { }); document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings); - // --- Backup export / import (modal listeners stay here, button listeners in renderSettings) --- - document.getElementById('closeBackupModalBtn').addEventListener('click', closeBackupModal); - document.getElementById('cancelBackupModalBtn').addEventListener('click', closeBackupModal); - document.getElementById('confirmBackupBtn').addEventListener('click', confirmBackupAction); - document.getElementById('backupPasswordModal').addEventListener('click', (e) => { - if (e.target.id === 'backupPasswordModal') closeBackupModal(); - }); - document.getElementById('backupPassword').addEventListener('keydown', (e) => { - if (e.key === 'Enter') confirmBackupAction(); - }); - document.getElementById('backupPasswordConfirm').addEventListener('keydown', (e) => { - if (e.key === 'Enter') confirmBackupAction(); - }); - document.getElementById('clearHistoryBtn').addEventListener('click', async () => { if (!confirm('Verlauf wirklich löschen?')) return; await window.api.clearHistory(); diff --git a/renderer/index.html b/renderer/index.html index 5d843ce..a84766e 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -237,30 +237,6 @@ -
-