dev-1.7.0 #294

Merged
ZacharyZcR merged 73 commits from main into dev-1.7.0 2025-09-25 04:56:32 +00:00
4 changed files with 177 additions and 90 deletions
Showing only changes of commit 59e4e2beae - Show all commits

View File

@@ -15,7 +15,7 @@ class DatabaseEncryption {
static async initialize(config: Partial<EncryptionContext> = {}) {
const keyManager = EncryptionKeyManager.getInstance();
const masterPassword =
config.masterPassword || (await keyManager.initializeKey());
config.masterPassword || (await keyManager.initializeKey(config.masterPassword));
this.context = {
masterPassword,

View File

@@ -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<string> {
async initializeKey(userPassword?: string): Promise<string> {
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,
};
}

View File

@@ -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<void> {
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<boolean> {

View File

@@ -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;
}