diff --git a/src/backend/utils/database-encryption.ts b/src/backend/utils/database-encryption.ts index 6662ceaa..b39c2047 100644 --- a/src/backend/utils/database-encryption.ts +++ b/src/backend/utils/database-encryption.ts @@ -15,7 +15,7 @@ class DatabaseEncryption { static async initialize(config: Partial = {}) { const keyManager = EncryptionKeyManager.getInstance(); const masterPassword = - config.masterPassword || (await keyManager.initializeKey()); + config.masterPassword || (await keyManager.initializeKey(config.masterPassword)); this.context = { masterPassword, diff --git a/src/backend/utils/encryption-key-manager.ts b/src/backend/utils/encryption-key-manager.ts index c3d802f7..bc307c85 100644 --- a/src/backend/utils/encryption-key-manager.ts +++ b/src/backend/utils/encryption-key-manager.ts @@ -17,6 +17,7 @@ class EncryptionKeyManager { private currentKey: string | null = null; private keyInfo: EncryptionKeyInfo | null = null; private jwtSecret: string | null = null; + private userPassword: string | null = null; private constructor() {} @@ -28,16 +29,33 @@ class EncryptionKeyManager { } private encodeKey(key: string): string { - return MasterKeyProtection.encryptMasterKey(key); + if (!this.userPassword) { + throw new Error("User password not set - call initializeKey() first"); + } + return MasterKeyProtection.encryptMasterKey(key, this.userPassword); } private decodeKey(encodedKey: string): string { + if (!this.userPassword) { + throw new Error("User password not set - call initializeKey() first"); + } + if (MasterKeyProtection.isProtectedKey(encodedKey)) { - return MasterKeyProtection.decryptMasterKey(encodedKey); + try { + return MasterKeyProtection.decryptMasterKey(encodedKey, this.userPassword); + } catch (error) { + // If decryption fails, it might be a v1 (hardware-based) key + databaseLogger.error("Failed to decrypt protected key", error, { + operation: "key_decryption_failed", + }); + throw new Error( + "Failed to decrypt encryption key. If this is a legacy installation, please regenerate encryption keys." + ); + } } databaseLogger.warn( - "Found legacy base64-encoded key, migrating to KEK protection", + "Found legacy base64-encoded key, migrating to password protection", { operation: "key_migration_legacy", }, @@ -46,8 +64,29 @@ class EncryptionKeyManager { return buffer.toString("hex"); } - async initializeKey(): Promise { + async initializeKey(userPassword?: string): Promise { try { + // Generate a default password if none provided (for backward compatibility) + if (!userPassword) { + const environmentKey = process.env.DB_ENCRYPTION_KEY; + if (environmentKey && environmentKey !== "default-key-change-me") { + userPassword = environmentKey; + databaseLogger.info("Using encryption key from environment variable as user password", { + operation: "key_init", + source: "environment", + }); + } else { + // Generate a random password for new installations + userPassword = crypto.randomBytes(32).toString("hex"); + databaseLogger.warn("Generated random user password for encryption", { + operation: "key_init", + generated: true, + }); + } + } + + this.userPassword = userPassword; + let existingKey = await this.getStoredKey(); if (existingKey) { @@ -59,36 +98,9 @@ class EncryptionKeyManager { 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", + "Generated new encryption key - PLEASE BACKUP YOUR PASSWORD", { operation: "key_init", generated: true, @@ -330,7 +342,7 @@ class EncryptionKeyManager { algorithm: keyInfo.algorithm, initialized: this.isInitialized(), kekProtected, - kekValid: kekProtected ? MasterKeyProtection.validateProtection() : false, + kekValid: kekProtected && this.userPassword ? MasterKeyProtection.validateProtection(this.userPassword) : false, }; } diff --git a/src/backend/utils/encryption-test.ts b/src/backend/utils/encryption-test.ts index bdfbec94..b16211dd 100644 --- a/src/backend/utils/encryption-test.ts +++ b/src/backend/utils/encryption-test.ts @@ -35,6 +35,7 @@ class EncryptionTest { { name: "Error Handling", test: () => this.testErrorHandling() }, { name: "Performance Test", test: () => this.testPerformance() }, { name: "JWT Secret Management", test: () => this.testJWTSecretManagement() }, + { name: "Password-Based KEK Security", test: () => this.testPasswordBasedKEK() }, ]; let passedTests = 0; @@ -301,6 +302,67 @@ class EncryptionTest { } console.log(" ✅ JWT secret generation, caching, and regeneration working correctly"); + console.log(" ✅ All secrets now use password-derived KEK instead of hardware fingerprint"); + } + + private async testPasswordBasedKEK(): Promise { + const { MasterKeyProtection } = await import("./master-key-protection.js"); + + const testPassword = "test-secure-password-12345"; + const testKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + + // Test encryption with password-based KEK + const encrypted = MasterKeyProtection.encryptMasterKey(testKey, testPassword); + + // Verify the encrypted data format + const protectedData = JSON.parse(encrypted); + if (protectedData.version !== "v2") { + throw new Error(`Expected version v2 (password-based), got ${protectedData.version}`); + } + + if (!protectedData.salt) { + throw new Error("Protected data should contain a salt field"); + } + + if (protectedData.fingerprint) { + throw new Error("Protected data should not contain hardware fingerprint"); + } + + // Test decryption with correct password + const decrypted = MasterKeyProtection.decryptMasterKey(encrypted, testPassword); + if (decrypted !== testKey) { + throw new Error("Decryption with correct password failed"); + } + + // Test that wrong password fails + try { + MasterKeyProtection.decryptMasterKey(encrypted, "wrong-password"); + throw new Error("Decryption should fail with wrong password"); + } catch (error) { + if (!(error as Error).message.includes("decryption failed")) { + throw new Error("Should fail with proper decryption error"); + } + } + + // Test that different passwords produce different encrypted data + const encrypted2 = MasterKeyProtection.encryptMasterKey(testKey, "different-password"); + if (encrypted === encrypted2) { + throw new Error("Different passwords should produce different encrypted data"); + } + + // Test protection info + const info = MasterKeyProtection.getProtectionInfo(encrypted); + if (!info?.isPasswordBased) { + throw new Error("Protection info should indicate password-based encryption"); + } + + if (info.saltLength !== 32) { + throw new Error(`Expected salt length 32, got ${info.saltLength}`); + } + + console.log(" ✅ Password-based KEK working correctly (no hardware fingerprint dependency)"); + console.log(" ✅ Different passwords produce different encryption (true randomness)"); + console.log(" ✅ Salt length: 32 bytes, Iterations: 100,000 (strong security)"); } static async validateProduction(): Promise { diff --git a/src/backend/utils/master-key-protection.ts b/src/backend/utils/master-key-protection.ts index 216c9a1e..c6f9642d 100644 --- a/src/backend/utils/master-key-protection.ts +++ b/src/backend/utils/master-key-protection.ts @@ -1,39 +1,25 @@ import crypto from "crypto"; import { databaseLogger } from "./logger.js"; -import { HardwareFingerprint } from "./hardware-fingerprint.js"; interface ProtectedKeyData { data: string; iv: string; tag: string; version: string; - fingerprint: string; + salt: string; } class MasterKeyProtection { - private static readonly VERSION = "v1"; - private static readonly KEK_SALT = "termix-kek-salt-v1"; - private static readonly KEK_ITERATIONS = 50000; + private static readonly VERSION = "v2"; + private static readonly KEK_ITERATIONS = 100000; - private static generateDeviceFingerprint(): string { - try { - const fingerprint = HardwareFingerprint.generate(); - - return fingerprint; - } catch (error) { - databaseLogger.error("Failed to generate hardware fingerprint", error, { - operation: "hardware_fingerprint_generation_failed", - }); - throw new Error("Hardware fingerprint generation failed"); + private static deriveKEK(userPassword: string, salt: Buffer): Buffer { + if (!userPassword) { + throw new Error("User password is required for KEK derivation"); } - } - - private static deriveKEK(): Buffer { - const fingerprint = this.generateDeviceFingerprint(); - const salt = Buffer.from(this.KEK_SALT); const kek = crypto.pbkdf2Sync( - fingerprint, + userPassword, salt, this.KEK_ITERATIONS, 32, @@ -43,13 +29,17 @@ class MasterKeyProtection { return kek; } - static encryptMasterKey(masterKey: string): string { + static encryptMasterKey(masterKey: string, userPassword: string): string { if (!masterKey) { throw new Error("Master key cannot be empty"); } + if (!userPassword) { + throw new Error("User password is required for encryption"); + } try { - const kek = this.deriveKEK(); + const salt = crypto.randomBytes(32); + const kek = this.deriveKEK(userPassword, salt); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv("aes-256-gcm", kek, iv) as any; @@ -62,15 +52,16 @@ class MasterKeyProtection { iv: iv.toString("hex"), tag: tag.toString("hex"), version: this.VERSION, - fingerprint: this.generateDeviceFingerprint().substring(0, 16), + salt: salt.toString("hex"), }; const result = JSON.stringify(protectedData); - databaseLogger.info("Master key encrypted with hardware KEK", { + databaseLogger.info("Master key encrypted with password-derived KEK", { operation: "master_key_encryption", version: this.VERSION, - fingerprintPrefix: protectedData.fingerprint, + saltLength: salt.length, + iterations: this.KEK_ITERATIONS, }); return result; @@ -82,36 +73,32 @@ class MasterKeyProtection { } } - static decryptMasterKey(encryptedKey: string): string { + static decryptMasterKey(encryptedKey: string, userPassword: string): string { if (!encryptedKey) { throw new Error("Encrypted key cannot be empty"); } + if (!userPassword) { + throw new Error("User password is required for decryption"); + } try { const protectedData: ProtectedKeyData = JSON.parse(encryptedKey); + // Support both v1 (hardware fingerprint) and v2 (password-based) for migration + if (protectedData.version === "v1") { + throw new Error( + "Legacy hardware-based encryption detected. Please regenerate encryption keys for improved security.", + ); + } + if (protectedData.version !== this.VERSION) { throw new Error( `Unsupported protection version: ${protectedData.version}`, ); } - const currentFingerprint = this.generateDeviceFingerprint().substring( - 0, - 16, - ); - if (protectedData.fingerprint !== currentFingerprint) { - databaseLogger.warn("Hardware fingerprint mismatch detected", { - operation: "master_key_decryption", - expected: protectedData.fingerprint, - current: currentFingerprint, - }); - throw new Error( - "Hardware fingerprint mismatch - key was encrypted on different hardware", - ); - } - - const kek = this.deriveKEK(); + const salt = Buffer.from(protectedData.salt, "hex"); + const kek = this.deriveKEK(userPassword, salt); const decipher = crypto.createDecipheriv( "aes-256-gcm", kek, @@ -122,6 +109,12 @@ class MasterKeyProtection { let decrypted = decipher.update(protectedData.data, "hex", "hex"); decrypted += decipher.final("hex"); + databaseLogger.info("Master key decrypted successfully", { + operation: "master_key_decryption", + version: protectedData.version, + saltLength: salt.length, + }); + return decrypted; } catch (error) { databaseLogger.error("Failed to decrypt master key", error, { @@ -136,29 +129,42 @@ class MasterKeyProtection { static isProtectedKey(data: string): boolean { try { const parsed = JSON.parse(data); - return !!( + + // Support both v1 (fingerprint) and v2 (salt) formats + const hasV1Format = !!( parsed.data && parsed.iv && parsed.tag && parsed.version && parsed.fingerprint ); + + const hasV2Format = !!( + parsed.data && + parsed.iv && + parsed.tag && + parsed.version && + parsed.salt + ); + + return hasV1Format || hasV2Format; } catch { return false; } } - static validateProtection(): boolean { + static validateProtection(userPassword: string): boolean { try { const testKey = crypto.randomBytes(32).toString("hex"); - const encrypted = this.encryptMasterKey(testKey); - const decrypted = this.decryptMasterKey(encrypted); + const encrypted = this.encryptMasterKey(testKey, userPassword); + const decrypted = this.decryptMasterKey(encrypted, userPassword); const isValid = decrypted === testKey; databaseLogger.info("Master key protection validation completed", { operation: "protection_validation", result: isValid ? "passed" : "failed", + version: this.VERSION, }); return isValid; @@ -172,8 +178,9 @@ class MasterKeyProtection { static getProtectionInfo(encryptedKey: string): { version: string; - fingerprint: string; - isCurrentDevice: boolean; + isPasswordBased: boolean; + saltLength?: number; + iterations?: number; } | null { try { if (!this.isProtectedKey(encryptedKey)) { @@ -181,16 +188,22 @@ class MasterKeyProtection { } const protectedData: ProtectedKeyData = JSON.parse(encryptedKey); - const currentFingerprint = this.generateDeviceFingerprint().substring( - 0, - 16, - ); - return { + const info = { version: protectedData.version, - fingerprint: protectedData.fingerprint, - isCurrentDevice: protectedData.fingerprint === currentFingerprint, + isPasswordBased: protectedData.version === "v2", }; + + // Add additional info for v2 format + if (protectedData.version === "v2" && protectedData.salt) { + return { + ...info, + saltLength: Buffer.from(protectedData.salt, "hex").length, + iterations: this.KEK_ITERATIONS, + }; + } + + return info; } catch { return null; }