101 lines
4.8 KiB
C#
101 lines
4.8 KiB
C#
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…");
|
||
var r1 = await http.PostAsJsonAsync("/api/snapshot/upload",
|
||
new SnapshotUploadRequest { EncryptedPayload = encPayload, DeviceType = deviceType }, ct);
|
||
r1.EnsureSuccessStatusCode();
|
||
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);
|
||
var r2 = await http.PostAsJsonAsync("/api/snapshot/upload",
|
||
new SnapshotUploadRequest { EncryptedPayload = encPayload,
|
||
EncryptedSyncKey = encKey, DeviceType = deviceType }, ct);
|
||
r2.EnsureSuccessStatusCode();
|
||
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…");
|
||
var resp = await http.GetAsync($"/api/snapshot/{sanitized}", ct);
|
||
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||
throw new SnapshotNotFoundException($"Code '{sanitized}' nicht gefunden oder abgelaufen.");
|
||
resp.EnsureSuccessStatusCode();
|
||
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);
|