diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 5f7f0df0..4b4f6a08 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -25,13 +25,22 @@ public static partial class PluginFactory /// /// Initializes the enterprise encryption service by reading the encryption secret - /// from the Windows Registry or environment variables. + /// from the effective enterprise source. /// /// The Rust service to use for reading the encryption secret. public static async Task InitializeEnterpriseEncryption(Services.RustService rustService) { - LOG.LogInformation("Initializing enterprise encryption service..."); var encryptionSecret = await rustService.EnterpriseEnvConfigEncryptionSecret(); + InitializeEnterpriseEncryption(encryptionSecret); + } + + /// + /// Initializes the enterprise encryption service using a prefetched secret value. + /// + /// The base64-encoded enterprise encryption secret. + public static void InitializeEnterpriseEncryption(string? encryptionSecret) + { + LOG.LogInformation("Initializing enterprise encryption service..."); var enterpriseEncryptionLogger = Program.LOGGER_FACTORY.CreateLogger(); EnterpriseEncryption = new EnterpriseEncryption(enterpriseEncryptionLogger, encryptionSecret); diff --git a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs index 4d38eb15..656d7358 100644 --- a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs +++ b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs @@ -1,4 +1,8 @@ using AIStudio.Tools.PluginSystem; +using AIStudio.Settings; + +using System.Security.Cryptography; +using System.Text; namespace AIStudio.Tools.Services; @@ -7,8 +11,14 @@ public sealed class EnterpriseEnvironmentService(ILogger CURRENT_ENVIRONMENTS = []; public static bool HasValidEnterpriseSnapshot { get; private set; } + + private static EnterpriseSecretSnapshot CURRENT_SECRET_SNAPSHOT; private readonly record struct EnterpriseEnvironmentSnapshot(Guid ConfigurationId, string ConfigurationServerUrl, string? ETag); + + private readonly record struct EnterpriseSecretSnapshot(bool HasSecret, string Fingerprint); + + private readonly record struct EnterpriseSecretTarget(string SecretId, string SecretName, SecretStoreType StoreType) : ISecretId; #if DEBUG private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(6); @@ -39,6 +49,7 @@ public sealed class EnterpriseEnvironmentService(ILogger(null, Event.ENTERPRISE_ENVIRONMENTS_CHANGED); } catch (Exception e) @@ -193,8 +229,81 @@ public sealed class EnterpriseEnvironmentService(ILogger BuildSecretSnapshot(string secret) + { + if (string.IsNullOrWhiteSpace(secret)) + return new EnterpriseSecretSnapshot(false, string.Empty); + + return new EnterpriseSecretSnapshot(true, await ComputeSecretFingerprint(secret)); + } + + private static async Task ComputeSecretFingerprint(string secret) + { + using var secretStream = new MemoryStream(Encoding.UTF8.GetBytes(secret)); + var hash = await SHA256.HashDataAsync(secretStream); + return Convert.ToHexString(hash); + } + private static string NormalizeServerUrl(string serverUrl) { return serverUrl.Trim().TrimEnd('/'); } + + private async Task RemoveEnterpriseManagedApiKeysAsync() + { + var secretTargets = GetEnterpriseManagedSecretTargets(); + if (secretTargets.Count == 0) + { + logger.LogInformation("No enterprise-managed API keys are currently known in the settings. No keyring cleanup is required."); + return; + } + + logger.LogInformation("Removing {SecretCount} enterprise-managed API key(s) from the OS keyring after an enterprise encryption secret change.", secretTargets.Count); + foreach (var target in secretTargets) + { + try + { + var deleteResult = await rustService.DeleteAPIKey(target, target.StoreType); + if (deleteResult.Success) + { + if (deleteResult.WasEntryFound) + logger.LogInformation("Successfully deleted enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName); + else + logger.LogInformation("Enterprise-managed API key '{SecretName}' was already absent from the OS keyring.", target.SecretName); + } + else + logger.LogWarning("Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring: {Issue}", target.SecretName, deleteResult.Issue); + } + catch (Exception e) + { + logger.LogWarning(e, "Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName); + } + } + } + + private static List GetEnterpriseManagedSecretTargets() + { + var configurationData = Program.SERVICE_PROVIDER.GetRequiredService().ConfigurationData; + var secretTargets = new HashSet(); + + AddEnterpriseManagedSecretTargets(configurationData.Providers, SecretStoreType.LLM_PROVIDER, secretTargets); + AddEnterpriseManagedSecretTargets(configurationData.EmbeddingProviders, SecretStoreType.EMBEDDING_PROVIDER, secretTargets); + AddEnterpriseManagedSecretTargets(configurationData.TranscriptionProviders, SecretStoreType.TRANSCRIPTION_PROVIDER, secretTargets); + + return secretTargets.ToList(); + } + + private static void AddEnterpriseManagedSecretTargets( + IEnumerable secrets, + SecretStoreType storeType, + ISet secretTargets) where TSecret : ISecretId, IConfigurationObject + { + foreach (var secret in secrets) + { + if (!secret.IsEnterpriseConfiguration || secret.EnterpriseConfigurationPluginId == Guid.Empty) + continue; + + secretTargets.Add(new EnterpriseSecretTarget(secret.SecretId, secret.SecretName, storeType)); + } + } } \ No newline at end of file