feat: Warnung + selbstverwalteter Wiederherstellungscode für verlorenen Sync-Schlüssel
Der Sync-Schlüssel verlässt nie den Server im Klartext - verliert das einzige Gerät sync.key, erzeugte AppBootstrapper bisher stillschweigend einen neuen, unabhängigen Schlüssel, ohne dass der Nutzer etwas davon merkt. Bereits synchronisierte Server-Daten werden dadurch dauerhaft unlesbar. AppBootstrapper erkennt jetzt, ob ein fehlender Schlüssel bei einem bereits eingeloggten Gerät auftritt (SyncKeyStatus) - SettingsViewModel zeigt dafür ein Warnbanner im Synchronisation-Tab mit der Empfehlung, im Zweifel über ein noch intaktes zweites Gerät neu zu koppeln. Zusätzlich ein komplett offline laufender, selbstverwalteter Wiederherstellungscode (kein neuer dauerhafter Server-Speicher für ein sicherheitsrelevantes Geheimnis): SyncCrypto bekommt einen neuen, vom Pairing-Code kryptografisch unabhängigen Recovery-Code (120 Bit, Base32), mit dem der Sync-Schlüssel lokal verschlüsselt und als Datei durch den Nutzer selbst gesichert werden kann. Neuer SyncKeyRecoveryService kapselt Erzeugen/Einlösen; Redeem schreibt den wiederhergestellten Schlüssel und setzt vorsorglich Pull-Cursor/Versionsverfolgung zurück. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,9 +22,56 @@ public partial class SettingsView : UserControl
|
||||
vm.OnConfirmHardDelete = ShowHardDeleteConfirmDialog;
|
||||
vm.OnConfirmPairingRestore = ShowPairingRestoreConfirmDialog;
|
||||
vm.OnShowPairingError = ShowPairingErrorDialog;
|
||||
vm.OnSaveRecoveryFile = SaveRecoveryFile;
|
||||
vm.OnPickRecoveryFile = PickRecoveryFile;
|
||||
vm.OnConfirmRecoveryRestore = ShowRecoveryRestoreConfirmDialog;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> SaveRecoveryFile(string content)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null) return false;
|
||||
|
||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Sync-Wiederherstellungsdatei speichern",
|
||||
SuggestedFileName = "LehrerApp-Sync-Wiederherstellung.json",
|
||||
FileTypeChoices = [new FilePickerFileType("JSON-Dateien") { Patterns = ["*.json"] }],
|
||||
});
|
||||
if (file is null) return false;
|
||||
await File.WriteAllTextAsync(file.Path.LocalPath, content);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<string?> PickRecoveryFile()
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null) return null;
|
||||
|
||||
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Sync-Wiederherstellungsdatei auswählen",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = [new FilePickerFileType("JSON-Dateien") { Patterns = ["*.json"] }],
|
||||
});
|
||||
return files.Count == 0 ? null : await File.ReadAllTextAsync(files[0].Path.LocalPath);
|
||||
}
|
||||
|
||||
private async Task<bool> ShowRecoveryRestoreConfirmDialog()
|
||||
{
|
||||
var info = new ConfirmDialogInfo
|
||||
{
|
||||
Title = "Sync-Schlüssel ersetzen?",
|
||||
Message = "Der aktuelle Sync-Schlüssel dieses Geräts wird durch den aus der Datei ersetzt. " +
|
||||
"Die App wird danach automatisch neu gestartet.",
|
||||
ConfirmText = "Ersetzen",
|
||||
};
|
||||
var dialog = new ConfirmDialog { DataContext = info };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
||||
}
|
||||
|
||||
private async Task<bool> ShowRestoreConfirmDialog(BackupListItem item)
|
||||
{
|
||||
var info = new ConfirmDialogInfo
|
||||
|
||||
Reference in New Issue
Block a user