using System.Net.Http.Json; using LehrerApp.Data; using LehrerApp.Sync.Crypto; using LehrerApp.Sync.Models; namespace LehrerApp.Sync; /// /// Device-Pairing via verschlüsseltem Snapshot + Einmal-Code. /// /// Sender: CreateAndUploadAsync() → zeigt Code dem Nutzer /// Empfänger: RestoreFromCodeAsync(code) → entschlüsselt Schlüssel + DB /// public class SnapshotService( HttpClient http, LiteDbContext db, byte[] syncKey, DeviceType deviceType, string dbPath, string keyPath) { public event Action? ProgressChanged; public async Task CreateAndUploadAsync(CancellationToken ct = default) { Report(SnapshotStep.Checkpointing, "Datenbank wird gesichert…"); db.Checkpoint(); Report(SnapshotStep.Reading, "Datenbank wird gelesen…"); var dbBytes = await File.ReadAllBytesAsync(dbPath, ct); Report(SnapshotStep.Encrypting, "Verschlüsselung läuft…"); var encPayload = Convert.ToBase64String(SyncCrypto.Encrypt(dbBytes, syncKey)); // Schritt 1: Upload ohne Key → Code erhalten Report(SnapshotStep.Uploading, "Code wird angefordert…"); using var request1 = SyncProtocol.CreateRequest(HttpMethod.Post, "/api/snapshot/upload"); request1.Content = JsonContent.Create( new SnapshotUploadRequest { EncryptedPayload = encPayload, DeviceType = deviceType }); using var r1 = await http.SendAsync(request1, ct); await SyncProtocol.EnsureCompatibleSuccessAsync(r1); var init = await r1.Content.ReadFromJsonAsync(ct) ?? throw new InvalidOperationException("Leere Server-Antwort."); // Schritt 2: Key mit Code verschlüsseln + erneut hochladen Report(SnapshotStep.Uploading, "Schlüssel wird verschlüsselt…"); var encKey = SyncCrypto.EncryptKeyWithCode(syncKey, init.Code); using var request2 = SyncProtocol.CreateRequest(HttpMethod.Post, "/api/snapshot/upload"); request2.Content = JsonContent.Create( new SnapshotUploadRequest { EncryptedPayload = encPayload, EncryptedSyncKey = encKey, DeviceType = deviceType, Code = init.Code }); using var r2 = await http.SendAsync(request2, ct); await SyncProtocol.EnsureCompatibleSuccessAsync(r2); var result = await r2.Content.ReadFromJsonAsync(ct) ?? throw new InvalidOperationException("Leere Server-Antwort."); Report(SnapshotStep.Done, $"Bereit – Code: {result.Code}"); return result; } public async Task RestoreFromCodeAsync(string code, string targetDbPath, CancellationToken ct = default) { var sanitized = code.Trim().ToUpperInvariant(); Report(SnapshotStep.Downloading, "Snapshot wird geladen…"); using var request = SyncProtocol.CreateRequest(HttpMethod.Get, $"/api/snapshot/{sanitized}"); using var resp = await http.SendAsync(request, ct); if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) throw new SnapshotNotFoundException($"Code '{sanitized}' nicht gefunden oder abgelaufen."); await SyncProtocol.EnsureCompatibleSuccessAsync(resp); var dl = await resp.Content.ReadFromJsonAsync(ct) ?? throw new InvalidOperationException("Leere Server-Antwort."); Report(SnapshotStep.Decrypting, "Schlüssel wird entschlüsselt…"); byte[] restoredKey; try { restoredKey = SyncCrypto.DecryptKeyWithCode(dl.EncryptedSyncKey, sanitized); } catch (System.Security.Cryptography.CryptographicException) { throw new InvalidOperationException("Schlüssel-Entschlüsselung fehlgeschlagen – Code korrekt?"); } SyncCrypto.SaveKey(restoredKey, keyPath); Report(SnapshotStep.Decrypting, "Datenbank wird entschlüsselt…"); var dbBytes = SyncCrypto.Decrypt(Convert.FromBase64String(dl.EncryptedPayload), restoredKey); Report(SnapshotStep.Writing, "Datenbank wird geschrieben…"); Directory.CreateDirectory(Path.GetDirectoryName(targetDbPath)!); if (File.Exists(targetDbPath)) File.Move(targetDbPath, $"{targetDbPath}.backup-{DateTime.Now:yyyyMMdd-HHmmss}"); await File.WriteAllBytesAsync(targetDbPath, dbBytes, ct); Report(SnapshotStep.Done, "Abgeschlossen – bitte App neu starten."); return restoredKey; } private void Report(SnapshotStep step, string msg) => ProgressChanged?.Invoke(new(step, msg)); } public record SnapshotProgress(SnapshotStep Step, string Message) { public int Percent => Step switch { SnapshotStep.Checkpointing => 10, SnapshotStep.Reading => 20, SnapshotStep.Encrypting => 35, SnapshotStep.Uploading => 60, SnapshotStep.Downloading => 35, SnapshotStep.Decrypting => 65, SnapshotStep.Writing => 85, SnapshotStep.Done => 100, _ => 0, }; public bool IsComplete => Step == SnapshotStep.Done; } public enum SnapshotStep { Idle, Checkpointing, Reading, Encrypting, Uploading, Downloading, Decrypting, Writing, Done } public class SnapshotNotFoundException(string msg) : Exception(msg);