Files
PolyTraderSharp/src/PolyTrader.Core/Models/ServerSettings.cs
T
RichardandClaude Opus 5 86b497bdbf
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s
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 <noreply@anthropic.com>
2026-09-13 14:34:32 +02:00

195 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Xml.Serialization;
namespace PolyTraderSharp.Models
{
/// <summary>Farbschema der Oberfläche.</summary>
public enum AppThemeVariant
{
/// <summary>Folgt der Einstellung des Betriebssystems.</summary>
System,
Hell,
Dunkel
}
public class ServerSettings
{
[Category("Module")]
[DisplayName("Deaktivierte Module")]
[Description("Namen der Module (z.B. \"CopyTrading\"), die beim App-Start NICHT geladen werden. " +
"Leer = alle Module aktiv. Änderungen greifen nach dem nächsten Neustart. " +
"Wird i.d.R. über das Dashboard gepflegt.")]
[XmlArray("DisabledModules")]
[XmlArrayItem("Module")]
public List<string> DisabledModules { get; set; } = new();
[Category("Anzeige")]
[DisplayName("Farbschema")]
[Description("Hell, Dunkel oder der Einstellung des Betriebssystems folgen. " +
"Lässt sich jederzeit über den Umschalter in der Fensterleiste wechseln; " +
"der zuletzt gewählte Stand wird hier gespeichert.")]
public AppThemeVariant Theme { get; set; } = AppThemeVariant.System;
[Category("Anzeige")]
[DisplayName("Zeitzone")]
[Description("Zeitzone für Anzeige und Logdatei-Tagesgrenzen, IANA-Schreibweise (z.B. \"Europe/Berlin\", " +
"\"America/New_York\"). Leer = Zeitzone des Systems. Wird üblicherweise EINMALIG bei der " +
"Installation gesetzt: eine spätere Änderung verschiebt Tagesgrenzen in Logs und Auswertungen. " +
"Greift erst nach einem Neustart.")]
public string ApplicationTimeZoneId { get; set; } = PolyTraderSharp.Services.AppTimeZone.DefaultId;
[Category("Mullvad VPN")]
[DisplayName("VPN Enabled")]
[Description("Enable or disable automatic VPN rotation.")]
public bool VpnEnabled { get; set; } = false;
[Category("Mullvad VPN")]
[PolyTrader.Core.Security.SensitiveSetting] // #567: im Desktop nur entsperrt sichtbar
[DisplayName("Mullvad Account")]
[Description("Account ID for Mullvad VPN. In server_settings.xml (gitignored) hinterlegen – NICHT im Code committen.")]
public string MullvadAccount { get; set; } = ""; // F3: Secret nicht mehr im Quellcode
[Category("Mullvad VPN")]
[DisplayName("VPN Location")]
[Description("Target VPN location (e.g. cz).")]
public string VpnLocation { get; set; } = "cz";
[Category("Mullvad VPN")]
[DisplayName("Mullvad CLI Path")]
[Description("Pfad zum Mullvad-Kommandozeilenwerkzeug. Der Standard richtet sich nach dem " +
"Betriebssystem: unter Linux /usr/bin/mullvad, unter Windows die mullvad.exe im " +
"Installationsverzeichnis.")]
public string MullvadCliPath { get; set; } = DefaultMullvadCliPath;
/// <summary>
/// Standardpfad des Mullvad-CLI je Plattform. Bis zum 26.08.2026 stand hier fest der
/// Windows-Pfad - auf dem Linux-Zielserver war der Standardwert damit sinnlos und die
/// VPN-Rotation scheiterte mit einer Meldung, der man die Ursache nicht ansieht
/// (Befund B-5 des Linux-Sichttests).
/// </summary>
public static string DefaultMullvadCliPath =>
System.OperatingSystem.IsWindows()
? @"C:\Program Files\Mullvad VPN\resources\mullvad.exe"
: "/usr/bin/mullvad";
[Category("Blockchain Listener")]
[DisplayName("Enable Blockchain Listener")]
[Description("If true, connects to Alchemy WSS for faster on-chain signal detection.")]
public bool EnableBlockchainListener { get; set; } = true;
[Category("Blockchain Listener")]
[PolyTrader.Core.Security.SensitiveSetting] // #567: enthaelt den Alchemy-Key
[DisplayName("Polygon RPC URL")]
[Description("RPC-/WSS-URL inkl. Alchemy-API-Key. In server_settings.xml (gitignored) hinterlegen – der Key gehört NICHT in den Quellcode/Git. Format: wss://polygon-mainnet.g.alchemy.com/v2/<KEY>")]
public string PolygonRpcUrl { get; set; } = ""; // F3: Alchemy-Key nicht mehr im Quellcode
[Category("Polymarket WebSockets")]
[DisplayName("Use Polymarket WebSockets")]
[Description("If true, connects to Polymarket WSS for live market prices and user events.")]
public bool UsePolymarketWebsockets { get; set; } = false;
[Category("Watchdog Monitoring")]
[DisplayName("Watchdog Enabled")]
[Description("Wenn true, sendet PolyTrader im festen Takt einen Heartbeat an den Watchdog " +
"des Deploymentcenters. Bleiben die Heartbeats aus, alarmiert der Watchdog " +
"(Dead-Man's-Switch). Ein nicht erreichbarer Watchdog beeinträchtigt PolyTrader NIE.")]
public bool WatchdogEnabled { get; set; } = false;
[Category("Watchdog Monitoring")]
[DisplayName("Deploymentcenter-URL")]
[Description("Basis-URL des Deploymentcenters, z.B. https://dc.mhdf.de (ersetzt den früheren " +
"eigenständigen Watchdog-Server). HTTPS ist Pflicht – http:// wird nur für localhost " +
"akzeptiert, damit der Agent-Token nicht im Klartext über das Netz geht.")]
public string WatchdogUrl { get; set; } = "";
/// <summary>
/// Agent-Token für den Header <c>X-Watchdog-Key</c>. Bewusst NICHT im PropertyGrid sichtbar:
/// Setzen erfolgt über den Button „Watchdog-Token setzen …" mit maskierter Eingabe, damit das
/// Secret nicht offen im Fenster steht und bei gesetztem Master-Key verschlüsselt
/// (<c>enc:v1:…</c>) in der gitignorierten server_settings.xml landet.
/// </summary>
[Browsable(false)]
public string WatchdogToken { get; set; } = "";
[Category("Watchdog Monitoring")]
[DisplayName("Agent-Token")]
[Description("Status des Agent-Tokens. Setzen/Ändern über den Button „Watchdog-Token setzen …" +
"\" in der Werkzeugleiste (maskierte Eingabe).")]
[ReadOnly(true)]
[XmlIgnore]
public string WatchdogTokenStatus =>
string.IsNullOrWhiteSpace(WatchdogToken)
? "nicht gesetzt"
: (WatchdogToken.StartsWith("enc:v1:", System.StringComparison.Ordinal)
? "gesetzt (verschlüsselt)"
: "gesetzt (Klartext)");
[Category("Watchdog Monitoring")]
[DisplayName("Source")]
[Description("Dienst-Kennung im Watchdog (muss dem dort angelegten Monitor entsprechen), z.B. \"polytrader\".")]
public string WatchdogSource { get; set; } = "polytrader";
[Category("Watchdog Monitoring")]
[DisplayName("Instance")]
[Description("Instanz-Kennung, falls mehrere PolyTrader-Instanzen laufen, z.B. \"prod-1\".")]
public string WatchdogInstance { get; set; } = "prod-1";
[Category("Watchdog Monitoring")]
[DisplayName("Heartbeat-Intervall (Sekunden)")]
[Description("Sende-Takt des Heartbeats. Der Watchdog leitet daraus seine Staleness-Frist ab " +
"(Intervall × Faktor + Grace). Minimum 10 s, Empfehlung 60 s.")]
public int WatchdogIntervalSeconds { get; set; } = 60;
/// <summary>
/// Lizenzschlüssel (LicenseLabrador). Wie der Watchdog-Token bewusst NICHT im PropertyGrid
/// sichtbar; gepflegt über den Button „Lizenzschlüssel setzen …" bzw. den Startdialog. Bei
/// gesetztem Master-Key verschlüsselt (<c>enc:v1:…</c>) in der gitignorierten server_settings.xml.
/// </summary>
[Browsable(false)]
public string LicenseKey { get; set; } = "";
[Category("Lizenz")]
[DisplayName("Lizenzschlüssel")]
[Description("Status des Lizenzschlüssels. Setzen/Ändern über den Button „Lizenzschlüssel setzen …\" " +
"in der Werkzeugleiste bzw. beim Programmstart, falls keine gültige Lizenz vorliegt.")]
[ReadOnly(true)]
[XmlIgnore]
public string LicenseKeyStatus =>
string.IsNullOrWhiteSpace(LicenseKey)
? "nicht gesetzt"
: (LicenseKey.StartsWith("enc:v1:", System.StringComparison.Ordinal)
? "gesetzt (verschlüsselt)"
: "gesetzt (Klartext)");
public static ServerSettings Load(string path)
{
if (!File.Exists(path))
return new ServerSettings();
try
{
var serializer = new XmlSerializer(typeof(ServerSettings));
using var fs = new FileStream(path, FileMode.Open);
return (ServerSettings?)serializer.Deserialize(fs) ?? new ServerSettings();
}
catch
{
return new ServerSettings();
}
}
public void Save(string path)
{
var serializer = new XmlSerializer(typeof(ServerSettings));
using var fs = new FileStream(path, FileMode.Create);
serializer.Serialize(fs, this);
}
}
}