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:
@@ -0,0 +1,61 @@
|
||||
using LehrerApp.Sync.Crypto;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Sync.Tests;
|
||||
|
||||
public sealed class SyncKeyRecoveryServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateRecoveryFileUndRestoreFromFile_Roundtrip()
|
||||
{
|
||||
using var temp = new TempKeyFile();
|
||||
var originalKey = SyncCrypto.GenerateKey();
|
||||
var service = new SyncKeyRecoveryService(originalKey, temp.KeyPath);
|
||||
const string code = "TEST-CODE-4711";
|
||||
|
||||
var fileContent = service.CreateRecoveryFile(code);
|
||||
// Ein zweites Gerät (anderer aktuell geladener Schlüssel) löst dieselbe Datei ein.
|
||||
var redeemer = new SyncKeyRecoveryService(SyncCrypto.GenerateKey(), temp.KeyPath);
|
||||
redeemer.RestoreFromFile(fileContent, code);
|
||||
|
||||
Assert.Equal(originalKey, SyncCrypto.LoadKey(temp.KeyPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreFromFile_FalscherCode_WirftUndSchreibtKeyPathNicht()
|
||||
{
|
||||
using var temp = new TempKeyFile();
|
||||
var service = new SyncKeyRecoveryService(SyncCrypto.GenerateKey(), temp.KeyPath);
|
||||
var fileContent = service.CreateRecoveryFile("RICHTIGER-CODE");
|
||||
|
||||
Assert.ThrowsAny<Exception>(() => service.RestoreFromFile(fileContent, "FALSCHER-CODE"));
|
||||
Assert.Null(SyncCrypto.LoadKey(temp.KeyPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreFromFile_UngueltigesDateiformat_WirftInvalidOperationException()
|
||||
{
|
||||
using var temp = new TempKeyFile();
|
||||
var service = new SyncKeyRecoveryService(SyncCrypto.GenerateKey(), temp.KeyPath);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => service.RestoreFromFile("{}", "IRGENDEIN-CODE"));
|
||||
}
|
||||
|
||||
private sealed class TempKeyFile : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(
|
||||
Path.GetTempPath(), $"lehrerapp-synckeyrecovery-tests-{Guid.NewGuid():N}");
|
||||
public string KeyPath { get; }
|
||||
|
||||
public TempKeyFile()
|
||||
{
|
||||
Directory.CreateDirectory(_directory);
|
||||
KeyPath = Path.Combine(_directory, "sync.key");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user