From 2e8033a8aeeae0ce4e446fd44e6538002d830c7f Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Sun, 21 Dec 2025 14:03:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AE=89=E5=85=A8):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20API=20Key=20=E5=8A=A0=E5=AF=86=E5=AD=98=E5=82=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 AES-256-GCM 算法加密 API Key - 支持加密/解密/检测功能 - 兼容旧的明文存储数据 --- src/lib/crypto.ts | 88 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/lib/crypto.ts diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts new file mode 100644 index 0000000..c2c5ebd --- /dev/null +++ b/src/lib/crypto.ts @@ -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:encryptedData,Base64编码) + */ +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; +}