fix(security): require passphrases for local backups

Derive every MDD2 key from a non-empty user passphrase and the per-backup scrypt salt while keeping the embedded application material isolated to read-only MDD1 migration imports. Normalize missing, wrong, and authentication-failure results to the same controlled decryption error.

Add the modal-based export confirmation and format-aware import flow across renderer, preload, IPC, and controller boundaries. Clear transient passphrase state on completion or cancellation, retain pending import data only until consumption, and keep passphrases out of results, snapshots, payloads, and logs.

Cover mismatch and cancellation paths, MDD1 passphrase-free migration, preload forwarding, successful UI/crypto round-trips, one-byte-short ciphertext, authenticated empty ciphertext, and truncated legacy envelopes.
This commit is contained in:
Sucukdeluxe
2026-08-11 22:32:58 +02:00
parent 6f94e13cb5
commit 1cb381fa50
17 changed files with 563 additions and 96 deletions
+6 -5
View File
@@ -752,7 +752,7 @@ public async checkDebridAccounts(settingsOverride?: AppSettings, persistValidOve
this.audit("INFO", "Download-Statistik zurückgesetzt");
}
public exportBackup(): Buffer {
public exportBackup(passphrase: string): Buffer {
let remoteDiagnostics: BackupRemoteDiagnostics | undefined;
if (Boolean(this.settings.backupIncludeRemoteDiagnostics)) {
const status = getDebugServerRuntimeStatus();
@@ -770,13 +770,14 @@ public async checkDebridAccounts(settingsOverride?: AppSettings, persistValidOve
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits()),
remoteDiagnostics
});
this.audit("INFO", "Backup exportiert", {
const encrypted = encryptBackup(JSON.stringify(payloadObj), passphrase);
this.audit("INFO", "Backup exportiert", {
kind: payloadObj.kind,
historyEntries: payloadObj.history ? payloadObj.history.length : 0,
sessionItems: payloadObj.session ? Object.keys(payloadObj.session.items).length : 0,
sessionPackages: payloadObj.session ? Object.keys(payloadObj.session.packages).length : 0
});
return encryptBackup(JSON.stringify(payloadObj));
return encrypted;
}
public async exportOnlineBackup(): Promise<{ key: string }> {
@@ -812,10 +813,10 @@ public async checkDebridAccounts(settingsOverride?: AppSettings, persistValidOve
return getSupportBundleDefaultFileName();
}
public importBackup(data: Buffer): { restored: boolean; relaunch: boolean; message: string } {
public importBackup(data: Buffer, passphrase?: string): { restored: boolean; relaunch: boolean; message: string } {
let parsed: Record<string, unknown>;
try {
const json = decryptBackup(data);
const json = decryptBackup(data, passphrase);
parsed = JSON.parse(json) as Record<string, unknown>;
} catch {
try {