fix: multiple backup import issues found in code review

- Single atomic write instead of two-phase (prevents split state on crash)
- Timestamped pre-import backup (multiple imports don't overwrite safety net)
- Fix UI refresh: correct function names + refresh globalSettings/alwaysOnTop
- Zero sensitive buffers (key, plaintext, decrypted) after use

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-11 19:28:25 +01:00
parent fb4dd94827
commit 60498fecc4
3 changed files with 24 additions and 12 deletions

View File

@ -27,6 +27,8 @@ function encrypt(config, password) {
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const tag = cipher.getAuthTag(); const tag = cipher.getAuthTag();
plaintext.fill(0);
key.fill(0);
return Buffer.concat([MAGIC, salt, iv, tag, encrypted]); return Buffer.concat([MAGIC, salt, iv, tag, encrypted]);
} }
@ -65,6 +67,9 @@ function decrypt(buffer, password) {
return JSON.parse(decrypted.toString('utf-8')); return JSON.parse(decrypted.toString('utf-8'));
} catch { } catch {
throw new Error('Entschlüsselte Daten sind kein gültiges JSON'); throw new Error('Entschlüsselte Daten sind kein gültiges JSON');
} finally {
decrypted.fill(0);
key.fill(0);
} }
} }

22
main.js
View File

@ -649,17 +649,19 @@ ipcMain.handle('import-backup', async (_event, password) => {
}); });
if (canceled || !filePaths.length) return { ok: false, canceled: true }; if (canceled || !filePaths.length) return { ok: false, canceled: true };
const buffer = fs.readFileSync(filePaths[0]); const buffer = fs.readFileSync(filePaths[0]);
const config = backupCrypto.decrypt(buffer, password); const imported = backupCrypto.decrypt(buffer, password);
// Safety net: save current config before overwriting // Safety net: timestamped backup so multiple imports don't overwrite each other
const preImportPath = configStore.filePath + '.pre-import.json'; const ts = new Date().toISOString().replace(/[:.]/g, '-');
const preImportPath = configStore.filePath.replace('.json', `.pre-import-${ts}.json`);
try { fs.copyFileSync(configStore.filePath, preImportPath); } catch {} try { fs.copyFileSync(configStore.filePath, preImportPath); } catch {}
await configStore.save(config); // Single atomic write — no split state, no TOCTOU race
if (config.history) { const merged = {
// Overwrite history too (save() doesn't touch history) hosters: imported.hosters,
const full = configStore.load(); hosterSettings: imported.hosterSettings,
full.history = config.history; globalSettings: imported.globalSettings,
await configStore._atomicWrite(JSON.stringify(full, null, 2)); history: imported.history || []
} };
await configStore._atomicWrite(JSON.stringify(merged, null, 2));
return { ok: true, config: configStore.load() }; return { ok: true, config: configStore.load() };
}); });

View File

@ -857,9 +857,14 @@ async function confirmBackupAction() {
if (result.ok) { if (result.ok) {
config = result.config; config = result.config;
hosterSettings = config.hosterSettings || {}; hosterSettings = config.hosterSettings || {};
// Refresh global settings state (always-on-top, etc.)
alwaysOnTopState = !!(config.globalSettings && config.globalSettings.alwaysOnTop);
window.api.setAlwaysOnTop(alwaysOnTopState);
closeBackupModal(); closeBackupModal();
renderSettingsPanel(); renderSettings();
renderAccountsList(); renderAccounts();
renderHosterSummary();
renderHosterModal();
loadHistory(); loadHistory();
showCopyToast('Backup importiert'); showCopyToast('Backup importiert');
} }