Files
LehrerApp/LehrerApp.Sync/SnapshotService.cs
T

107 lines
5.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net.Http.Json;
using LehrerApp.Data;
using LehrerApp.Sync.Crypto;
using LehrerApp.Sync.Models;
namespace LehrerApp.Sync;
/// <summary>
/// Device-Pairing via verschlüsseltem Snapshot + Einmal-Code.
///
/// Sender: CreateAndUploadAsync() → zeigt Code dem Nutzer
/// Empfänger: RestoreFromCodeAsync(code) → entschlüsselt Schlüssel + DB
/// </summary>
public class SnapshotService(
HttpClient http, LiteDbContext db, byte[] syncKey,
DeviceType deviceType, string dbPath, string keyPath)
{
public event Action<SnapshotProgress>? ProgressChanged;
public async Task<SnapshotUploadResponse> 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<SnapshotUploadResponse>(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<SnapshotUploadResponse>(ct)
?? throw new InvalidOperationException("Leere Server-Antwort.");
Report(SnapshotStep.Done, $"Bereit Code: {result.Code}");
return result;
}
public async Task<byte[]> 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<SnapshotDownloadResponse>(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);