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
};
}