Datensicherheit: Backup, Verschlüsselung, App-Sperre (Kapitel 13.3)

Automatisches rollierendes Backup der Datenbank beim Start mit
Wiederherstellung über die Einstellungen, versionierte Schema-Migration,
optionale Passwort-Verschlüsselung der LiteDB-Datei und eine App-Sperre
nach Inaktivität mit eigenem Passwort.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 13:07:36 +02:00
co-authored by Claude Sonnet 5
parent db1afff7e4
commit 48d07a2b99
25 changed files with 1228 additions and 21 deletions
@@ -0,0 +1,68 @@
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
{
public bool IsEncrypted(string dbPath)
{
if (!File.Exists(dbPath)) return false;
try
{
using var db = new LiteDatabase(dbPath);
return false;
}
catch (LiteException)
{
return true;
}
}
public bool VerifyPassword(string dbPath, string password)
{
try
{
using var db = new LiteDatabase(new ConnectionString(dbPath) { Password = password });
return true;
}
catch (LiteException)
{
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);
}
}
+35 -3
View File
@@ -8,15 +8,21 @@ namespace LehrerApp.Data;
/// </summary>
public class LiteDbContext : IDisposable
{
/// Aktuelle Schema-Version. Migrationsschritte werden versioniert unter
/// <see cref="RunVersionedMigrations"/> ergänzt, statt bei jedem Start erneut
/// (idempotent, aber unnötig) über alle Daten zu laufen.
private const int CurrentSchemaVersion = 1;
private readonly LiteDatabase _db;
public LiteDbContext(string databasePath)
public LiteDbContext(string databasePath, string? password = null)
{
_db = new LiteDatabase(new ConnectionString(databasePath)
{
Connection = ConnectionType.Shared,
Password = password,
});
MigrateExistingData();
RunVersionedMigrations();
EnsureIndexes();
}
@@ -24,7 +30,7 @@ public class LiteDbContext : IDisposable
public LiteDbContext(Stream stream)
{
_db = new LiteDatabase(stream);
MigrateExistingData();
RunVersionedMigrations();
EnsureIndexes();
}
@@ -51,6 +57,32 @@ public class LiteDbContext : IDisposable
public void Checkpoint() => _db.Checkpoint();
public int SchemaVersion => ReadSchemaVersion();
private void RunVersionedMigrations()
{
var version = ReadSchemaVersion();
if (version < 1)
{
MigrateExistingData();
version = 1;
}
WriteSchemaVersion(version);
}
private int ReadSchemaVersion()
{
var meta = _db.GetCollection<BsonDocument>("meta").FindById(1);
return meta?["SchemaVersion"].AsInt32 ?? 0;
}
private void WriteSchemaVersion(int version) =>
_db.GetCollection<BsonDocument>("meta").Upsert(new BsonDocument
{
["_id"] = 1,
["SchemaVersion"] = version,
});
private void MigrateExistingData()
{
MigrateMemberships();