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

@@ -5152,6 +5152,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"]
-- AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T476576809"] = "AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions."
-- Applies when you export providers, data sources, or tool settings with their API keys, tokens, or passwords. The protected format makes AI Studio refuse every value which was encrypted with another secret, but versions before v26.10.1 cannot read it. Keep the compatible format until every computer in your organization runs v26.10.1 or newer. Both formats use the same encryption secret.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602260056"] = "Applies when you export providers, data sources, or tool settings with their API keys, tokens, or passwords. The protected format makes AI Studio refuse every value which was encrypted with another secret, but versions before v26.10.1 cannot read it. Keep the compatible format until every computer in your organization runs v26.10.1 or newer. Both formats use the same encryption secret."
-- Navigation bar behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
@@ -5185,6 +5188,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
-- Format for encrypted secrets in exports
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T938676916"] = "Format for encrypted secrets in exports"
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1052533048"] = "Show provider's confidence level?"
@@ -11551,6 +11557,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3137986690
-- Delete disappearing chats older than 180 days
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707"] = "Delete disappearing chats older than 180 days"
-- Compatible with all versions of AI Studio
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3559426588"] = "Compatible with all versions of AI Studio"
-- Install updates automatically
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Install updates automatically"
@@ -11563,6 +11572,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107
-- Toggle the sidebar: show the workspaces next to the chat when desired
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137"] = "Toggle the sidebar: show the workspaces next to the chat when desired"
-- Protected against wrong encryption secrets, needs AI Studio v26.10.1 or newer
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3831574746"] = "Protected against wrong encryption secrets, needs AI Studio v26.10.1 or newer"
-- Updates disabled by your organization
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4048970098"] = "Updates disabled by your organization"
@@ -366,7 +366,7 @@ public partial class DataSourceManagement : MSGComponentBase
}
var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION);
if (!encryption.TryEncrypt(decryptedSecret, out var encryptedSecret))
if (!encryption.TryEncrypt(decryptedSecret, this.SettingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, out var encryptedSecret))
{
await this.DialogService.ShowMessageBox(
T("Export ERI Data Source"),
@@ -71,10 +71,13 @@
<MudButton StartIcon="@Icons.Material.Filled.Key"
Variant="Variant.Filled"
Color="Color.Primary"
Class="mb-6"
OnClick="@this.GenerateEncryptionSecret">
@T("Generate an encryption secret and copy it to the clipboard")
</MudButton>
<ConfigurationSelect OptionDescription="@T("Format for encrypted secrets in exports")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat)" Data="@ConfigurationSelectDataFactory.GetEnterpriseEncryptionFormatData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat = selectedValue)" OptionHelp="@T("Applies when you export providers, data sources, or tool settings with their API keys, tokens, or passwords. The protected format makes AI Studio refuse every value which was encrypted with another secret, but versions before v26.10.1 cannot read it. Keep the compatible format until every computer in your organization runs v26.10.1 or newer. Both formats use the same encryption secret.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnterpriseEncryptionExportFormat, out var meta) && meta.IsLocked"/>
<MudText Typo="Typo.h6" Class="mt-6 mb-3">
@T("External HTTPS certificates")
</MudText>
@@ -41,7 +41,7 @@ public abstract class SettingsPanelProviderBase : SettingsPanelBase
if (encryption?.IsAvailable == true)
{
var decryptedApiKey = await apiKeyResponse.Secret.Decrypt(Program.ENCRYPTION);
if (encryption.TryEncrypt(decryptedApiKey, out var encrypted))
if (encryption.TryEncrypt(decryptedApiKey, this.SettingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, out var encrypted))
encryptedApiKey = encrypted;
}
else
@@ -154,12 +154,15 @@ CONFIG["LLM_PROVIDERS"] = {}
--
-- -- Optional: Encrypted API key for cloud providers or secured on-premise models.
-- -- The API key must be encrypted using the enterprise encryption secret.
-- -- Format: "ENC:v1:<base64-encoded encrypted data>"
-- -- Format: "ENC:v2:<base64-encoded encrypted data>", which AI Studio v26.10.1 or newer reads,
-- -- or "ENC:v1:<base64-encoded encrypted data>", which every version of AI Studio reads. Both
-- -- formats use the same secret. DataApp.EnterpriseEncryptionExportFormat below decides which
-- -- one your exports write.
-- -- The encryption secret must be configured via:
-- -- Windows Registry: HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT\config_encryption_secret
-- -- Environment variable: MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET
-- -- You can export an encrypted API key from an existing provider using the export button in the settings.
-- -- ["APIKey"] = "ENC:v1:<base64-encoded encrypted data>",
-- -- ["APIKey"] = "ENC:v2:<base64-encoded encrypted data>",
--
-- -- Optional: let each user set their own API key for this otherwise locked provider,
-- -- instead of (or in addition to not) embedding one centrally. Host, model, instance
@@ -193,7 +196,7 @@ CONFIG["TRANSCRIPTION_PROVIDERS"] = {}
-- -- ["IconPath"] = "assets/project-icon.svg",
--
-- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details)
-- -- ["APIKey"] = "ENC:v1:<base64-encoded encrypted data>",
-- -- ["APIKey"] = "ENC:v2:<base64-encoded encrypted data>",
--
-- -- Optional: let each user set their own API key for this otherwise locked transcription
-- -- provider (see LLM_PROVIDERS example for details). Mutually exclusive with "APIKey"
@@ -230,7 +233,7 @@ CONFIG["EMBEDDING_PROVIDERS"] = {}
-- -- ["IconPath"] = "assets/project-icon.svg",
--
-- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details)
-- -- ["APIKey"] = "ENC:v1:<base64-encoded encrypted data>",
-- -- ["APIKey"] = "ENC:v2:<base64-encoded encrypted data>",
--
-- -- Optional: tokenizer path for this provider relative to the plugin directory.
-- -- ["TokenizerPath"] = "",
@@ -269,7 +272,7 @@ CONFIG["DATA_SOURCES"] = {}
-- ["Hostname"] = "<https address of the ERI server>",
-- ["Port"] = 443,
-- ["AuthMethod"] = "TOKEN",
-- ["Token"] = "ENC:v1:<base64-encoded encrypted token>",
-- ["Token"] = "ENC:v2:<base64-encoded encrypted token>",
-- ["SecurityPolicy"] = "SELF_HOSTED",
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
-- ["MaxMatches"] = 10,
@@ -285,7 +288,7 @@ CONFIG["DATA_SOURCES"] = {}
-- ["AuthMethod"] = "USERNAME_PASSWORD",
-- ["UsernamePasswordMode"] = "SHARED_USERNAME_AND_PASSWORD",
-- ["Username"] = "<shared username>",
-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>",
-- ["Password"] = "ENC:v2:<base64-encoded encrypted password>",
-- ["SecurityPolicy"] = "SELF_HOSTED",
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
-- ["MaxMatches"] = 10,
@@ -300,7 +303,7 @@ CONFIG["DATA_SOURCES"] = {}
-- ["Port"] = 443,
-- ["AuthMethod"] = "USERNAME_PASSWORD",
-- ["UsernamePasswordMode"] = "OS_USERNAME_SHARED_PASSWORD",
-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>",
-- ["Password"] = "ENC:v2:<base64-encoded encrypted password>",
-- ["SecurityPolicy"] = "SELF_HOSTED",
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
-- ["MaxMatches"] = 10,
@@ -482,6 +485,22 @@ CONFIG["SETTINGS"] = {}
-- Configure whether administration settings are visible in the UI:
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
-- Configure the format in which exports encrypt API keys, tokens, and passwords with the
-- enterprise encryption secret. Both formats use the same secret.
-- Allowed values are: V1, V2
-- V2 is protected: AI Studio refuses every value which was encrypted with another secret. It
-- needs AI Studio v26.10.1 or newer on every computer which reads it. V1 is read by every
-- version of AI Studio, and AI Studio keeps reading it for good. New installations start with
-- V2, installations which existed before v26.10.1 keep V1. Keep V1 until every computer in
-- your organization runs v26.10.1 or newer, then switch to V2.
-- CONFIG["SETTINGS"]["DataApp.EnterpriseEncryptionExportFormat"] = "V1"
--
-- Allow users to change the export format locally.
-- Allowed values are: true, false
-- When set to true, the configured format becomes the organization default,
-- but users can still choose another format in the enterprise administration settings.
-- CONFIG["SETTINGS"]["DataApp.EnterpriseEncryptionExportFormat.AllowUserOverride"] = true
-- Configure whether AI answers which users copy or export end with a notice that they were
-- generated using artificial intelligence (AI). The notice is written in the language of the app.
-- It also ends the files of the batch processing. Code blocks, single files or tables taken from
@@ -896,10 +915,10 @@ CONFIG["SETTINGS"] = {}
-- DefaultToolSettings - pre-fills the setting; a value the user saves afterwards wins.
--
-- A tool field marked as secret, such as an API key, can be rolled out as well — but only
-- encrypted with the enterprise encryption secret, in the same "ENC:v1:<base64>" form the
-- providers above use, and only through LockedToolSettings. A locked secret leaves whatever
-- the user entered untouched, so their own key returns when you stop deploying yours. A
-- plaintext secret is refused with a warning in the log rather than used.
-- encrypted with the enterprise encryption secret, in the same "ENC:v2:<base64>" or
-- "ENC:v1:<base64>" form the providers above use, and only through LockedToolSettings. A locked
-- secret leaves whatever the user entered untouched, so their own key returns when you stop
-- deploying yours. A plaintext secret is refused with a warning in the log rather than used.
--
-- Field names of the Web Search tool. At least one of its search services has to be configured
-- before the tool can be used; which one you pick is up to you, since all three can be rolled
@@ -979,7 +998,7 @@ CONFIG["SETTINGS"] = {}
-- ["web_search.searxng.baseUrl"] = "https://searxng.example.org/",
-- ["web_search.defaultLanguage"] = "de-DE",
-- ["web_search.backendStrategy"] = "FAILOVER",
-- ["web_search.tavily.apiKey"] = "ENC:v1:<base64-encoded encrypted data>",
-- ["web_search.tavily.apiKey"] = "ENC:v2:<base64-encoded encrypted data>",
-- ["read_web_page.freeAddressChoice"] = "OFF",
-- ["read_web_page.allowedPrivateHosts"] = "example.org, *.example.org",
-- ["search_confluence.baseUrl"] = "https://wiki.example.org/confluence/"
@@ -5154,6 +5154,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"]
-- AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T476576809"] = "AI Studio kann in dieser Installation keine Updates installieren. Wenden Sie sich für neue Versionen an die Person oder Organisation, die die Installation vorgenommen hat."
-- Applies when you export providers, data sources, or tool settings with their API keys, tokens, or passwords. The protected format makes AI Studio refuse every value which was encrypted with another secret, but versions before v26.10.1 cannot read it. Keep the compatible format until every computer in your organization runs v26.10.1 or newer. Both formats use the same encryption secret.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602260056"] = "Gilt, wenn Sie Anbieter, Datenquellen oder Werkzeugeinstellungen zusammen mit den zugehörigen API-Schlüsseln, Token oder Passwörtern exportieren. Im geschützten Format lehnt AI Studio alle Werte ab, die mit einem anderen Geheimnis verschlüsselt wurden. Versionen vor v26.10.1 können dieses Format jedoch nicht lesen. Verwenden Sie weiterhin das kompatible Format, bis auf allen Rechnern Ihrer Organisation v26.10.1 oder eine neuere Version installiert ist. Beide Formate verwenden dasselbe Geheimnis für die Verschlüsselung."
-- Navigation bar behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Verhalten der Navigationsleiste"
@@ -5187,6 +5190,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Die Optionen für die Administration sind nicht sichtbar."
-- Format for encrypted secrets in exports
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T938676916"] = "Format für verschlüsselte Geheimnisse in Exporten"
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1052533048"] = "Vertrauensniveau des Anbieters anzeigen?"
@@ -11553,6 +11559,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3137986690
-- Delete disappearing chats older than 180 days
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707"] = "Selbstlöschende Chats: lösche Chats die älter als 180 Tage sind"
-- Compatible with all versions of AI Studio
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3559426588"] = "Kompatibel mit allen Versionen von AI Studio"
-- Install updates automatically
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Updates automatisch installieren"
@@ -11565,6 +11574,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107
-- Toggle the sidebar: show the workspaces next to the chat when desired
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137"] = "Seitenleiste umschalten: Arbeitsbereiche bei Bedarf neben dem Chat anzeigen"
-- Protected against wrong encryption secrets, needs AI Studio v26.10.1 or newer
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3831574746"] = "Geschützt vor falschen Geheimnissen für die Verschlüsselung, erfordert AI Studio v26.10.1 oder neuer"
-- Updates disabled by your organization
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4048970098"] = "Updates wurden von Ihrer Organisation deaktiviert"
@@ -5154,6 +5154,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T436881267"]
-- AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T476576809"] = "AI Studio cannot install updates into this installation. Contact the person or organization that installed it for new versions."
-- Applies when you export providers, data sources, or tool settings with their API keys, tokens, or passwords. The protected format makes AI Studio refuse every value which was encrypted with another secret, but versions before v26.10.1 cannot read it. Keep the compatible format until every computer in your organization runs v26.10.1 or newer. Both formats use the same encryption secret.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602260056"] = "Applies when you export providers, data sources, or tool settings with their API keys, tokens, or passwords. The protected format makes AI Studio refuse every value which was encrypted with another secret, but versions before v26.10.1 cannot read it. Keep the compatible format until every computer in your organization runs v26.10.1 or newer. Both formats use the same encryption secret."
-- Navigation bar behavior
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
@@ -5187,6 +5190,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
-- Format for encrypted secrets in exports
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T938676916"] = "Format for encrypted secrets in exports"
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1052533048"] = "Show provider's confidence level?"
@@ -11553,6 +11559,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3137986690
-- Delete disappearing chats older than 180 days
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707"] = "Disappearing chats: delete chats older than 180 days"
-- Compatible with all versions of AI Studio
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3559426588"] = "Compatible with all versions of AI Studio"
-- Install updates automatically
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Install updates automatically"
@@ -11565,6 +11574,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107
-- Toggle the sidebar: show the workspaces next to the chat when desired
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137"] = "Toggle the sidebar: show the workspaces on demand next to the chat"
-- Protected against wrong encryption secrets, needs AI Studio v26.10.1 or newer
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3831574746"] = "Protected against wrong encryption secrets, needs AI Studio v26.10.1 or newer"
-- Updates disabled by your organization
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4048970098"] = "Updates disabled by your organization"
@@ -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;
@@ -13,25 +13,68 @@ namespace AIStudio.Tools;
/// is designed to prevent casual exposure of API keys in configuration files. It
/// also protects against accidental leaks while sharing configuration snippets,
/// as the encrypted values cannot be decrypted without the secret key.
/// <br/><br/>
/// Both formats use the same secret. "ENC:v2:" is authenticated: AES-256-GCM refuses
/// every value which was encrypted with another secret or changed afterward. "ENC:v1:"
/// uses AES-256-CBC without authentication, so a wrong secret passes its padding check
/// in about one of 256 cases; decoding the result strictly as UTF-8 catches nearly all
/// of those. Versions before v26.10.1 read only v1, which is why AI Studio still writes
/// it when asked to.
/// </remarks>
public sealed class EnterpriseEncryption
{
/// <summary>
/// The number of iterations to derive the key and IV from the password.
/// The number of iterations to derive the key and IV of a v1 value from the password.
/// We use a higher iteration count here because the secret is static
/// (not regenerated each startup like the IPC encryption).
/// </summary>
private const int ITERATIONS = 10_000;
private const int V1_ITERATIONS = 10_000;
/// <summary>
/// The length of the salt in bytes.
/// The length of the salt in bytes, in both formats.
/// </summary>
private const int SALT_LENGTH = 16;
/// <summary>
/// The prefix for encrypted values.
/// The length of the AES-GCM nonce of a v2 value in bytes.
/// </summary>
private const string PREFIX = "ENC:v1:";
private const int V2_NONCE_LENGTH = 12;
/// <summary>
/// The length of the AES-GCM authentication tag of a v2 value in bytes.
/// </summary>
private const int V2_TAG_LENGTH = 16;
/// <summary>
/// The prefix all encrypted values share, whatever their format.
/// </summary>
private const string ENCRYPTED_PREFIX = "ENC:";
/// <summary>
/// The prefix of v1 values.
/// </summary>
private const string PREFIX_V1 = "ENC:v1:";
/// <summary>
/// The prefix of v2 values.
/// </summary>
private const string PREFIX_V2 = "ENC:v2:";
/// <summary>
/// What HKDF binds the key of a v2 value to, so that the key serves no other purpose.
/// </summary>
private static readonly byte[] V2_KEY_INFO = "MindWork AI Studio ENC:v2"u8.ToArray();
/// <summary>
/// The prefix is authenticated together with the ciphertext, so a v2 payload counts under
/// this prefix only.
/// </summary>
private static readonly byte[] V2_ASSOCIATED_DATA = Encoding.UTF8.GetBytes(PREFIX_V2);
/// <summary>
/// Decodes UTF-8 and throws on invalid bytes instead of replacing them.
/// </summary>
private static readonly UTF8Encoding STRICT_UTF8 = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
private readonly ILogger<EnterpriseEncryption> logger;
private readonly byte[]? secretKey;
@@ -79,16 +122,21 @@ public sealed class EnterpriseEncryption
}
/// <summary>
/// Checks if the given value is encrypted (has the encryption prefix).
/// Checks if the given value is encrypted, in whatever format.
/// </summary>
/// <remarks>
/// A format which only a newer version of AI Studio knows counts as encrypted, too. Such a
/// value cannot be decrypted here, and that is what the user has to learn, rather than that
/// it was stored in plaintext.
/// </remarks>
/// <param name="value">The value to check.</param>
/// <returns>True if the value starts with the encryption prefix; otherwise, false.</returns>
public static bool IsEncrypted(string? value) => value?.StartsWith(PREFIX, StringComparison.Ordinal) ?? false;
public static bool IsEncrypted(string? value) => value?.StartsWith(ENCRYPTED_PREFIX, StringComparison.Ordinal) ?? false;
/// <summary>
/// Tries to decrypt an encrypted value.
/// </summary>
/// <param name="encryptedValue">The encrypted value (with ENC:v1: prefix).</param>
/// <param name="encryptedValue">The encrypted value, with the ENC:v1: or ENC:v2: prefix.</param>
/// <param name="decryptedValue">When successful, contains the decrypted plaintext.</param>
/// <returns>True if decryption was successful; otherwise, false.</returns>
public bool TryDecrypt(string encryptedValue, out string decryptedValue)
@@ -108,35 +156,26 @@ public sealed class EnterpriseEncryption
try
{
// Extract the base64-encoded data after the prefix:
var base64Data = encryptedValue[PREFIX.Length..];
var encryptedBytes = Convert.FromBase64String(base64Data);
if (encryptedBytes.Length < SALT_LENGTH + 1)
byte[]? decryptedBytes;
if (encryptedValue.StartsWith(PREFIX_V2, StringComparison.Ordinal))
decryptedBytes = this.DecryptV2(Convert.FromBase64String(encryptedValue[PREFIX_V2.Length..]));
else if (encryptedValue.StartsWith(PREFIX_V1, StringComparison.Ordinal))
decryptedBytes = this.DecryptV1(Convert.FromBase64String(encryptedValue[PREFIX_V1.Length..]));
else
{
this.logger.LogWarning("Cannot decrypt: Encrypted data is too short.");
this.logger.LogWarning("Cannot decrypt: The value uses an encryption format which this version of AI Studio does not know. A newer version may have encrypted it.");
return false;
}
// Extract salt and encrypted content:
var salt = encryptedBytes[..SALT_LENGTH];
var cipherText = encryptedBytes[SALT_LENGTH..];
// Derive key and IV using PBKDF2:
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, ITERATIONS, HashAlgorithmName.SHA512);
var key = keyDerivation.GetBytes(32); // AES-256
var iv = keyDerivation.GetBytes(16); // AES block size
// Decrypt using AES-256-CBC:
using var aes = Aes.Create();
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var decryptor = aes.CreateDecryptor();
var decryptedBytes = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length);
decryptedValue = Encoding.UTF8.GetString(decryptedBytes);
if (decryptedBytes is null)
return false;
//
// AI Studio always encrypted valid UTF-8, so decoding strictly never refuses a genuine
// value. It refuses the random bytes, though, which a wrong secret yields for a v1 value
// whenever the padding happens to check out:
//
decryptedValue = STRICT_UTF8.GetString(decryptedBytes);
return true;
}
catch (FormatException ex)
@@ -149,15 +188,21 @@ public sealed class EnterpriseEncryption
this.logger.LogWarning(ex, "Failed to decrypt value. The encryption secret may be incorrect.");
return false;
}
catch (DecoderFallbackException ex)
{
this.logger.LogWarning(ex, "Failed to decrypt value, because the result is no text. The encryption secret may be incorrect.");
return false;
}
}
/// <summary>
/// Encrypts a plaintext value.
/// </summary>
/// <param name="plaintext">The plaintext to encrypt.</param>
/// <param name="format">The format to write. A v2 value is authenticated, but only AI Studio v26.10.1 and later can read it.</param>
/// <param name="encryptedValue">When successful, contains the encrypted value with prefix.</param>
/// <returns>True if encryption was successful; otherwise, false.</returns>
public bool TryEncrypt(string plaintext, out string encryptedValue)
public bool TryEncrypt(string plaintext, EnterpriseEncryptionFormat format, out string encryptedValue)
{
encryptedValue = string.Empty;
if (!this.IsAvailable)
@@ -168,33 +213,21 @@ public sealed class EnterpriseEncryption
try
{
// Generate a random salt:
var salt = RandomNumberGenerator.GetBytes(SALT_LENGTH);
// Derive key and IV using PBKDF2:
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, ITERATIONS, HashAlgorithmName.SHA512);
var key = keyDerivation.GetBytes(32); // AES-256
var iv = keyDerivation.GetBytes(16); // AES block size
// Encrypt using AES-256-CBC:
using var aes = Aes.Create();
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var encryptor = aes.CreateEncryptor();
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
var cipherText = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length);
switch (format)
{
case EnterpriseEncryptionFormat.V1:
encryptedValue = this.EncryptV1(plaintextBytes);
return true;
// Combine salt and ciphertext
var combined = new byte[SALT_LENGTH + cipherText.Length];
Array.Copy(salt, 0, combined, 0, SALT_LENGTH);
Array.Copy(cipherText, 0, combined, SALT_LENGTH, cipherText.Length);
case EnterpriseEncryptionFormat.V2:
encryptedValue = this.EncryptV2(plaintextBytes);
return true;
// Encode to base64 and add the prefix:
encryptedValue = PREFIX + Convert.ToBase64String(combined);
return true;
default:
this.logger.LogWarning($"Cannot encrypt: The encryption format '{format}' is unknown.");
return false;
}
}
catch (CryptographicException ex)
{
@@ -208,4 +241,118 @@ public sealed class EnterpriseEncryption
/// </summary>
/// <returns>A base64-encoded 32-byte secret key.</returns>
public static string GenerateSecret() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
/// <summary>
/// Encrypts into a v1 value: the salt, followed by the AES-256-CBC ciphertext.
/// </summary>
private string EncryptV1(byte[] plaintextBytes)
{
// Generate a random salt:
var salt = RandomNumberGenerator.GetBytes(SALT_LENGTH);
// Encrypt using AES-256-CBC:
using var aes = this.CreateAesV1(salt);
using var encryptor = aes.CreateEncryptor();
var cipherText = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length);
// Combine salt and ciphertext, encode to base64, and add the prefix:
byte[] combined = [..salt, ..cipherText];
return PREFIX_V1 + Convert.ToBase64String(combined);
}
/// <summary>
/// Decrypts the payload of a v1 value: the salt, followed by the AES-256-CBC ciphertext.
/// </summary>
/// <remarks>
/// Organizations deployed configuration plugins in this format, so AI Studio reads it for
/// good, as decided on 2026-10-17. This is no compatibility shim with a removal date.
/// </remarks>
/// <returns>The decrypted bytes, or null when the payload is too short.</returns>
private byte[]? DecryptV1(byte[] encryptedBytes)
{
if (encryptedBytes.Length < SALT_LENGTH + 1)
{
this.logger.LogWarning("Cannot decrypt: Encrypted data is too short.");
return null;
}
// Extract salt and encrypted content:
var salt = encryptedBytes[..SALT_LENGTH];
var cipherText = encryptedBytes[SALT_LENGTH..];
// Decrypt using AES-256-CBC:
using var aes = this.CreateAesV1(salt);
using var decryptor = aes.CreateDecryptor();
return decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length);
}
/// <summary>
/// Creates the AES-256-CBC instance for a v1 value, with key and IV derived from the secret
/// and the salt of that value.
/// </summary>
private Aes CreateAesV1(byte[] salt)
{
// Derive key and IV using PBKDF2:
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, V1_ITERATIONS, HashAlgorithmName.SHA512);
var aes = Aes.Create();
aes.Key = keyDerivation.GetBytes(32); // AES-256
aes.IV = keyDerivation.GetBytes(16); // AES block size
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
return aes;
}
/// <summary>
/// Encrypts into a v2 value: the salt, the nonce, the AES-256-GCM ciphertext, and its tag.
/// </summary>
private string EncryptV2(byte[] plaintextBytes)
{
// The random salt gives every value a key of its own; a random nonce on top costs nothing:
var salt = RandomNumberGenerator.GetBytes(SALT_LENGTH);
var nonce = RandomNumberGenerator.GetBytes(V2_NONCE_LENGTH);
var cipherText = new byte[plaintextBytes.Length];
var tag = new byte[V2_TAG_LENGTH];
using var aes = new AesGcm(this.DeriveKeyV2(salt), V2_TAG_LENGTH);
aes.Encrypt(nonce, plaintextBytes, cipherText, tag, V2_ASSOCIATED_DATA);
byte[] combined = [..salt, ..nonce, ..cipherText, ..tag];
return PREFIX_V2 + Convert.ToBase64String(combined);
}
/// <summary>
/// Decrypts the payload of a v2 value: the salt, the nonce, the AES-256-GCM ciphertext, and its tag.
/// </summary>
/// <remarks>
/// Throws an AuthenticationTagMismatchException, which is a CryptographicException, when the
/// tag does not match: for a wrong secret as well as for a changed value.
/// </remarks>
/// <returns>The decrypted bytes, or null when the payload is too short.</returns>
private byte[]? DecryptV2(byte[] encryptedBytes)
{
if (encryptedBytes.Length < SALT_LENGTH + V2_NONCE_LENGTH + V2_TAG_LENGTH)
{
this.logger.LogWarning("Cannot decrypt: Encrypted data is too short.");
return null;
}
var salt = encryptedBytes[..SALT_LENGTH];
var nonce = encryptedBytes[SALT_LENGTH..(SALT_LENGTH + V2_NONCE_LENGTH)];
var cipherText = encryptedBytes[(SALT_LENGTH + V2_NONCE_LENGTH)..^V2_TAG_LENGTH];
var tag = encryptedBytes[^V2_TAG_LENGTH..];
using var aes = new AesGcm(this.DeriveKeyV2(salt), V2_TAG_LENGTH);
var decryptedBytes = new byte[cipherText.Length];
aes.Decrypt(nonce, cipherText, tag, decryptedBytes, V2_ASSOCIATED_DATA);
return decryptedBytes;
}
/// <summary>
/// Derives the AES-256 key of a v2 value from the secret and the salt of that value.
/// </summary>
/// <remarks>
/// Unlike a password, the secret is meant to be 256 random bits, as GenerateSecret makes it.
/// Stretching it, as PBKDF2 does for v1 values, adds nothing, so HKDF suffices.
/// </remarks>
private byte[] DeriveKeyV2(byte[] salt) => HKDF.DeriveKey(HashAlgorithmName.SHA256, this.secretKey!, 32, salt, V2_KEY_INFO);
}
@@ -0,0 +1,19 @@
namespace AIStudio.Tools;
/// <summary>
/// The formats in which AI Studio encrypts the secrets of enterprise configuration plugins.
/// </summary>
public enum EnterpriseEncryptionFormat
{
/// <summary>
/// "ENC:v1:", which every version of AI Studio reads. It is not authenticated, so a wrong
/// encryption secret is not always detected.
/// </summary>
V1,
/// <summary>
/// "ENC:v2:", which is authenticated: AI Studio refuses every value encrypted with another
/// secret or changed afterward. Versions before v26.10.1 cannot read it.
/// </summary>
V2,
}
@@ -305,6 +305,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: show administration settings?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowAdminSettings, this.Id, settingsTable, dryRun);
// Config: in which format do exports encrypt secrets?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.EnterpriseEncryptionExportFormat, this.Id, settingsTable, dryRun);
// Config: add a disclosure to copied and exported AI-generated content?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AddAIGeneratedContentDisclosure, this.Id, settingsTable, dryRun);
@@ -73,7 +73,7 @@ public sealed partial class ToolSettingsService
continue;
var values = await this.GetSettingsAsync(tool.Definition);
var issue = CollectSettings(tool.Definition, areasOfTool, values, options, PluginFactory.EnterpriseEncryption, lockedValues, defaultValues);
var issue = CollectSettings(tool.Definition, areasOfTool, values, options, PluginFactory.EnterpriseEncryption, settingsManager.ConfigurationData.App.EnterpriseEncryptionExportFormat, lockedValues, defaultValues);
if (issue is not null)
return new(ErrorMessage: issue);
}
@@ -98,7 +98,7 @@ public sealed partial class ToolSettingsService
/// Adds the selected settings of one tool to the values to export, by their managed key.
/// </summary>
/// <returns>Why the export failed, or null when it may go on.</returns>
private static string? CollectSettings(ToolDefinition definition, IEnumerable<ExportableSettings> selectedAreas, IReadOnlyDictionary<string, string> values, ToolSettingsExportOptions options, EnterpriseEncryption? encryption, Dictionary<string, string> lockedValues, Dictionary<string, string> defaultValues)
private static string? CollectSettings(ToolDefinition definition, IEnumerable<ExportableSettings> selectedAreas, IReadOnlyDictionary<string, string> values, ToolSettingsExportOptions options, EnterpriseEncryption? encryption, EnterpriseEncryptionFormat encryptionFormat, Dictionary<string, string> lockedValues, Dictionary<string, string> defaultValues)
{
foreach (var fieldName in GetSelectedFieldNames(definition, selectedAreas))
{
@@ -114,7 +114,7 @@ public sealed partial class ToolSettingsService
if (encryption?.IsAvailable is not true)
return TB("Cannot export encrypted tool secrets: No enterprise encryption secret is configured.");
if (!encryption.TryEncrypt(value, out var encrypted))
if (!encryption.TryEncrypt(value, encryptionFormat, out var encrypted))
return TB("The tool secrets could not be encrypted. Nothing was exported.");
lockedValues[key] = encrypted;
@@ -182,7 +182,7 @@ public sealed partial class ToolSettingsService(SettingsManager settingsManager,
if (!EnterpriseEncryption.IsEncrypted(encryptedSecret))
{
logger.LogWarning("The managed tool setting '{ManagedKey}' holds a plaintext secret. Only encrypted secrets, starting with 'ENC:v1:', are supported.", managedKey);
logger.LogWarning("The managed tool setting '{ManagedKey}' holds a plaintext secret. Only encrypted secrets, starting with 'ENC:', are supported.", managedKey);
return false;
}
@@ -19,6 +19,8 @@
- Added organization-wide management for importing configurations which somebody exported from AI Studio. IT departments can allow or prevent such imports, either altogether or for each kind, such as providers or chat templates. A locked Import button stays visible.
- Added an optional notice for AI-generated content. When you switch it on in the app settings, the AI answers you copy or export end with a note that AI generated them, in the language of the app, for example, when rules on transparency such as the EU AI Act ask for one. Thanks, Sabrina (`Sabrina-devops`), for this contribution.
- Added organization-wide management for the notice on AI-generated content. IT departments can switch it on for everybody, either fixed or as a default which users may change.
- Added a choice of how AI Studio encrypts API keys, tokens, and passwords when you export providers, data sources, or tool settings for your organization. The new protected format needs AI Studio v26.10.1 or newer on every computer that reads it, and your encryption secret stays the same. If you used AI Studio before, your exports keep the previous format until you switch, so your organization can update all computers first. You find the choice in the app settings, under Enterprise Administration.
- Added organization-wide management for the format in which exports encrypt API keys, tokens, and passwords. IT departments can set it for all their administrators, either fixed or as a default which users may change.
- Improved the Read Web Page tool. When you have not allowed the AI to choose web addresses freely, the tool now refuses every address that does not appear in your chat or in what the tools returned, instead of only asking the AI not to read it.
- Improved the installation on macOS: AI Studio is now signed and checked by Apple, so it opens like any other app. You no longer need the Terminal.
- Improved the installation on Windows: AI Studio is now signed, so Windows shows who published it. For a while, Windows may still ask you to confirm the installation until it knows the app better.
@@ -79,6 +81,7 @@
- Fixed building AI Studio from source on Windows, where the checks that run before every build failed because two of them expected the line breaks and file paths of macOS and Linux. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
- Fixed a problem on Windows where AI Studio could fail to read the configuration of your organization when the name of an environment variable on your computer contained umlauts or other special characters.
- Fixed the localization assistant writing the translation instead of the English text into the comment above an entry of a language plugin. This happened when the English language plugin did not contain that text yet.
- Fixed AI Studio occasionally accepting an API key, token, or password which was encrypted with the secret of another organization, instead of refusing it. AI Studio then used a wrong key, so the provider, data source, or tool failed later with a misleading error. Such a value is now always refused in the new protected format, and almost always in the previous one.
- Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far.
- Upgraded several libraries to improve security.
- Upgraded to Rust v1.99.0