v0.3.6: 恢复丢失的11个模块 + 接线基础设施
恢复内容: - quark驱动拆解为7个子模块 (quark-api/auth/share/storage/cleanup/rename/ad-cleanup) - 工具模块: utils/crypto, utils/logger, utils/proxy-agent - 配置校验: config/startup-validator - 接线: main.ts(checkStartup), credential.service.ts(加密Cookie), admin.routes.ts(代理测试) - quark.driver.ts 从1533行巨兽瘦身到130行壳子
This commit is contained in:
98
source_clean/src/utils/crypto.ts
Normal file
98
source_clean/src/utils/crypto.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* AES-256-GCM encryption/decryption for protecting cloud drive cookies stored in DB.
|
||||
*
|
||||
* Encryption key is derived from COOKIE_ENCRYPTION_KEY env var via SHA-256.
|
||||
* If unset, uses a built-in default key (stable across container restarts).
|
||||
* Production MUST set COOKIE_ENCRYPTION_KEY!
|
||||
*/
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const IV_LENGTH = 12; // 96-bit nonce for GCM
|
||||
const TAG_LENGTH = 16; // 128-bit auth tag
|
||||
const KEY_LENGTH = 32; // 256-bit key
|
||||
|
||||
let ENCRYPTION_KEY: Buffer | null = null;
|
||||
|
||||
function getKey(): Buffer {
|
||||
if (ENCRYPTION_KEY) return ENCRYPTION_KEY;
|
||||
|
||||
const envKey = process.env.COOKIE_ENCRYPTION_KEY;
|
||||
if (envKey && envKey.length >= 32) {
|
||||
ENCRYPTION_KEY = crypto.createHash('sha256').update(envKey).digest();
|
||||
console.log('[Crypto] Cookie encryption enabled (key from COOKIE_ENCRYPTION_KEY)');
|
||||
} else if (envKey) {
|
||||
ENCRYPTION_KEY = crypto.createHash('sha256').update(envKey).digest();
|
||||
console.log('[Crypto] Cookie encryption enabled (key from COOKIE_ENCRYPTION_KEY, SHA-256 derived)');
|
||||
} else {
|
||||
// Default stable key (not ephemeral) — data survives container restart
|
||||
ENCRYPTION_KEY = crypto.createHash('sha256').update('cloudsearch-cookie-key-v1').digest();
|
||||
console.log('[Crypto] Cookie encryption enabled (built-in default key — set COOKIE_ENCRYPTION_KEY in .env for extra security)');
|
||||
}
|
||||
return ENCRYPTION_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt plaintext. Returns base64-encoded ciphertext (includes IV + auth tag).
|
||||
*/
|
||||
export function encrypt(plaintext: string): string {
|
||||
if (!plaintext) return '';
|
||||
const key = getKey();
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(plaintext, 'utf8'),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
// Format: iv (12) + tag (16) + ciphertext
|
||||
const combined = Buffer.concat([iv, tag, encrypted]);
|
||||
return combined.toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt base64-encoded ciphertext. Returns original plaintext.
|
||||
* Returns empty string if decryption fails (corrupted data or wrong key).
|
||||
*/
|
||||
export function decrypt(encoded: string): string {
|
||||
if (!encoded) return '';
|
||||
try {
|
||||
const key = getKey();
|
||||
const combined = Buffer.from(encoded, 'base64');
|
||||
if (combined.length < IV_LENGTH + TAG_LENGTH + 1) {
|
||||
console.warn('[Crypto] Ciphertext too short, returning as-is (possibly unencrypted legacy data)');
|
||||
return encoded;
|
||||
}
|
||||
const iv = combined.subarray(0, IV_LENGTH);
|
||||
const tag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
||||
const ciphertext = combined.subarray(IV_LENGTH + TAG_LENGTH);
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(ciphertext),
|
||||
decipher.final(),
|
||||
]);
|
||||
return decrypted.toString('utf8');
|
||||
} catch (err: any) {
|
||||
if (err.message?.includes('unsupported state') || err.message?.includes('authentication')) {
|
||||
console.warn('[Crypto] Decryption failed (possibly legacy plaintext), returning as-is');
|
||||
return encoded;
|
||||
}
|
||||
console.error('[Crypto] Decryption error:', err.message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string appears to be encrypted (base64 with IV+tag prefix).
|
||||
* Used for migration: re-encrypt legacy plaintext cookies.
|
||||
*/
|
||||
export function isEncrypted(value: string): boolean {
|
||||
if (!value) return false;
|
||||
try {
|
||||
const combined = Buffer.from(value, 'base64');
|
||||
return combined.length > IV_LENGTH + TAG_LENGTH;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user