笔记10:API密钥与凭证管理
一、引言
API密钥和凭证是现代应用程序的关键安全资产,尤其在AI代理系统中扮演着至关重要的角色。本笔记将探讨密钥管理安全原则,分析OpenHands的凭证处理机制,并实现一个安全的凭证存储系统,确保敏感信息不被泄露同时保持易用性。
二、密钥管理安全原则
2.1 凭证安全的核心挑战
- 敏感性: API密钥等同于密码,泄露可能导致未授权访问和资源滥用
- 分发困难: 需要安全地将密钥分发给授权用户和服务
- 轮换复杂性: 定期更新密钥以提高安全性但增加管理难度
- 可追溯性: 追踪密钥使用以检测滥用和异常行为
- 范围控制: 限制每个密钥的权限范围和能力
- 生命周期管理: 从创建到撤销的完整管理流程
2.2 常见的安全漏洞
- 硬编码凭证: 将密钥直接嵌入代码中
- 明文存储: 在配置文件中以明文形式保存密钥
- 不安全传输: 通过不加密的通道传输密钥
- 过度共享: 多个服务或用户共享同一密钥
- 权限过大: 使用具有不必要高权限的密钥
- 缺乏审计: 无法追踪密钥的使用情况
- 忽略轮换: 长期使用相同密钥增加泄露风险
2.3 密钥管理最佳实践
隔离存储:
- 使用专用的密钥管理服务
- 分离应用代码和凭证
加密保护:
- 静态加密(at rest)
- 传输中加密(in transit)
最小权限原则:
- 限制每个密钥的权限范围
- 创建服务特定的密钥
安全分发:
- 使用安全通道传递密钥
- 实施临时访问机制
定期轮换:
- 建立密钥轮换流程
- 自动化轮换过程
监控与审计:
- 记录所有密钥访问
- 检测异常使用模式
响应计划:
- 制定密钥泄露应对方案
- 快速撤销和替换机制
2.4 常见密钥管理工具
工具类型 | 例子 | 特点 |
---|---|---|
密钥管理服务 | AWS KMS, Google Cloud KMS, Azure Key Vault | 云托管、高可用性、集成审计 |
密码管理器 | HashiCorp Vault, Bitwarden, 1Password | 多类型凭证、团队共享、访问控制 |
环境变量管理 | dotenv, direnv | 简单易用、开发友好、隔离环境 |
容器机密 | Kubernetes Secrets, Docker Secrets | 容器化环境、集成编排平台 |
本地加密存储 | 加密文件系统、加密数据库 | 无需外部依赖、完全控制 |
三、OpenHands凭证管理分析
从README_CN.md中,我们可以了解到OpenHands的凭证管理机制:
3.1 OpenHands凭证需求
LLM提供商API密钥:
- 需要在应用启动时提供
- 支持多种LLM提供商,如Anthropic Claude
持久化存储:
- 使用
~/.openhands
目录存储数据 - 可能包括API密钥和配置信息
- 使用
多模式支持:
- Web界面、无头模式、CLI模式均需要凭证
- 不同运行环境下的安全考量不同
3.2 潜在安全考量
- 本地存储安全: 确保存储在本地的API密钥受到保护
- 容器内凭证: 在Docker容器内如何安全存储密钥
- 会话传递: 如何在会话间安全地传递和验证密钥
- 密钥范围限制: 如何限制每个LLM API密钥的使用范围
- 本地与远程凭证: 本地运行和云服务的凭证管理差异
四、实践项目:构建OpenHands安全凭证存储系统
4.1 设计安全凭证管理架构
┌─────────────────┐ ┌───────────────────┐
│ │ │ │
│ 用户界面 ├─────►│ 凭证管理器 │
│ │ │ │
└─────────────────┘ └───────┬───────────┘
│
┌───────────────────┬─▼──────────────┬───────────────────┐
│ │ │ │
┌─────────▼────────┐ ┌────────▼────────┐ ┌─────▼─────────────┐ ┌──▼───────────────┐
│ │ │ │ │ │ │ │
│ 本地加密存储 │ │ 环境变量 │ │ 系统钥匙链 │ │ 外部密钥服务 │
│ │ │ │ │ │ │ │
└──────────────────┘ └─────────────────┘ └───────────────────┘ └──────────────────┘
4.2 实现凭证管理核心模块
// src/services/credentialManager.js
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const os = require('os');
const { promisify } = require('util');
const logger = require('../config/logger');
// 对称加密密钥生成和派生函数
const generateKey = async (password, salt) => {
const pbkdf2 = promisify(crypto.pbkdf2);
return pbkdf2(password, salt, 100000, 32, 'sha512');
};
class CredentialManager {
constructor(options = {}) {
this.options = {
storageDir: options.storageDir || path.join(os.homedir(), '.openhands', 'credentials'),
masterKeyPath: options.masterKeyPath || path.join(os.homedir(), '.openhands', '.master-key'),
encryptionAlgorithm: options.encryptionAlgorithm || 'aes-256-gcm',
keyPrefix: options.keyPrefix || 'api-key-',
...options
};
this.credentials = new Map();
this.masterKey = null;
this.initialized = false;
}
/**
* 初始化凭证管理器
*/
async initialize() {
try {
// 确保存储目录存在
await this._ensureDirectory(this.options.storageDir);
// 尝试加载或创建主密钥
await this._initializeMasterKey();
// 加载存储的凭证
await this._loadCredentials();
this.initialized = true;
logger.info('Credential manager initialized');
return true;
} catch (error) {
logger.error('Failed to initialize credential manager:', error);
throw error;
}
}
/**
* 确保目录存在
* @private
*/
async _ensureDirectory(dir) {
try {
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
} catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
// 确保目录权限正确
await fs.chmod(dir, 0o700);
}
}
/**
* 初始化主密钥
* @private
*/
async _initializeMasterKey() {
try {
// 尝试加载现有主密钥
await fs.access(this.options.masterKeyPath);
const keyData = await fs.readFile(this.options.masterKeyPath);
// 验证密钥格式
if (keyData.length !== 64) { // 预期32字节密钥 + 32字节盐
throw new Error('Invalid master key format');
}
this.masterKey = {
key: keyData.slice(0, 32),
salt: keyData.slice(32)
};
logger.debug('Loaded existing master key');
} catch (error) {
if (error.code === 'ENOENT') {
// 主密钥不存在,生成新密钥
logger.info('Generating new master key');
// 生成随机密钥和盐
const key = crypto.randomBytes(32);
const salt = crypto.randomBytes(32);
// 保存主密钥
await this._ensureDirectory(path.dirname(this.options.masterKeyPath));
await fs.writeFile(this.options.masterKeyPath, Buffer.concat([key, salt]), { mode: 0o600 });
// 设置主密钥
this.masterKey = { key, salt };
} else {
throw error;
}
}
}
/**
* 加载存储的凭证
* @private
*/
async _loadCredentials() {
try {
const files = await fs.readdir(this.options.storageDir);
const credentialFiles = files.filter(file => file.startsWith(this.options.keyPrefix) && file.endsWith('.enc'));
for (const file of credentialFiles) {
try {
const providerId = file.slice(this.options.keyPrefix.length, -4); // 删除前缀和.enc扩展名
const credentialData = await fs.readFile(path.join(this.options.storageDir, file));
// 不要立即解密,仅在需要时解密
this.credentials.set(providerId, {
encrypted: credentialData,
decrypted: null,
lastAccessed: null
});
logger.debug(`Loaded encrypted credential for: ${providerId}`);
} catch (error) {
logger.error(`Failed to load credential file ${file}:`, error);
}
}
logger.info(`Loaded ${this.credentials.size} encrypted credentials`);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
// 目录不存在,这是正常的初次运行情况
}
}
/**
* 加密数据
* @private
*/
_encrypt(data, key = this.masterKey.key) {
// 生成初始化向量
const iv = crypto.randomBytes(12);
// 创建加密器
const cipher = crypto.createCipheriv(this.options.encryptionAlgorithm, key, iv);
// 加密数据
const encrypted = Buffer.concat([
cipher.update(Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8')),
cipher.final()
]);
// 获取认证标签
const authTag = cipher.getAuthTag();
// 返回IV + 认证标签 + 加密数据
return Buffer.concat([iv, authTag, encrypted]);
}
/**
* 解密数据
* @private
*/
_decrypt(encryptedData, key = this.masterKey.key) {
// 从加密数据中提取IV和认证标签
const iv = encryptedData.slice(0, 12);
const authTag = encryptedData.slice(12, 28);
const ciphertext = encryptedData.slice(28);
// 创建解密器
const decipher = crypto.createDecipheriv(this.options.encryptionAlgorithm, key, iv);
decipher.setAuthTag(authTag);
// 解密数据
return Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]).toString('utf8');
}
/**
* 存储凭证
* @param {string} providerId - 提供商ID
* @param {string} credential - 凭证值
* @param {Object} metadata - 元数据
*/
async storeCredential(providerId, credential, metadata = {}) {
if (!this.initialized) {
throw new Error('Credential manager not initialized');
}
if (!providerId || !credential) {
throw new Error('Provider ID and credential are required');
}
try {
// 准备凭证数据
const credentialData = JSON.stringify({
value: credential,
metadata: {
createdAt: new Date().toISOString(),
...metadata
}
});
// 加密凭证
const encryptedData = this._encrypt(credentialData);
// 保存到内存
this.credentials.set(providerId, {
encrypted: encryptedData,
decrypted: credentialData,
lastAccessed: new Date()
});
// 保存到磁盘
const filePath = path.join(
this.options.storageDir,
`${this.options.keyPrefix}${providerId}.enc`
);
await fs.writeFile(filePath, encryptedData, { mode: 0o600 });
logger.info(`Stored credential for provider: ${providerId}`);
return true;
} catch (error) {
logger.error(`Failed to store credential for ${providerId}:`, error);
throw error;
}
}
/**
* 获取凭证
* @param {string} providerId - 提供商ID
* @returns {Promise<string>} - 凭证值
*/
async getCredential(providerId) {
if (!this.initialized) {
throw new Error('Credential manager not initialized');
}
if (!providerId) {
throw new Error('Provider ID is required');
}
// 检查凭证是否存在
const credential = this.credentials.get(providerId);
if (!credential) {
throw new Error(`Credential not found for provider: ${providerId}`);
}
try {
// 如果内存中没有解密版本,则解密
if (!credential.decrypted) {
const decryptedData = this._decrypt(credential.encrypted);
credential.decrypted = decryptedData;
}
// 更新最后访问时间
credential.lastAccessed = new Date();
// 解析并返回凭证值
const credentialData = JSON.parse(credential.decrypted);
return credentialData.value;
} catch (error) {
logger.error(`Failed to get credential for ${providerId}:`, error);
throw new Error(`Failed to access credential: ${error.message}`);
}
}
/**
* 获取凭证元数据
* @param {string} providerId - 提供商ID
* @returns {Promise<Object>} - 凭证元数据
*/
async getCredentialMetadata(providerId) {
if (!this.initialized) {
throw new Error('Credential manager not initialized');
}
if (!providerId) {
throw new Error('Provider ID is required');
}
// 检查凭证是否存在
const credential = this.credentials.get(providerId);
if (!credential) {
throw new Error(`Credential not found for provider: ${providerId}`);
}
try {
// 如果内存中没有解密版本,则解密
if (!credential.decrypted) {
const decryptedData = this._decrypt(credential.encrypted);
credential.decrypted = decryptedData;
}
// 解析并返回凭证元数据
const credentialData = JSON.parse(credential.decrypted);
return {
...credentialData.metadata,
lastAccessed: credential.lastAccessed
};
} catch (error) {
logger.error(`Failed to get credential metadata for ${providerId}:`, error);
throw new Error(`Failed to access credential metadata: ${error.message}`);
}
}
/**
* 删除凭证
* @param {string} providerId - 提供商ID
* @returns {Promise<boolean>} - 是否成功删除
*/
async deleteCredential(providerId) {
if (!this.initialized) {
throw new Error('Credential manager not initialized');
}
if (!providerId) {
throw new Error('Provider ID is required');
}
// 检查凭证是否存在
if (!this.credentials.has(providerId)) {
return false;
}
try {
// 从内存中删除
this.credentials.delete(providerId);
// 从磁盘删除
const filePath = path.join(
this.options.storageDir,
`${this.options.keyPrefix}${providerId}.enc`
);
await fs.unlink(filePath);
logger.info(`Deleted credential for provider: ${providerId}`);
return true;
} catch (error) {
if (error.code === 'ENOENT') {
// 文件不存在,但我们仍算成功
return true;
}
logger.error(`Failed to delete credential for ${providerId}:`, error);
throw error;
}
}
/**
* 列出所有提供商ID
* @returns {Promise<Array<string>>} - 提供商ID列表
*/
async listProviders() {
if (!this.initialized) {
throw new Error('Credential manager not initialized');
}
return Array.from(this.credentials.keys());
}
/**
* 检查提供商凭证是否存在
* @param {string} providerId - 提供商ID
* @returns {Promise<boolean>} - 凭证是否存在
*/
async hasCredential(providerId) {
if (!this.initialized) {
throw new Error('Credential manager not initialized');
}
return this.credentials.has(providerId);
}
/**
* 轮换主密钥
* @param {string} password - 可选密码用于派生新主密钥
* @returns {Promise<boolean>} - 是否成功轮换
*/
async rotateMasterKey(password = null) {
if (!this.initialized) {
throw new Error('Credential manager not initialized');
}
try {
// 生成新的主密钥
const newSalt = crypto.randomBytes(32);
let newKey;
if (password) {
// 如果提供了密码,使用密码派生密钥
newKey = await generateKey(password, newSalt);
} else {
// 否则生成随机密钥
newKey = crypto.randomBytes(32);
}
// 重新加密所有凭证
for (const [providerId, credential] of this.credentials.entries()) {
// 确保凭证已解密
if (!credential.decrypted) {
credential.decrypted = this._decrypt(credential.encrypted);
}
// 使用新密钥重新加密
const reencrypted = this._encrypt(credential.decrypted, newKey);
// 更新内存中的凭证
this.credentials.set(providerId, {
encrypted: reencrypted,
decrypted: credential.decrypted,
lastAccessed: credential.lastAccessed
});
// 更新磁盘上的凭证
const filePath = path.join(
this.options.storageDir,
`${this.options.keyPrefix}${providerId}.enc`
);
await fs.writeFile(filePath, reencrypted, { mode: 0o600 });
}
// 保存新主密钥
await fs.writeFile(
this.options.masterKeyPath,
Buffer.concat([newKey, newSalt]),
{ mode: 0o600 }
);
// 更新当前主密钥
this.masterKey = { key: newKey, salt: newSalt };
logger.info('Master key rotated successfully');
return true;
} catch (error) {
logger.error('Failed to rotate master key:', error);
throw error;
}
}
/**
* 清除内存中的解密凭证
* 安全措施,减少内存中明文凭证的暴露
*/
clearDecryptedCredentials() {
for (const [providerId, credential] of this.credentials.entries()) {
if (credential.decrypted) {
this.credentials.set(providerId, {
encrypted: credential.encrypted,
decrypted: null,
lastAccessed: credential.lastAccessed
});
}
}
logger.debug('Cleared decrypted credentials from memory');
}
/**
* 关闭凭证管理器
*/
async shutdown() {
// 清除内存中的敏感数据
this.clearDecryptedCredentials();
this.masterKey = null;
logger.info('Credential manager shut down');
}
}
// 创建单例实例
const credentialManager = new CredentialManager();
module.exports = credentialManager;
4.3 实现系统钥匙链集成
// src/services/systemKeychain.js
const keytar = require('keytar');
const logger = require('../config/logger');
// 服务名前缀
const SERVICE_PREFIX = 'openhands-api-';
class SystemKeychain {
constructor(options = {}) {
this.options = {
servicePrefix: options.servicePrefix || SERVICE_PREFIX,
...options
};
}
/**
* 验证系统钥匙链可用性
*/
async isAvailable() {
try {
// 尝试使用keytar执行操作
await keytar.findCredentials('openhands-test');
return true;
} catch (error) {
logger.warn('System keychain not available:', error.message);
return false;
}
}
/**
* 获取完整服务名
* @private
*/
_getServiceName(providerId) {
return `${this.options.servicePrefix}${providerId}`;
}
/**
* 存储API密钥
* @param {string} providerId - 提供商ID
* @param {string} apiKey - API密钥
* @param {string} account - 账户名(可选)
*/
async storeApiKey(providerId, apiKey, account = 'default') {
if (!providerId || !apiKey) {
throw new Error('Provider ID and API key are required');
}
try {
const serviceName = this._getServiceName(providerId);
await keytar.setPassword(serviceName, account, apiKey);
logger.info(`Stored API key for ${providerId} in system keychain`);
return true;
} catch (error) {
logger.error(`Failed to store API key for ${providerId} in system keychain:`, error);
throw new Error(`Failed to store API key: ${error.message}`);
}
}
/**
* 获取API密钥
* @param {string} providerId - 提供商ID
* @param {string} account - 账户名(可选)
*/
async getApiKey(providerId, account = 'default') {
if (!providerId) {
throw new Error('Provider ID is required');
}
try {
const serviceName = this._getServiceName(providerId);
const apiKey = await keytar.getPassword(serviceName, account);
if (!apiKey) {
throw new Error(`API key not found for provider: ${providerId}`);
}
return apiKey;
} catch (error) {
logger.error(`Failed to get API key for ${providerId} from system keychain:`, error);
throw new Error(`Failed to get API key: ${error.message}`);
}
}
/**
* 删除API密钥
* @param {string} providerId - 提供商ID
* @param {string} account - 账户名(可选)
*/
async deleteApiKey(providerId, account = 'default') {
if (!providerId) {
throw new Error('Provider ID is required');
}
try {
const serviceName = this._getServiceName(providerId);
const result = await keytar.deletePassword(serviceName, account);
if (result) {
logger.info(`Deleted API key for ${providerId} from system keychain`);
} else {
logger.warn(`API key not found for ${providerId} in system keychain`);
}
return result;
} catch (error) {
logger.error(`Failed to delete API key for ${providerId} from system keychain:`, error);
throw new Error(`Failed to delete API key: ${error.message}`);
}
}
/**
* 获取所有存储的API密钥
*/
async getAllApiKeys() {
try {
const results = {};
// 查找所有服务
const services = await keytar.findCredentials('');
// 过滤出OpenHands相关服务
for (const service of services) {
if (service.account.startsWith(this.options.servicePrefix)) {
const providerId = service.account.slice(this.options.servicePrefix.length);
results[providerId] = service.password;
}
}
return results;
} catch (error) {
logger.error('Failed to get all API keys from system keychain:', error);
throw new Error(`Failed to get all API keys: ${error.message}`);
}
}
}
// 创建实例
const systemKeychain = new SystemKeychain();
module.exports = systemKeychain;
4.4 实现统一凭证接口
// src/services/unifiedCredentials.js
const credentialManager = require('./credentialManager');
const systemKeychain = require('./systemKeychain');
const envCredentials = require('./envCredentials');
const logger = require('../config/logger');
/**
* 统一凭证服务
* 提供多后端凭证存储支持,按优先级顺序尝试不同存储方式
*/
class UnifiedCredentialService {
constructor() {
this.initialized = false;
this.availableBackends = [];
this.backends = {
system: systemKeychain,
file: credentialManager,
env: envCredentials
};
// 默认后端优先级
this.backendPriority = ['system', 'file', 'env'];
}
/**
* 初始化凭证服务
*/
async initialize() {
try {
// 检查系统钥匙链是否可用
const systemAvailable = await systemKeychain.isAvailable();
if (systemAvailable) {
this.availableBackends.push('system');
logger.info('System keychain available');
} else {
logger.info('System keychain not available, using file-based storage');
}
// 初始化文件凭证管理器
await credentialManager.initialize();
this.availableBackends.push('file');
// 环境变量总是可用
this.availableBackends.push('env');
this.initialized = true;
logger.info(`Initialized credential service with backends: ${this.availableBackends.join(', ')}`);
return true;
} catch (error) {
logger.error('Failed to initialize unified credential service:', error);
throw error;
}
}
/**
* 获取凭证
* @param {string} providerId - 提供商ID
* @param {Object} options - 选项
* @returns {Promise<string>} - 凭证值
*/
async getCredential(providerId, options = {}) {
if (!this.initialized) {
throw new Error('Credential service not initialized');
}
if (!providerId) {
throw new Error('Provider ID is required');
}
// 确定要尝试的后端顺序
const backends = options.backends || this.backendPriority;
let credential = null;
let errors = [];
// 按顺序尝试每个后端
for (const backend of backends) {
if (!this.availableBackends.includes(backend)) {
continue;
}
try {
if (backend === 'system') {
credential = await systemKeychain.getApiKey(providerId);
} else if (backend === 'file') {
credential = await credentialManager.getCredential(providerId);
} else if (backend === 'env') {
credential = await envCredentials.getCredential(providerId);
}
if (credential) {
logger.debug(`Retrieved credential for ${providerId} from ${backend} backend`);
return credential;
}
} catch (error) {
errors.push(`${backend}: ${error.message}`);
}
}
// 所有后端都失败了
throw new Error(`Failed to get credential for ${providerId}: ${errors.join('; ')}`);
}
/**
* 存储凭证
* @param {string} providerId - 提供商ID
* @param {string} credential - 凭证值
* @param {Object} options - 选项
* @returns {Promise<boolean>} - 是否成功存储
*/
async storeCredential(providerId, credential, options = {}) {
if (!this.initialized) {
throw new Error('Credential service not initialized');
}
if (!providerId || !credential) {
throw new Error('Provider ID and credential are required');
}
// 优先使用指定的后端
const primaryBackend = options.backend || this.backendPriority[0];
// 确保后端可用
if (!this.availableBackends.includes(primaryBackend)) {
throw new Error(`Requested backend ${primaryBackend} is not available`);
}
try {
if (primaryBackend === 'system') {
await systemKeychain.storeApiKey(providerId, credential);
} else if (primaryBackend === 'file') {
await credentialManager.storeCredential(providerId, credential, options.metadata);
} else if (primaryBackend === 'env') {
throw new Error('Cannot store credentials in environment variables');
}
logger.info(`Stored credential for ${providerId} in ${primaryBackend} backend`);
return true;
} catch (error) {
logger.error(`Failed to store credential for ${providerId} in ${primaryBackend} backend:`, error);
throw error;
}
}
/**
* 删除凭证
* @param {string} providerId - 提供商ID
* @param {Object} options - 选项
* @returns {Promise<boolean>} - 是否成功删除
*/
async deleteCredential(providerId, options = {}) {
if (!this.initialized) {
throw new Error('Credential service not initialized');
}
if (!providerId) {
throw new Error('Provider ID is required');
}
let success = false;
const deleteAll = options.deleteAll || false;
const backends = deleteAll ? this.availableBackends : [options.backend || this.backendPriority[0]];
for (const backend of backends) {
try {
if (backend === 'system') {
success = await systemKeychain.deleteApiKey(providerId) || success;
} else if (backend === 'file') {
success = await credentialManager.deleteCredential(providerId) || success;
} else if (backend === 'env') {
logger.warn('Cannot delete credentials from environment variables');
}
} catch (error) {
logger.warn(`Failed to delete credential for ${providerId} from ${backend}:`, error);
}
}
if (success) {
logger.info(`Deleted credential for ${providerId}`);
} else {
logger.warn(`No credential found for ${providerId}`);
}
return success;
}
/**
* 列出所有提供商ID
* @returns {Promise<Array<string>>} - 提供商ID列表
*/
async listProviders() {
if (!this.initialized) {
throw new Error('Credential service not initialized');
}
const providers = new Set();
// 从所有后端收集提供商
if (this.availableBackends.includes('file')) {
const fileProviders = await credentialManager.listProviders();
fileProviders.forEach(provider => providers.add(provider));
}
if (this.availableBackends.includes('system')) {
try {
const keys = await systemKeychain.getAllApiKeys();
Object.keys(keys).forEach(provider => providers.add(provider));
} catch (error) {
logger.warn('Failed to list providers from system keychain:', error);
}
}
if (this.availableBackends.includes('env')) {
const envProviders = await envCredentials.listProviders();
envProviders.forEach(provider => providers.add(provider));
}
return Array.from(providers);
}
/**
* 检查提供商凭证是否存在
* @param {string} providerId - 提供商ID
* @returns {Promise<boolean>} - 凭证是否存在
*/
async hasCredential(providerId) {
if (!this.initialized) {
throw new Error('Credential service not initialized');
}
if (!providerId) {
throw new Error('Provider ID is required');
}
// 检查所有后端
for (const backend of this.availableBackends) {
try {
if (backend === 'system') {
try {
await systemKeychain.getApiKey(providerId);
return true;
} catch {
// 继续检查下一个后端
}
} else if (backend === 'file') {
if (await credentialManager.hasCredential(providerId)) {
return true;
}
} else if (backend === 'env') {
if (await envCredentials.hasCredential(providerId)) {
return true;
}
}
} catch (error) {
logger.warn(`Error checking ${backend} for ${providerId}:`, error);
}
}
return false;
}
/**
* 关闭凭证服务
*/
async shutdown() {
// 关闭各个后端
if (this.availableBackends.includes('file')) {
await credentialManager.shutdown();
}
// 清理内存中的敏感数据
this.availableBackends = [];
logger.info('Unified credential service shut down');
}
}
// 创建单例实例
const unifiedCredentialService = new UnifiedCredentialService();
module.exports = unifiedCredentialService;
4.5 实现环境变量凭证服务
// src/services/envCredentials.js
const logger = require('../config/logger');
// 环境变量前缀
const ENV_PREFIX = 'OPENHANDS_API_KEY_';
/**
* 环境变量凭证服务
* 从环境变量中获取API密钥
*/
class EnvCredentials {
/**
* 获取凭证
* @param {string} providerId - 提供商ID
* @returns {Promise<string>} - 凭证值
*/
async getCredential(providerId) {
if (!providerId) {
throw new Error('Provider ID is required');
}
// 尝试不同的环境变量命名格式
const envNames = [
// 标准格式: OPENHANDS_API_KEY_ANTHROPIC
`${ENV_PREFIX}${providerId.toUpperCase()}`,
// 直接格式: ANTHROPIC_API_KEY
`${providerId.toUpperCase()}_API_KEY`,
// 原始格式: ANTHROPIC_KEY
`${providerId.toUpperCase()}_KEY`
];
for (const envName of envNames) {
const value = process.env[envName];
if (value) {
logger.debug(`Found credential for ${providerId} in environment variable ${envName}`);
return value;
}
}
throw new Error(`Credential not found in environment variables for provider: ${providerId}`);
}
/**
* 列出所有环境变量中的提供商
* @returns {Promise<Array<string>>} - 提供商ID列表
*/
async listProviders() {
const providers = new Set();
// 遍历所有环境变量
for (const key in process.env) {
// 检查标准格式
if (key.startsWith(ENV_PREFIX)) {
const providerId = key.slice(ENV_PREFIX.length).toLowerCase();
providers.add(providerId);
continue;
}
// 检查直接格式
if (key.endsWith('_API_KEY')) {
const providerId = key.slice(0, -8).toLowerCase();
providers.add(providerId);
continue;
}
// 检查原始格式
if (key.endsWith('_KEY')) {
const providerId = key.slice(0, -4).toLowerCase();
providers.add(providerId);
}
}
return Array.from(providers);
}
/**
* 检查提供商凭证是否存在
* @param {string} providerId - 提供商ID
* @returns {Promise<boolean>} - 凭证是否存在
*/
async hasCredential(providerId) {
try {
await this.getCredential(providerId);
return true;
} catch {
return false;
}
}
}
// 创建单例实例
const envCredentials = new EnvCredentials();
module.exports = envCredentials;
4.6 实现API控制器
// src/controllers/credentialController.js
const { validationResult } = require('express-validator');
const logger = require('../config/logger');
const unifiedCredentialService = require('../services/unifiedCredentials');
/**
* 获取所有提供商
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
* @param {Function} next - 下一个中间件
*/
exports.listProviders = async (req, res, next) => {
try {
const providers = await unifiedCredentialService.listProviders();
// 为每个提供商检查凭证状态
const providersWithStatus = await Promise.all(
providers.map(async (providerId) => ({
id: providerId,
hasCredential: await unifiedCredentialService.hasCredential(providerId)
}))
);
res.json({
success: true,
providers: providersWithStatus
});
} catch (error) {
logger.error('Error in listProviders controller:', error);
next(error);
}
};
/**
* 设置API密钥
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
* @param {Function} next - 下一个中间件
*/
exports.setApiKey = async (req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { providerId, apiKey, backend = 'file' } = req.body;
// 存储凭证
await unifiedCredentialService.storeCredential(providerId, apiKey, {
backend,
metadata: {
setBy: req.user?.username || 'api',
setAt: new Date().toISOString()
}
});
res.json({
success: true,
providerId,
message: `API key for ${providerId} stored successfully`
});
} catch (error) {
logger.error('Error in setApiKey controller:', error);
next(error);
}
};
/**
* 检查API密钥
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
* @param {Function} next - 下一个中间件
*/
exports.checkApiKey = async (req, res, next) => {
try {
const { providerId } = req.params;
if (!providerId) {
return res.status(400).json({
success: false,
error: 'Provider ID is required'
});
}
const hasCredential = await unifiedCredentialService.hasCredential(providerId);
res.json({
success: true,
providerId,
hasCredential
});
} catch (error) {
logger.error('Error in checkApiKey controller:', error);
next(error);
}
};
/**
* 删除API密钥
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
* @param {Function} next - 下一个中间件
*/
exports.deleteApiKey = async (req, res, next) => {
try {
const { providerId } = req.params;
const { deleteAll = false } = req.query;
if (!providerId) {
return res.status(400).json({
success: false,
error: 'Provider ID is required'
});
}
const result = await unifiedCredentialService.deleteCredential(providerId, {
deleteAll: deleteAll === 'true'
});
res.json({
success: result,
providerId,
message: result
? `API key for ${providerId} deleted successfully`
: `No API key found for ${providerId}`
});
} catch (error) {
logger.error('Error in deleteApiKey controller:', error);
next(error);
}
};
4.7 实现API路由
// src/routes/credentialRoutes.js
const express = require('express');
const { body, param } = require('express-validator');
const credentialController = require('../controllers/credentialController');
const authMiddleware = require('../middlewares/authMiddleware');
const router = express.Router();
/**
* @route GET /api/credentials
* @desc 获取所有提供商
* @access Private
*/
router.get(
'/',
authMiddleware.requireAuth,
credentialController.listProviders
);
/**
* @route POST /api/credentials
* @desc 设置API密钥
* @access Private
*/
router.post(
'/',
authMiddleware.requireAuth,
[
body('providerId').notEmpty().withMessage('Provider ID is required'),
body('apiKey').notEmpty().withMessage('API key is required'),
body('backend').optional().isIn(['file', 'system']).withMessage('Invalid backend')
],
credentialController.setApiKey
);
/**
* @route GET /api/credentials/:providerId
* @desc 检查API密钥
* @access Private
*/
router.get(
'/:providerId',
authMiddleware.requireAuth,
[
param('providerId').notEmpty().withMessage('Provider ID is required')
],
credentialController.checkApiKey
);
/**
* @route DELETE /api/credentials/:providerId
* @desc 删除API密钥
* @access Private
*/
router.delete(
'/:providerId',
authMiddleware.requireAuth,
[
param('providerId').notEmpty().withMessage('Provider ID is required')
],
credentialController.deleteApiKey
);
module.exports = router;
4.8 安全最佳实践实现
// src/utils/securityUtils.js
const crypto = require('crypto');
const fs = require('fs').promises;
const path = require('path');
/**
* 安全工具函数集
* 提供各种安全相关的工具函数
*/
class SecurityUtils {
/**
* 生成安全随机字符串
* @param {number} length - 字符串长度
* @returns {string} - 随机字符串
*/
static generateRandomString(length = 32) {
return crypto.randomBytes(Math.ceil(length / 2))
.toString('hex')
.slice(0, length);
}
/**
* 安全比较两个字符串
* 防止时序攻击
* @param {string} a - 字符串A
* @param {string} b - 字符串B
* @returns {boolean} - 是否相等
*/
static safeCompare(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(a, 'utf8'),
Buffer.from(b, 'utf8')
);
}
/**
* 哈希密码
* @param {string} password - 密码
* @param {string} salt - 盐(可选)
* @returns {Promise<Object>} - 哈希结果
*/
static async hashPassword(password, salt = null) {
const useSalt = salt || crypto.randomBytes(16);
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, useSalt, 100000, 64, 'sha512', (err, derivedKey) => {
if (err) return reject(err);
resolve({
hash: derivedKey.toString('hex'),
salt: useSalt.toString('hex')
});
});
});
}
/**
* 验证密码
* @param {string} password - 密码
* @param {string} hash - 哈希值
* @param {string} salt - 盐
* @returns {Promise<boolean>} - 是否有效
*/
static async verifyPassword(password, hash, salt) {
const result = await this.hashPassword(password, Buffer.from(salt, 'hex'));
return result.hash === hash;
}
/**
* 安全删除文件
* 覆盖文件内容后删除
* @param {string} filePath - 文件路径
* @returns {Promise<boolean>} - 是否成功
*/
static async secureDeleteFile(filePath) {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
// 获取文件大小
const size = stats.size;
// 打开文件
const fd = await fs.open(filePath, 'w');
try {
// 用随机数据覆盖文件
const buffer = crypto.randomBytes(8192);
let position = 0;
while (position < size) {
const writeSize = Math.min(buffer.length, size - position);
await fs.write(fd.fd, buffer, 0, writeSize, position);
position += writeSize;
}
// 关闭文件
await fd.close();
// 删除文件
await fs.unlink(filePath);
return true;
} catch (error) {
if (fd) {
await fd.close();
}
throw error;
}
}
return false;
} catch (error) {
if (error.code === 'ENOENT') {
// 文件不存在,视为成功
return true;
}
throw error;
}
}
/**
* 检测敏感信息泄露
* @param {string} content - 要检查的内容
* @returns {Object} - 检测结果
*/
static detectSensitiveInfo(content) {
if (typeof content !== 'string') {
return { detected: false };
}
const patterns = {
apiKey: /(['"])?(api[_-]?key|api[_-]?secret|access[_-]?key|access[_-]?token)['"]\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,})['"]|(['"])[a-zA-Z0-9_\-]{20,}\4/i,
password: /(['"])?password['"]?\s*[:=]\s*['"]([^'"]{8,})['"]|PASSWORD\s*=\s*['"]\S+['"]/i,
privateKey: /-----BEGIN\s+PRIVATE\s+KEY-----/,
awsKey: /AKIA[0-9A-Z]{16}/,
googleApiKey: /AIza[0-9A-Za-z\-_]{35}/,
anthropicKey: /sk-ant-[a-zA-Z0-9]{32,}/,
openaiKey: /sk-[a-zA-Z0-9]{32,}/
};
const results = {};
let detected = false;
for (const [type, pattern] of Object.entries(patterns)) {
const matches = content.match(pattern);
if (matches) {
results[type] = true;
detected = true;
}
}
return {
detected,
types: Object.keys(results)
};
}
/**
* 混淆API密钥用于日志显示
* @param {string} apiKey - API密钥
* @returns {string} - 混淆后的密钥
*/
static obfuscateApiKey(apiKey) {
if (!apiKey || typeof apiKey !== 'string') {
return '[undefined]';
}
if (apiKey.length <= 8) {
return '****';
}
// 保留前4和后4个字符,其余替换为*
return `${apiKey.slice(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.slice(-4)}`;
}
}
module.exports = SecurityUtils;
五、安全最佳实践与风险防范
5.1 API密钥安全策略
API密钥是访问外部服务的关键凭证,保护它们至关重要。以下是在OpenHands中实施的API密钥安全策略:
多层次存储:
- 系统钥匙链: 操作系统级别的安全存储
- 加密文件: 使用AES-256-GCM加密的本地文件
- 环境变量: 仅用于开发和CI/CD环境
密钥轮换:
- 支持主密钥轮换功能
- 轮换时重新加密所有存储的凭证
- 定期轮换建议提醒
最小暴露:
- 内存中的凭证定期清除
- 日志中混淆API密钥
- 仅在需要时解密凭证
访问控制:
- 基于角色的API密钥访问
- 需要身份验证才能管理凭证
- 文件权限限制(0600)
5.2 常见安全风险与防范
风险 | 防范措施 | 实现方式 |
---|---|---|
密钥泄露 | 加密存储 | AES-256-GCM加密 |
未授权访问 | 访问控制 | 身份验证中间件 |
密钥被窃取 | 密钥轮换 | 支持主密钥轮换 |
日志泄露 | 敏感信息过滤 | 日志混淆工具 |
代码泄露 | 分离凭证与代码 | 独立存储系统 |
中间人攻击 | 传输加密 | HTTPS/TLS |
内部威胁 | 审计日志 | 凭证访问记录 |
5.3 凭证生命周期管理
完整的凭证生命周期管理包括以下阶段:
创建:
- 生成强随机密钥
- 记录元数据(创建时间、创建者)
存储:
- 选择适当的后端(系统钥匙链、加密文件)
- 加密保护
使用:
- 按需访问
- 记录使用情况
- 内存中临时保留
轮换:
- 定期更新
- 无缝过渡
- 旧密钥失效
撤销:
- 安全删除
- 覆盖原始数据
- 更新依赖系统
5.4 容器环境中的凭证管理
在Docker容器中运行OpenHands时,凭证管理面临特殊挑战:
环境变量注入:
- 使用Docker secrets或环境变量传递凭证
- 避免在Dockerfile中硬编码
卷挂载:
- 将加密凭证存储挂载到容器
- 确保主机和容器权限正确
临时性:
- 容器重启时重新加载凭证
- 不依赖容器内持久存储
隔离性:
- 每个容器使用独立凭证
- 限制容器间凭证共享
六、总结与思考
6.1 凭证管理的关键挑战
在开发OpenHands这样的AI代理系统时,凭证管理面临几个关键挑战:
安全与便利性平衡:
- 过于复杂的安全措施可能降低用户体验
- 过于简单的方案可能带来安全风险
- 需要在两者间找到平衡点
多环境支持:
- 本地开发环境
- 容器化部署
- CI/CD流水线
- 生产环境
- 每种环境有不同的安全需求和限制
用户友好的密钥管理:
- 简化API密钥设置流程
- 提供清晰的错误信息
- 自动检测常见配置问题
多LLM提供商支持:
- 不同提供商有不同的API密钥格式
- 需要统一的管理接口
- 支持平滑切换提供商
6.2 设计决策与权衡
在实现OpenHands的凭证管理系统时,我们做出了以下设计决策:
选择多后端架构:
- 优点: 适应不同环境和安全需求
- 缺点: 增加了系统复杂性
- 权衡: 通过统一接口简化使用
使用对称加密:
- 优点: 简单高效,无需外部依赖
- 缺点: 主密钥管理成为单点风险
- 权衡: 实现主密钥轮换机制
文件系统存储:
- 优点: 广泛兼容,简单实现
- 缺点: 依赖文件系统安全性
- 权衡: 严格的文件权限和加密
内存中临时保留解密凭证:
- 优点: 提高性能,减少解密操作
- 缺点: 增加内存泄露风险
- 权衡: 定期清理内存中的凭证
6.3 未来改进方向
OpenHands凭证管理系统可以在以下方面进一步改进:
硬件安全模块(HSM)集成:
- 使用专用硬件保护主密钥
- 提供更高级别的安全保证
多因素认证:
- 添加额外的认证层保护凭证访问
- 支持生物识别或硬件令牌
云密钥管理服务集成:
- AWS KMS, Google Cloud KMS等
- 适用于云部署场景
凭证使用分析:
- 监控API密钥使用模式
- 检测异常访问行为
- 自动阻止可疑活动
自动化密钥轮换:
- 定期自动轮换API密钥
- 与LLM提供商API集成实现无缝轮换