Files
Termix/src/backend/utils/encryption-key-manager.ts
T
ZacharyZcR a501df8dde Fix database encryption write operations and initialization
This commit fixes critical issues with the database encryption system:

**Database Write Operations Fixed:**
- Modified credential creation/update operations to use EncryptedDBOperations
- Fixed SSH data and credential access to properly decrypt data
- All sensitive data writes now go through encryption layer

**Database Schema Migration:**
- Added missing columns (private_key, public_key, detected_key_type) to ssh_credentials table
- Fixed "no such column" SQLite errors during encrypted operations

**Application Startup Order:**
- Fixed DatabaseEncryption initialization timing issues
- Moved encryption-dependent modules to load after encryption initialization
- Prevents "DatabaseEncryption not initialized" errors

**Key Management Improvements:**
- Enhanced EncryptedDBOperations.insert() to return properly decrypted data with all fields
- Fixed TypeScript type issues with database insert operations
- Improved error handling for database encryption context

All credential operations now properly encrypt sensitive data including SSH keys,
passwords, and authentication tokens before writing to database.
2025-09-16 09:52:30 +08:00

323 lines
8.8 KiB
TypeScript

import crypto from 'crypto';
import { db } from '../database/db/index.js';
import { settings } from '../database/db/schema.js';
import { eq } from 'drizzle-orm';
import { databaseLogger } from './logger.js';
import { MasterKeyProtection } from './master-key-protection.js';
interface EncryptionKeyInfo {
hasKey: boolean;
keyId?: string;
createdAt?: string;
algorithm: string;
}
class EncryptionKeyManager {
private static instance: EncryptionKeyManager;
private currentKey: string | null = null;
private keyInfo: EncryptionKeyInfo | null = null;
private constructor() {}
static getInstance(): EncryptionKeyManager {
if (!this.instance) {
this.instance = new EncryptionKeyManager();
}
return this.instance;
}
private encodeKey(key: string): string {
return MasterKeyProtection.encryptMasterKey(key);
}
private decodeKey(encodedKey: string): string {
if (MasterKeyProtection.isProtectedKey(encodedKey)) {
return MasterKeyProtection.decryptMasterKey(encodedKey);
}
databaseLogger.warn('Found legacy base64-encoded key, migrating to KEK protection', {
operation: 'key_migration_legacy'
});
const buffer = Buffer.from(encodedKey, 'base64');
return buffer.toString('hex');
}
async initializeKey(): Promise<string> {
databaseLogger.info('Initializing encryption key system...', {
operation: 'key_init'
});
try {
let existingKey = await this.getStoredKey();
if (existingKey) {
databaseLogger.success('Found existing encryption key', {
operation: 'key_init',
hasKey: true
});
this.currentKey = existingKey;
return existingKey;
}
const environmentKey = process.env.DB_ENCRYPTION_KEY;
if (environmentKey && environmentKey !== 'default-key-change-me') {
if (!this.validateKeyStrength(environmentKey)) {
databaseLogger.error('Environment encryption key is too weak', undefined, {
operation: 'key_init',
source: 'environment',
keyLength: environmentKey.length
});
throw new Error('DB_ENCRYPTION_KEY is too weak. Must be at least 32 characters with good entropy.');
}
databaseLogger.info('Using encryption key from environment variable', {
operation: 'key_init',
source: 'environment'
});
await this.storeKey(environmentKey);
this.currentKey = environmentKey;
return environmentKey;
}
const newKey = await this.generateNewKey();
databaseLogger.warn('Generated new encryption key - PLEASE BACKUP THIS KEY', {
operation: 'key_init',
generated: true,
keyPreview: newKey.substring(0, 8) + '...'
});
return newKey;
} catch (error) {
databaseLogger.error('Failed to initialize encryption key', error, {
operation: 'key_init_failed'
});
throw error;
}
}
async generateNewKey(): Promise<string> {
const newKey = crypto.randomBytes(32).toString('hex');
const keyId = crypto.randomBytes(8).toString('hex');
await this.storeKey(newKey, keyId);
this.currentKey = newKey;
databaseLogger.success('Generated new encryption key', {
operation: 'key_generated',
keyId,
keyLength: newKey.length
});
return newKey;
}
private async storeKey(key: string, keyId?: string): Promise<void> {
const now = new Date().toISOString();
const id = keyId || crypto.randomBytes(8).toString('hex');
const keyData = {
key: this.encodeKey(key),
keyId: id,
createdAt: now,
algorithm: 'aes-256-gcm'
};
const encodedData = JSON.stringify(keyData);
try {
const existing = await db.select().from(settings).where(eq(settings.key, 'db_encryption_key'));
if (existing.length > 0) {
await db.update(settings)
.set({ value: encodedData })
.where(eq(settings.key, 'db_encryption_key'));
} else {
await db.insert(settings).values({
key: 'db_encryption_key',
value: encodedData
});
}
const existingCreated = await db.select().from(settings).where(eq(settings.key, 'encryption_key_created'));
if (existingCreated.length > 0) {
await db.update(settings)
.set({ value: now })
.where(eq(settings.key, 'encryption_key_created'));
} else {
await db.insert(settings).values({
key: 'encryption_key_created',
value: now
});
}
this.keyInfo = {
hasKey: true,
keyId: id,
createdAt: now,
algorithm: 'aes-256-gcm'
};
} catch (error) {
databaseLogger.error('Failed to store encryption key', error, {
operation: 'key_store_failed'
});
throw error;
}
}
private async getStoredKey(): Promise<string | null> {
try {
const result = await db.select().from(settings).where(eq(settings.key, 'db_encryption_key'));
if (result.length === 0) {
return null;
}
const encodedData = result[0].value;
let keyData;
try {
keyData = JSON.parse(encodedData);
} catch {
databaseLogger.warn('Found legacy base64-encoded key data, migrating', {
operation: 'key_data_migration_legacy'
});
keyData = JSON.parse(Buffer.from(encodedData, 'base64').toString());
}
this.keyInfo = {
hasKey: true,
keyId: keyData.keyId,
createdAt: keyData.createdAt,
algorithm: keyData.algorithm
};
const decodedKey = this.decodeKey(keyData.key);
if (!MasterKeyProtection.isProtectedKey(keyData.key)) {
databaseLogger.info('Auto-migrating legacy key to KEK protection', {
operation: 'key_auto_migration',
keyId: keyData.keyId
});
await this.storeKey(decodedKey, keyData.keyId);
}
return decodedKey;
} catch (error) {
databaseLogger.error('Failed to retrieve stored encryption key', error, {
operation: 'key_retrieve_failed'
});
return null;
}
}
getCurrentKey(): string | null {
return this.currentKey;
}
async getKeyInfo(): Promise<EncryptionKeyInfo> {
if (!this.keyInfo) {
const hasKey = await this.getStoredKey() !== null;
return {
hasKey,
algorithm: 'aes-256-gcm'
};
}
return this.keyInfo;
}
async regenerateKey(): Promise<string> {
databaseLogger.info('Regenerating encryption key', {
operation: 'key_regenerate'
});
const oldKeyInfo = await this.getKeyInfo();
const newKey = await this.generateNewKey();
databaseLogger.warn('Encryption key regenerated - ALL DATA MUST BE RE-ENCRYPTED', {
operation: 'key_regenerated',
oldKeyId: oldKeyInfo.keyId,
newKeyId: this.keyInfo?.keyId
});
return newKey;
}
private validateKeyStrength(key: string): boolean {
if (key.length < 32) return false;
const hasLower = /[a-z]/.test(key);
const hasUpper = /[A-Z]/.test(key);
const hasDigit = /\d/.test(key);
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(key);
const entropyTest = new Set(key).size / key.length;
const complexity = Number(hasLower) + Number(hasUpper) + Number(hasDigit) + Number(hasSpecial);
return complexity >= 3 && entropyTest > 0.4;
}
async validateKey(key?: string): Promise<boolean> {
const testKey = key || this.currentKey;
if (!testKey) return false;
try {
const testData = 'validation-test-' + Date.now();
const testBuffer = Buffer.from(testKey, 'hex');
if (testBuffer.length !== 32) {
return false;
}
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', testBuffer, iv) as any;
cipher.update(testData, 'utf8');
cipher.final();
cipher.getAuthTag();
return true;
} catch {
return false;
}
}
isInitialized(): boolean {
return this.currentKey !== null;
}
async getEncryptionStatus() {
const keyInfo = await this.getKeyInfo();
const isValid = await this.validateKey();
const kekProtected = await this.isKEKProtected();
return {
hasKey: keyInfo.hasKey,
keyValid: isValid,
keyId: keyInfo.keyId,
createdAt: keyInfo.createdAt,
algorithm: keyInfo.algorithm,
initialized: this.isInitialized(),
kekProtected,
kekValid: kekProtected ? MasterKeyProtection.validateProtection() : false
};
}
private async isKEKProtected(): Promise<boolean> {
try {
const result = await db.select().from(settings).where(eq(settings.key, 'db_encryption_key'));
if (result.length === 0) return false;
const keyData = JSON.parse(result[0].value);
return MasterKeyProtection.isProtectedKey(keyData.key);
} catch {
return false;
}
}
}
export { EncryptionKeyManager };
export type { EncryptionKeyInfo };