/** * API Key安全存储服务 * * 功能描述: * - 实现Zulip API Key的加密存储 * - 提供安全日志记录功能 * - 检测异常操作并记录安全事件 * - 支持API Key的安全获取和更新 * * 主要方法: * - storeApiKey(): 加密存储API Key * - getApiKey(): 安全获取API Key * - updateApiKey(): 更新API Key * - deleteApiKey(): 删除API Key * - logSecurityEvent(): 记录安全事件 * * 使用场景: * - 用户首次绑定Zulip账户 * - Zulip客户端创建时获取API Key * - 检测到异常操作时记录安全日志 * * 依赖模块: * - AppLoggerService: 日志记录服务 * - IRedisService: Redis缓存服务 * * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ import { Injectable, Inject, Logger } from '@nestjs/common'; import * as crypto from 'crypto'; import { IRedisService } from '../../../core/redis/redis.interface'; /** * 安全事件类型枚举 */ export enum SecurityEventType { API_KEY_STORED = 'api_key_stored', API_KEY_ACCESSED = 'api_key_accessed', API_KEY_UPDATED = 'api_key_updated', API_KEY_DELETED = 'api_key_deleted', API_KEY_DECRYPTION_FAILED = 'api_key_decryption_failed', SUSPICIOUS_ACCESS = 'suspicious_access', RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded', INVALID_KEY_FORMAT = 'invalid_key_format', UNAUTHORIZED_ACCESS = 'unauthorized_access', } /** * 安全事件严重级别 */ export enum SecuritySeverity { INFO = 'info', WARNING = 'warning', CRITICAL = 'critical', } /** * 安全事件记录接口 */ export interface SecurityEvent { eventType: SecurityEventType; severity: SecuritySeverity; userId: string; details: Record; timestamp: Date; ipAddress?: string; userAgent?: string; } /** * 加密后的API Key存储结构 */ export interface EncryptedApiKey { encryptedKey: string; iv: string; authTag: string; createdAt: Date; updatedAt: Date; accessCount: number; lastAccessedAt?: Date; } /** * API Key存储结果 */ export interface StoreApiKeyResult { success: boolean; message: string; userId?: string; } /** * API Key获取结果 */ export interface GetApiKeyResult { success: boolean; apiKey?: string; message?: string; } @Injectable() export class ApiKeySecurityService { private readonly logger = new Logger(ApiKeySecurityService.name); private readonly API_KEY_PREFIX = 'zulip:api_key:'; private readonly SECURITY_LOG_PREFIX = 'zulip:security_log:'; private readonly ACCESS_COUNT_PREFIX = 'zulip:api_key_access:'; private readonly ENCRYPTION_ALGORITHM = 'aes-256-gcm'; private readonly KEY_LENGTH = 32; // 256 bits private readonly IV_LENGTH = 16; // 128 bits private readonly AUTH_TAG_LENGTH = 16; // 128 bits private readonly MAX_ACCESS_PER_MINUTE = 60; // 每分钟最大访问次数 private readonly SECURITY_LOG_RETENTION = 30 * 24 * 3600; // 30天 // 加密密钥(生产环境应从环境变量或密钥管理服务获取) private readonly encryptionKey: Buffer; constructor( @Inject('REDIS_SERVICE') private readonly redisService: IRedisService, ) { // 从环境变量获取加密密钥,如果没有则生成一个默认密钥(仅用于开发) const keyFromEnv = process.env.ZULIP_API_KEY_ENCRYPTION_KEY; if (keyFromEnv) { this.encryptionKey = Buffer.from(keyFromEnv, 'hex'); } else { // 开发环境使用固定密钥(生产环境必须配置环境变量) this.encryptionKey = crypto.scryptSync('default-dev-key', 'salt', this.KEY_LENGTH); this.logger.warn('使用默认加密密钥,生产环境请配置ZULIP_API_KEY_ENCRYPTION_KEY环境变量'); } this.logger.log('ApiKeySecurityService初始化完成'); } /** * 加密存储API Key * * 功能描述: * 使用AES-256-GCM算法加密API Key并存储到Redis * * 业务逻辑: * 1. 验证API Key格式 * 2. 生成随机IV * 3. 使用AES-256-GCM加密 * 4. 存储加密后的数据到Redis * 5. 记录安全日志 * * @param userId 用户ID * @param apiKey Zulip API Key * @param metadata 可选的元数据(如IP地址) * @returns Promise 存储结果 */ async storeApiKey( userId: string, apiKey: string, metadata?: { ipAddress?: string; userAgent?: string } ): Promise { const startTime = Date.now(); this.logger.log(`开始存储API Key: ${userId}`); try { // 1. 参数验证 if (!userId || !userId.trim()) { await this.logSecurityEvent({ eventType: SecurityEventType.INVALID_KEY_FORMAT, severity: SecuritySeverity.WARNING, userId: userId || 'unknown', details: { reason: 'empty_user_id' }, timestamp: new Date(), ...metadata, }); return { success: false, message: '用户ID不能为空' }; } if (!apiKey || !apiKey.trim()) { await this.logSecurityEvent({ eventType: SecurityEventType.INVALID_KEY_FORMAT, severity: SecuritySeverity.WARNING, userId, details: { reason: 'empty_api_key' }, timestamp: new Date(), ...metadata, }); return { success: false, message: 'API Key不能为空' }; } // 2. 验证API Key格式(Zulip API Key通常是32字符的字母数字字符串) if (!this.isValidApiKeyFormat(apiKey)) { await this.logSecurityEvent({ eventType: SecurityEventType.INVALID_KEY_FORMAT, severity: SecuritySeverity.WARNING, userId, details: { reason: 'invalid_format', keyLength: apiKey.length }, timestamp: new Date(), ...metadata, }); return { success: false, message: 'API Key格式无效' }; } // 3. 加密API Key const encrypted = this.encrypt(apiKey); // 4. 构建存储数据 const storageData: EncryptedApiKey = { encryptedKey: encrypted.encryptedData, iv: encrypted.iv, authTag: encrypted.authTag, createdAt: new Date(), updatedAt: new Date(), accessCount: 0, }; // 5. 存储到Redis const storageKey = `${this.API_KEY_PREFIX}${userId}`; await this.redisService.set(storageKey, JSON.stringify(storageData)); // 6. 记录安全日志 await this.logSecurityEvent({ eventType: SecurityEventType.API_KEY_STORED, severity: SecuritySeverity.INFO, userId, details: { action: 'store', keyLength: apiKey.length, }, timestamp: new Date(), ...metadata, }); const duration = Date.now() - startTime; this.logger.log(`API Key存储成功: ${userId}`); return { success: true, message: 'API Key存储成功', userId }; } catch (error) { const err = error as Error; const duration = Date.now() - startTime; this.logger.error('API Key存储失败', { operation: 'storeApiKey', userId, error: err.message, duration, timestamp: new Date().toISOString(), }, err.stack); return { success: false, message: '存储失败,请稍后重试' }; } } /** * 安全获取API Key * * 功能描述: * 从Redis获取加密的API Key并解密返回 * * 业务逻辑: * 1. 检查访问频率限制 * 2. 从Redis获取加密数据 * 3. 解密API Key * 4. 更新访问计数 * 5. 记录访问日志 * * @param userId 用户ID * @param metadata 可选的元数据 * @returns Promise 获取结果 */ async getApiKey( userId: string, metadata?: { ipAddress?: string; userAgent?: string } ): Promise { const startTime = Date.now(); this.logger.debug('开始获取API Key', { operation: 'getApiKey', userId, timestamp: new Date().toISOString(), }); try { // 1. 参数验证 if (!userId || !userId.trim()) { return { success: false, message: '用户ID不能为空' }; } // 2. 检查访问频率限制 const rateLimitCheck = await this.checkAccessRateLimit(userId); if (!rateLimitCheck.allowed) { await this.logSecurityEvent({ eventType: SecurityEventType.RATE_LIMIT_EXCEEDED, severity: SecuritySeverity.WARNING, userId, details: { currentCount: rateLimitCheck.currentCount, limit: this.MAX_ACCESS_PER_MINUTE, }, timestamp: new Date(), ...metadata, }); return { success: false, message: '访问频率过高,请稍后重试' }; } // 3. 从Redis获取加密数据 const storageKey = `${this.API_KEY_PREFIX}${userId}`; const encryptedData = await this.redisService.get(storageKey); if (!encryptedData) { this.logger.debug('API Key不存在', { operation: 'getApiKey', userId, }); return { success: false, message: 'API Key不存在' }; } // 4. 解析存储数据 const storageData: EncryptedApiKey = JSON.parse(encryptedData); // 5. 解密API Key let apiKey: string; try { apiKey = this.decrypt( storageData.encryptedKey, storageData.iv, storageData.authTag ); } catch (decryptError) { await this.logSecurityEvent({ eventType: SecurityEventType.API_KEY_DECRYPTION_FAILED, severity: SecuritySeverity.CRITICAL, userId, details: { error: (decryptError as Error).message, }, timestamp: new Date(), ...metadata, }); return { success: false, message: 'API Key解密失败' }; } // 6. 更新访问计数和时间 storageData.accessCount += 1; storageData.lastAccessedAt = new Date(); await this.redisService.set(storageKey, JSON.stringify(storageData)); // 7. 记录访问日志 await this.logSecurityEvent({ eventType: SecurityEventType.API_KEY_ACCESSED, severity: SecuritySeverity.INFO, userId, details: { accessCount: storageData.accessCount, }, timestamp: new Date(), ...metadata, }); const duration = Date.now() - startTime; this.logger.debug('API Key获取成功', { operation: 'getApiKey', userId, accessCount: storageData.accessCount, duration, }); return { success: true, apiKey }; } catch (error) { const err = error as Error; const duration = Date.now() - startTime; this.logger.error('API Key获取失败', { operation: 'getApiKey', userId, error: err.message, duration, timestamp: new Date().toISOString(), }, err.stack); return { success: false, message: '获取失败,请稍后重试' }; } } /** * 更新API Key * * 功能描述: * 更新用户的Zulip API Key * * @param userId 用户ID * @param newApiKey 新的API Key * @param metadata 可选的元数据 * @returns Promise 更新结果 */ async updateApiKey( userId: string, newApiKey: string, metadata?: { ipAddress?: string; userAgent?: string } ): Promise { this.logger.log(`开始更新API Key: ${userId}`); try { // 1. 检查原API Key是否存在 const storageKey = `${this.API_KEY_PREFIX}${userId}`; const existingData = await this.redisService.get(storageKey); if (!existingData) { // 如果不存在,则创建新的 return this.storeApiKey(userId, newApiKey, metadata); } // 2. 验证新API Key格式 if (!this.isValidApiKeyFormat(newApiKey)) { await this.logSecurityEvent({ eventType: SecurityEventType.INVALID_KEY_FORMAT, severity: SecuritySeverity.WARNING, userId, details: { reason: 'invalid_format', action: 'update' }, timestamp: new Date(), ...metadata, }); return { success: false, message: 'API Key格式无效' }; } // 3. 解析现有数据 const oldStorageData: EncryptedApiKey = JSON.parse(existingData); // 4. 加密新API Key const encrypted = this.encrypt(newApiKey); // 5. 更新存储数据 const newStorageData: EncryptedApiKey = { encryptedKey: encrypted.encryptedData, iv: encrypted.iv, authTag: encrypted.authTag, createdAt: oldStorageData.createdAt, updatedAt: new Date(), accessCount: oldStorageData.accessCount, lastAccessedAt: oldStorageData.lastAccessedAt, }; await this.redisService.set(storageKey, JSON.stringify(newStorageData)); // 6. 记录安全日志 await this.logSecurityEvent({ eventType: SecurityEventType.API_KEY_UPDATED, severity: SecuritySeverity.INFO, userId, details: { action: 'update', previousAccessCount: oldStorageData.accessCount, }, timestamp: new Date(), ...metadata, }); this.logger.log(`API Key更新成功: ${userId}`); return { success: true, message: 'API Key更新成功', userId }; } catch (error) { const err = error as Error; this.logger.error('API Key更新失败', { operation: 'updateApiKey', userId, error: err.message, timestamp: new Date().toISOString(), }, err.stack); return { success: false, message: '更新失败,请稍后重试' }; } } /** * 删除API Key * * 功能描述: * 安全删除用户的API Key * * @param userId 用户ID * @param metadata 可选的元数据 * @returns Promise 是否删除成功 */ async deleteApiKey( userId: string, metadata?: { ipAddress?: string; userAgent?: string } ): Promise { this.logger.log(`开始删除API Key: ${userId}`); try { const storageKey = `${this.API_KEY_PREFIX}${userId}`; await this.redisService.del(storageKey); // 记录安全日志 await this.logSecurityEvent({ eventType: SecurityEventType.API_KEY_DELETED, severity: SecuritySeverity.INFO, userId, details: { action: 'delete' }, timestamp: new Date(), ...metadata, }); this.logger.log(`API Key删除成功: ${userId}`); return true; } catch (error) { const err = error as Error; this.logger.error('API Key删除失败', { operation: 'deleteApiKey', userId, error: err.message, timestamp: new Date().toISOString(), }, err.stack); return false; } } /** * 检查API Key是否存在 * * @param userId 用户ID * @returns Promise 是否存在 */ async hasApiKey(userId: string): Promise { try { const storageKey = `${this.API_KEY_PREFIX}${userId}`; return await this.redisService.exists(storageKey); } catch (error) { this.logger.error('检查API Key存在性失败', { operation: 'hasApiKey', userId, error: (error as Error).message, }); return false; } } /** * 记录安全事件 * * 功能描述: * 记录安全相关的事件到Redis,用于审计和监控 * * @param event 安全事件 * @returns Promise */ async logSecurityEvent(event: SecurityEvent): Promise { try { const logKey = `${this.SECURITY_LOG_PREFIX}${event.userId}:${Date.now()}`; await this.redisService.setex( logKey, this.SECURITY_LOG_RETENTION, JSON.stringify(event) ); // 根据严重级别记录到应用日志 const logContext = { operation: 'logSecurityEvent', eventType: event.eventType, severity: event.severity, userId: event.userId, details: event.details, ipAddress: event.ipAddress, timestamp: event.timestamp.toISOString(), }; switch (event.severity) { case SecuritySeverity.CRITICAL: this.logger.error('安全事件 - 严重', logContext); break; case SecuritySeverity.WARNING: this.logger.warn('安全事件 - 警告', logContext); break; case SecuritySeverity.INFO: default: this.logger.log('安全事件 - 信息', logContext); break; } } catch (error) { this.logger.error('记录安全事件失败', { operation: 'logSecurityEvent', event, error: (error as Error).message, }); } } /** * 记录可疑访问 * * 功能描述: * 当检测到异常操作时记录可疑访问事件 * * @param userId 用户ID * @param reason 可疑原因 * @param details 详细信息 * @param metadata 元数据 * @returns Promise */ async logSuspiciousAccess( userId: string, reason: string, details: Record, metadata?: { ipAddress?: string; userAgent?: string } ): Promise { await this.logSecurityEvent({ eventType: SecurityEventType.SUSPICIOUS_ACCESS, severity: SecuritySeverity.WARNING, userId, details: { reason, ...details, }, timestamp: new Date(), ...metadata, }); } /** * 获取用户安全事件历史 * * @param userId 用户ID * @param limit 返回数量限制 * @returns Promise 安全事件列表 */ async getSecurityEventHistory(userId: string, limit: number = 100): Promise { // 注意:这是一个简化实现,实际应该使用Redis的有序集合或扫描功能 // 当前实现仅作为示例 this.logger.debug('获取安全事件历史', { operation: 'getSecurityEventHistory', userId, limit, }); return []; } /** * 获取API Key统计信息 * * @param userId 用户ID * @returns Promise<{exists: boolean, accessCount?: number, lastAccessedAt?: Date, createdAt?: Date}> */ async getApiKeyStats(userId: string): Promise<{ exists: boolean; accessCount?: number; lastAccessedAt?: Date; createdAt?: Date; updatedAt?: Date; }> { try { const storageKey = `${this.API_KEY_PREFIX}${userId}`; const data = await this.redisService.get(storageKey); if (!data) { return { exists: false }; } const storageData: EncryptedApiKey = JSON.parse(data); return { exists: true, accessCount: storageData.accessCount, lastAccessedAt: storageData.lastAccessedAt ? new Date(storageData.lastAccessedAt) : undefined, createdAt: new Date(storageData.createdAt), updatedAt: new Date(storageData.updatedAt), }; } catch (error) { this.logger.error('获取API Key统计信息失败', { operation: 'getApiKeyStats', userId, error: (error as Error).message, }); return { exists: false }; } } // ==================== 私有方法 ==================== /** * 加密数据 * * @param plaintext 明文 * @returns 加密结果 * @private */ private encrypt(plaintext: string): { encryptedData: string; iv: string; authTag: string; } { const iv = crypto.randomBytes(this.IV_LENGTH); const cipher = crypto.createCipheriv( this.ENCRYPTION_ALGORITHM, this.encryptionKey, iv ); let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { encryptedData: encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex'), }; } /** * 解密数据 * * @param encryptedData 加密数据 * @param ivHex IV(十六进制) * @param authTagHex 认证标签(十六进制) * @returns 解密后的明文 * @private */ private decrypt(encryptedData: string, ivHex: string, authTagHex: string): string { const iv = Buffer.from(ivHex, 'hex'); const authTag = Buffer.from(authTagHex, 'hex'); const decipher = crypto.createDecipheriv( this.ENCRYPTION_ALGORITHM, this.encryptionKey, iv ); decipher.setAuthTag(authTag); let decrypted = decipher.update(encryptedData, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } /** * 验证API Key格式 * * @param apiKey API Key * @returns boolean 是否有效 * @private */ private isValidApiKeyFormat(apiKey: string): boolean { // Zulip API Key通常是32字符的字母数字字符串 // 这里放宽限制以支持不同格式 if (!apiKey || apiKey.length < 16 || apiKey.length > 128) { return false; } // 只允许字母、数字和一些特殊字符 return /^[a-zA-Z0-9_-]+$/.test(apiKey); } /** * 检查访问频率限制 * * @param userId 用户ID * @returns Promise<{allowed: boolean, currentCount: number}> * @private */ private async checkAccessRateLimit(userId: string): Promise<{ allowed: boolean; currentCount: number; }> { try { const rateLimitKey = `${this.ACCESS_COUNT_PREFIX}${userId}`; const currentCount = await this.redisService.get(rateLimitKey); const count = currentCount ? parseInt(currentCount, 10) : 0; if (count >= this.MAX_ACCESS_PER_MINUTE) { return { allowed: false, currentCount: count }; } // 增加计数 if (count === 0) { await this.redisService.setex(rateLimitKey, 60, '1'); } else { await this.redisService.incr(rateLimitKey); } return { allowed: true, currentCount: count + 1 }; } catch (error) { // 频率检查失败时默认允许 this.logger.warn('访问频率检查失败', { operation: 'checkAccessRateLimit', userId, error: (error as Error).message, }); return { allowed: true, currentCount: 0 }; } } }