fix: harden cloud storage security
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const ENCRYPTION_PREFIX = 'enc:v1:';
|
||||
|
||||
/**
|
||||
* 从环境变量获取加密密钥
|
||||
@@ -111,8 +112,8 @@ function encryptSecret(plaintext) {
|
||||
authTag
|
||||
]);
|
||||
|
||||
// 返回 Base64 编码的结果
|
||||
return combined.toString('base64');
|
||||
// 返回带版本前缀的 Base64 编码结果,避免旧明文被误判为密文。
|
||||
return ENCRYPTION_PREFIX + combined.toString('base64');
|
||||
} catch (error) {
|
||||
console.error('[加密] 加密失败:', error);
|
||||
throw new Error('数据加密失败: ' + error.message);
|
||||
@@ -134,24 +135,26 @@ function encryptSecret(plaintext) {
|
||||
* // 输出: 'my-secret-key'
|
||||
*/
|
||||
function decryptSecret(ciphertext) {
|
||||
// 如果是 null 或 undefined,直接返回
|
||||
if (!ciphertext) {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
const rawValue = String(ciphertext);
|
||||
const hasPrefix = rawValue.startsWith(ENCRYPTION_PREFIX);
|
||||
const encodedValue = hasPrefix ? rawValue.slice(ENCRYPTION_PREFIX.length) : rawValue;
|
||||
|
||||
if (!hasPrefix && !isEncrypted(encodedValue)) {
|
||||
if (/[+/=]/.test(rawValue)) {
|
||||
throw new Error('数据解密失败: 疑似密文格式无效');
|
||||
}
|
||||
console.warn('[加密] 检测到未加密的旧密钥,建议重新保存以完成加密');
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
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');
|
||||
const combined = Buffer.from(encodedValue, 'base64');
|
||||
|
||||
// 提取各部分
|
||||
// IV: 前 12 字节
|
||||
@@ -175,13 +178,9 @@ function decryptSecret(ciphertext) {
|
||||
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
// 如果解密失败,可能是旧数据(明文),直接返回
|
||||
console.error('[加密] 解密失败,可能是未加密的旧数据:', error.message);
|
||||
|
||||
// 在开发环境抛出错误,生产环境尝试返回原值
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.error('[加密] 生产环境中解密失败,返回原值(可能导致 OSS 连接失败)');
|
||||
return ciphertext;
|
||||
if (!hasPrefix && !/[+/=]/.test(rawValue)) {
|
||||
console.warn('[加密] 旧格式数据解密失败,按未加密旧密钥处理:', error.message);
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
throw new Error('数据解密失败: ' + error.message);
|
||||
@@ -240,6 +239,10 @@ function isEncrypted(data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const encodedValue = data.startsWith(ENCRYPTION_PREFIX)
|
||||
? data.slice(ENCRYPTION_PREFIX.length)
|
||||
: data;
|
||||
|
||||
// 加密后的数据特征:
|
||||
// 1. 是有效的 Base64
|
||||
// 2. 长度至少为 (12 + 16) * 4/3 = 38 字符(IV + authTag 的 Base64)
|
||||
@@ -247,7 +250,11 @@ function isEncrypted(data) {
|
||||
|
||||
try {
|
||||
// 尝试解码 Base64
|
||||
const buffer = Buffer.from(data, 'base64');
|
||||
if (!/^[A-Za-z0-9+/=]+$/.test(encodedValue) || encodedValue.length % 4 !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(encodedValue, 'base64');
|
||||
|
||||
// 检查长度(至少包含 IV + authTag)
|
||||
// AES-GCM: 12字节IV + 至少1字节密文 + 16字节authTag = 29字节
|
||||
|
||||
Reference in New Issue
Block a user