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
@@ -0,0 +1,78 @@
import { FormEvent, ReactElement, useState } from "react";
import { BackupPassphraseMode, validateBackupPassphrase } from "../backup-flow";
import { Dialog } from "./Dialog";
export interface BackupPassphraseDialogProps {
mode: BackupPassphraseMode;
onCancel: () => void;
onSubmit: (passphrase: string) => void;
}
export function BackupPassphraseDialog({ mode, onCancel, onSubmit }: BackupPassphraseDialogProps): ReactElement {
const [passphrase, setPassphrase] = useState("");
const [confirmation, setConfirmation] = useState("");
const [error, setError] = useState("");
const clear = (): void => {
setPassphrase("");
setConfirmation("");
setError("");
};
const cancel = (): void => {
clear();
onCancel();
};
const submit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
const validationError = validateBackupPassphrase(mode, passphrase, confirmation);
if (validationError) {
setError(validationError);
return;
}
const submittedPassphrase = passphrase;
clear();
onSubmit(submittedPassphrase);
};
return (
<Dialog actions={null} className="backup-passphrase-modal" onClose={cancel} open title={mode === "export" ? "Sicherung schützen" : "Sicherung entsperren"}>
<p>{mode === "export" ? "Lege eine Passphrase für diese Sicherung fest. Sie wird nicht gespeichert und wird beim Import erneut benötigt." : "Diese Sicherung ist mit einer Passphrase geschützt."}</p>
<form className="backup-passphrase-form" onSubmit={submit}>
<div className="backup-passphrase-fields">
<label>
Passphrase
<input
aria-label="Backup-Passphrase"
autoComplete="off"
autoFocus
onChange={(event) => { setPassphrase(event.target.value); setError(""); }}
spellCheck={false}
type="password"
value={passphrase}
/>
</label>
{mode === "export" && (
<label>
Passphrase bestätigen
<input
aria-label="Backup-Passphrase bestätigen"
autoComplete="off"
onChange={(event) => { setConfirmation(event.target.value); setError(""); }}
spellCheck={false}
type="password"
value={confirmation}
/>
</label>
)}
</div>
{error && <div className="backup-passphrase-error" role="alert">{error}</div>}
<div className="modal-actions">
<button className="btn" onClick={cancel} type="button">Abbrechen</button>
<button className="btn primary" type="submit">{mode === "export" ? "Sicherung exportieren" : "Sicherung importieren"}</button>
</div>
</form>
</Dialog>
);
}