Files
LehrerApp/LehrerApp.Data/DatabaseEncryptionService.cs
T
adminandClaude Sonnet 5 ecf64fff88
CI / build-and-test (push) Canceled after 0s
fix: Sync-Ausfälle aktiv melden, Einstellungen-Absturz durch Connection-Konflikt beheben
Ein Sync-Ausfall (offline/Fehler) war bisher nur als blasser Text in der Statuszeile
sichtbar und konnte so stundenlang unbemerkt bleiben, bis auf einem anderen Gerät
Daten fehlten. SyncStatusViewModel meldet neue Ausfälle jetzt einmalig aktiv per Toast
(nicht bei jedem automatischen Retry erneut).

DatabaseEncryptionService.IsEncrypted/VerifyPassword öffneten die Datenbankdatei mit
einer zweiten, exklusiven (Direct) LiteDB-Verbindung, während LiteDbContext dieselbe
Datei bereits im Shared-Modus offen hält - jeder Aufruf (z.B. beim Öffnen der
Einstellungen) crashte reproduzierbar mit IOException. Beide öffnen jetzt ebenfalls im
Shared-Modus und erzwingen einen tatsächlichen Lesezugriff, da Shared-Verbindungen die
Datei lazy öffnen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 00:07:33 +02:00

103 lines
4.2 KiB
C#

using LiteDB;
namespace LehrerApp.Data;
/// <summary>
/// Prüft und ändert den Passwortschutz einer LiteDB-Datei.
/// Passwort-Änderungen laufen über eine Kopie (neue Datei mit Zielpasswort, alle
/// Collections umkopiert, dann Austausch) statt über <c>LiteDatabase.Rebuild</c> mit
/// Passwort, das in LiteDB 5.0.21 nachweislich fehlschlägt (per Skript verifiziert).
/// </summary>
public class DatabaseEncryptionService
{
// Beide Methoden werden auch gegen den Pfad der laufenden, bereits geöffneten Datenbank
// aufgerufen (z.B. bei jedem Öffnen der Einstellungen bzw. vor einer Passwort-Änderung) -
// LiteDbContext hält dafür längst eine ConnectionType.Shared-Verbindung offen. Ohne Shared
// hier ebenfalls zu setzen, würde die zweite Verbindung im Standard-Modus (Direct) einen
// exklusiven Dateizugriff verlangen und mit "The process cannot access the file" scheitern.
public bool IsEncrypted(string dbPath)
{
if (!File.Exists(dbPath)) return false;
try
{
// Shared-Verbindungen öffnen die Datei lazy - anders als Direct wirft der Konstruktor
// bei falschem/fehlendem Passwort noch nicht, erst ein tatsächlicher Zugriff liest den
// (ggf. verschlüsselten) Header.
using var db = new LiteDatabase(new ConnectionString(dbPath) { Connection = ConnectionType.Shared });
_ = db.GetCollectionNames().ToList();
return false;
}
catch (LiteException)
{
return true;
}
}
public bool VerifyPassword(string dbPath, string password)
{
try
{
using var db = new LiteDatabase(new ConnectionString(dbPath)
{ Password = password, Connection = ConnectionType.Shared });
_ = db.GetCollectionNames().ToList();
return true;
}
catch (LiteException)
{
return false;
}
}
/// Öffnet die Datei probeweise und liest die Sammlungsliste, um eine strukturell gültige
/// (nicht nur vorhandene) LiteDB-Datei zu bestätigen — z.B. direkt nach dem Anlegen eines
/// Backups, das durch einen vollen Datenträger oder einen vorzeitig entfernten USB-Stick
/// beschädigt worden sein könnte. Bewusst breites Catch: jede Art von Fehlschlag beim Öffnen
/// oder Lesen bedeutet hier "Backup nicht verifizierbar", nicht nur ein falsches Passwort.
public bool CanOpenAndRead(string dbPath, string? password)
{
if (!File.Exists(dbPath)) return false;
try
{
var connection = password is null
? new ConnectionString(dbPath)
: new ConnectionString(dbPath) { Password = password };
using var db = new LiteDatabase(connection);
_ = db.GetCollectionNames().ToList();
return true;
}
catch
{
return false;
}
}
/// Setzt (newPassword != null), ändert oder entfernt (newPassword == null) das
/// Passwort der Datenbank. Der Aufrufer muss sicherstellen, dass keine andere
/// Verbindung (z.B. der laufende <see cref="LiteDbContext"/>) die Datei offen hält.
public void SetPassword(string dbPath, string? currentPassword, string? newPassword)
{
var tempPath = dbPath + ".reencrypt.tmp";
if (File.Exists(tempPath)) File.Delete(tempPath);
var srcConnection = currentPassword is null
? new ConnectionString(dbPath)
: new ConnectionString(dbPath) { Password = currentPassword };
var dstConnection = newPassword is null
? new ConnectionString(tempPath)
: new ConnectionString(tempPath) { Password = newPassword };
using (var src = new LiteDatabase(srcConnection))
using (var dst = new LiteDatabase(dstConnection))
{
foreach (var name in src.GetCollectionNames())
{
var docs = src.GetCollection<BsonDocument>(name).FindAll().ToList();
if (docs.Count > 0) dst.GetCollection<BsonDocument>(name).InsertBulk(docs);
}
}
File.Copy(tempPath, dbPath, overwrite: true);
File.Delete(tempPath);
}
}