Skip to content

fix(security): 服务商 API Key 加密改用环境变量密钥与 AES/GCM - #5

Open
NEDONION wants to merge 1 commit into
mainfrom
fix/config-encryption
Open

NEDONION wants to merge 1 commit into
mainfrom
fix/config-encryption

Conversation

@NEDONION

Copy link
Copy Markdown
Owner

⚠️ 合并前必读

合并这个 PR 会触发自动部署,而新代码要求服务器上必须有 CONFIG_ENCRYPTION_KEY,否则后端起不来。

请先在服务器上执行:

cd /home/admin/rag-agent-platform
echo "CONFIG_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> .env

确认 .env 里有这一行之后,再合并。


问题

服务商配置(内含用户填写的模型 API Key)落库前会加密,但密钥硬编码在源码里:

// ValidationUtils.java:70
private static final String SECRET_KEY = "1234567890123456";
Cipher.getInstance("AES");   // 默认 ECB 模式

本仓库是公开的,密钥等同公开信息 —— 加密实际形同虚设。 任何拿到数据库内容的人(备份泄露、SQL 注入、云快照、运维越权)都能解出全部用户的模型服务商 API Key。

修复

新增 ConfigCrypto 替代原实现:

项 修复前 修复后
密钥来源 源码硬编码 环境变量 CONFIG_ENCRYPTION_KEY,缺失即启动失败
算法 AES/ECB AES/GCM/NoPadding
IV 无 每条记录随机 12 字节
完整性 无 128 位认证标签,篡改则解密失败
密钥轮换 不可能 换环境变量即可

密文格式 v2: + Base64(IV ‖ 密文+标签)。不带前缀的旧密文仍可解密,写入时一律转新格式,因此存量数据会随使用逐步迁移。

另外两处防呆:

  • ConfigCryptoInitializer 在启动阶段主动校验,让配置缺失在启动时暴露,而不是等某个用户第一次读写服务商配置时才炸
  • 若把 CONFIG_ENCRYPTION_KEY 误设为旧的泄露密钥,同样启动失败

顺带清理

  • 删除 ConfigEncryptor —— 零调用死代码,同样硬编码密钥 "AgentX-Config-Key",留着容易被误用
  • 删除 ValidationUtils.EncryptUtils,加密实现收敛为一套
  • 修复 ProviderConfigConverter.getNullableResult(ResultSet, int) 直接 return null 的未实现分支 —— 该路径此前会静默拿到空配置而非报错

本仓库的第一个测试

ConfigCryptoTest 9 个用例,锁定的是安全属性而非实现细节:

  • 密钥必须来自外部注入,缺失时报错
  • 拒绝沿用已泄露的旧密钥
  • 相同明文两次加密结果必须不同(ECB 的问题就出在这)
  • 密文被篡改时必须失败,而不是返回垃圾数据
  • 换一把密钥解不出原密文
  • 旧格式数据仍能读取(保证升级后存量配置不失效)
  • 新写入一律新格式

CI 相应增加 mvn test 步骤 —— 否则这些约束没人守。

合并后还要做两件事

① 迁移存量数据

读取兼容旧格式、写入才转新格式,所以不活跃的记录会长期以旧格式留存:

# 临时开启,重启后自动执行一次
CONFIG_CRYPTO_MIGRATE=true docker compose up -d backend
# 日志出现「配置加密格式迁移完成」后改回 false

迁移全部完成后,可以删掉 ConfigCrypto.decryptLegacy(),彻底断开对旧密钥的依赖。

② 通知用户轮换 API Key

加密方式修好了不等于旧密钥安全了 —— 那把密钥早已随公开仓库外泄。存量 API Key 应视为已泄露。

验证

  • mvn test 9/9 通过
  • spotless 格式化通过
  • 文档链接校验 167 条全部有效

🤖 Generated with Claude Code

服务商配置(内含用户填写的模型 API Key)落库前会加密,但密钥硬编码在源码中:

    private static final String SECRET_KEY = "1234567890123456";
    Cipher.getInstance("AES");   // 默认 ECB 模式

本仓库是公开的,密钥等同公开信息——加密实际形同虚设。任何拿到数据库内容的人
(备份泄露、注入、云快照、运维越权)都能解出全部用户的模型服务商 API Key。

新增 ConfigCrypto 替代原实现:
- 密钥来自环境变量 CONFIG_ENCRYPTION_KEY,缺失时启动即失败,不提供任何默认值
- 若把该变量设为旧的泄露密钥,同样启动失败,避免"改了配置但没换密钥"
- AES/GCM/NoPadding,每条记录随机 12 字节 IV,128 位认证标签
- 密文格式 v2: + Base64(IV ‖ 密文+标签)
- 不带前缀的旧密文仍可解密,写入时一律转新格式,存量数据随使用逐步迁移
- 新增 ConfigCryptoInitializer 在启动阶段主动校验,避免故障延迟到业务链路才暴露
- 新增 ConfigCryptoMigration,设 CONFIG_CRYPTO_MIGRATE=true 可一次性迁移存量数据

同时清理:
- 删除 ConfigEncryptor(零调用死代码,同样硬编码密钥 "AgentX-Config-Key")
- 删除 ValidationUtils.EncryptUtils,加密实现收敛为一套
- 修复 ProviderConfigConverter.getNullableResult(ResultSet, int) 直接返回 null 的
  未实现分支——该路径此前会静默拿到空配置而非报错

新增本仓库的第一个测试 ConfigCryptoTest(9 个用例),锁定的是安全属性而非实现细节:
密钥必须来自外部注入、相同明文不得产生相同密文、密文被篡改必须失败、换密钥解不出、
缺密钥必须报错、拒绝沿用泄露密钥、旧格式仍可读、新写入一律新格式、null 与空串不抛异常。
CI 相应增加 mvn test 步骤,否则这些约束无人守护。

⚠️ 部署前必须先在服务器 .env 配置 CONFIG_ENCRYPTION_KEY,否则后端起不来。
⚠️ 加密方式的修复不会让已泄露的密钥重新安全——存量 API Key 应视为已泄露,
   需通知用户轮换。

验证:mvn test 全部通过(9/9);spotless 格式化通过;文档链接校验 167 条全部有效。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant