/** * 加密工具模块 * * 功能: * - 使用 AES-256-GCM 加密敏感数据(OSS Access Key Secret) * - 提供加密和解密函数 * - 自动处理初始化向量(IV)和认证标签 * * 安全特性: * - AES-256-GCM 提供认证加密(AEAD) * - 每次加密使用随机 IV,防止模式泄露 * - 使用认证标签验证数据完整性 * - 密钥从环境变量读取,不存在硬编码 * * @module utils/encryption */ const crypto = require('crypto'); /** * 从环境变量获取加密密钥 * * 要求: * - 必须是 32 字节的十六进制字符串(64个字符) * - 如果未设置或格式错误,启动时抛出错误 * * @returns {Buffer} 32字节的加密密钥 * @throws {Error} 如果密钥未配置或格式错误 */ function getEncryptionKey() { const keyHex = process.env.ENCRYPTION_KEY; if (!keyHex) { throw new Error(` ╔═══════════════════════════════════════════════════════════════╗ ║ ⚠️ 安全错误 ⚠️ ║ ╠═══════════════════════════════════════════════════════════════╣ ║ ENCRYPTION_KEY 未配置! ║ ║ ║ ║ 此密钥用于加密 OSS Access Key Secret 等敏感信息 ║ ║ ║ ║ 生成方法: ║ ║ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ║ ║ ║ 在 backend/.env 文件中添加: ║ ║ ENCRYPTION_KEY=你生成的64位十六进制密钥 ║ ╚═══════════════════════════════════════════════════════════════╝ `); } // 验证密钥格式(必须是64个十六进制字符) if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) { throw new Error(` ╔═══════════════════════════════════════════════════════════════╗ ║ ⚠️ 配置错误 ⚠️ ║ ╠═══════════════════════════════════════════════════════════════╣ ║ ENCRYPTION_KEY 格式错误! ║ ║ ║ ║ 要求: 64位十六进制字符串(32字节) ║ ║ 当前长度: ${keyHex.length} 字符 ║ ║ ║ ║ 正确的生成方法: ║ ║ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ╚═══════════════════════════════════════════════════════════════╝ `); } return Buffer.from(keyHex, 'hex'); } /** * 加密明文字符串 * * 使用 AES-256-GCM 算法加密数据,输出格式: * - Base64(IV + ciphertext + authTag) * - IV: 12字节(随机) * - ciphertext: 加密后的数据 * - authTag: 16字节(认证标签) * * @param {string} plaintext - 要加密的明文字符串 * @returns {string} Base64编码的加密结果(包含 IV 和 authTag) * @throws {Error} 如果加密失败 * * @example * const encrypted = encryptSecret('my-secret-key'); * // 输出: 'base64-encoded-string-with-iv-and-tag' */ function encryptSecret(plaintext) { try { // 获取加密密钥 const key = getEncryptionKey(); // 生成随机初始化向量(IV) // GCM 模式推荐 12 字节 IV const iv = crypto.randomBytes(12); // 创建加密器 const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); // 加密数据 let encrypted = cipher.update(plaintext, 'utf8', 'binary'); encrypted += cipher.final('binary'); // 获取认证标签(用于验证数据完整性) const authTag = cipher.getAuthTag(); // 组合:IV + encrypted + authTag const combined = Buffer.concat([ iv, Buffer.from(encrypted, 'binary'), authTag ]); // 返回 Base64 编码的结果 return combined.toString('base64'); } catch (error) { console.error('[加密] 加密失败:', error); throw new Error('数据加密失败: ' + error.message); } } /** * 解密密文字符串 * * 解密由 encryptSecret() 加密的数据 * 自动验证认证标签,确保数据完整性 * * @param {string} ciphertext - Base64编码的密文(由 encryptSecret 生成) * @returns {string} 解密后的明文字符串 * @throws {Error} 如果解密失败或认证标签验证失败 * * @example * const decrypted = decryptSecret(encrypted); * // 输出: 'my-secret-key' */ function decryptSecret(ciphertext) { try { // 如果是 null 或 undefined,直接返回 if (!ciphertext) { return ciphertext; } // 检查是否为加密格式(Base64) // 如果不是 Base64,可能是旧数据(明文),直接返回 if (!/^[A-Za-z0-9+/=]+$/.test(ciphertext)) { console.warn('[加密] 检测到未加密的密钥,建议重新加密'); return ciphertext; } // 获取加密密钥 const key = getEncryptionKey(); // 解析 Base64 const combined = Buffer.from(ciphertext, 'base64'); // 提取各部分 // IV: 前 12 字节 const iv = combined.slice(0, 12); // authTag: 最后 16 字节 const authTag = combined.slice(-16); // ciphertext: 中间部分 const encrypted = combined.slice(12, -16); // 创建解密器 const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); // 设置认证标签 decipher.setAuthTag(authTag); // 解密数据 let decrypted = decipher.update(encrypted, 'binary', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { // 如果解密失败,可能是旧数据(明文),直接返回 console.error('[加密] 解密失败,可能是未加密的旧数据:', error.message); // 在开发环境抛出错误,生产环境尝试返回原值 if (process.env.NODE_ENV === 'production') { console.error('[加密] 生产环境中解密失败,返回原值(可能导致 OSS 连接失败)'); return ciphertext; } throw new Error('数据解密失败: ' + error.message); } } /** * 验证加密系统是否正常工作 * * 在应用启动时调用,确保: * 1. ENCRYPTION_KEY 已配置 * 2. 加密/解密功能正常 * * @returns {boolean} true 如果验证通过 * @throws {Error} 如果验证失败 */ function validateEncryption() { try { const testData = 'test-secret-123'; // 测试加密 const encrypted = encryptSecret(testData); // 验证加密结果不为空且不等于原文 if (!encrypted || encrypted === testData) { throw new Error('加密结果异常'); } // 测试解密 const decrypted = decryptSecret(encrypted); // 验证解密结果等于原文 if (decrypted !== testData) { throw new Error('解密结果不匹配'); } console.log('[安全] ✓ 加密系统验证通过'); return true; } catch (error) { console.error('[安全] ✗ 加密系统验证失败:', error.message); throw error; } } /** * 检查字符串是否已加密 * * 通过格式判断是否为加密数据 * 注意:这不是加密学验证,仅用于提示 * * @param {string} data - 要检查的数据 * @returns {boolean} true 如果看起来像是加密数据 */ function isEncrypted(data) { if (!data || typeof data !== 'string') { return false; } // 加密后的数据特征: // 1. 是有效的 Base64 // 2. 长度至少为 (12 + 16) * 4/3 = 38 字符(IV + authTag 的 Base64) // 3. 通常会比原文长 try { // 尝试解码 Base64 const buffer = Buffer.from(data, 'base64'); // 检查长度(至少包含 IV + authTag) // AES-GCM: 12字节IV + 至少1字节密文 + 16字节authTag = 29字节 // Base64编码后: 29 * 4/3 ≈ 39 字符 if (buffer.length < 29) { return false; } return true; } catch (error) { return false; } } module.exports = { encryptSecret, decryptSecret, validateEncryption, isEncrypted, getEncryptionKey };