mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-05 04:27:21 +00:00
Authenticated encryption for enterprise secrets (#1078)
This commit is contained in:
1 parent
031874cebf
commit
274d718314
26 files changed
+740
-89
No files matched your search
@@ -370,4 +370,10 @@ public static class ConfigurationSelectDataFactory
|
||||
yield return new(bitrate.GetName(), bitrate);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<ConfigurationSelectData<EnterpriseEncryptionFormat>> GetEnterpriseEncryptionFormatData()
|
||||
{
|
||||
yield return new(TB("Compatible with all versions of AI Studio"), EnterpriseEncryptionFormat.V1);
|
||||
yield return new(TB("Protected against wrong encryption secrets, needs AI Studio v26.10.1 or newer"), EnterpriseEncryptionFormat.V2);
|
||||
}
|
||||
}
|
||||
@@ -315,6 +315,17 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
||||
/// </summary>
|
||||
public bool ShowAdminSettings { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowAdminSettings, false);
|
||||
|
||||
/// <summary>
|
||||
/// In which format do exports encrypt API keys, tokens, and passwords with the enterprise encryption secret?
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// New installations write the authenticated format v2. Installations which existed before
|
||||
/// v26.10.1 keep writing v1, which older versions read as well: large organizations package
|
||||
/// updates themselves, so their administrators run a new version weeks before everybody else.
|
||||
/// SettingsManager tells the two kinds of installations apart.
|
||||
/// </remarks>
|
||||
public EnterpriseEncryptionFormat EnterpriseEncryptionExportFormat { get; set; } = ManagedConfiguration.Register(configSelection, n => n.EnterpriseEncryptionExportFormat, EnterpriseEncryptionFormat.V2);
|
||||
|
||||
/// <summary>
|
||||
/// Should AI answers which are copied or exported end with a notice that AI generated them?
|
||||
/// </summary>
|
||||
|
||||
@@ -391,7 +391,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
|
||||
if (!EnterpriseEncryption.IsEncrypted(encryptedSecret))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} contains a plaintext {secretFieldName}. Only encrypted secrets (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||
LOGGER.LogWarning($"The configured data source {idx} contains a plaintext {secretFieldName}. Only encrypted secrets (starting with 'ENC:') are supported. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,10 @@ public sealed class DataTools(Expression<Func<Data, DataTools>>? configSelection
|
||||
/// known at compile time, yet an organization has to be able to configure them the same way.
|
||||
/// <br/><br/>
|
||||
/// A secret field travels here too, but only encrypted with the enterprise secret, in the
|
||||
/// same "ENC:v1:" form the providers use for their API keys. What is stored is therefore
|
||||
/// ciphertext, worthless without a secret that lives outside every deployed file. A plaintext
|
||||
/// secret is refused rather than used, and a secret is never accepted as a pre-filled default
|
||||
/// — see the tool settings service for both rules.
|
||||
/// same "ENC:v2:" or "ENC:v1:" form the providers use for their API keys. What is stored is
|
||||
/// therefore ciphertext, worthless without a secret that lives outside every deployed file. A
|
||||
/// plaintext secret is refused rather than used, and a secret is never accepted as a
|
||||
/// pre-filled default — see the tool settings service for both rules.
|
||||
/// </remarks>
|
||||
public Dictionary<string, string> LockedToolSettings { get; set; } = ManagedConfiguration.Register(
|
||||
configSelection,
|
||||
|
||||
@@ -182,7 +182,7 @@ public sealed record EmbeddingProvider(
|
||||
else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
||||
{
|
||||
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
||||
LOGGER.LogWarning($"The configured embedding provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||
LOGGER.LogWarning($"The configured embedding provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:') are supported. (Plugin ID: {configPluginId})");
|
||||
else
|
||||
{
|
||||
var encryption = PluginFactory.EnterpriseEncryption;
|
||||
|
||||
@@ -203,7 +203,7 @@ public sealed record Provider(
|
||||
else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
||||
{
|
||||
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
||||
LOGGER.LogWarning($"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||
LOGGER.LogWarning($"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:') are supported. (Plugin ID: {configPluginId})");
|
||||
else
|
||||
{
|
||||
var encryption = PluginFactory.EnterpriseEncryption;
|
||||
|
||||
@@ -186,6 +186,11 @@ public sealed class SettingsManager
|
||||
|
||||
this.logger.LogInformation("No valid current-version settings backup was found. Migrating the settings file.");
|
||||
settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion.Version, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
|
||||
|
||||
// Compatibility shim, remove after 2027-10-31, see KeepTheLegacyEncryptionExportFormat.
|
||||
// Settings of an older version always come from before the export format existed:
|
||||
settingsData.App.EnterpriseEncryptionExportFormat = EnterpriseEncryptionFormat.V1;
|
||||
|
||||
this.PrepareLoadedSettings(settingsData);
|
||||
await this.StoreSettingsSnapshot(settingsData, settingsPath);
|
||||
await this.StoreCurrentVersionBackup(settingsData);
|
||||
@@ -291,7 +296,8 @@ public sealed class SettingsManager
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsData = JsonSerializer.Deserialize<Data>(await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
|
||||
var settingsJson = await File.ReadAllTextAsync(settingsPath);
|
||||
var settingsData = JsonSerializer.Deserialize<Data>(settingsJson, JSON_OPTIONS);
|
||||
if(settingsData is null)
|
||||
{
|
||||
this.logger.LogError($"Failed to parse the {sourceDescription} '{settingsPath}'.");
|
||||
@@ -304,6 +310,7 @@ public sealed class SettingsManager
|
||||
return new(null, SettingsWriteBlockReason.CURRENT_VERSION_INVALID);
|
||||
}
|
||||
|
||||
KeepTheLegacyEncryptionExportFormat(settingsData, settingsJson);
|
||||
return new(settingsData, SettingsWriteBlockReason.NONE);
|
||||
}
|
||||
catch(Exception e)
|
||||
@@ -313,6 +320,28 @@ public sealed class SettingsManager
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lets an installation which existed before v26.10.1 keep exporting encrypted secrets in the
|
||||
/// format v1, which every version of AI Studio reads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Compatibility shim, remove after 2027-10-31, see
|
||||
/// documentation/compatibility-shims/2026-10-enterprise-encryption-export-format.md.<br/><br/>
|
||||
/// New installations export in the authenticated format v2. An existing installation, however,
|
||||
/// may belong to an administrator whose organization still runs an older version, which reads
|
||||
/// v1 only. Settings written before v26.10.1 lack the export format, and nothing else tells
|
||||
/// such an installation apart. Once the settings are stored, they hold the format, and this
|
||||
/// changes nothing anymore.
|
||||
/// </remarks>
|
||||
private static void KeepTheLegacyEncryptionExportFormat(Data settingsData, string settingsJson)
|
||||
{
|
||||
using var settingsDocument = JsonDocument.Parse(settingsJson);
|
||||
if(settingsDocument.RootElement.TryGetProperty(nameof(Data.App), out var appElement) && appElement.ValueKind is JsonValueKind.Object && appElement.TryGetProperty(nameof(DataApp.EnterpriseEncryptionExportFormat), out _))
|
||||
return;
|
||||
|
||||
settingsData.App.EnterpriseEncryptionExportFormat = EnterpriseEncryptionFormat.V1;
|
||||
}
|
||||
|
||||
private void BlockSettingsWrites(SettingsWriteBlockReason reason, string message)
|
||||
{
|
||||
this.SettingsWriteBlockReason = reason;
|
||||
|
||||
@@ -152,7 +152,7 @@ public sealed record TranscriptionProvider(
|
||||
else if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
||||
{
|
||||
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
||||
LOGGER.LogWarning($"The configured transcription provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||
LOGGER.LogWarning($"The configured transcription provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:') are supported. (Plugin ID: {configPluginId})");
|
||||
else
|
||||
{
|
||||
var encryption = PluginFactory.EnterpriseEncryption;
|
||||
|
||||
Reference in new issue
Block a user