feat(安全): 添加 API Key 加密存储功能

- 使用 AES-256-GCM 算法加密 API Key
- 支持加密/解密/检测功能
- 兼容旧的明文存储数据
This commit is contained in:
gaoziman 2025-12-21 14:03:46 +08:00
parent 269fc798aa
commit 2e8033a8ae

88
src/lib/crypto.ts Normal file
View File

@ -0,0 +1,88 @@
import crypto from 'crypto';
// 加密密钥32字节 = 256位用于 AES-256
// 生产环境应使用环境变量
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'lioncode-encryption-key-2024-sec';
// 确保密钥长度为32字节
const getKey = (): Buffer => {
const key = ENCRYPTION_KEY;
// 使用 SHA-256 哈希确保密钥长度为32字节
return crypto.createHash('sha256').update(key).digest();
};
// IV 长度AES-256-GCM 推荐使用12字节
const IV_LENGTH = 12;
// Auth Tag 长度
const AUTH_TAG_LENGTH = 16;
/**
* API Key
* 使 AES-256-GCM
* @param plainText API Key
* @returns iv:authTag:encryptedDataBase64编码
*/
export function encryptApiKey(plainText: string): string {
if (!plainText) return '';
const key = getKey();
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plainText, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();
// 组合 IV + AuthTag + 加密数据,用冒号分隔
return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
}
/**
* API Key
* @param encryptedText
* @returns API Key
*/
export function decryptApiKey(encryptedText: string): string {
if (!encryptedText) return '';
try {
const parts = encryptedText.split(':');
if (parts.length !== 3) {
// 兼容旧的明文存储(迁移期间)
console.warn('检测到未加密的 API Key返回原值');
return encryptedText;
}
const [ivBase64, authTagBase64, encrypted] = parts;
const key = getKey();
const iv = Buffer.from(ivBase64, 'base64');
const authTag = Buffer.from(authTagBase64, 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('API Key 解密失败:', error);
// 如果解密失败,可能是旧的明文数据,返回原值
return encryptedText;
}
}
/**
*
* @param text
* @returns
*/
export function isEncrypted(text: string): boolean {
if (!text) return false;
const parts = text.split(':');
return parts.length === 3;
}