diff --git a/admin-web/src/views/ModelService.vue b/admin-web/src/views/ModelService.vue index 1c649f6..524cb62 100644 --- a/admin-web/src/views/ModelService.vue +++ b/admin-web/src/views/ModelService.vue @@ -201,7 +201,7 @@ function sourceLabel(source: ApiKeyState['source']) { 删除 -

服务端需配置 Secrets__EncryptionKey 才能加密保存,值为 Base64 编码的 32 字节密钥。

+

保存时由服务端自动加密,不需要额外配置加密密钥。

@@ -234,7 +234,6 @@ function sourceLabel(source: ApiKeyState['source']) { .key-status strong { font-variant-numeric: tabular-nums; letter-spacing: .04em; } .key-editor { display: grid; grid-template-columns: minmax(240px, 1fr) auto auto; gap: 10px; } .key-note { margin: 10px 0 0; color: #5e6772; font-size: 12px; } -.key-note code { color: #414eb8; } .result-alert { margin-bottom: 18px; } @media (max-width: 760px) { .page-heading { flex-direction: column; } diff --git a/backend/MiaoJiZhang.Api.Tests/LlmConfigurationTests.cs b/backend/MiaoJiZhang.Api.Tests/LlmConfigurationTests.cs index 087586c..04a74d9 100644 --- a/backend/MiaoJiZhang.Api.Tests/LlmConfigurationTests.cs +++ b/backend/MiaoJiZhang.Api.Tests/LlmConfigurationTests.cs @@ -9,11 +9,11 @@ public sealed class LlmSecretProtectorTests [Fact] public void Protect_RoundTripsWithoutEmbeddingPlaintext() { - var encryptionKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - ["Secrets:EncryptionKey"] = encryptionKey, + ["Jwt:Secret"] = Convert.ToBase64String( + RandomNumberGenerator.GetBytes(48)), }) .Build(); var protector = new LlmSecretProtector(configuration); @@ -26,14 +26,14 @@ public sealed class LlmSecretProtectorTests } [Fact] - public void Protect_RejectsMissingEncryptionKey() + public void Protect_RejectsMissingServerSecret() { var protector = new LlmSecretProtector(new ConfigurationBuilder().Build()); var exception = Assert.Throws( () => protector.Protect("sk-test")); - Assert.Contains("Secrets__EncryptionKey", exception.Message); + Assert.Contains("JWT 密钥", exception.Message); } [Fact] diff --git a/backend/MiaoJiZhang.Api/Services/LlmSecretProtector.cs b/backend/MiaoJiZhang.Api/Services/LlmSecretProtector.cs index 17c22e0..f189aa8 100644 --- a/backend/MiaoJiZhang.Api/Services/LlmSecretProtector.cs +++ b/backend/MiaoJiZhang.Api/Services/LlmSecretProtector.cs @@ -65,13 +65,11 @@ public sealed class LlmSecretProtector(IConfiguration configuration) private byte[] ReadEncryptionKey() { - var raw = configuration["Secrets:EncryptionKey"]; - byte[]? key = null; - try { key = string.IsNullOrWhiteSpace(raw) ? null : Convert.FromBase64String(raw); } - catch (FormatException) { } - if (key?.Length != 32) - throw new InvalidOperationException( - "请通过 Secrets__EncryptionKey 配置 base64 编码的 32 字节密钥后再保存 API Key"); - return key; + var jwtSecret = configuration["Jwt:Secret"]; + if (string.IsNullOrWhiteSpace(jwtSecret) || jwtSecret.Length < 32) + throw new InvalidOperationException("服务端 JWT 密钥配置无效,无法保护 API Key"); + return HMACSHA256.HashData( + Encoding.UTF8.GetBytes(jwtSecret), + Encoding.UTF8.GetBytes("jizhi:llm-api-key-encryption:v1")); } } diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index 297889d..0aff3b0 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -45,7 +45,6 @@ cd backend export Admin__BootstrapUsername='admin' export Admin__BootstrapPassword='replace-with-a-password-longer-than-5-characters' -export Secrets__EncryptionKey='base64-encoded-32-byte-key' dotnet build # 重启 powershell -Command "Get-Process dotnet | Stop-Process -Force" @@ -57,16 +56,9 @@ dotnet run --project MiaoJiZhang.Api 引导变量。正式环境必须使用 HTTPS 并保持 `Admin__CookieSecure=true`。本地纯 HTTP 调试时才可 临时设置 `Admin__CookieSecure=false`。 -后台“AI 配置 → 模型服务”可以保存和替换 LLM API Key。实际 API Key 使用 AES-GCM -加密后写入配置表,服务端只需通过 `Secrets__EncryptionKey` 提供一个固定的 32 字节 -加密主密钥;页面和接口只显示 API Key 尾号。可使用 PowerShell 生成: - -```powershell -[Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) -``` - -请将该值保存到部署平台的密钥管理中,不要提交到仓库。更换或丢失主密钥会导致后台已保存的 -LLM API Key 无法解密。旧的 `LLM_API_KEY` 仍作为回退配置;后台保存的密钥优先。 +后台“AI 配置 → 模型服务”可以直接保存和替换 LLM API Key。实际 API Key 使用 AES-GCM +加密后写入配置表,加密密钥由服务端从必填的 `Jwt__Secret` 自动派生,无需增加部署变量; +页面和接口只显示 API Key 尾号。旧的 `LLM_API_KEY` 仍作为回退配置,后台保存的密钥优先。 ### 2. Admin Web ```powershell