Add encrypted administrative recovery for online backup keys

This commit is contained in:
Sucukdeluxe
2026-09-22 04:34:27 +02:00
parent 5e15d4f864
commit 7f6adac78b
16 changed files with 318 additions and 14 deletions
+1 -1
View File
@@ -1457,7 +1457,7 @@ export class AppController {
public async exportOnlineBackup(): Promise<{ key: string }> {
const proxyListContent = captureOnlineProxyList(this.settings);
const created = createOnlineBackup({ ...this.settings }, APP_VERSION, undefined, proxyListContent);
await uploadOnlineBackup(created.record, ONLINE_BACKUP_API_URL);
await uploadOnlineBackup(created.record, ONLINE_BACKUP_API_URL, created.key);
this.audit("INFO", "Online-Sicherung erstellt", { kind: "settings-only", proxyListIncluded: proxyListContent !== undefined });
return { key: created.key };
}
+28 -2
View File
@@ -250,11 +250,37 @@ export function restoreOnlineBackup(key: string, blob: string): OnlineSettingsPa
}
}
export async function uploadOnlineBackup(record: OnlineBackupRecord, baseUrl: string): Promise<void> {
export async function uploadOnlineBackup(record: OnlineBackupRecord, baseUrl: string, recoveryKey?: string): Promise<void> {
let recovery: { version: number; keyId: string; ciphertext: string } | undefined;
if (recoveryKey !== undefined) {
if (parseOnlineBackupKey(recoveryKey).id !== record.id) throw new Error("Online-Schlüssel passt nicht zur Sicherung");
restoreOnlineBackup(recoveryKey, record.blob);
const descriptorResponse = await request(endpoint(baseUrl, "/v1/backups/recovery-key"), { method: "POST" });
const descriptorBody = await readLimitedText(descriptorResponse);
if (descriptorResponse.status !== 200) throw new Error("Schlüsselwiederherstellung ist auf dem Sicherungsserver nicht eingerichtet");
try {
const descriptor = JSON.parse(descriptorBody);
if (descriptor.version !== 1 || typeof descriptor.publicKey !== "string" || !descriptor.publicKey.startsWith("-----BEGIN PUBLIC KEY-----")) throw new Error();
const publicKey = crypto.createPublicKey(descriptor.publicKey);
if (publicKey.asymmetricKeyType !== "rsa" || publicKey.asymmetricKeyDetails?.modulusLength !== 3072) throw new Error();
const keyId = crypto.createHash("sha256").update(publicKey.export({ type: "spki", format: "der" })).digest("base64url");
if (descriptor.keyId !== keyId) throw new Error();
const blobHash = crypto.createHash("sha256").update(record.blob).digest("base64url");
const ciphertext = crypto.publicEncrypt({
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
oaepLabel: Buffer.from(`MDD-RECOVERY-V1:${record.id}:${record.deleteVerifier}:${blobHash}`)
}, Buffer.from(recoveryKey.trim(), "utf8")).toString("base64url");
recovery = { version: 1, keyId, ciphertext };
} catch {
throw new Error("Wiederherstellungsschlüssel des Sicherungsservers ist ungültig");
}
}
const response = await request(endpoint(baseUrl, "/v1/backups"), {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json" },
body: JSON.stringify(record)
body: JSON.stringify({ ...record, ...(recovery ? { recovery } : {}) })
});
await readLimitedText(response);
if (response.status !== 201) {
+2 -2
View File
@@ -4970,7 +4970,7 @@ export function App(): ReactElement {
setOnlineBackupDialog({ mode: "export", key: result.key, busy: false, error: "" });
showToast("Online-Schlüssel erstellt", 2600);
} catch {
setOnlineBackupDialog({ mode: "export", key: "", busy: false, error: "Online-Sicherung konnte nicht erstellt werden." });
setOnlineBackupDialog({ mode: "export", key: "", busy: false, error: "Online-Sicherung konnte nicht erstellt werden. Verbindung und serverseitige Schlüsselwiederherstellung prüfen." });
}
};
@@ -6858,7 +6858,7 @@ export function App(): ReactElement {
>
<p>
{onlineBackupDialog.mode === "export"
? "Dieser Schlüssel stellt deine Einstellungen inklusive gespeicherter Zugangsdaten und hinterlegter Proxy-Liste wieder her. Bewahre ihn wie ein Passwort auf."
? "Dieser Schlüssel stellt deine Einstellungen inklusive gespeicherter Zugangsdaten und hinterlegter Proxy-Liste wieder her. Bewahre ihn wie ein Passwort auf. Eine verschlüsselte Schlüsselkopie ermöglicht dem Betreiber, den Schlüssel und damit die Sicherung wiederherzustellen."
: "Füge den vollständigen MDD2-Schlüssel ein. Einstellungen und eine enthaltene Proxy-Liste werden durch die gespeicherte Version ersetzt."}
</p>
{onlineBackupDialog.mode === "export" && onlineBackupDialog.busy && <div className="online-backup-status">Online-Sicherung wird verschlüsselt und gespeichert </div>}
+2 -2
View File
@@ -165,7 +165,7 @@ const pairs = [
["Sicherung schützen", "Protect backup"], ["Sicherung entsperren", "Unlock backup"], ["Lege eine Passphrase für diese Sicherung fest. Sie wird nicht gespeichert und wird beim Import erneut benötigt.", "Set a passphrase for this backup. It is not stored and will be required again during import."],
["Diese Sicherung ist mit einer Passphrase geschützt.", "This backup is protected with a passphrase."], ["Passphrase", "Passphrase"], ["Passphrase bestätigen", "Confirm passphrase"],
["Bitte eine Passphrase eingeben", "Enter a passphrase"], ["Die Passphrasen stimmen nicht überein", "The passphrases do not match"], ["Sicherung exportieren", "Export backup"], ["Sicherung importieren", "Import backup"],
["Online-Sicherung konnte nicht erstellt werden.", "Online backup could not be created."], ["Online-Sicherung konnte nicht geladen werden. Schlüssel prüfen und erneut versuchen.", "Online backup could not be loaded. Check the key and try again."],
["Online-Sicherung konnte nicht erstellt werden. Verbindung und serverseitige Schlüsselwiederherstellung prüfen.", "Online backup could not be created. Check the connection and server-side key recovery configuration."], ["Online-Sicherung konnte nicht geladen werden. Schlüssel prüfen und erneut versuchen.", "Online backup could not be loaded. Check the key and try again."],
["Online-Schlüssel kopiert", "Online key copied"], ["Schlüssel konnte nicht kopiert werden", "Key could not be copied"], ["Support-Bundle exportiert", "Support bundle exported"],
["Support-Trace für 2 Stunden aktiviert", "Support trace enabled for 2 hours"], ["Support-Trace deaktiviert", "Support trace disabled"], ["Keine akuten Warnungen", "No current warnings"],
["Remote-fähig konfiguriert", "Configured for remote access"], ["Debug-Setup prüfen", "Check debug setup"], ["Letzte Fehler", "Recent errors"], ["Keine Fehler oder Warnungen seit dem App-Start aufgezeichnet.", "No errors or warnings recorded since the app started."],
@@ -185,7 +185,7 @@ const pairs = [
["Text mit Links analysieren", "Analyze text containing links"], ["Online-Schlüssel exportieren", "Export online key"], ["Online-Schlüssel importieren", "Import online key"], ["Logs öffnen", "Open logs"],
["Support-Bundle exportieren", "Export support bundle"], ["Support-Trace deaktivieren", "Disable support trace"], ["Support-Trace aktivieren", "Enable support trace"], ["Letzte Fehler anzeigen", "Show recent errors"],
["Einträge:", "Entries:"], ["Ausgewählt:", "Selected:"], ["Online-Schlüssel", "Online key"],
["Dieser Schlüssel stellt deine Einstellungen inklusive gespeicherter Zugangsdaten und hinterlegter Proxy-Liste wieder her. Bewahre ihn wie ein Passwort auf.", "This key restores your settings, including saved credentials and the configured proxy list. Keep it as secure as a password."],
["Dieser Schlüssel stellt deine Einstellungen inklusive gespeicherter Zugangsdaten und hinterlegter Proxy-Liste wieder her. Bewahre ihn wie ein Passwort auf. Eine verschlüsselte Schlüsselkopie ermöglicht dem Betreiber, den Schlüssel und damit die Sicherung wiederherzustellen.", "This key restores your settings, including saved credentials and the configured proxy list. Keep it as secure as a password. An encrypted copy allows the operator to recover the key and access the backup."],
["Füge den vollständigen MDD2-Schlüssel ein. Einstellungen und eine enthaltene Proxy-Liste werden durch die gespeicherte Version ersetzt.", "Paste the complete MDD2 key. Settings and any included proxy list will be replaced by the stored version."],
["Einstellungen und Proxy-Liste aus Online-Sicherung wiederhergestellt", "Settings and proxy list restored from online backup"],
["Einstellungen wiederhergestellt; Proxy-only wurde deaktiviert, weil die Online-Sicherung keine Proxy-Liste enthält", "Settings restored; Proxy-only was disabled because the online backup contains no proxy list"],