using System.Text.Json; using ClawdDotNet.Core.Config; namespace ClawdDotNet.Core.Security; /// /// Weiß, welche Felder einer Konfiguration Zugangsdaten enthalten, und wendet den /// darauf an. /// /// Beide Richtungen sind gefahrlos mehrfach anwendbar: Schützen überspringt bereits /// geschützte Werte, Entschlüsseln gibt Klartext unverändert zurück. Dadurch werden /// bestehende Konfigurationen beim ersten Speichern automatisch übernommen. /// public static class ConfigSecrets { /// /// Schlüsselnamen in Tool-Konfigurationen, deren Werte als Zugangsdaten gelten. /// Die Tool-Konfiguration ist ein freies Wörterbuch — deshalb wird hier nach /// Namen entschieden. /// private static readonly HashSet SecretKeys = new(StringComparer.OrdinalIgnoreCase) { "password", "password2fa", "passwort", "apikey", "api_key", "xapikey", "openrouterapikey", "apihash", "apisecret", "token", "accesstoken", "bottoken", "authtoken", "webuitoken", "secret", "clientsecret", "connectionstring" }; public static bool IsSecretKey(string key) => SecretKeys.Contains(key); // ─── Instanz ─── public static void Protect(InstanceConfig config) => Apply(config, SecretProtector.Protect); public static void Unprotect(InstanceConfig config) => Apply(config, SecretProtector.Unprotect); private static void Apply(InstanceConfig config, Func transform) { config.OpenRouterApiKey = transform(config.OpenRouterApiKey) ?? ""; // Das Zugangstoken der Weboberflaeche ist ein Zugangsdatum wie jedes andere. // Ohne diese Zeile stuende es im Klartext neben dem geschuetzten // OpenRouter-Schluessel — der Name allein in SecretKeys traegt hier nicht, weil // InstanceConfig kein freies Woerterbuch ist, sondern feste Felder hat. config.WebUiToken = transform(config.WebUiToken) ?? ""; if (config.TelegramClient is { } telegram) { telegram.ApiHash = transform(telegram.ApiHash) ?? ""; telegram.Password2FA = transform(telegram.Password2FA); } if (config.Watchdog is { } watchdog) watchdog.AgentToken = transform(watchdog.AgentToken) ?? ""; // #712: the Claude Code account — a key with a bill behind it. if (config.ClaudeCode is { } claudeCode) { claudeCode.ApiKey = transform(claudeCode.ApiKey) ?? ""; claudeCode.OAuthToken = transform(claudeCode.OAuthToken) ?? ""; } foreach (var agent in config.Agents) Apply(agent, transform); // #197: an MCP server's headers carry its token — every value is a secret. foreach (var server in config.McpServers) foreach (var key in server.Headers.Keys.ToList()) if (server.Headers[key] is { Length: > 0 } header) server.Headers[key] = transform(header) ?? ""; // #692: everything a unit holds for its members is a secret, whatever its name. foreach (var unit in config.Units) foreach (var settings in unit.Credentials?.Values ?? Enumerable.Empty>()) foreach (var key in settings.Keys.ToList()) if (settings[key] is { Length: > 0 } value) settings[key] = transform(value) ?? ""; } // ─── Agent ─── public static void Protect(AgentConfig agent) => Apply(agent, SecretProtector.Protect); public static void Unprotect(AgentConfig agent) => Apply(agent, SecretProtector.Unprotect); private static void Apply(AgentConfig agent, Func transform) { foreach (var tool in agent.Tools.Values) { foreach (var key in tool.Keys.ToList()) { if (!IsSecretKey(key)) continue; if (ReadString(tool[key]) is not { } current || current.Length == 0) continue; tool[key] = transform(current); } } } /// /// Liest einen Zeichenkettenwert. Nach dem Deserialisieren stecken die Werte als /// JsonElement im Wörterbuch, nach einer Bearbeitung als einfache Zeichenkette. /// private static string? ReadString(object? value) => value switch { null => null, string s => s, JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(), _ => null }; }