动手实践OpenHands系列学习笔记10:API密钥与凭证管理

发布于:2025-07-06 ⋅ 阅读:(18) ⋅ 点赞:(0)

笔记10:API密钥与凭证管理

一、引言

API密钥和凭证是现代应用程序的关键安全资产,尤其在AI代理系统中扮演着至关重要的角色。本笔记将探讨密钥管理安全原则,分析OpenHands的凭证处理机制,并实现一个安全的凭证存储系统,确保敏感信息不被泄露同时保持易用性。

二、密钥管理安全原则

2.1 凭证安全的核心挑战

  • 敏感性: API密钥等同于密码,泄露可能导致未授权访问和资源滥用
  • 分发困难: 需要安全地将密钥分发给授权用户和服务
  • 轮换复杂性: 定期更新密钥以提高安全性但增加管理难度
  • 可追溯性: 追踪密钥使用以检测滥用和异常行为
  • 范围控制: 限制每个密钥的权限范围和能力
  • 生命周期管理: 从创建到撤销的完整管理流程

2.2 常见的安全漏洞

  1. 硬编码凭证: 将密钥直接嵌入代码中
  2. 明文存储: 在配置文件中以明文形式保存密钥
  3. 不安全传输: 通过不加密的通道传输密钥
  4. 过度共享: 多个服务或用户共享同一密钥
  5. 权限过大: 使用具有不必要高权限的密钥
  6. 缺乏审计: 无法追踪密钥的使用情况
  7. 忽略轮换: 长期使用相同密钥增加泄露风险

2.3 密钥管理最佳实践

  1. 隔离存储:

    • 使用专用的密钥管理服务
    • 分离应用代码和凭证
  2. 加密保护:

    • 静态加密(at rest)
    • 传输中加密(in transit)
  3. 最小权限原则:

    • 限制每个密钥的权限范围
    • 创建服务特定的密钥
  4. 安全分发:

    • 使用安全通道传递密钥
    • 实施临时访问机制
  5. 定期轮换:

    • 建立密钥轮换流程
    • 自动化轮换过程
  6. 监控与审计:

    • 记录所有密钥访问
    • 检测异常使用模式
  7. 响应计划:

    • 制定密钥泄露应对方案
    • 快速撤销和替换机制

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凭证需求

  1. LLM提供商API密钥:

    • 需要在应用启动时提供
    • 支持多种LLM提供商,如Anthropic Claude
  2. 持久化存储:

    • 使用~/.openhands目录存储数据
    • 可能包括API密钥和配置信息
  3. 多模式支持:

    • 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密钥安全策略:

  1. 多层次存储:

    • 系统钥匙链: 操作系统级别的安全存储
    • 加密文件: 使用AES-256-GCM加密的本地文件
    • 环境变量: 仅用于开发和CI/CD环境
  2. 密钥轮换:

    • 支持主密钥轮换功能
    • 轮换时重新加密所有存储的凭证
    • 定期轮换建议提醒
  3. 最小暴露:

    • 内存中的凭证定期清除
    • 日志中混淆API密钥
    • 仅在需要时解密凭证
  4. 访问控制:

    • 基于角色的API密钥访问
    • 需要身份验证才能管理凭证
    • 文件权限限制(0600)

5.2 常见安全风险与防范

风险 防范措施 实现方式
密钥泄露 加密存储 AES-256-GCM加密
未授权访问 访问控制 身份验证中间件
密钥被窃取 密钥轮换 支持主密钥轮换
日志泄露 敏感信息过滤 日志混淆工具
代码泄露 分离凭证与代码 独立存储系统
中间人攻击 传输加密 HTTPS/TLS
内部威胁 审计日志 凭证访问记录

5.3 凭证生命周期管理

完整的凭证生命周期管理包括以下阶段:

  1. 创建:

    • 生成强随机密钥
    • 记录元数据(创建时间、创建者)
  2. 存储:

    • 选择适当的后端(系统钥匙链、加密文件)
    • 加密保护
  3. 使用:

    • 按需访问
    • 记录使用情况
    • 内存中临时保留
  4. 轮换:

    • 定期更新
    • 无缝过渡
    • 旧密钥失效
  5. 撤销:

    • 安全删除
    • 覆盖原始数据
    • 更新依赖系统

5.4 容器环境中的凭证管理

在Docker容器中运行OpenHands时,凭证管理面临特殊挑战:

  1. 环境变量注入:

    • 使用Docker secrets或环境变量传递凭证
    • 避免在Dockerfile中硬编码
  2. 卷挂载:

    • 将加密凭证存储挂载到容器
    • 确保主机和容器权限正确
  3. 临时性:

    • 容器重启时重新加载凭证
    • 不依赖容器内持久存储
  4. 隔离性:

    • 每个容器使用独立凭证
    • 限制容器间凭证共享

六、总结与思考

6.1 凭证管理的关键挑战

在开发OpenHands这样的AI代理系统时,凭证管理面临几个关键挑战:

  1. 安全与便利性平衡:

    • 过于复杂的安全措施可能降低用户体验
    • 过于简单的方案可能带来安全风险
    • 需要在两者间找到平衡点
  2. 多环境支持:

    • 本地开发环境
    • 容器化部署
    • CI/CD流水线
    • 生产环境
    • 每种环境有不同的安全需求和限制
  3. 用户友好的密钥管理:

    • 简化API密钥设置流程
    • 提供清晰的错误信息
    • 自动检测常见配置问题
  4. 多LLM提供商支持:

    • 不同提供商有不同的API密钥格式
    • 需要统一的管理接口
    • 支持平滑切换提供商

6.2 设计决策与权衡

在实现OpenHands的凭证管理系统时,我们做出了以下设计决策:

  1. 选择多后端架构:

    • 优点: 适应不同环境和安全需求
    • 缺点: 增加了系统复杂性
    • 权衡: 通过统一接口简化使用
  2. 使用对称加密:

    • 优点: 简单高效,无需外部依赖
    • 缺点: 主密钥管理成为单点风险
    • 权衡: 实现主密钥轮换机制
  3. 文件系统存储:

    • 优点: 广泛兼容,简单实现
    • 缺点: 依赖文件系统安全性
    • 权衡: 严格的文件权限和加密
  4. 内存中临时保留解密凭证:

    • 优点: 提高性能,减少解密操作
    • 缺点: 增加内存泄露风险
    • 权衡: 定期清理内存中的凭证

6.3 未来改进方向

OpenHands凭证管理系统可以在以下方面进一步改进:

  1. 硬件安全模块(HSM)集成:

    • 使用专用硬件保护主密钥
    • 提供更高级别的安全保证
  2. 多因素认证:

    • 添加额外的认证层保护凭证访问
    • 支持生物识别或硬件令牌
  3. 云密钥管理服务集成:

    • AWS KMS, Google Cloud KMS等
    • 适用于云部署场景
  4. 凭证使用分析:

    • 监控API密钥使用模式
    • 检测异常访问行为
    • 自动阻止可疑活动
  5. 自动化密钥轮换:

    • 定期自动轮换API密钥
    • 与LLM提供商API集成实现无缝轮换

七、参考资源

  1. OWASP密钥管理指南
  2. NIST密码学标准
  3. Node.js加密模块文档
  4. Keytar: 系统钥匙链访问库
  5. Anthropic API密钥管理最佳实践
  6. Docker Secrets文档
  7. 环境变量安全指南
  8. AWS密钥管理服务(KMS)
  9. HashiCorp Vault文档
  10. 加密存储最佳实践