SnapshotService.CreateAndUploadAsync lädt in zwei Schritten hoch: Schritt 1 holt einen Code vom Server, Schritt 2 verschlüsselt den Sync-Schlüssel mit diesem Code und lädt erneut hoch. SnapshotStore.Store() vergab bei jedem Aufruf bedingungslos einen neuen Zufallscode - der dem Nutzer am Ende angezeigte Code war dadurch nie derselbe, mit dem der Schlüssel tatsächlich verschlüsselt wurde. Jede Kopplung musste deterministisch an der Schlüssel-Entschlüsselung scheitern. SnapshotUploadRequest bekommt ein optionales Code-Feld; Store() aktualisiert bei vorhandenem, passendem Code denselben Eintrag statt einen neuen mit neuem Code anzulegen. Betrifft LehrerApp.Api - der Server muss neu deployt werden. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
81 lines
3.5 KiB
C#
81 lines
3.5 KiB
C#
using LiteDB;
|
|
using LehrerApp.Sync.Models;
|
|
|
|
namespace LehrerApp.Api;
|
|
|
|
public class SnapshotStore(string dataPath) : IDisposable
|
|
{
|
|
private readonly LiteDatabase _db =
|
|
new(Path.Combine(dataPath, "snapshots.db"));
|
|
private readonly Timer _cleanup;
|
|
|
|
public SnapshotStore(string dataPath, bool unused = false) : this(dataPath)
|
|
{
|
|
Directory.CreateDirectory(dataPath);
|
|
_cleanup = new(_ => Clean(), null, TimeSpan.FromHours(1), TimeSpan.FromHours(1));
|
|
}
|
|
|
|
private ILiteCollection<SnapshotEntry> Col => _db.GetCollection<SnapshotEntry>("snap");
|
|
|
|
public SnapshotUploadResponse Store(string userId, SnapshotUploadRequest req)
|
|
{
|
|
// Zweiter Upload-Schritt (Schlüssel nachreichen, mit dem im ersten Schritt vergebenen Code
|
|
// verschlüsselt): denselben Eintrag aktualisieren statt einen neuen mit neuem Code
|
|
// anzulegen — sonst würde der Schlüssel dauerhaft mit dem falschen Code verknüpft bleiben
|
|
// und die Entschlüsselung auf dem Empfängergerät schlägt fehl (Bug, siehe TODO.md 10.3.1).
|
|
if (!string.IsNullOrEmpty(req.Code))
|
|
{
|
|
var existing = Col.FindOne(e => e.UserId == userId && e.Code == req.Code.ToUpperInvariant());
|
|
if (existing is not null)
|
|
{
|
|
existing.EncryptedPayload = req.EncryptedPayload;
|
|
existing.EncryptedSyncKey = req.EncryptedSyncKey;
|
|
Col.Update(existing);
|
|
return new() { Code = existing.Code, ExpiresAt = existing.ExpiresAt };
|
|
}
|
|
}
|
|
|
|
Col.DeleteMany(e => e.UserId == userId);
|
|
var code = NewCode();
|
|
var entry = new SnapshotEntry { Code = code, UserId = userId,
|
|
EncryptedPayload = req.EncryptedPayload,
|
|
EncryptedSyncKey = req.EncryptedSyncKey,
|
|
SourceDeviceType = req.DeviceType,
|
|
CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddHours(24) };
|
|
Col.Insert(entry);
|
|
return new() { Code = code, ExpiresAt = entry.ExpiresAt };
|
|
}
|
|
|
|
public SnapshotDownloadResponse? Retrieve(string userId, string code)
|
|
{
|
|
var e = Col.FindOne(x => x.UserId == userId && x.Code == code.ToUpperInvariant());
|
|
if (e is null || e.ExpiresAt < DateTime.UtcNow) { if (e is not null) Col.Delete(e.Id); return null; }
|
|
Col.Delete(e.Id);
|
|
return new() { EncryptedPayload = e.EncryptedPayload, EncryptedSyncKey = e.EncryptedSyncKey,
|
|
CreatedAt = e.CreatedAt, SourceDeviceType = e.SourceDeviceType };
|
|
}
|
|
|
|
private void Clean() => Col.DeleteMany(e => e.ExpiresAt < DateTime.UtcNow);
|
|
|
|
private static string NewCode()
|
|
{
|
|
string[] animals = ["TIGER","ADLER","DACHS","LUCHS","FALKE","IGEL","ELCH","FUCHS","RABE","WOLF","BISON","LAMM","EULE","BIBER","STORCH"];
|
|
string[] colors = ["BLAU","GRUEN","ROT","GOLD","GRAU","CYAN","ROSA","LILA","SAND","MINT"];
|
|
return $"{animals[Random.Shared.Next(animals.Length)]}-{Random.Shared.Next(10,99)}-{colors[Random.Shared.Next(colors.Length)]}";
|
|
}
|
|
|
|
public void Dispose() { _cleanup?.Dispose(); _db.Dispose(); }
|
|
}
|
|
|
|
internal class SnapshotEntry
|
|
{
|
|
public ObjectId Id { get; set; } = ObjectId.NewObjectId();
|
|
public string Code { get; set; } = "";
|
|
public string UserId { get; set; } = "";
|
|
public string EncryptedPayload { get; set; } = "";
|
|
public string EncryptedSyncKey { get; set; } = "";
|
|
public DeviceType SourceDeviceType { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
public DateTime ExpiresAt { get; set; }
|
|
}
|