From 86b497bdbf8c4043036699a897fd9375d20a2f91 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Sep 2026 14:34:32 +0200 Subject: [PATCH] Desktop: Passwortabfrage vor Konten, Geheimnissen und WebUI-Zugang Umsetzung von #567. Entscheidung Richard, 13.09.2026: Geheimnisse verwalten wir ausschliesslich im Desktop, und selbst dort erst nach zusaetzlicher Passwortabfrage. Bisher sah und aenderte jeder, der die Desktop-Oberflaeche offen vor sich hatte: - Polymarket-Konten mit ApiKey, ApiSecret, ApiPassphrase und PrivateKey - Master-Key, OpenRouter-Key, Watchdog-Token und Lizenzschluessel - Mullvad-Account und Polygon-RPC-URL mit Alchemy-Key, offen im Editor - das WebUI-Token, per "Anzeigen" lesbar Jetzt gesperrt, bis entsperrt: - Tab "Polymarket Accounts": Gesperrt stehen die Konten gar nicht im visuellen Baum. Das Raster bleibt leer, der Editor mit den Private Keys ebenfalls; nichts ist nur verdeckt. - Master-Key erzeugen, OpenRouter-, Watchdog- und Lizenzschluessel setzen. - Mullvad-Account und Polygon-RPC-URL: mit [SensitiveSetting] markiert und im Editor ausgeblendet. - WebUI-Zugang: Gesperrt steht das Token nicht einmal maskiert im Feld. Speichern ist mitgesperrt, weil es den Feldinhalt als Token schreibt; gesperrt und leer haette es das Token geloescht. - Frei bleiben Lizenzpruefung und Test-Heartbeat, weil sie keinen Schluessel zeigen. Kern im Core (PolyTrader.Core.Security), weil nur dort testbar: - SettingsPasswordStore: PBKDF2-HMAC-SHA256, 600 000 Iterationen, Salt, eigene Datei settings-password.json neben master.key, atomar geschrieben und nur fuer den Besitzer lesbar. Nicht in server_settings.xml, die seit #561 das WebUI mitschreibt. Ein vorhandenes Passwort wird nie ueberschrieben. - SettingsLock: - erstes Festlegen mit Wiederholung und mindestens 10 Zeichen - zwei freie Fehlversuche, danach 5 s bis 5 min Wartezeit, in der auch das richtige Passwort nicht geprueft wird - Sperre nach 5 Minuten ohne geschuetzte Aktion und beim Verlassen der Einstellungen - eine unlesbare Passwortdatei entsperrt nie und gilt auch nicht still als "kein Passwort" - Protokoll ohne eingegebene Werte Oberflaeche: PasswordDialog deklarativ in .axaml, Sperr-Leiste ueber den Tabs. Die PBKDF2-Pruefung dauert etwa eine halbe Sekunde und laeuft nicht im UI-Thread. Grenzen: - Die Sperre schuetzt gegen jemanden an einer entsperrten Desktop-Sitzung. Wer Dateizugriff hat, liest master.key selbst. Deshalb gibt es kein "Passwort vergessen": Wer settings-password.json loeschen kann, braucht keins. - Die Wartezeit nach Fehlversuchen beginnt nach einem Neustart von vorn. - "Datenbankeinstellungen": Eine Ansicht dafuer gibt es im Desktop nicht; die Verbindungszeichenfolge steht nur in appsettings.Local.json. Entsteht eine, gehoert sie hinter dieselbe Sperre. Tests: 20 fuer Passwortdatei und Sperre (Festlegen, Fehlversuche, Wartezeit, Leerlauf, unlesbare Datei, Aendern, Iterationszahl aus der Datei, Dateirechte) und 5 fuer die Markierung der Geheimnisfelder. Die Oberflaeche selbst wird auf der VM abgenommen. Co-Authored-By: Claude Opus 5 --- .gitignore | 2 + .../Controls/SettingsEditor.axaml.cs | 5 +- src/PolyTrader.App.Avalonia/Program.cs | 8 + .../Shell/CoreViews.cs | 3 +- .../ViewModels/SettingsFields.cs | 7 +- .../Views/PasswordDialog.axaml | 32 ++ .../Views/PasswordDialog.axaml.cs | 76 +++++ .../Views/SettingsWindow.axaml | 23 +- .../Views/SettingsWindow.axaml.cs | 181 ++++++++-- src/PolyTrader.Core/Models/ServerSettings.cs | 2 + .../Security/SensitiveSettingAttribute.cs | 14 + src/PolyTrader.Core/Security/SettingsLock.cs | 313 ++++++++++++++++++ .../SensitiveSettingsTests.cs | 47 +++ tests/PolyTrader.Tests/SettingsLockTests.cs | 224 +++++++++++++ 14 files changed, 913 insertions(+), 24 deletions(-) create mode 100644 src/PolyTrader.App.Avalonia/Views/PasswordDialog.axaml create mode 100644 src/PolyTrader.App.Avalonia/Views/PasswordDialog.axaml.cs create mode 100644 src/PolyTrader.Core/Security/SensitiveSettingAttribute.cs create mode 100644 src/PolyTrader.Core/Security/SettingsLock.cs create mode 100644 tests/PolyTrader.Tests/SensitiveSettingsTests.cs create mode 100644 tests/PolyTrader.Tests/SettingsLockTests.cs diff --git a/.gitignore b/.gitignore index f7eb152..2ac8ff8 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,8 @@ MongoDB/ .gitea-token master.key openrouter.key +# #567: Hash des Einstellungs-Passworts (PBKDF2) - gehoert zur Installation, nicht ins Repository +settings-password.json # Packager-Zugangsdaten (FTP, updateservice:publish-Token) – Vorlage in deploy/packager.config.example.json deploy/packager.config.json diff --git a/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs index 5dd24d9..92d8f68 100644 --- a/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs +++ b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs @@ -17,10 +17,11 @@ namespace PolyTrader.App.Avalonia.Controls public SettingsEditor() => AvaloniaXamlLoader.Load(this); /// Setzt das anzuzeigende Objekt (oder null, um zu leeren). - public void Show(object? target) + /// false blendet Geheimnisfelder aus (#567). + public void Show(object? target, bool includeSensitive = true) { var list = this.FindControl("sections")!; - list.ItemsSource = target == null ? null : SettingsModelBuilder.Build(target); + list.ItemsSource = target == null ? null : SettingsModelBuilder.Build(target, includeSensitive); } } } diff --git a/src/PolyTrader.App.Avalonia/Program.cs b/src/PolyTrader.App.Avalonia/Program.cs index 1318629..90cbf1f 100644 --- a/src/PolyTrader.App.Avalonia/Program.cs +++ b/src/PolyTrader.App.Avalonia/Program.cs @@ -293,6 +293,14 @@ namespace PolyTrader.App.Avalonia // Einstellungsdatei zur Laufzeit fortzuschreiben - gemeinsam fuer Desktop und WebUI. services.AddSingleton(new KnownModules(allModules)); services.AddSingleton(new ServerSettingsFile(ServerSettingsPath)); + + // #567: Passwortschutz fuer Konten, Geheimnisse und WebUI-Zugang in der Desktop-Oberflaeche. + // Eigene Datei neben master.key - nicht in server_settings.xml, die das WebUI mitschreibt. + services.AddSingleton(sp => new SettingsLock( + new SettingsPasswordStore( + Path.Combine(AppContext.BaseDirectory, "settings-password.json"), + onWarning: m => sp.GetRequiredService().Warning(m)), + log: m => sp.GetRequiredService().Info(m))); services.AddSingleton(logger); services.AddSingleton(); services.AddSingleton(); diff --git a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs index c650fc3..2fbc3d0 100644 --- a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs +++ b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs @@ -64,7 +64,8 @@ namespace PolyTrader.App.Avalonia.Shell services.GetService(), services.GetService(), services.GetService(), - services.GetService()) + services.GetService(), + services.GetRequiredService()) }); host.RegisterView(new ModuleView diff --git a/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs index 9046c13..ff610df 100644 --- a/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs +++ b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs @@ -244,12 +244,17 @@ namespace PolyTrader.App.Avalonia.ViewModels /// public static class SettingsModelBuilder { - public static IReadOnlyList Build(object target) + /// + /// false blendet Felder mit aus – + /// solange die geschützten Einstellungen gesperrt sind (#567). + /// + public static IReadOnlyList Build(object target, bool includeSensitive = true) { var fields = target.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.GetIndexParameters().Length == 0) .Where(p => p.GetCustomAttribute()?.Browsable != false) + .Where(p => includeSensitive || p.GetCustomAttribute() == null) .Select(p => new { Property = p, diff --git a/src/PolyTrader.App.Avalonia/Views/PasswordDialog.axaml b/src/PolyTrader.App.Avalonia/Views/PasswordDialog.axaml new file mode 100644 index 0000000..bc317cc --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Views/PasswordDialog.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + +