feat: 实现Vue驱动的云存储系统初始功能
- 后端: Node.js + Express + SQLite架构 - 前端: Vue 3 + Axios实现 - 功能: 用户认证、文件上传/下载、分享链接、密码重置 - 安全: 密码加密、分享链接过期机制、缓存一致性 - 部署: Docker + Nginx容器化配置 - 测试: 完整的边界测试、并发测试和状态一致性测试
This commit is contained in:
271
backend/utils/encryption.js
Normal file
271
backend/utils/encryption.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* 加密工具模块
|
||||
*
|
||||
* 功能:
|
||||
* - 使用 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
|
||||
};
|
||||
Reference in New Issue
Block a user