- Standardize author attribution across 27 files in the Zulip integration module - Maintain consistent code documentation and authorship tracking
800 lines
22 KiB
TypeScript
800 lines
22 KiB
TypeScript
/**
|
||
* 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<string, any>;
|
||
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<StoreApiKeyResult> 存储结果
|
||
*/
|
||
async storeApiKey(
|
||
userId: string,
|
||
apiKey: string,
|
||
metadata?: { ipAddress?: string; userAgent?: string }
|
||
): Promise<StoreApiKeyResult> {
|
||
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<GetApiKeyResult> 获取结果
|
||
*/
|
||
async getApiKey(
|
||
userId: string,
|
||
metadata?: { ipAddress?: string; userAgent?: string }
|
||
): Promise<GetApiKeyResult> {
|
||
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<StoreApiKeyResult> 更新结果
|
||
*/
|
||
async updateApiKey(
|
||
userId: string,
|
||
newApiKey: string,
|
||
metadata?: { ipAddress?: string; userAgent?: string }
|
||
): Promise<StoreApiKeyResult> {
|
||
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<boolean> 是否删除成功
|
||
*/
|
||
async deleteApiKey(
|
||
userId: string,
|
||
metadata?: { ipAddress?: string; userAgent?: string }
|
||
): Promise<boolean> {
|
||
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<boolean> 是否存在
|
||
*/
|
||
async hasApiKey(userId: string): Promise<boolean> {
|
||
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<void>
|
||
*/
|
||
async logSecurityEvent(event: SecurityEvent): Promise<void> {
|
||
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<void>
|
||
*/
|
||
async logSuspiciousAccess(
|
||
userId: string,
|
||
reason: string,
|
||
details: Record<string, any>,
|
||
metadata?: { ipAddress?: string; userAgent?: string }
|
||
): Promise<void> {
|
||
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<SecurityEvent[]> 安全事件列表
|
||
*/
|
||
async getSecurityEventHistory(userId: string, limit: number = 100): Promise<SecurityEvent[]> {
|
||
// 注意:这是一个简化实现,实际应该使用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 };
|
||
}
|
||
}
|
||
}
|
||
|
||
|