Secrets 存储方案设计
目录
1. 概述
1.1 目标
实现一套本地加密的 Secrets 存储方案,用于安全存储 API Keys 和 Verification Tokens。
1.2 设计原则
- 本地存储:不依赖外部服务,所有数据存储在本地文件系统
- 加密保护:Secrets 加密后存储,防止泄露
- 按需解密: 发起 LLM 请求时从加密文件解密获取 API key,请求完成后立即销毁,不在内存中长期驻留
- 多密钥支持:支持 WhiteBox、TEE/SDF、HSM 三种密钥提供方式
1.3 支持的密钥类型
| 密钥类型 | 说明 | 加密方式 | 适用环境 |
|---|---|---|---|
| WhiteBox | 软件级白盒密钥 | AES-256-GCM | ⚠️ 仅测试环境 |
| TEE/SDF | 国密硬件模块 | SDF 国密接口 | 生产环境 |
| HSM | 硬件安全模块 | PKCS#11 | 生产环境 |
2. 整体设计方案
2.1 配置项
[vault]
enabled = true # 是否启用加密存储
use_sdf = false # false=WhiteBox+AES-GCM, true=SDF国密
2.2 vault.enabled = true 时
数据保存方法
- 首次启动:从环境变量读取 API Key → 加密 → 保存到
llm_secrets.json - 后续启动:从
llm_secrets.json加载 → 解密 → 按需提供给调用方
数据存储位置
{config_dir}/
└── llm_secrets.json # 加密后的 Secrets 数据
数据格式
{
"api_keys": {
"OPENROUTER_API_KEY": "sk-or-v1-xxx..."
},
"tokens": {
"FEISHU_VERIFICATION_TOKEN": "xxx"
}
}
文件内容为加密后的二进制数据。
数据获取方式
按需获取,用完即销毁:
- 发起 LLM 请求时从加密文件解密获取 API key
- 请求完成后立即销毁,不在内存中长期驻留
- 每次请求都会重新解密(除非文件不存在才 fallback 到环境变量)
2.3 vault.enabled = false 时
数据保存方法
不保存任何 Secrets 到文件。
数据获取方式
直接使用环境变量和 config.toml 配置。
2.4 对比总结
| 配置 | 数据保存 | 数据获取 | 安全性 |
|---|---|---|---|
vault.enabled=true |
加密保存到 llm_secrets.json |
按需解密获取,用完即销毁 | 高 |
vault.enabled=false |
不保存 | 直接使用环境变量 | 低 |
3. 详细设计方案
3.1 WhiteBox + AES-GCM 流程
加密流程
┌─────────────────────────────────────────────────────────────────────┐
│ WhiteBox + AES-GCM 加密流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. WhiteBoxKeyProvider.get_key() │
│ │ │
│ │ 从代码碎片重建 32 字节 master key │
│ ▼ │
│ 2. 生成 12 字节随机 nonce │
│ 3. AES-256-GCM 加密 │
│ │ │
│ │ plaintext → [version(1) + nonce(12) + ciphertext] │
│ ▼ │
│ 4. 写入 llm_secrets.json │
│ │
└─────────────────────────────────────────────────────────────────────┘
解密流程(按需获取,用完即销毁)
┌─────────────────────────────────────────────────────────────────────┐
│ WhiteBox + AES-GCM 解密流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 读取 llm_secrets.json │
│ 2. 解析: version(1) + nonce(12) + ciphertext │
│ 3. WhiteBoxKeyProvider.get_key() │
│ │ │
│ │ 从代码碎片重建 32 字节 master key │
│ ▼ │
│ 4. AES-256-GCM 解密 │
│ 5. 发起 LLM 请求时按需获取密钥 │
│ 6. 请求完成后立即销毁(不驻留内存) │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.2 WhiteBox 密钥配置
3.2.1 当前状态
⚠️ 安全警告: WhiteBox 密钥当前设为 NULL(全零),仅适用于测试环境。
生产环境请使用 SDF 国密 (
use_sdf=true) 或 HSM 方案。
文件位置: apps/vault/src/whitebox.rs
当前配置:
const FRAG_A: [u8; 8] = [0u8; 8]; // 全零,仅测试用
const FRAG_B: [u8; 8] = [0u8; 8]; // 全零,仅测试用
const FRAG_C: [u8; 8] = [0u8; 8]; // 全零,仅测试用
const FRAG_D: [u8; 8] = [0u8; 8]; // 全零,仅测试用
3.2.2 自定义密钥配置参考
仅适用于测试环境。生产环境请使用 SDF 国密方案。
如果需要在测试环境中使用自定义密钥,可通过修改代码碎片实现:
// 示例: 设置自定义密钥 "MySecretKey12345678901234567890" (32 bytes)
const FRAG_A: [u8; 8] = [
'M' ^ 0x5A, 'y' ^ 0x5A, 'S' ^ 0x5A, 'e' ^ 0x5A,
'c' ^ 0x5A, 'r' ^ 0x5A, 'e' ^ 0x5A, 't' ^ 0x5A,
];
const FRAG_B: [u8; 8] = [
'K' ^ 0xA5, 'e' ^ 0xA5, 'y' ^ 0xA5, '1' ^ 0xA5,
'2' ^ 0xA5, '3' ^ 0xA5, '4' ^ 0xA5, '5' ^ 0xA5,
];
const FRAG_C: [u8; 8] = [
'6' ^ 0x3C, '7' ^ 0x3C, '8' ^ 0x3C, '9' ^ 0x3C,
'0' ^ 0x3C, '1' ^ 0x3C, '2' ^ 0x3C, '3' ^ 0x3C,
];
const FRAG_D: [u8; 8] = [
'4' ^ 0x7E, '5' ^ 0x7E, '6' ^ 0x7E, '7' ^ 0x7E,
'8' ^ 0x7E, '9' ^ 0x7E, '0' ^ 0x7E, '1' ^ 0x7E,
];
密钥生成方法:
# 生成随机 32 字节密钥
openssl rand -hex 32
# 或使用 Python
python3 -c "import secrets; print(secrets.token_hex(32))"
3.3 TEE/SDF 国密流程
加密流程
┌─────────────────────────────────────────────────────────────────────┐
│ SDF 国密加密流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. init_sdf_provider("/usr/local/sdf/lib/libsdf.so") │
│ │ │
│ │ 加载 SDF 动态库 │
│ ▼ │
│ 2. SDF_OpenDevice() → device_handle │
│ 3. SDF_OpenSession() → session_handle │
│ 4. SDF_GetKEKAccessRight() → 获取 KEK 权限 │
│ 5. SDF_GenerateKeyWithKEK() → 生成会话密钥并用 KEK 加密导出 │
│ 6. SDF_Encrypt() → 使用会话密钥进行国密加密 │
│ 7. 写入 llm_secrets.json (包含加密后的会话密钥 + 密文) │
│ 8. SDF_ReleaseKEKAccessRight() → 释放权限 │
│ 9. SDF_CloseSession() │
│ 10. SDF_CloseDevice() │
│ │
└─────────────────────────────────────────────────────────────────────┘
解密流程(按需获取,用完即销毁)
┌─────────────────────────────────────────────────────────────────────┐
│ SDF 国密解密流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 读取 llm_secrets.json │
│ 2. init_sdf_provider("/usr/local/sdf/lib/libsdf.so") │
│ 3. SDF_OpenDevice() → device_handle │
│ 4. SDF_OpenSession() → session_handle │
│ 5. SDF_GetKEKAccessRight() → 获取 KEK 权限 │
│ 6. SDF_ImportKeyWithKEK() → 导入加密的会话密钥 │
│ 7. SDF_Decrypt() → 使用会话密钥进行国密解密 │
│ 8. 发起 LLM 请求时按需获取密钥 │
│ 9. 请求完成后立即销毁(不驻留内存) │
│ 10. SDF_ReleaseKEKAccessRight() → 释放权限 │
│ 11. SDF_CloseSession() │
│ 12. SDF_CloseDevice() │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.4 按需密钥获取机制
解密后的 Secrets 不驻留内存,采用按需获取方式:
pub fn get_decrypted_api_key(env_name: &str) -> Option<String>
pub fn init_secret_provider(secrets_path: PathBuf, use_sdf: bool)
特点:
- 每次 LLM 请求时按需解密获取 API key
- 请求完成后立即销毁,不在内存中长期驻留
- 通过
SecretProvider统一管理解密逻辑
3.5 API Key 解析优先级
1. HTTP 请求 / CLI 参数中的 api_key 覆盖(仅 Daemon HTTP API 的 llm.api_key 字段或 CLI 的 --api-key 参数)
↓
2. llm_secrets.json (按需解密获取,通过 api_key_env 解析)
↓
3. 环境变量 (fallback,通过 api_key_env 解析)
Note:
[llm]配置段中没有api_key直接配置字段。API key 只能通过api_key_env指向环境变量或加密文件,或通过 HTTP 请求 / CLI 参数临时覆盖。
4. 文件结构
4.1 apps/vault
密钥提供者核心库。
apps/vault/src/
├── lib.rs # 主入口,导出所有 public API
├── key_provider.rs # KeyProvider trait 定义
├── types.rs # KeyMaterial, KeyProviderConfig
├── key_provider_error.rs # KeyProviderError
├── whitebox.rs # WhiteBoxKeyProvider 实现
├── sdf.rs # SDF 国密接口封装 + TeeKeyProvider
└── hsm.rs # HSM PKCS#11 接口(预留)
4.2 apps/shared (Secrets 存储与按需解密)
Secrets 存储和按需解密逻辑在 xiaoo-shared crate 中,CLI/TUI/Daemon 共用。
apps/shared/src/
├── llm_secrets.rs # 本地加密存储管理
│ # - auto_save_from_env()
│ # - load_llm_secrets_to_memory()
│ # - encrypt_aes_gcm() / decrypt_aes_gcm()
│ # - init_on_demand_secret_provider()
│ # - llm_secrets_path()
└── gateway/
├── mod.rs # 导出
└── decrypted_api_keys.rs # SecretProvider 按需解密
# - init_secret_provider()
# - get_decrypted_api_key()
4.3 apps/endside (TUI 配置入口)
apps/endside/src/
├── main.rs # TUI 启动入口,调用 config::load_llm_secrets_to_memory
└── support/
└── config.rs # TUI 配置集成,重新导出 save_llm_secret /
# load_llm_secrets_to_memory,并读取 [vault] 段
4.4 各文件作用
| 文件 | 作用 |
|---|---|
apps/vault/src/whitebox.rs |
白盒密钥,从代码碎片重建 master key |
apps/vault/src/sdf.rs |
SDF 国密接口封装,包含 encrypt_secret/decrypt_secret |
apps/vault/src/hsm.rs |
HSM PKCS#11 接口(预留) |
apps/shared/src/llm_secrets.rs |
加密/解密、加密文件读写、按需 provider 初始化 |
apps/shared/src/gateway/decrypted_api_keys.rs |
SecretProvider 按需解密机制 |
apps/endside/src/support/config.rs |
TUI 启动时初始化 SecretProvider、读取 [vault] 配置 |
5. 部署与测试
5.1 部署视图
┌─────────────────────────────────────────────────────────────────────┐
│ 部署视图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ xiaoo 进程 │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ xiaoo │ │ xiaoo --cli │ │ xiaoo-daemon │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ │ │ │ │ │ │
│ │ └────────────────┼──────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────▼───────────┐ │ │
│ │ │ llm_secrets.rs │ │ │
│ │ │ - encrypt_aes_gcm() │ │ │
│ │ │ - decrypt_aes_gcm() │ │ │
│ │ └───────────┬───────────┘ │ │
│ │ │ │ │
│ │ ┌───────────▼───────────┐ │ │
│ │ │ vault (whitebox/sdf) │ │ │
│ │ └───────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────▼───────────┐ │ │
│ │ │ llm_secrets.json │ │ │
│ │ │ (加密文件) │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
5.2 配置文件
5.2.1 [vault] 配置项
[vault]
enabled = false # 是否启用加密存储
use_sdf = false # 加密方式
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
bool | false |
是否启用加密存储到 llm_secrets.json |
use_sdf |
bool | false |
false=WhiteBox(仅测试),true=SDF国密(仅鲲鹏服务器) |
5.2.2 配置文件示例
vault.enabled = true (加密存储 - WhiteBox)
[vault]
enabled = true
use_sdf = false # 仅测试环境使用
[llm]
provider = "openrouter"
api_key_env = "OPENROUTER_API_KEY"
⚠️ WhiteBox 警告:
use_sdf=false仅适用于测试环境,生产环境请使用use_sdf=true。
vault.enabled = true (加密存储 - SDF 国密)
[vault]
enabled = true
use_sdf = true # 仅适用于鲲鹏服务器
⚠️ SDF 国密要求:
use_sdf=true仅支持鲲鹏系列服务器,并需要部署 SDF 国密模块。详细方法请参考:
vault.enabled = false (不加密存储)
[vault]
enabled = false # 不保存 Secrets,使用环境变量
[llm]
provider = "openrouter"
api_key_env = "OPENROUTER_API_KEY"
5.2.3 配置行为说明
| 配置 | 首次启动 | 后续启动 |
|---|---|---|
enabled=false |
不保存,直接用环境变量 | 不保存,直接用环境变量 |
enabled=true, use_sdf=false |
环境变量→AES-GCM加密保存(仅测试) | 从文件按需解密(仅测试) |
enabled=true, use_sdf=true |
环境变量→SDF加密保存(仅鲲鹏) | 从文件按需解密(仅鲲鹏) |
5.3 环境变量
| 环境变量 | 说明 | 默认值 |
|---|---|---|
USE_SDF |
使用 SDF 国密 | false |
LD_LIBRARY_PATH |
SDF 动态库路径 | /usr/local/sdf/lib |
5.4 SDF 国密部署要求
⚠️ 重要: SDF 国密 (
use_sdf=true) 仅支持鲲鹏系列服务器。详细部署方法请参考:鲲鹏商密应用使用指南
前置条件:
- 服务器必须是鲲鹏系列 ( Kunpeng ARM64 )
- 需要部署 SDF 国密模块 (
libsdf.so) 及依赖库 - 需要在 config.toml 中设置
use_sdf = true
5.5 测试方法
编译
# 默认编译 (WhiteBox + AES-GCM,仅测试环境)
cargo build --release -p xiaoo-endside --bin xiaoo
# 启用 SDF 国密 (仅鲲鹏服务器)
cargo build --release -p xiaoo-endside --bin xiaoo --features tee_sdf
单元测试
cargo test --package vault
cargo test --package xiaoo-shared
功能验证
# 设置环境变量
export OPENROUTER_API_KEY='sk-or-v1-xxx'
export USE_SDF=false
# 运行 TUI (vault.enabled=true 时自动保存)
./target/release/xiaoo --config config.toml
# 检查加密文件
hexdump -C ~/.xiaoo/config/llm_secrets.json | head
6. 关键 API
6.1 llm_secrets 模块
// 自动从环境变量保存到加密文件
pub fn auto_save_from_env(config_path: &Path) -> Result<()>;
// 加载加密文件到进程内存
pub fn load_llm_secrets_to_memory(config_path: &Path) -> Result<()>;
// 保存 API Key
pub fn save_llm_secret(config_path: &Path, env_name: &str, secret: &str) -> Result<()>;
// 保存 Token
pub fn save_token(config_path: &Path, token_name: &str, token: &str) -> Result<()>;
6.2 按需密钥获取 API
// 初始化 SecretProvider
pub fn init_secret_provider(secrets_path: PathBuf, use_sdf: bool);
// 按需获取密钥(每次解密,用完即销毁)
pub fn get_decrypted_api_key(env_name: &str) -> Option<String>;
6.3 密钥提供者
// WhiteBox
pub struct WhiteBoxKeyProvider { ... }
impl KeyProvider for WhiteBoxKeyProvider { ... }
// SDF 国密
pub fn init_sdf_provider(path: &str) -> Result<()>;
pub fn encrypt_secret(data: &[u8]) -> Result<Vec<u8>>;
pub fn decrypt_secret(encrypted: &[u8]) -> Result<Vec<u8>>;
// TEE 密钥提供者
pub struct TeeKeyProvider { ... }
impl KeyProvider for TeeKeyProvider { ... }
// HSM (预留)
pub struct HsmKeyProvider { ... }
impl KeyProvider for HsmKeyProvider { ... }
7. 已知限制
7.1 WhiteBox
- 密钥在软件中重建,理论上可被内存抓取攻击获取
- 仅适用于开发/测试环境
7.2 SDF 国密
- 需要 libsdf.so 及依赖库
- KEK 口令默认为 NULL,需要配置真实 KEK
- libcrypto 版本需与 libsdf.so 匹配
7.3 HSM
- 接口预留,实现待完成
附录:加密格式
WhiteBox + AES-GCM
┌─────────────────────────────────────────────────────────────┐
│ Version (1 byte) │ Nonce (12 bytes) │ Ciphertext │
│ 1 │ random │ (含 auth tag) │
└─────────────────────────────────────────────────────────────┘
SDF 国密
┌─────────────────────────────────────────────────────────────┐
│ Version (1 byte) │ IV (16 bytes) │ Ciphertext │
│ 1 │ random │ (国密加密结果) │
└─────────────────────────────────────────────────────────────┘