dev-1.7.0 #294
@@ -15,7 +15,7 @@ class DatabaseEncryption {
|
|||||||
static async initialize(config: Partial<EncryptionContext> = {}) {
|
static async initialize(config: Partial<EncryptionContext> = {}) {
|
||||||
const keyManager = EncryptionKeyManager.getInstance();
|
const keyManager = EncryptionKeyManager.getInstance();
|
||||||
const masterPassword =
|
const masterPassword =
|
||||||
config.masterPassword || (await keyManager.initializeKey());
|
config.masterPassword || (await keyManager.initializeKey(config.masterPassword));
|
||||||
|
|
||||||
this.context = {
|
this.context = {
|
||||||
masterPassword,
|
masterPassword,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class EncryptionKeyManager {
|
|||||||
private currentKey: string | null = null;
|
private currentKey: string | null = null;
|
||||||
private keyInfo: EncryptionKeyInfo | null = null;
|
private keyInfo: EncryptionKeyInfo | null = null;
|
||||||
private jwtSecret: string | null = null;
|
private jwtSecret: string | null = null;
|
||||||
|
private userPassword: string | null = null;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@@ -28,16 +29,33 @@ class EncryptionKeyManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private encodeKey(key: string): string {
|
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 {
|
private decodeKey(encodedKey: string): string {
|
||||||
|
if (!this.userPassword) {
|
||||||
|
throw new Error("User password not set - call initializeKey() first");
|
||||||
|
}
|
||||||
|
|
||||||
if (MasterKeyProtection.isProtectedKey(encodedKey)) {
|
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(
|
databaseLogger.warn(
|
||||||
"Found legacy base64-encoded key, migrating to KEK protection",
|
"Found legacy base64-encoded key, migrating to password protection",
|
||||||
{
|
{
|
||||||
operation: "key_migration_legacy",
|
operation: "key_migration_legacy",
|
||||||
},
|
},
|
||||||
@@ -46,8 +64,29 @@ class EncryptionKeyManager {
|
|||||||
return buffer.toString("hex");
|
return buffer.toString("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeKey(): Promise<string> {
|
async initializeKey(userPassword?: string): Promise<string> {
|
||||||
try {
|
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();
|
let existingKey = await this.getStoredKey();
|
||||||
|
|
||||||
if (existingKey) {
|
if (existingKey) {
|
||||||
@@ -59,36 +98,9 @@ class EncryptionKeyManager {
|
|||||||
return 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();
|
const newKey = await this.generateNewKey();
|
||||||
databaseLogger.warn(
|
databaseLogger.warn(
|
||||||
"Generated new encryption key - PLEASE BACKUP THIS KEY",
|
"Generated new encryption key - PLEASE BACKUP YOUR PASSWORD",
|
||||||
{
|
{
|
||||||
operation: "key_init",
|
operation: "key_init",
|
||||||
generated: true,
|
generated: true,
|
||||||
@@ -330,7 +342,7 @@ class EncryptionKeyManager {
|
|||||||
algorithm: keyInfo.algorithm,
|
algorithm: keyInfo.algorithm,
|
||||||
initialized: this.isInitialized(),
|
initialized: this.isInitialized(),
|
||||||
kekProtected,
|
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: "Error Handling", test: () => this.testErrorHandling() },
|
||||||
{ name: "Performance Test", test: () => this.testPerformance() },
|
{ name: "Performance Test", test: () => this.testPerformance() },
|
||||||
{ name: "JWT Secret Management", test: () => this.testJWTSecretManagement() },
|
{ name: "JWT Secret Management", test: () => this.testJWTSecretManagement() },
|
||||||
|
{ name: "Password-Based KEK Security", test: () => this.testPasswordBasedKEK() },
|
||||||
];
|
];
|
||||||
|
|
||||||
let passedTests = 0;
|
let passedTests = 0;
|
||||||
@@ -301,6 +302,67 @@ class EncryptionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(" ✅ JWT secret generation, caching, and regeneration working correctly");
|
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> {
|
static async validateProduction(): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,39 +1,25 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { databaseLogger } from "./logger.js";
|
import { databaseLogger } from "./logger.js";
|
||||||
import { HardwareFingerprint } from "./hardware-fingerprint.js";
|
|
||||||
|
|
||||||
interface ProtectedKeyData {
|
interface ProtectedKeyData {
|
||||||
data: string;
|
data: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
version: string;
|
version: string;
|
||||||
fingerprint: string;
|
salt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MasterKeyProtection {
|
class MasterKeyProtection {
|
||||||
private static readonly VERSION = "v1";
|
private static readonly VERSION = "v2";
|
||||||
private static readonly KEK_SALT = "termix-kek-salt-v1";
|
private static readonly KEK_ITERATIONS = 100000;
|
||||||
private static readonly KEK_ITERATIONS = 50000;
|
|
||||||
|
|
||||||
private static generateDeviceFingerprint(): string {
|
private static deriveKEK(userPassword: string, salt: Buffer): Buffer {
|
||||||
try {
|
if (!userPassword) {
|
||||||
const fingerprint = HardwareFingerprint.generate();
|
throw new Error("User password is required for KEK derivation");
|
||||||
|
|
||||||
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(): Buffer {
|
|
||||||
const fingerprint = this.generateDeviceFingerprint();
|
|
||||||
const salt = Buffer.from(this.KEK_SALT);
|
|
||||||
|
|
||||||
const kek = crypto.pbkdf2Sync(
|
const kek = crypto.pbkdf2Sync(
|
||||||
fingerprint,
|
userPassword,
|
||||||
salt,
|
salt,
|
||||||
this.KEK_ITERATIONS,
|
this.KEK_ITERATIONS,
|
||||||
32,
|
32,
|
||||||
@@ -43,13 +29,17 @@ class MasterKeyProtection {
|
|||||||
return kek;
|
return kek;
|
||||||
}
|
}
|
||||||
|
|
||||||
static encryptMasterKey(masterKey: string): string {
|
static encryptMasterKey(masterKey: string, userPassword: string): string {
|
||||||
if (!masterKey) {
|
if (!masterKey) {
|
||||||
throw new Error("Master key cannot be empty");
|
throw new Error("Master key cannot be empty");
|
||||||
}
|
}
|
||||||
|
if (!userPassword) {
|
||||||
|
throw new Error("User password is required for encryption");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const kek = this.deriveKEK();
|
const salt = crypto.randomBytes(32);
|
||||||
|
const kek = this.deriveKEK(userPassword, salt);
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv("aes-256-gcm", kek, iv) as any;
|
const cipher = crypto.createCipheriv("aes-256-gcm", kek, iv) as any;
|
||||||
|
|
||||||
@@ -62,15 +52,16 @@ class MasterKeyProtection {
|
|||||||
iv: iv.toString("hex"),
|
iv: iv.toString("hex"),
|
||||||
tag: tag.toString("hex"),
|
tag: tag.toString("hex"),
|
||||||
version: this.VERSION,
|
version: this.VERSION,
|
||||||
fingerprint: this.generateDeviceFingerprint().substring(0, 16),
|
salt: salt.toString("hex"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = JSON.stringify(protectedData);
|
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",
|
operation: "master_key_encryption",
|
||||||
version: this.VERSION,
|
version: this.VERSION,
|
||||||
fingerprintPrefix: protectedData.fingerprint,
|
saltLength: salt.length,
|
||||||
|
iterations: this.KEK_ITERATIONS,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -82,36 +73,32 @@ class MasterKeyProtection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static decryptMasterKey(encryptedKey: string): string {
|
static decryptMasterKey(encryptedKey: string, userPassword: string): string {
|
||||||
if (!encryptedKey) {
|
if (!encryptedKey) {
|
||||||
throw new Error("Encrypted key cannot be empty");
|
throw new Error("Encrypted key cannot be empty");
|
||||||
}
|
}
|
||||||
|
if (!userPassword) {
|
||||||
|
throw new Error("User password is required for decryption");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const protectedData: ProtectedKeyData = JSON.parse(encryptedKey);
|
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) {
|
if (protectedData.version !== this.VERSION) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unsupported protection version: ${protectedData.version}`,
|
`Unsupported protection version: ${protectedData.version}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentFingerprint = this.generateDeviceFingerprint().substring(
|
const salt = Buffer.from(protectedData.salt, "hex");
|
||||||
0,
|
const kek = this.deriveKEK(userPassword, salt);
|
||||||
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 decipher = crypto.createDecipheriv(
|
const decipher = crypto.createDecipheriv(
|
||||||
"aes-256-gcm",
|
"aes-256-gcm",
|
||||||
kek,
|
kek,
|
||||||
@@ -122,6 +109,12 @@ class MasterKeyProtection {
|
|||||||
let decrypted = decipher.update(protectedData.data, "hex", "hex");
|
let decrypted = decipher.update(protectedData.data, "hex", "hex");
|
||||||
decrypted += decipher.final("hex");
|
decrypted += decipher.final("hex");
|
||||||
|
|
||||||
|
databaseLogger.info("Master key decrypted successfully", {
|
||||||
|
operation: "master_key_decryption",
|
||||||
|
version: protectedData.version,
|
||||||
|
saltLength: salt.length,
|
||||||
|
});
|
||||||
|
|
||||||
return decrypted;
|
return decrypted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
databaseLogger.error("Failed to decrypt master key", error, {
|
databaseLogger.error("Failed to decrypt master key", error, {
|
||||||
@@ -136,29 +129,42 @@ class MasterKeyProtection {
|
|||||||
static isProtectedKey(data: string): boolean {
|
static isProtectedKey(data: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data);
|
const parsed = JSON.parse(data);
|
||||||
return !!(
|
|
||||||
|
// Support both v1 (fingerprint) and v2 (salt) formats
|
||||||
|
const hasV1Format = !!(
|
||||||
parsed.data &&
|
parsed.data &&
|
||||||
parsed.iv &&
|
parsed.iv &&
|
||||||
parsed.tag &&
|
parsed.tag &&
|
||||||
parsed.version &&
|
parsed.version &&
|
||||||
parsed.fingerprint
|
parsed.fingerprint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasV2Format = !!(
|
||||||
|
parsed.data &&
|
||||||
|
parsed.iv &&
|
||||||
|
parsed.tag &&
|
||||||
|
parsed.version &&
|
||||||
|
parsed.salt
|
||||||
|
);
|
||||||
|
|
||||||
|
return hasV1Format || hasV2Format;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateProtection(): boolean {
|
static validateProtection(userPassword: string): boolean {
|
||||||
try {
|
try {
|
||||||
const testKey = crypto.randomBytes(32).toString("hex");
|
const testKey = crypto.randomBytes(32).toString("hex");
|
||||||
const encrypted = this.encryptMasterKey(testKey);
|
const encrypted = this.encryptMasterKey(testKey, userPassword);
|
||||||
const decrypted = this.decryptMasterKey(encrypted);
|
const decrypted = this.decryptMasterKey(encrypted, userPassword);
|
||||||
|
|
||||||
const isValid = decrypted === testKey;
|
const isValid = decrypted === testKey;
|
||||||
|
|
||||||
databaseLogger.info("Master key protection validation completed", {
|
databaseLogger.info("Master key protection validation completed", {
|
||||||
operation: "protection_validation",
|
operation: "protection_validation",
|
||||||
result: isValid ? "passed" : "failed",
|
result: isValid ? "passed" : "failed",
|
||||||
|
version: this.VERSION,
|
||||||
});
|
});
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
@@ -172,8 +178,9 @@ class MasterKeyProtection {
|
|||||||
|
|
||||||
static getProtectionInfo(encryptedKey: string): {
|
static getProtectionInfo(encryptedKey: string): {
|
||||||
version: string;
|
version: string;
|
||||||
fingerprint: string;
|
isPasswordBased: boolean;
|
||||||
isCurrentDevice: boolean;
|
saltLength?: number;
|
||||||
|
iterations?: number;
|
||||||
} | null {
|
} | null {
|
||||||
try {
|
try {
|
||||||
if (!this.isProtectedKey(encryptedKey)) {
|
if (!this.isProtectedKey(encryptedKey)) {
|
||||||
@@ -181,16 +188,22 @@ class MasterKeyProtection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const protectedData: ProtectedKeyData = JSON.parse(encryptedKey);
|
const protectedData: ProtectedKeyData = JSON.parse(encryptedKey);
|
||||||
const currentFingerprint = this.generateDeviceFingerprint().substring(
|
|
||||||
0,
|
|
||||||
16,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
const info = {
|
||||||
version: protectedData.version,
|
version: protectedData.version,
|
||||||
fingerprint: protectedData.fingerprint,
|
isPasswordBased: protectedData.version === "v2",
|
||||||
isCurrentDevice: protectedData.fingerprint === currentFingerprint,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user