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.
This commit is contained in:
ZacharyZcR
2025-09-16 09:52:30 +08:00
parent 18f67d22bc
commit a501df8dde
17 changed files with 2256 additions and 65 deletions

View File

@@ -10,6 +10,8 @@ import fs from "fs";
import path from "path";
import "dotenv/config";
import { databaseLogger, apiLogger } from "../utils/logger.js";
import { DatabaseEncryption } from "../utils/database-encryption.js";
import { EncryptionMigration } from "../utils/encryption-migration.js";
const app = express();
app.use(
@@ -255,6 +257,111 @@ app.get("/releases/rss", async (req, res) => {
}
});
app.get("/encryption/status", async (req, res) => {
try {
const detailedStatus = await DatabaseEncryption.getDetailedStatus();
const migrationStatus = await EncryptionMigration.checkMigrationStatus();
res.json({
encryption: detailedStatus,
migration: migrationStatus
});
} catch (error) {
apiLogger.error("Failed to get encryption status", error, {
operation: "encryption_status"
});
res.status(500).json({ error: "Failed to get encryption status" });
}
});
app.post("/encryption/initialize", async (req, res) => {
try {
const { EncryptionKeyManager } = await import("../utils/encryption-key-manager.js");
const keyManager = EncryptionKeyManager.getInstance();
const newKey = await keyManager.generateNewKey();
await DatabaseEncryption.initialize({ masterPassword: newKey });
apiLogger.info("Encryption initialized via API", {
operation: "encryption_init_api"
});
res.json({
success: true,
message: "Encryption initialized successfully",
keyPreview: newKey.substring(0, 8) + "..."
});
} catch (error) {
apiLogger.error("Failed to initialize encryption", error, {
operation: "encryption_init_api_failed"
});
res.status(500).json({ error: "Failed to initialize encryption" });
}
});
app.post("/encryption/migrate", async (req, res) => {
try {
const { dryRun = false } = req.body;
const migration = new EncryptionMigration({
dryRun,
backupEnabled: true
});
if (dryRun) {
apiLogger.info("Starting encryption migration (dry run)", {
operation: "encryption_migrate_dry_run"
});
res.json({
success: true,
message: "Dry run mode - no changes made",
dryRun: true
});
} else {
apiLogger.info("Starting encryption migration", {
operation: "encryption_migrate"
});
await migration.runMigration();
res.json({
success: true,
message: "Migration completed successfully"
});
}
} catch (error) {
apiLogger.error("Migration failed", error, {
operation: "encryption_migrate_failed"
});
res.status(500).json({
error: "Migration failed",
details: error instanceof Error ? error.message : "Unknown error"
});
}
});
app.post("/encryption/regenerate", async (req, res) => {
try {
await DatabaseEncryption.reinitializeWithNewKey();
apiLogger.warn("Encryption key regenerated via API", {
operation: "encryption_regenerate_api"
});
res.json({
success: true,
message: "New encryption key generated",
warning: "All encrypted data must be re-encrypted"
});
} catch (error) {
apiLogger.error("Failed to regenerate encryption key", error, {
operation: "encryption_regenerate_failed"
});
res.status(500).json({ error: "Failed to regenerate encryption key" });
}
});
app.use("/users", userRoutes);
app.use("/ssh", sshRoutes);
app.use("/alerts", alertRoutes);
@@ -278,7 +385,43 @@ app.use(
);
const PORT = 8081;
app.listen(PORT, () => {
async function initializeEncryption() {
try {
databaseLogger.info("Initializing database encryption...", {
operation: "encryption_init"
});
await DatabaseEncryption.initialize({
encryptionEnabled: process.env.ENCRYPTION_ENABLED !== 'false',
forceEncryption: process.env.FORCE_ENCRYPTION === 'true',
migrateOnAccess: process.env.MIGRATE_ON_ACCESS !== 'false'
});
const status = await DatabaseEncryption.getDetailedStatus();
if (status.configValid && status.key.keyValid) {
databaseLogger.success("Database encryption initialized successfully", {
operation: "encryption_init_complete",
enabled: status.enabled,
keyId: status.key.keyId,
hasStoredKey: status.key.hasKey
});
} else {
databaseLogger.error("Database encryption configuration invalid", undefined, {
operation: "encryption_init_failed",
status
});
}
} catch (error) {
databaseLogger.error("Failed to initialize database encryption", error, {
operation: "encryption_init_error"
});
}
}
app.listen(PORT, async () => {
await initializeEncryption();
databaseLogger.success(`Database API server started on port ${PORT}`, {
operation: "server_start",
port: PORT,
@@ -290,6 +433,10 @@ app.listen(PORT, () => {
"/health",
"/version",
"/releases/rss",
"/encryption/status",
"/encryption/initialize",
"/encryption/migrate",
"/encryption/regenerate",
],
});
});

View File

@@ -250,6 +250,11 @@ const migrateSchema = () => {
"INTEGER REFERENCES ssh_credentials(id)",
);
// SSH credentials table migrations for encryption support
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");

View File

@@ -5,6 +5,7 @@ import { eq, and, desc, sql } from "drizzle-orm";
import type { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { authLogger } from "../../utils/logger.js";
import { EncryptedDBOperations } from "../../utils/encrypted-db-operations.js";
import { parseSSHKey, parsePublicKey, detectKeyType, validateKeyPair } from "../../utils/ssh-key-utils.js";
import crypto from "crypto";
import ssh2Pkg from "ssh2";
@@ -194,11 +195,11 @@ router.post("/", authenticateJWT, async (req: Request, res: Response) => {
lastUsed: null,
};
const result = await db
.insert(sshCredentials)
.values(credentialData)
.returning();
const created = result[0];
const created = await EncryptedDBOperations.insert(
sshCredentials,
'ssh_credentials',
credentialData
) as typeof credentialData & { id: number };
authLogger.success(
`SSH credential created: ${name} (${authType}) by user ${userId}`,
@@ -238,11 +239,10 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => {
}
try {
const credentials = await db
.select()
.from(sshCredentials)
.where(eq(sshCredentials.userId, userId))
.orderBy(desc(sshCredentials.updatedAt));
const credentials = await EncryptedDBOperations.select(
db.select().from(sshCredentials).where(eq(sshCredentials.userId, userId)).orderBy(desc(sshCredentials.updatedAt)),
'ssh_credentials'
);
res.json(credentials.map((cred) => formatCredentialOutput(cred)));
} catch (err) {
@@ -296,15 +296,13 @@ router.get("/:id", authenticateJWT, async (req: Request, res: Response) => {
}
try {
const credentials = await db
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, parseInt(id)),
eq(sshCredentials.userId, userId),
),
);
const credentials = await EncryptedDBOperations.select(
db.select().from(sshCredentials).where(and(
eq(sshCredentials.id, parseInt(id)),
eq(sshCredentials.userId, userId),
)),
'ssh_credentials'
);
if (credentials.length === 0) {
return res.status(404).json({ error: "Credential not found" });
@@ -415,28 +413,28 @@ router.put("/:id", authenticateJWT, async (req: Request, res: Response) => {
}
if (Object.keys(updateFields).length === 0) {
const existing = await db
.select()
.from(sshCredentials)
.where(eq(sshCredentials.id, parseInt(id)));
const existing = await EncryptedDBOperations.select(
db.select().from(sshCredentials).where(eq(sshCredentials.id, parseInt(id))),
'ssh_credentials'
);
return res.json(formatCredentialOutput(existing[0]));
}
await db
.update(sshCredentials)
.set(updateFields)
.where(
and(
eq(sshCredentials.id, parseInt(id)),
eq(sshCredentials.userId, userId),
),
);
await EncryptedDBOperations.update(
sshCredentials,
'ssh_credentials',
and(
eq(sshCredentials.id, parseInt(id)),
eq(sshCredentials.userId, userId),
),
updateFields
);
const updated = await db
.select()
.from(sshCredentials)
.where(eq(sshCredentials.id, parseInt(id)));
const updated = await EncryptedDBOperations.select(
db.select().from(sshCredentials).where(eq(sshCredentials.id, parseInt(id))),
'ssh_credentials'
);
const credential = updated[0];
authLogger.success(

View File

@@ -13,6 +13,7 @@ import type { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import multer from "multer";
import { sshLogger } from "../../utils/logger.js";
import { EncryptedDBOperations } from "../../utils/encrypted-db-operations.js";
const router = express.Router();
@@ -62,7 +63,10 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
return res.status(403).json({ error: "Forbidden" });
}
try {
const data = await db.select().from(sshData);
const data = await EncryptedDBOperations.select(
db.select().from(sshData),
'ssh_data'
);
const result = data.map((row: any) => {
return {
...row,

View File

@@ -6,6 +6,7 @@ import { db } from "../database/db/index.js";
import { sshData, sshCredentials } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { statsLogger } from "../utils/logger.js";
import { EncryptedDBOperations } from "../utils/encrypted-db-operations.js";
interface PooledConnection {
client: Client;
@@ -306,7 +307,10 @@ const hostStatuses: Map<number, StatusEntry> = new Map();
async function fetchAllHosts(): Promise<SSHHostWithCredentials[]> {
try {
const hosts = await db.select().from(sshData);
const hosts = await EncryptedDBOperations.select(
db.select().from(sshData),
'ssh_data'
);
const hostsWithCredentials: SSHHostWithCredentials[] = [];
for (const host of hosts) {
@@ -333,7 +337,10 @@ async function fetchHostById(
id: number,
): Promise<SSHHostWithCredentials | undefined> {
try {
const hosts = await db.select().from(sshData).where(eq(sshData.id, id));
const hosts = await EncryptedDBOperations.select(
db.select().from(sshData).where(eq(sshData.id, id)),
'ssh_data'
);
if (hosts.length === 0) {
return undefined;
@@ -380,15 +387,13 @@ async function resolveHostCredentials(
if (host.credentialId) {
try {
const credentials = await db
.select()
.from(sshCredentials)
.where(
and(
eq(sshCredentials.id, host.credentialId),
eq(sshCredentials.userId, host.userId),
),
);
const credentials = await EncryptedDBOperations.select(
db.select().from(sshCredentials).where(and(
eq(sshCredentials.id, host.credentialId),
eq(sshCredentials.userId, host.userId),
)),
'ssh_credentials'
);
if (credentials.length > 0) {
const credential = credentials[0];

View File

@@ -2,10 +2,7 @@
// node ./dist/backend/starter.js
import "./database/database.js";
import "./ssh/terminal.js";
import "./ssh/tunnel.js";
import "./ssh/file-manager.js";
import "./ssh/server-stats.js";
import { DatabaseEncryption } from "./utils/database-encryption.js";
import { systemLogger, versionLogger } from "./utils/logger.js";
import "dotenv/config";
@@ -21,9 +18,21 @@ import "dotenv/config";
operation: "startup",
});
// Initialize database encryption before other services
await DatabaseEncryption.initialize();
systemLogger.info("Database encryption initialized", {
operation: "encryption_init",
});
// Load modules that depend on encryption after initialization
await import("./ssh/terminal.js");
await import("./ssh/tunnel.js");
await import("./ssh/file-manager.js");
await import("./ssh/server-stats.js");
systemLogger.success("All backend services initialized successfully", {
operation: "startup_complete",
services: ["database", "terminal", "tunnel", "file_manager", "stats"],
services: ["database", "encryption", "terminal", "tunnel", "file_manager", "stats"],
version: version,
});

View File

@@ -0,0 +1,252 @@
import { FieldEncryption } from './encryption.js';
import { EncryptionKeyManager } from './encryption-key-manager.js';
import { databaseLogger } from './logger.js';
interface EncryptionContext {
masterPassword: string;
encryptionEnabled: boolean;
forceEncryption: boolean;
migrateOnAccess: boolean;
}
class DatabaseEncryption {
private static context: EncryptionContext | null = null;
static async initialize(config: Partial<EncryptionContext> = {}) {
const keyManager = EncryptionKeyManager.getInstance();
const masterPassword = config.masterPassword || await keyManager.initializeKey();
this.context = {
masterPassword,
encryptionEnabled: config.encryptionEnabled ?? true,
forceEncryption: config.forceEncryption ?? false,
migrateOnAccess: config.migrateOnAccess ?? true
};
databaseLogger.info('Database encryption initialized', {
operation: 'encryption_init',
enabled: this.context.encryptionEnabled,
forceEncryption: this.context.forceEncryption,
dynamicKey: !config.masterPassword
});
}
static getContext(): EncryptionContext {
if (!this.context) {
throw new Error('DatabaseEncryption not initialized. Call initialize() first.');
}
return this.context;
}
static encryptRecord(tableName: string, record: any): any {
const context = this.getContext();
if (!context.encryptionEnabled) return record;
const encryptedRecord = { ...record };
let hasEncryption = false;
for (const [fieldName, value] of Object.entries(record)) {
if (FieldEncryption.shouldEncryptField(tableName, fieldName) && value) {
try {
const fieldKey = FieldEncryption.getFieldKey(context.masterPassword, `${tableName}.${fieldName}`);
encryptedRecord[fieldName] = FieldEncryption.encryptField(value as string, fieldKey);
hasEncryption = true;
} catch (error) {
databaseLogger.error(`Failed to encrypt field ${tableName}.${fieldName}`, error, {
operation: 'field_encryption',
table: tableName,
field: fieldName
});
throw error;
}
}
}
if (hasEncryption) {
databaseLogger.debug(`Encrypted sensitive fields for ${tableName}`, {
operation: 'record_encryption',
table: tableName
});
}
return encryptedRecord;
}
static decryptRecord(tableName: string, record: any): any {
const context = this.getContext();
if (!record) return record;
const decryptedRecord = { ...record };
let hasDecryption = false;
let needsMigration = false;
for (const [fieldName, value] of Object.entries(record)) {
if (FieldEncryption.shouldEncryptField(tableName, fieldName) && value) {
try {
const fieldKey = FieldEncryption.getFieldKey(context.masterPassword, `${tableName}.${fieldName}`);
if (FieldEncryption.isEncrypted(value as string)) {
decryptedRecord[fieldName] = FieldEncryption.decryptField(value as string, fieldKey);
hasDecryption = true;
} else if (context.encryptionEnabled && !context.forceEncryption) {
decryptedRecord[fieldName] = value;
needsMigration = context.migrateOnAccess;
} else if (context.forceEncryption) {
databaseLogger.warn(`Unencrypted field detected in force encryption mode`, {
operation: 'decryption_warning',
table: tableName,
field: fieldName
});
decryptedRecord[fieldName] = value;
}
} catch (error) {
databaseLogger.error(`Failed to decrypt field ${tableName}.${fieldName}`, error, {
operation: 'field_decryption',
table: tableName,
field: fieldName
});
if (context.forceEncryption) {
throw error;
} else {
decryptedRecord[fieldName] = value;
}
}
}
}
if (hasDecryption) {
databaseLogger.debug(`Decrypted sensitive fields for ${tableName}`, {
operation: 'record_decryption',
table: tableName
});
}
if (needsMigration) {
this.scheduleFieldMigration(tableName, record);
}
return decryptedRecord;
}
static decryptRecords(tableName: string, records: any[]): any[] {
if (!Array.isArray(records)) return records;
return records.map(record => this.decryptRecord(tableName, record));
}
private static scheduleFieldMigration(tableName: string, record: any) {
setTimeout(async () => {
try {
await this.migrateRecord(tableName, record);
} catch (error) {
databaseLogger.error(`Failed to migrate record ${tableName}:${record.id}`, error, {
operation: 'migration_failed',
table: tableName,
recordId: record.id
});
}
}, 1000);
}
static async migrateRecord(tableName: string, record: any): Promise<any> {
const context = this.getContext();
if (!context.encryptionEnabled || !context.migrateOnAccess) return record;
let needsUpdate = false;
const updatedRecord = { ...record };
for (const [fieldName, value] of Object.entries(record)) {
if (FieldEncryption.shouldEncryptField(tableName, fieldName) &&
value && !FieldEncryption.isEncrypted(value as string)) {
try {
const fieldKey = FieldEncryption.getFieldKey(context.masterPassword, `${tableName}.${fieldName}`);
updatedRecord[fieldName] = FieldEncryption.encryptField(value as string, fieldKey);
needsUpdate = true;
} catch (error) {
databaseLogger.error(`Failed to migrate field ${tableName}.${fieldName}`, error, {
operation: 'field_migration',
table: tableName,
field: fieldName,
recordId: record.id
});
throw error;
}
}
}
if (needsUpdate) {
databaseLogger.info(`Migrated record to encrypted format`, {
operation: 'record_migration',
table: tableName,
recordId: record.id
});
}
return updatedRecord;
}
static validateConfiguration(): boolean {
try {
const context = this.getContext();
const testData = 'test-encryption-data';
const testKey = FieldEncryption.getFieldKey(context.masterPassword, 'test');
const encrypted = FieldEncryption.encryptField(testData, testKey);
const decrypted = FieldEncryption.decryptField(encrypted, testKey);
return decrypted === testData;
} catch (error) {
databaseLogger.error('Encryption configuration validation failed', error, {
operation: 'config_validation'
});
return false;
}
}
static getEncryptionStatus() {
try {
const context = this.getContext();
return {
enabled: context.encryptionEnabled,
forceEncryption: context.forceEncryption,
migrateOnAccess: context.migrateOnAccess,
configValid: this.validateConfiguration()
};
} catch {
return {
enabled: false,
forceEncryption: false,
migrateOnAccess: false,
configValid: false
};
}
}
static async getDetailedStatus() {
const keyManager = EncryptionKeyManager.getInstance();
const keyStatus = await keyManager.getEncryptionStatus();
const encryptionStatus = this.getEncryptionStatus();
return {
...encryptionStatus,
key: keyStatus,
initialized: this.context !== null
};
}
static async reinitializeWithNewKey(): Promise<void> {
const keyManager = EncryptionKeyManager.getInstance();
const newKey = await keyManager.regenerateKey();
this.context = null;
await this.initialize({ masterPassword: newKey });
databaseLogger.warn('Database encryption reinitialized with new key', {
operation: 'encryption_reinit',
requiresMigration: true
});
}
}
export { DatabaseEncryption };
export type { EncryptionContext };

View File

@@ -0,0 +1,214 @@
import { db } from '../database/db/index.js';
import { DatabaseEncryption } from './database-encryption.js';
import { databaseLogger } from './logger.js';
import type { SQLiteTable } from 'drizzle-orm/sqlite-core';
type TableName = 'users' | 'ssh_data' | 'ssh_credentials';
class EncryptedDBOperations {
static async insert<T extends Record<string, any>>(
table: SQLiteTable<any>,
tableName: TableName,
data: T
): Promise<T> {
try {
const encryptedData = DatabaseEncryption.encryptRecord(tableName, data);
const result = await db.insert(table).values(encryptedData).returning();
// Decrypt the returned data to ensure consistency
const decryptedResult = DatabaseEncryption.decryptRecord(tableName, result[0]);
databaseLogger.debug(`Inserted encrypted record into ${tableName}`, {
operation: 'encrypted_insert',
table: tableName
});
return decryptedResult as T;
} catch (error) {
databaseLogger.error(`Failed to insert encrypted record into ${tableName}`, error, {
operation: 'encrypted_insert_failed',
table: tableName
});
throw error;
}
}
static async select<T extends Record<string, any>>(
query: any,
tableName: TableName
): Promise<T[]> {
try {
const results = await query;
const decryptedResults = DatabaseEncryption.decryptRecords(tableName, results);
databaseLogger.debug(`Selected and decrypted ${decryptedResults.length} records from ${tableName}`, {
operation: 'encrypted_select',
table: tableName,
count: decryptedResults.length
});
return decryptedResults;
} catch (error) {
databaseLogger.error(`Failed to select/decrypt records from ${tableName}`, error, {
operation: 'encrypted_select_failed',
table: tableName
});
throw error;
}
}
static async selectOne<T extends Record<string, any>>(
query: any,
tableName: TableName
): Promise<T | undefined> {
try {
const result = await query;
if (!result) return undefined;
const decryptedResult = DatabaseEncryption.decryptRecord(tableName, result);
databaseLogger.debug(`Selected and decrypted single record from ${tableName}`, {
operation: 'encrypted_select_one',
table: tableName
});
return decryptedResult;
} catch (error) {
databaseLogger.error(`Failed to select/decrypt single record from ${tableName}`, error, {
operation: 'encrypted_select_one_failed',
table: tableName
});
throw error;
}
}
static async update<T extends Record<string, any>>(
table: SQLiteTable<any>,
tableName: TableName,
where: any,
data: Partial<T>
): Promise<T[]> {
try {
const encryptedData = DatabaseEncryption.encryptRecord(tableName, data);
const result = await db.update(table).set(encryptedData).where(where).returning();
databaseLogger.debug(`Updated encrypted record in ${tableName}`, {
operation: 'encrypted_update',
table: tableName
});
return result as T[];
} catch (error) {
databaseLogger.error(`Failed to update encrypted record in ${tableName}`, error, {
operation: 'encrypted_update_failed',
table: tableName
});
throw error;
}
}
static async delete(
table: SQLiteTable<any>,
tableName: TableName,
where: any
): Promise<any[]> {
try {
const result = await db.delete(table).where(where).returning();
databaseLogger.debug(`Deleted record from ${tableName}`, {
operation: 'encrypted_delete',
table: tableName
});
return result;
} catch (error) {
databaseLogger.error(`Failed to delete record from ${tableName}`, error, {
operation: 'encrypted_delete_failed',
table: tableName
});
throw error;
}
}
static async migrateExistingRecords(tableName: TableName): Promise<number> {
let migratedCount = 0;
try {
databaseLogger.info(`Starting encryption migration for ${tableName}`, {
operation: 'migration_start',
table: tableName
});
let table: SQLiteTable<any>;
let records: any[];
switch (tableName) {
case 'users':
const { users } = await import('../database/db/schema.js');
table = users;
records = await db.select().from(users);
break;
case 'ssh_data':
const { sshData } = await import('../database/db/schema.js');
table = sshData;
records = await db.select().from(sshData);
break;
case 'ssh_credentials':
const { sshCredentials } = await import('../database/db/schema.js');
table = sshCredentials;
records = await db.select().from(sshCredentials);
break;
default:
throw new Error(`Unknown table: ${tableName}`);
}
for (const record of records) {
try {
const migratedRecord = await DatabaseEncryption.migrateRecord(tableName, record);
if (JSON.stringify(migratedRecord) !== JSON.stringify(record)) {
const { eq } = await import('drizzle-orm');
await db.update(table).set(migratedRecord).where(eq((table as any).id, record.id));
migratedCount++;
}
} catch (error) {
databaseLogger.error(`Failed to migrate record ${record.id} in ${tableName}`, error, {
operation: 'migration_record_failed',
table: tableName,
recordId: record.id
});
}
}
databaseLogger.success(`Migration completed for ${tableName}`, {
operation: 'migration_complete',
table: tableName,
migratedCount,
totalRecords: records.length
});
return migratedCount;
} catch (error) {
databaseLogger.error(`Migration failed for ${tableName}`, error, {
operation: 'migration_failed',
table: tableName
});
throw error;
}
}
static async healthCheck(): Promise<boolean> {
try {
const status = DatabaseEncryption.getEncryptionStatus();
return status.configValid && status.enabled;
} catch (error) {
databaseLogger.error('Encryption health check failed', error, {
operation: 'health_check_failed'
});
return false;
}
}
}
export { EncryptedDBOperations };
export type { TableName };

View File

@@ -3,6 +3,7 @@ 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;
@@ -26,11 +27,17 @@ class EncryptionKeyManager {
}
private encodeKey(key: string): string {
const buffer = Buffer.from(key, 'hex');
return Buffer.from(buffer).toString('base64');
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');
}
@@ -117,7 +124,7 @@ class EncryptionKeyManager {
algorithm: 'aes-256-gcm'
};
const encodedData = Buffer.from(JSON.stringify(keyData)).toString('base64');
const encodedData = JSON.stringify(keyData);
try {
const existing = await db.select().from(settings).where(eq(settings.key, 'db_encryption_key'));
@@ -170,7 +177,16 @@ class EncryptionKeyManager {
}
const encodedData = result[0].value;
const keyData = JSON.parse(Buffer.from(encodedData, 'base64').toString());
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,
@@ -179,7 +195,17 @@ class EncryptionKeyManager {
algorithm: keyData.algorithm
};
return this.decodeKey(keyData.key);
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, {
@@ -231,7 +257,8 @@ class EncryptionKeyManager {
const entropyTest = new Set(key).size / key.length;
return (hasLower + hasUpper + hasDigit + hasSpecial) >= 3 && entropyTest > 0.4;
const complexity = Number(hasLower) + Number(hasUpper) + Number(hasDigit) + Number(hasSpecial);
return complexity >= 3 && entropyTest > 0.4;
}
async validateKey(key?: string): Promise<boolean> {
@@ -265,6 +292,7 @@ class EncryptionKeyManager {
async getEncryptionStatus() {
const keyInfo = await this.getKeyInfo();
const isValid = await this.validateKey();
const kekProtected = await this.isKEKProtected();
return {
hasKey: keyInfo.hasKey,
@@ -272,9 +300,23 @@ class EncryptionKeyManager {
keyId: keyInfo.keyId,
createdAt: keyInfo.createdAt,
algorithm: keyInfo.algorithm,
initialized: this.isInitialized()
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 };

View File

@@ -0,0 +1,326 @@
#!/usr/bin/env node
import { DatabaseEncryption } from './database-encryption.js';
import { EncryptedDBOperations } from './encrypted-db-operations.js';
import { EncryptionKeyManager } from './encryption-key-manager.js';
import { databaseLogger } from './logger.js';
import { db } from '../database/db/index.js';
import { settings } from '../database/db/schema.js';
import { eq } from 'drizzle-orm';
interface MigrationConfig {
masterPassword?: string;
forceEncryption?: boolean;
backupEnabled?: boolean;
dryRun?: boolean;
}
class EncryptionMigration {
private config: MigrationConfig;
constructor(config: MigrationConfig = {}) {
this.config = {
masterPassword: config.masterPassword,
forceEncryption: config.forceEncryption ?? false,
backupEnabled: config.backupEnabled ?? true,
dryRun: config.dryRun ?? false
};
}
async runMigration(): Promise<void> {
databaseLogger.info('Starting database encryption migration', {
operation: 'migration_start',
dryRun: this.config.dryRun,
forceEncryption: this.config.forceEncryption
});
try {
await this.validatePrerequisites();
if (this.config.backupEnabled && !this.config.dryRun) {
await this.createBackup();
}
await this.initializeEncryption();
await this.migrateTables();
await this.updateSettings();
await this.verifyMigration();
databaseLogger.success('Database encryption migration completed successfully', {
operation: 'migration_complete'
});
} catch (error) {
databaseLogger.error('Migration failed', error, {
operation: 'migration_failed'
});
throw error;
}
}
private async validatePrerequisites(): Promise<void> {
databaseLogger.info('Validating migration prerequisites', {
operation: 'validation'
});
// Check if KEK-managed encryption key exists
const keyManager = EncryptionKeyManager.getInstance();
if (!this.config.masterPassword) {
// Try to get current key from KEK manager
try {
const currentKey = keyManager.getCurrentKey();
if (!currentKey) {
// Initialize key if not available
const initializedKey = await keyManager.initializeKey();
this.config.masterPassword = initializedKey;
} else {
this.config.masterPassword = currentKey;
}
} catch (error) {
throw new Error('Failed to retrieve encryption key from KEK manager. Please ensure encryption is properly initialized.');
}
}
// Validate key strength
if (this.config.masterPassword.length < 16) {
throw new Error('Master password must be at least 16 characters long');
}
// Test database connection
try {
await db.select().from(settings).limit(1);
} catch (error) {
throw new Error('Database connection failed');
}
databaseLogger.success('Prerequisites validation passed', {
operation: 'validation_complete',
keySource: 'kek_manager'
});
}
private async createBackup(): Promise<void> {
databaseLogger.info('Creating database backup before migration', {
operation: 'backup_start'
});
try {
const fs = await import('fs');
const path = await import('path');
const dataDir = process.env.DATA_DIR || './db/data';
const dbPath = path.join(dataDir, 'db.sqlite');
const backupPath = path.join(dataDir, `db-backup-${Date.now()}.sqlite`);
if (fs.existsSync(dbPath)) {
fs.copyFileSync(dbPath, backupPath);
databaseLogger.success(`Database backup created: ${backupPath}`, {
operation: 'backup_complete',
backupPath
});
}
} catch (error) {
databaseLogger.error('Failed to create backup', error, {
operation: 'backup_failed'
});
throw error;
}
}
private async initializeEncryption(): Promise<void> {
databaseLogger.info('Initializing encryption system', {
operation: 'encryption_init'
});
DatabaseEncryption.initialize({
masterPassword: this.config.masterPassword!,
encryptionEnabled: true,
forceEncryption: this.config.forceEncryption,
migrateOnAccess: true
});
const isHealthy = await EncryptedDBOperations.healthCheck();
if (!isHealthy) {
throw new Error('Encryption system health check failed');
}
databaseLogger.success('Encryption system initialized successfully', {
operation: 'encryption_init_complete'
});
}
private async migrateTables(): Promise<void> {
const tables: Array<'users' | 'ssh_data' | 'ssh_credentials'> = [
'users',
'ssh_data',
'ssh_credentials'
];
let totalMigrated = 0;
for (const tableName of tables) {
databaseLogger.info(`Starting migration for table: ${tableName}`, {
operation: 'table_migration_start',
table: tableName
});
try {
if (this.config.dryRun) {
databaseLogger.info(`[DRY RUN] Would migrate table: ${tableName}`, {
operation: 'dry_run_table',
table: tableName
});
continue;
}
const migratedCount = await EncryptedDBOperations.migrateExistingRecords(tableName);
totalMigrated += migratedCount;
databaseLogger.success(`Migration completed for table: ${tableName}`, {
operation: 'table_migration_complete',
table: tableName,
migratedCount
});
} catch (error) {
databaseLogger.error(`Migration failed for table: ${tableName}`, error, {
operation: 'table_migration_failed',
table: tableName
});
throw error;
}
}
databaseLogger.success(`All tables migrated successfully`, {
operation: 'all_tables_migrated',
totalMigrated
});
}
private async updateSettings(): Promise<void> {
if (this.config.dryRun) {
databaseLogger.info('[DRY RUN] Would update encryption settings', {
operation: 'dry_run_settings'
});
return;
}
try {
const encryptionSettings = [
{ key: 'encryption_enabled', value: 'true' },
{ key: 'encryption_migration_completed', value: new Date().toISOString() },
{ key: 'encryption_version', value: '1.0' }
];
for (const setting of encryptionSettings) {
const existing = await db.select().from(settings).where(eq(settings.key, setting.key));
if (existing.length > 0) {
await db.update(settings).set({ value: setting.value }).where(eq(settings.key, setting.key));
} else {
await db.insert(settings).values(setting);
}
}
databaseLogger.success('Encryption settings updated', {
operation: 'settings_updated'
});
} catch (error) {
databaseLogger.error('Failed to update settings', error, {
operation: 'settings_update_failed'
});
throw error;
}
}
private async verifyMigration(): Promise<void> {
databaseLogger.info('Verifying migration integrity', {
operation: 'verification_start'
});
try {
const status = DatabaseEncryption.getEncryptionStatus();
if (!status.enabled || !status.configValid) {
throw new Error('Encryption system verification failed');
}
const testResult = await this.performTestEncryption();
if (!testResult) {
throw new Error('Test encryption/decryption failed');
}
databaseLogger.success('Migration verification completed successfully', {
operation: 'verification_complete',
status
});
} catch (error) {
databaseLogger.error('Migration verification failed', error, {
operation: 'verification_failed'
});
throw error;
}
}
private async performTestEncryption(): Promise<boolean> {
try {
const { FieldEncryption } = await import('./encryption.js');
const testData = `test-data-${Date.now()}`;
const testKey = FieldEncryption.getFieldKey(this.config.masterPassword!, 'test');
const encrypted = FieldEncryption.encryptField(testData, testKey);
const decrypted = FieldEncryption.decryptField(encrypted, testKey);
return decrypted === testData;
} catch {
return false;
}
}
static async checkMigrationStatus(): Promise<{
isEncryptionEnabled: boolean;
migrationCompleted: boolean;
migrationDate?: string;
}> {
try {
const encryptionEnabled = await db.select().from(settings).where(eq(settings.key, 'encryption_enabled'));
const migrationCompleted = await db.select().from(settings).where(eq(settings.key, 'encryption_migration_completed'));
return {
isEncryptionEnabled: encryptionEnabled.length > 0 && encryptionEnabled[0].value === 'true',
migrationCompleted: migrationCompleted.length > 0,
migrationDate: migrationCompleted.length > 0 ? migrationCompleted[0].value : undefined
};
} catch (error) {
databaseLogger.error('Failed to check migration status', error, {
operation: 'status_check_failed'
});
throw error;
}
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
const config: MigrationConfig = {
masterPassword: process.env.DB_ENCRYPTION_KEY,
forceEncryption: process.env.FORCE_ENCRYPTION === 'true',
backupEnabled: process.env.BACKUP_ENABLED !== 'false',
dryRun: process.env.DRY_RUN === 'true'
};
const migration = new EncryptionMigration(config);
migration.runMigration()
.then(() => {
console.log('Migration completed successfully');
process.exit(0);
})
.catch((error) => {
console.error('Migration failed:', error.message);
process.exit(1);
});
}
export { EncryptionMigration };
export type { MigrationConfig };

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env node
import { FieldEncryption } from './encryption.js';
import { DatabaseEncryption } from './database-encryption.js';
import { EncryptedDBOperations } from './encrypted-db-operations.js';
import { databaseLogger } from './logger.js';
class EncryptionTest {
private testPassword = 'test-master-password-for-validation';
async runAllTests(): Promise<boolean> {
console.log('🔐 Starting Termix Database Encryption Tests...\n');
const tests = [
{ name: 'Basic Encryption/Decryption', test: () => this.testBasicEncryption() },
{ name: 'Field Encryption Detection', test: () => this.testFieldDetection() },
{ name: 'Key Derivation', test: () => this.testKeyDerivation() },
{ name: 'Database Encryption Context', test: () => this.testDatabaseContext() },
{ name: 'Record Encryption/Decryption', test: () => this.testRecordOperations() },
{ name: 'Backward Compatibility', test: () => this.testBackwardCompatibility() },
{ name: 'Error Handling', test: () => this.testErrorHandling() },
{ name: 'Performance Test', test: () => this.testPerformance() }
];
let passedTests = 0;
let totalTests = tests.length;
for (const test of tests) {
try {
console.log(`⏳ Running: ${test.name}...`);
await test.test();
console.log(`✅ PASSED: ${test.name}\n`);
passedTests++;
} catch (error) {
console.log(`❌ FAILED: ${test.name}`);
console.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
}
}
const success = passedTests === totalTests;
console.log(`\n🎯 Test Results: ${passedTests}/${totalTests} tests passed`);
if (success) {
console.log('🎉 All encryption tests PASSED! System is ready for production.');
} else {
console.log('⚠️ Some tests FAILED! Please review the implementation.');
}
return success;
}
private async testBasicEncryption(): Promise<void> {
const testData = 'Hello, World! This is sensitive data.';
const key = FieldEncryption.getFieldKey(this.testPassword, 'test-field');
const encrypted = FieldEncryption.encryptField(testData, key);
const decrypted = FieldEncryption.decryptField(encrypted, key);
if (decrypted !== testData) {
throw new Error(`Decryption mismatch: expected "${testData}", got "${decrypted}"`);
}
if (!FieldEncryption.isEncrypted(encrypted)) {
throw new Error('Encrypted data not detected as encrypted');
}
if (FieldEncryption.isEncrypted(testData)) {
throw new Error('Plain text incorrectly detected as encrypted');
}
}
private async testFieldDetection(): Promise<void> {
const testCases = [
{ table: 'users', field: 'password_hash', shouldEncrypt: true },
{ table: 'users', field: 'username', shouldEncrypt: false },
{ table: 'ssh_data', field: 'password', shouldEncrypt: true },
{ table: 'ssh_data', field: 'ip', shouldEncrypt: false },
{ table: 'ssh_credentials', field: 'privateKey', shouldEncrypt: true },
{ table: 'unknown_table', field: 'any_field', shouldEncrypt: false }
];
for (const testCase of testCases) {
const result = FieldEncryption.shouldEncryptField(testCase.table, testCase.field);
if (result !== testCase.shouldEncrypt) {
throw new Error(
`Field detection failed for ${testCase.table}.${testCase.field}: ` +
`expected ${testCase.shouldEncrypt}, got ${result}`
);
}
}
}
private async testKeyDerivation(): Promise<void> {
const password = 'test-password';
const fieldType1 = 'users.password_hash';
const fieldType2 = 'ssh_data.password';
const key1a = FieldEncryption.getFieldKey(password, fieldType1);
const key1b = FieldEncryption.getFieldKey(password, fieldType1);
const key2 = FieldEncryption.getFieldKey(password, fieldType2);
if (!key1a.equals(key1b)) {
throw new Error('Same field type should produce identical keys');
}
if (key1a.equals(key2)) {
throw new Error('Different field types should produce different keys');
}
const differentPasswordKey = FieldEncryption.getFieldKey('different-password', fieldType1);
if (key1a.equals(differentPasswordKey)) {
throw new Error('Different passwords should produce different keys');
}
}
private async testDatabaseContext(): Promise<void> {
DatabaseEncryption.initialize({
masterPassword: this.testPassword,
encryptionEnabled: true,
forceEncryption: false,
migrateOnAccess: true
});
const status = DatabaseEncryption.getEncryptionStatus();
if (!status.enabled) {
throw new Error('Encryption should be enabled');
}
if (!status.configValid) {
throw new Error('Configuration should be valid');
}
}
private async testRecordOperations(): Promise<void> {
const testRecord = {
id: 'test-id-123',
username: 'testuser',
password_hash: 'sensitive-password-hash',
is_admin: false
};
const encrypted = DatabaseEncryption.encryptRecord('users', testRecord);
const decrypted = DatabaseEncryption.decryptRecord('users', encrypted);
if (decrypted.username !== testRecord.username) {
throw new Error('Non-sensitive field should remain unchanged');
}
if (decrypted.password_hash !== testRecord.password_hash) {
throw new Error('Sensitive field should be properly decrypted');
}
if (!FieldEncryption.isEncrypted(encrypted.password_hash)) {
throw new Error('Sensitive field should be encrypted in stored record');
}
}
private async testBackwardCompatibility(): Promise<void> {
const plaintextRecord = {
id: 'legacy-id-456',
username: 'legacyuser',
password_hash: 'plain-text-password-hash',
is_admin: false
};
const decrypted = DatabaseEncryption.decryptRecord('users', plaintextRecord);
if (decrypted.password_hash !== plaintextRecord.password_hash) {
throw new Error('Plain text fields should be returned as-is for backward compatibility');
}
if (decrypted.username !== plaintextRecord.username) {
throw new Error('Non-sensitive fields should be unchanged');
}
}
private async testErrorHandling(): Promise<void> {
const key = FieldEncryption.getFieldKey(this.testPassword, 'test');
try {
FieldEncryption.decryptField('invalid-json-data', key);
throw new Error('Should have thrown error for invalid JSON');
} catch (error) {
if (!error || !(error as Error).message.includes('decryption failed')) {
throw new Error('Should throw appropriate decryption error');
}
}
try {
const fakeEncrypted = JSON.stringify({ data: 'fake', iv: 'fake', tag: 'fake' });
FieldEncryption.decryptField(fakeEncrypted, key);
throw new Error('Should have thrown error for invalid encrypted data');
} catch (error) {
if (!error || !(error as Error).message.includes('Decryption failed')) {
throw new Error('Should throw appropriate error for corrupted data');
}
}
}
private async testPerformance(): Promise<void> {
const testData = 'Performance test data that is reasonably long to simulate real SSH keys and passwords.';
const key = FieldEncryption.getFieldKey(this.testPassword, 'performance-test');
const iterations = 100;
const startTime = Date.now();
for (let i = 0; i < iterations; i++) {
const encrypted = FieldEncryption.encryptField(testData, key);
const decrypted = FieldEncryption.decryptField(encrypted, key);
if (decrypted !== testData) {
throw new Error(`Performance test failed at iteration ${i}`);
}
}
const endTime = Date.now();
const totalTime = endTime - startTime;
const avgTime = totalTime / iterations;
console.log(` ⚡ Performance: ${iterations} encrypt/decrypt cycles in ${totalTime}ms (${avgTime.toFixed(2)}ms avg)`);
if (avgTime > 50) {
console.log(' ⚠️ Warning: Encryption operations are slower than expected');
}
}
static async validateProduction(): Promise<boolean> {
console.log('🔒 Validating production encryption setup...\n');
try {
const encryptionKey = process.env.DB_ENCRYPTION_KEY;
if (!encryptionKey) {
console.log('❌ DB_ENCRYPTION_KEY environment variable not set');
return false;
}
if (encryptionKey === 'default-key-change-me') {
console.log('❌ DB_ENCRYPTION_KEY is using default value (INSECURE)');
return false;
}
if (encryptionKey.length < 16) {
console.log('❌ DB_ENCRYPTION_KEY is too short (minimum 16 characters)');
return false;
}
DatabaseEncryption.initialize({
masterPassword: encryptionKey,
encryptionEnabled: true
});
const status = DatabaseEncryption.getEncryptionStatus();
if (!status.configValid) {
console.log('❌ Encryption configuration validation failed');
return false;
}
console.log('✅ Production encryption setup is valid');
return true;
} catch (error) {
console.log(`❌ Production validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
return false;
}
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
const testMode = process.argv[2];
if (testMode === 'production') {
EncryptionTest.validateProduction()
.then((success) => {
process.exit(success ? 0 : 1);
})
.catch((error) => {
console.error('Test execution failed:', error);
process.exit(1);
});
} else {
const test = new EncryptionTest();
test.runAllTests()
.then((success) => {
process.exit(success ? 0 : 1);
})
.catch((error) => {
console.error('Test execution failed:', error);
process.exit(1);
});
}
}
export { EncryptionTest };

View File

@@ -0,0 +1,240 @@
import crypto from 'crypto';
import os from 'os';
import fs from 'fs';
import path from 'path';
import { databaseLogger } from './logger.js';
interface ProtectedKeyData {
data: string;
iv: string;
tag: string;
version: string;
fingerprint: 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 generateDeviceFingerprint(): string {
try {
const features = [
os.hostname(),
os.platform(),
os.arch(),
process.cwd(),
this.getFileSystemFingerprint(),
this.getNetworkFingerprint()
];
const fingerprint = crypto.createHash('sha256')
.update(features.join('|'))
.digest('hex');
databaseLogger.debug('Generated device fingerprint', {
operation: 'fingerprint_generation',
fingerprintPrefix: fingerprint.substring(0, 8)
});
return fingerprint;
} catch (error) {
databaseLogger.error('Failed to generate device fingerprint', error, {
operation: 'fingerprint_generation_failed'
});
throw new Error('Device fingerprint generation failed');
}
}
private static getFileSystemFingerprint(): string {
try {
const stat = fs.statSync(process.cwd());
return `${stat.ino}-${stat.dev}`;
} catch {
return 'fs-unknown';
}
}
private static getNetworkFingerprint(): string {
try {
const networkInterfaces = os.networkInterfaces();
const macAddresses = [];
for (const interfaceName in networkInterfaces) {
const interfaces = networkInterfaces[interfaceName];
if (interfaces) {
for (const iface of interfaces) {
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
macAddresses.push(iface.mac);
}
}
}
}
// 使用第一个有效的MAC地址如果没有则使用fallback
return macAddresses.length > 0 ? macAddresses.sort()[0] : 'no-mac-found';
} catch {
return 'network-unknown';
}
}
private static deriveKEK(): Buffer {
const fingerprint = this.generateDeviceFingerprint();
const salt = Buffer.from(this.KEK_SALT);
const kek = crypto.pbkdf2Sync(
fingerprint,
salt,
this.KEK_ITERATIONS,
32,
'sha256'
);
databaseLogger.debug('Derived KEK from device fingerprint', {
operation: 'kek_derivation',
iterations: this.KEK_ITERATIONS
});
return kek;
}
static encryptMasterKey(masterKey: string): string {
if (!masterKey) {
throw new Error('Master key cannot be empty');
}
try {
const kek = this.deriveKEK();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', kek, iv) as any;
let encrypted = cipher.update(masterKey, 'hex', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
const protectedData: ProtectedKeyData = {
data: encrypted,
iv: iv.toString('hex'),
tag: tag.toString('hex'),
version: this.VERSION,
fingerprint: this.generateDeviceFingerprint().substring(0, 16)
};
const result = JSON.stringify(protectedData);
databaseLogger.info('Master key encrypted with device KEK', {
operation: 'master_key_encryption',
version: this.VERSION,
fingerprintPrefix: protectedData.fingerprint
});
return result;
} catch (error) {
databaseLogger.error('Failed to encrypt master key', error, {
operation: 'master_key_encryption_failed'
});
throw new Error('Master key encryption failed');
}
}
static decryptMasterKey(encryptedKey: string): string {
if (!encryptedKey) {
throw new Error('Encrypted key cannot be empty');
}
try {
const protectedData: ProtectedKeyData = JSON.parse(encryptedKey);
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('Device fingerprint mismatch detected', {
operation: 'master_key_decryption',
expected: protectedData.fingerprint,
current: currentFingerprint
});
throw new Error('Device fingerprint mismatch - key was encrypted on different machine');
}
const kek = this.deriveKEK();
const decipher = crypto.createDecipheriv('aes-256-gcm', kek, Buffer.from(protectedData.iv, 'hex')) as any;
decipher.setAuthTag(Buffer.from(protectedData.tag, 'hex'));
let decrypted = decipher.update(protectedData.data, 'hex', 'hex');
decrypted += decipher.final('hex');
databaseLogger.debug('Master key decrypted successfully', {
operation: 'master_key_decryption',
version: protectedData.version
});
return decrypted;
} catch (error) {
databaseLogger.error('Failed to decrypt master key', error, {
operation: 'master_key_decryption_failed'
});
throw new Error(`Master key decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
static isProtectedKey(data: string): boolean {
try {
const parsed = JSON.parse(data);
return !!(parsed.data && parsed.iv && parsed.tag && parsed.version && parsed.fingerprint);
} catch {
return false;
}
}
static validateProtection(): boolean {
try {
const testKey = crypto.randomBytes(32).toString('hex');
const encrypted = this.encryptMasterKey(testKey);
const decrypted = this.decryptMasterKey(encrypted);
const isValid = decrypted === testKey;
databaseLogger.info('Master key protection validation completed', {
operation: 'protection_validation',
result: isValid ? 'passed' : 'failed'
});
return isValid;
} catch (error) {
databaseLogger.error('Master key protection validation failed', error, {
operation: 'protection_validation_failed'
});
return false;
}
}
static getProtectionInfo(encryptedKey: string): {
version: string;
fingerprint: string;
isCurrentDevice: boolean;
} | null {
try {
if (!this.isProtectedKey(encryptedKey)) {
return null;
}
const protectedData: ProtectedKeyData = JSON.parse(encryptedKey);
const currentFingerprint = this.generateDeviceFingerprint().substring(0, 16);
return {
version: protectedData.version,
fingerprint: protectedData.fingerprint,
isCurrentDevice: protectedData.fingerprint === currentFingerprint
};
} catch {
return null;
}
}
}
export { MasterKeyProtection };
export type { ProtectedKeyData };