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:
@@ -1989,7 +1989,52 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba
|
||||
reicht den in Schritt 1 erhaltenen Code in Schritt 2 zurück. **Wichtig für den Rollout:**
|
||||
diese Änderung betrifft `LehrerApp.Api` (Server) — ein reines Neubauen des Desktop-Clients
|
||||
reicht nicht, der Server muss neu deployt werden.
|
||||
- [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
|
||||
- [x] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
|
||||
|
||||
**Ausgangslage:** Der Sync-Schlüssel verlässt nie den Server im Klartext (siehe
|
||||
`SyncCrypto`-Kopfkommentar) — verliert das einzige Gerät mit dem Schlüssel `sync.key`
|
||||
(Neuinstallation, defekte Platte), erzeugt `AppBootstrapper` bisher stillschweigend einen
|
||||
neuen, unabhängigen Schlüssel; bereits synchronisierte Server-Daten werden für dieses Gerät
|
||||
dauerhaft unlesbar, ohne dass der Nutzer etwas davon merkt. Eine ECHTE Wiederherstellung
|
||||
danach ist nur möglich, wenn vorher proaktiv ein Rettungsanker angelegt wurde — dem Nutzer
|
||||
zur Wahl gestellt (drei Stufen: nur Warnung / selbstverwalteter Code / dauerhafte
|
||||
Server-Hinterlegung), Entscheidung: **selbstverwalteter Recovery-Code, kein neuer
|
||||
dauerhafter Server-Speicher** für ein sicherheitsrelevantes Geheimnis.
|
||||
|
||||
**Umsetzung, zweiteilig:**
|
||||
1. **Warnung.** `AppBootstrapper` erfasst `keyExistedBefore = File.Exists(keyPath)`
|
||||
synchron VOR jedem möglichen Schreibzugriff und registriert `SyncKeyStatus
|
||||
{ KeyWasRegenerated = !keyExistedBefore && syncSettings.IsLoggedIn }` — ein fehlender
|
||||
Schlüssel bei einem noch nie eingeloggten Gerät ist der normale Erstlauf, kein Grund zur
|
||||
Warnung. `SettingsViewModel` zeigt bei `SyncKeyWasRegenerated` ein rotes Banner oben im
|
||||
Synchronisation-Tab mit der konkreten Handlungsempfehlung: solange noch ein anderes Gerät
|
||||
den alten Schlüssel hat, DAMIT neu koppeln (10.3.1, bereits vorhanden); sonst hilft nur
|
||||
ein zuvor gesicherter Recovery-Code.
|
||||
2. **Wiederherstellungscode**, komplett offline, ohne Server-Beteiligung. Neue
|
||||
`SyncCrypto`-Bausteine mit eigenem Salt (bewusst getrennt von `DeriveKeyFromCode` des
|
||||
kurzlebigen Pairing-Codes, damit ein Schema-Wechsel am einen den anderen nicht
|
||||
mitbeeinflusst): `GenerateRecoveryCode()` (15 Byte Zufall, 120 Bit, Base32 in 4er-Gruppen,
|
||||
z. B. `ABCD-EFGH-…`), `EncryptKeyWithRecoveryCode`/`DecryptKeyWithRecoveryCode`
|
||||
(PBKDF2-Ableitung, Code-Eingabe tolerant gegenüber Klein-/Großschreibung und fehlenden
|
||||
Trennstrichen). Neuer `SyncKeyRecoveryService` (analog `SnapshotService`, aber ohne
|
||||
HTTP): "Code erzeugen" verschlüsselt den aktuellen Schlüssel mit einem einmalig
|
||||
angezeigten Code und lässt den Nutzer die verschlüsselte Datei selbst sichern (USB-Stick,
|
||||
eigene Cloud o. ä.) — Datei und Code getrennt aufbewahren, erst beides zusammen ergibt
|
||||
den Schlüssel. "Wiederherstellungscode einlösen" liest Datei + Code, schreibt den
|
||||
entschlüsselten Schlüssel als neues `sync.key`, setzt vorsorglich Pull-Cursor und
|
||||
Push-Versionsverfolgung zurück (falls zwischenzeitlich unter dem falschen Schlüssel schon
|
||||
etwas synchronisiert wurde, analog 10.3.5) und startet die App neu — der im Speicher
|
||||
gehaltene (falsche) Schlüssel ist ein DI-Singleton und kann zur Laufzeit nicht getauscht
|
||||
werden, gleiches Muster wie `RedeemPairingCode`.
|
||||
**Bewusst nicht getestet:** der erfolgreiche Redeem-Pfad endet in
|
||||
`AppBootstrapper.RestartApplication()` (`Environment.Exit(0)`) — das darf ein Test nie
|
||||
auslösen, deshalb sind nur die Zweige VOR diesem Aufruf ViewModel-getestet; derselbe Grund,
|
||||
aus dem `RedeemPairingCode` ebenfalls keinen Erfolgstest hat. Neue Tests: `SyncCryptoTests`
|
||||
(neu, bisher gab es keine dedizierte Testdatei — Schlüsselerzeugung, Verschlüsselungs-
|
||||
Rundtrip, Code-Rundtrip inkl. Fehlertoleranz, eigenständige Ableitung ggü. dem
|
||||
Pairing-Salt), `SyncKeyRecoveryServiceTests` (Datei-Rundtrip, falscher Code, ungültiges
|
||||
Dateiformat), `SettingsViewModelTests` (Warnzustand, Code-Erzeugung inkl. Abbruch,
|
||||
Redeem-Guards).
|
||||
- [x] **10.3.3** Prüfen, welche Daten unverschlüsselt über `PlainEventStore` laufen —
|
||||
personenbezogene Daten dürfen das nicht. `Grade`/`ExamResult` (beide mit `StudentId` plus
|
||||
Notenwert/-kommentar) trugen bisher personenbezogene Daten im Klartext über den
|
||||
|
||||
Reference in New Issue
Block a user