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>
195 lines
9.1 KiB
C#
195 lines
9.1 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|