Authenticated encryption for enterprise secrets (#1078)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-17 20:36:34 +02:00
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;
+1 -1
View File
@@ -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;