dev-1.7.0 #294
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user