dev-1.7.0 #294
@@ -679,7 +679,7 @@ app.post("/database/backup", async (req, res) => {
|
|||||||
const backupPath = path.join(backupDir, backupFileName);
|
const backupPath = path.join(backupDir, backupFileName);
|
||||||
|
|
||||||
// Create encrypted backup directly from memory buffer
|
// Create encrypted backup directly from memory buffer
|
||||||
DatabaseFileEncryption.encryptDatabaseFromBuffer(dbBuffer, backupPath);
|
await DatabaseFileEncryption.encryptDatabaseFromBuffer(dbBuffer, backupPath);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -718,7 +718,7 @@ app.post("/database/restore", async (req, res) => {
|
|||||||
|
|
||||||
// Hardware compatibility check removed - no longer required
|
// Hardware compatibility check removed - no longer required
|
||||||
|
|
||||||
const restoredPath = DatabaseFileEncryption.restoreFromEncryptedBackup(
|
const restoredPath = await DatabaseFileEncryption.restoreFromEncryptedBackup(
|
||||||
backupPath,
|
backupPath,
|
||||||
targetPath,
|
targetPath,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { databaseLogger } from "../../utils/logger.js";
|
import { databaseLogger } from "../../utils/logger.js";
|
||||||
import { DatabaseFileEncryption } from "../../utils/database-file-encryption.js";
|
import { DatabaseFileEncryption } from "../../utils/database-file-encryption.js";
|
||||||
|
import { SystemCrypto } from "../../utils/system-crypto.js";
|
||||||
|
|
||||||
const dataDir = process.env.DATA_DIR || "./db/data";
|
const dataDir = process.env.DATA_DIR || "./db/data";
|
||||||
const dbDir = path.resolve(dataDir);
|
const dbDir = path.resolve(dataDir);
|
||||||
@@ -25,6 +26,13 @@ const encryptedDbPath = `${dbPath}.encrypted`;
|
|||||||
let actualDbPath = ":memory:"; // Always use memory database
|
let actualDbPath = ":memory:"; // Always use memory database
|
||||||
let memoryDatabase: Database.Database;
|
let memoryDatabase: Database.Database;
|
||||||
let isNewDatabase = false;
|
let isNewDatabase = false;
|
||||||
|
let sqlite: Database.Database; // Module-level sqlite instance
|
||||||
|
|
||||||
|
// Async initialization function to handle SystemCrypto and DatabaseFileEncryption
|
||||||
|
async function initializeDatabaseAsync(): Promise<void> {
|
||||||
|
// Initialize SystemCrypto database key first
|
||||||
|
const systemCrypto = SystemCrypto.getInstance();
|
||||||
|
await systemCrypto.initializeDatabaseKey();
|
||||||
|
|
||||||
if (enableFileEncryption) {
|
if (enableFileEncryption) {
|
||||||
try {
|
try {
|
||||||
@@ -38,11 +46,9 @@ if (enableFileEncryption) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Hardware compatibility check removed - using fixed seed encryption
|
// Decrypt database content to memory buffer (now async)
|
||||||
|
|
||||||
// Decrypt database content to memory buffer
|
|
||||||
const decryptedBuffer =
|
const decryptedBuffer =
|
||||||
DatabaseFileEncryption.decryptDatabaseToBuffer(encryptedDbPath);
|
await DatabaseFileEncryption.decryptDatabaseToBuffer(encryptedDbPath);
|
||||||
|
|
||||||
// Create in-memory database from decrypted buffer
|
// Create in-memory database from decrypted buffer
|
||||||
memoryDatabase = new Database(decryptedBuffer);
|
memoryDatabase = new Database(decryptedBuffer);
|
||||||
@@ -90,7 +96,6 @@ if (enableFileEncryption) {
|
|||||||
oldDb.close();
|
oldDb.close();
|
||||||
|
|
||||||
isNewDatabase = false;
|
isNewDatabase = false;
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -110,6 +115,12 @@ if (enableFileEncryption) {
|
|||||||
memoryDatabase = new Database(":memory:");
|
memoryDatabase = new Database(":memory:");
|
||||||
isNewDatabase = true;
|
isNewDatabase = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main async initialization function that combines database setup with schema creation
|
||||||
|
async function initializeCompleteDatabase(): Promise<void> {
|
||||||
|
// First initialize the database and SystemCrypto
|
||||||
|
await initializeDatabaseAsync();
|
||||||
|
|
||||||
databaseLogger.info(`Initializing SQLite database`, {
|
databaseLogger.info(`Initializing SQLite database`, {
|
||||||
operation: "db_init",
|
operation: "db_init",
|
||||||
@@ -121,7 +132,8 @@ databaseLogger.info(`Initializing SQLite database`, {
|
|||||||
isNewDatabase,
|
isNewDatabase,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sqlite = memoryDatabase;
|
// Create module-level sqlite instance after database is initialized
|
||||||
|
sqlite = memoryDatabase;
|
||||||
|
|
||||||
sqlite.exec(`
|
sqlite.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
@@ -244,6 +256,33 @@ sqlite.exec(`
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Run schema migrations
|
||||||
|
migrateSchema();
|
||||||
|
|
||||||
|
// Initialize default settings
|
||||||
|
try {
|
||||||
|
const row = sqlite
|
||||||
|
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
|
||||||
|
.get();
|
||||||
|
if (!row) {
|
||||||
|
databaseLogger.info("Initializing default settings", {
|
||||||
|
operation: "db_init",
|
||||||
|
setting: "allow_registration",
|
||||||
|
});
|
||||||
|
sqlite
|
||||||
|
.prepare(
|
||||||
|
"INSERT INTO settings (key, value) VALUES ('allow_registration', 'true')",
|
||||||
|
|
|||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
databaseLogger.warn("Could not initialize default settings", {
|
||||||
|
operation: "db_init",
|
||||||
|
error: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addColumnIfNotExists = (
|
const addColumnIfNotExists = (
|
||||||
table: string,
|
table: string,
|
||||||
column: string,
|
column: string,
|
||||||
@@ -366,33 +405,6 @@ const migrateSchema = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializeDatabase = async (): Promise<void> => {
|
|
||||||
migrateSchema();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const row = sqlite
|
|
||||||
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
|
|
||||||
.get();
|
|
||||||
if (!row) {
|
|
||||||
databaseLogger.info("Initializing default settings", {
|
|
||||||
operation: "db_init",
|
|
||||||
setting: "allow_registration",
|
|
||||||
});
|
|
||||||
sqlite
|
|
||||||
.prepare(
|
|
||||||
"INSERT INTO settings (key, value) VALUES ('allow_registration', 'true')",
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
databaseLogger.warn("Could not initialize default settings", {
|
|
||||||
operation: "db_init",
|
|
||||||
error: e,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to save in-memory database to encrypted file
|
// Function to save in-memory database to encrypted file
|
||||||
async function saveMemoryDatabaseToFile() {
|
async function saveMemoryDatabaseToFile() {
|
||||||
if (!memoryDatabase || !enableFileEncryption) return;
|
if (!memoryDatabase || !enableFileEncryption) return;
|
||||||
@@ -401,8 +413,8 @@ async function saveMemoryDatabaseToFile() {
|
|||||||
// Export in-memory database to buffer
|
// Export in-memory database to buffer
|
||||||
const buffer = memoryDatabase.serialize();
|
const buffer = memoryDatabase.serialize();
|
||||||
|
|
||||||
// Encrypt and save to file
|
// Encrypt and save to file (now async)
|
||||||
DatabaseFileEncryption.encryptDatabaseFromBuffer(buffer, encryptedDbPath);
|
await DatabaseFileEncryption.encryptDatabaseFromBuffer(buffer, encryptedDbPath);
|
||||||
|
|
||||||
databaseLogger.debug("In-memory database saved to encrypted file", {
|
databaseLogger.debug("In-memory database saved to encrypted file", {
|
||||||
operation: "memory_db_save",
|
operation: "memory_db_save",
|
||||||
@@ -498,7 +510,7 @@ async function handlePostInitFileEncryption() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDatabase()
|
initializeCompleteDatabase()
|
||||||
.then(() => handlePostInitFileEncryption())
|
.then(() => handlePostInitFileEncryption())
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
databaseLogger.error("Failed to initialize database", error, {
|
databaseLogger.error("Failed to initialize database", error, {
|
||||||
|
|||||||
@@ -2,54 +2,44 @@ import crypto from "crypto";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { databaseLogger } from "./logger.js";
|
import { databaseLogger } from "./logger.js";
|
||||||
|
import { SystemCrypto } from "./system-crypto.js";
|
||||||
|
|
||||||
interface EncryptedFileMetadata {
|
interface EncryptedFileMetadata {
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
version: string;
|
version: string;
|
||||||
fingerprint: string;
|
fingerprint: string;
|
||||||
salt: string;
|
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
|
keySource?: string; // Track where the key comes from (SystemCrypto) - v2 only
|
||||||
|
salt?: string; // Legacy v1 format only
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database file encryption - encrypts the entire SQLite database file at rest
|
* Database file encryption - encrypts the entire SQLite database file at rest
|
||||||
* This provides an additional security layer on top of field-level encryption
|
* Uses SystemCrypto for key management - no more fixed seed garbage!
|
||||||
|
*
|
||||||
|
* Linus principles applied:
|
||||||
|
* - Remove hardcoded keys security disaster
|
||||||
|
* - Use SystemCrypto instance keys for proper per-instance security
|
||||||
|
* - Simple and direct, no complex key derivation
|
||||||
*/
|
*/
|
||||||
class DatabaseFileEncryption {
|
class DatabaseFileEncryption {
|
||||||
private static readonly VERSION = "v1";
|
private static readonly VERSION = "v2";
|
||||||
private static readonly ALGORITHM = "aes-256-gcm";
|
private static readonly ALGORITHM = "aes-256-gcm";
|
||||||
private static readonly KEY_ITERATIONS = 100000;
|
|
||||||
private static readonly ENCRYPTED_FILE_SUFFIX = ".encrypted";
|
private static readonly ENCRYPTED_FILE_SUFFIX = ".encrypted";
|
||||||
private static readonly METADATA_FILE_SUFFIX = ".meta";
|
private static readonly METADATA_FILE_SUFFIX = ".meta";
|
||||||
|
private static systemCrypto = SystemCrypto.getInstance();
|
||||||
/**
|
|
||||||
* Generate file encryption key from fixed seed (no hardware dependency)
|
|
||||||
*/
|
|
||||||
private static generateFileEncryptionKey(salt: Buffer): Buffer {
|
|
||||||
// Use fixed seed for file encryption - simpler and more reliable than hardware fingerprint
|
|
||||||
const fixedSeed = process.env.DB_FILE_KEY || "termix-database-file-encryption-seed-v1";
|
|
||||||
|
|
||||||
const key = crypto.pbkdf2Sync(
|
|
||||||
fixedSeed,
|
|
||||||
salt,
|
|
||||||
this.KEY_ITERATIONS,
|
|
||||||
32, // 256 bits for AES-256
|
|
||||||
"sha256",
|
|
||||||
);
|
|
||||||
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt database from buffer (for in-memory databases)
|
* Encrypt database from buffer (for in-memory databases)
|
||||||
*/
|
*/
|
||||||
static encryptDatabaseFromBuffer(buffer: Buffer, targetPath: string): string {
|
static async encryptDatabaseFromBuffer(buffer: Buffer, targetPath: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
|
// Get database key from SystemCrypto (no more fixed seed garbage!)
|
||||||
|
const key = await this.systemCrypto.getDatabaseKey();
|
||||||
|
|
||||||
// Generate encryption components
|
// Generate encryption components
|
||||||
const salt = crypto.randomBytes(32);
|
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const key = this.generateFileEncryptionKey(salt);
|
|
||||||
|
|
||||||
// Encrypt the buffer
|
// Encrypt the buffer
|
||||||
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
|
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
|
||||||
@@ -61,9 +51,9 @@ class DatabaseFileEncryption {
|
|||||||
iv: iv.toString("hex"),
|
iv: iv.toString("hex"),
|
||||||
tag: tag.toString("hex"),
|
tag: tag.toString("hex"),
|
||||||
version: this.VERSION,
|
version: this.VERSION,
|
||||||
fingerprint: "termix-v1-file", // Fixed identifier instead of hardware fingerprint
|
fingerprint: "termix-v2-systemcrypto", // SystemCrypto managed key
|
||||||
salt: salt.toString("hex"),
|
|
||||||
algorithm: this.ALGORITHM,
|
algorithm: this.ALGORITHM,
|
||||||
|
keySource: "SystemCrypto",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write encrypted file and metadata
|
// Write encrypted file and metadata
|
||||||
@@ -86,7 +76,7 @@ class DatabaseFileEncryption {
|
|||||||
/**
|
/**
|
||||||
* Encrypt database file
|
* Encrypt database file
|
||||||
*/
|
*/
|
||||||
static encryptDatabaseFile(sourcePath: string, targetPath?: string): string {
|
static async encryptDatabaseFile(sourcePath: string, targetPath?: string): Promise<string> {
|
||||||
if (!fs.existsSync(sourcePath)) {
|
if (!fs.existsSync(sourcePath)) {
|
||||||
throw new Error(`Source database file does not exist: ${sourcePath}`);
|
throw new Error(`Source database file does not exist: ${sourcePath}`);
|
||||||
}
|
}
|
||||||
@@ -99,10 +89,11 @@ class DatabaseFileEncryption {
|
|||||||
// Read source file
|
// Read source file
|
||||||
const sourceData = fs.readFileSync(sourcePath);
|
const sourceData = fs.readFileSync(sourcePath);
|
||||||
|
|
||||||
|
// Get database key from SystemCrypto (no more fixed seed garbage!)
|
||||||
|
const key = await this.systemCrypto.getDatabaseKey();
|
||||||
|
|
||||||
// Generate encryption components
|
// Generate encryption components
|
||||||
const salt = crypto.randomBytes(32);
|
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const key = this.generateFileEncryptionKey(salt);
|
|
||||||
|
|
||||||
// Encrypt the file
|
// Encrypt the file
|
||||||
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
|
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
|
||||||
@@ -117,9 +108,9 @@ class DatabaseFileEncryption {
|
|||||||
iv: iv.toString("hex"),
|
iv: iv.toString("hex"),
|
||||||
tag: tag.toString("hex"),
|
tag: tag.toString("hex"),
|
||||||
version: this.VERSION,
|
version: this.VERSION,
|
||||||
fingerprint: "termix-v1-file", // Fixed identifier instead of hardware fingerprint
|
fingerprint: "termix-v2-systemcrypto", // SystemCrypto managed key
|
||||||
salt: salt.toString("hex"),
|
|
||||||
algorithm: this.ALGORITHM,
|
algorithm: this.ALGORITHM,
|
||||||
|
keySource: "SystemCrypto",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write encrypted file and metadata
|
// Write encrypted file and metadata
|
||||||
@@ -151,7 +142,7 @@ class DatabaseFileEncryption {
|
|||||||
/**
|
/**
|
||||||
* Decrypt database file to buffer (for in-memory usage)
|
* Decrypt database file to buffer (for in-memory usage)
|
||||||
*/
|
*/
|
||||||
static decryptDatabaseToBuffer(encryptedPath: string): Buffer {
|
static async decryptDatabaseToBuffer(encryptedPath: string): Promise<Buffer> {
|
||||||
if (!fs.existsSync(encryptedPath)) {
|
if (!fs.existsSync(encryptedPath)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Encrypted database file does not exist: ${encryptedPath}`,
|
`Encrypted database file does not exist: ${encryptedPath}`,
|
||||||
@@ -168,19 +159,29 @@ class DatabaseFileEncryption {
|
|||||||
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
||||||
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
||||||
|
|
||||||
// Validate metadata version
|
|
||||||
if (metadata.version !== this.VERSION) {
|
|
||||||
throw new Error(`Unsupported encryption version: ${metadata.version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hardware fingerprint validation removed - no longer required
|
|
||||||
|
|
||||||
// Read encrypted data
|
// Read encrypted data
|
||||||
const encryptedData = fs.readFileSync(encryptedPath);
|
const encryptedData = fs.readFileSync(encryptedPath);
|
||||||
|
|
||||||
// Generate decryption key
|
// Get decryption key based on version
|
||||||
|
let key: Buffer;
|
||||||
|
if (metadata.version === "v2") {
|
||||||
|
// New v2 format: use SystemCrypto key
|
||||||
|
key = await this.systemCrypto.getDatabaseKey();
|
||||||
|
} else if (metadata.version === "v1") {
|
||||||
|
// Legacy v1 format: use deprecated salt-based key derivation
|
||||||
|
databaseLogger.warn("Decrypting legacy v1 encrypted database - consider upgrading", {
|
||||||
|
operation: "decrypt_legacy_v1",
|
||||||
|
path: encryptedPath
|
||||||
|
});
|
||||||
|
if (!metadata.salt) {
|
||||||
|
throw new Error("v1 encrypted file missing required salt field");
|
||||||
|
}
|
||||||
const salt = Buffer.from(metadata.salt, "hex");
|
const salt = Buffer.from(metadata.salt, "hex");
|
||||||
const key = this.generateFileEncryptionKey(salt);
|
const fixedSeed = process.env.DB_FILE_KEY || "termix-database-file-encryption-seed-v1";
|
||||||
|
key = crypto.pbkdf2Sync(fixedSeed, salt, 100000, 32, "sha256");
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported encryption version: ${metadata.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Decrypt to buffer
|
// Decrypt to buffer
|
||||||
const decipher = crypto.createDecipheriv(
|
const decipher = crypto.createDecipheriv(
|
||||||
@@ -210,10 +211,10 @@ class DatabaseFileEncryption {
|
|||||||
/**
|
/**
|
||||||
* Decrypt database file
|
* Decrypt database file
|
||||||
*/
|
*/
|
||||||
static decryptDatabaseFile(
|
static async decryptDatabaseFile(
|
||||||
encryptedPath: string,
|
encryptedPath: string,
|
||||||
targetPath?: string,
|
targetPath?: string,
|
||||||
): string {
|
): Promise<string> {
|
||||||
if (!fs.existsSync(encryptedPath)) {
|
if (!fs.existsSync(encryptedPath)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Encrypted database file does not exist: ${encryptedPath}`,
|
`Encrypted database file does not exist: ${encryptedPath}`,
|
||||||
@@ -233,19 +234,29 @@ class DatabaseFileEncryption {
|
|||||||
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
||||||
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
||||||
|
|
||||||
// Validate metadata version
|
|
||||||
if (metadata.version !== this.VERSION) {
|
|
||||||
throw new Error(`Unsupported encryption version: ${metadata.version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hardware fingerprint validation removed - no longer required
|
|
||||||
|
|
||||||
// Read encrypted data
|
// Read encrypted data
|
||||||
const encryptedData = fs.readFileSync(encryptedPath);
|
const encryptedData = fs.readFileSync(encryptedPath);
|
||||||
|
|
||||||
// Generate decryption key
|
// Get decryption key based on version
|
||||||
|
let key: Buffer;
|
||||||
|
if (metadata.version === "v2") {
|
||||||
|
// New v2 format: use SystemCrypto key
|
||||||
|
key = await this.systemCrypto.getDatabaseKey();
|
||||||
|
} else if (metadata.version === "v1") {
|
||||||
|
// Legacy v1 format: use deprecated salt-based key derivation
|
||||||
|
databaseLogger.warn("Decrypting legacy v1 encrypted database - consider upgrading", {
|
||||||
|
operation: "decrypt_legacy_v1",
|
||||||
|
path: encryptedPath
|
||||||
|
});
|
||||||
|
if (!metadata.salt) {
|
||||||
|
throw new Error("v1 encrypted file missing required salt field");
|
||||||
|
}
|
||||||
const salt = Buffer.from(metadata.salt, "hex");
|
const salt = Buffer.from(metadata.salt, "hex");
|
||||||
const key = this.generateFileEncryptionKey(salt);
|
const fixedSeed = process.env.DB_FILE_KEY || "termix-database-file-encryption-seed-v1";
|
||||||
|
key = crypto.pbkdf2Sync(fixedSeed, salt, 100000, 32, "sha256");
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported encryption version: ${metadata.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Decrypt the file
|
// Decrypt the file
|
||||||
const decipher = crypto.createDecipheriv(
|
const decipher = crypto.createDecipheriv(
|
||||||
@@ -344,10 +355,10 @@ class DatabaseFileEncryption {
|
|||||||
/**
|
/**
|
||||||
* Securely backup database by creating encrypted copy
|
* Securely backup database by creating encrypted copy
|
||||||
*/
|
*/
|
||||||
static createEncryptedBackup(
|
static async createEncryptedBackup(
|
||||||
databasePath: string,
|
databasePath: string,
|
||||||
backupDir: string,
|
backupDir: string,
|
||||||
): string {
|
): Promise<string> {
|
||||||
if (!fs.existsSync(databasePath)) {
|
if (!fs.existsSync(databasePath)) {
|
||||||
throw new Error(`Database file does not exist: ${databasePath}`);
|
throw new Error(`Database file does not exist: ${databasePath}`);
|
||||||
}
|
}
|
||||||
@@ -363,7 +374,7 @@ class DatabaseFileEncryption {
|
|||||||
const backupPath = path.join(backupDir, backupFileName);
|
const backupPath = path.join(backupDir, backupFileName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encryptedPath = this.encryptDatabaseFile(databasePath, backupPath);
|
const encryptedPath = await this.encryptDatabaseFile(databasePath, backupPath);
|
||||||
|
|
||||||
databaseLogger.info("Encrypted database backup created", {
|
databaseLogger.info("Encrypted database backup created", {
|
||||||
operation: "database_backup",
|
operation: "database_backup",
|
||||||
@@ -386,16 +397,16 @@ class DatabaseFileEncryption {
|
|||||||
/**
|
/**
|
||||||
* Restore database from encrypted backup
|
* Restore database from encrypted backup
|
||||||
*/
|
*/
|
||||||
static restoreFromEncryptedBackup(
|
static async restoreFromEncryptedBackup(
|
||||||
backupPath: string,
|
backupPath: string,
|
||||||
targetPath: string,
|
targetPath: string,
|
||||||
): string {
|
): Promise<string> {
|
||||||
if (!this.isEncryptedDatabaseFile(backupPath)) {
|
if (!this.isEncryptedDatabaseFile(backupPath)) {
|
||||||
throw new Error("Invalid encrypted backup file");
|
throw new Error("Invalid encrypted backup file");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const restoredPath = this.decryptDatabaseFile(backupPath, targetPath);
|
const restoredPath = await this.decryptDatabaseFile(backupPath, targetPath);
|
||||||
|
|
||||||
databaseLogger.info("Database restored from encrypted backup", {
|
databaseLogger.info("Database restored from encrypted backup", {
|
||||||
operation: "database_restore",
|
operation: "database_restore",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { eq } from "drizzle-orm";
|
|||||||
import { databaseLogger } from "./logger.js";
|
import { databaseLogger } from "./logger.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SystemCrypto - Open source friendly JWT key management
|
* SystemCrypto - Open source friendly system key management
|
||||||
*
|
*
|
||||||
* Linus principles:
|
* Linus principles:
|
||||||
* - Remove complex "system master key" layer - doesn't solve real threats
|
* - Remove complex "system master key" layer - doesn't solve real threats
|
||||||
@@ -18,10 +18,13 @@ import { databaseLogger } from "./logger.js";
|
|||||||
class SystemCrypto {
|
class SystemCrypto {
|
||||||
private static instance: SystemCrypto;
|
private static instance: SystemCrypto;
|
||||||
private jwtSecret: string | null = null;
|
private jwtSecret: string | null = null;
|
||||||
|
private databaseKey: Buffer | null = null;
|
||||||
|
|
||||||
// Storage path configuration
|
// Storage path configuration
|
||||||
private static readonly JWT_SECRET_FILE = path.join(process.cwd(), '.termix', 'jwt.key');
|
private static readonly JWT_SECRET_FILE = path.join(process.cwd(), '.termix', 'jwt.key');
|
||||||
private static readonly JWT_SECRET_DB_KEY = 'system_jwt_secret';
|
private static readonly JWT_SECRET_DB_KEY = 'system_jwt_secret';
|
||||||
|
private static readonly DATABASE_KEY_FILE = path.join(process.cwd(), '.termix', 'db.key');
|
||||||
|
private static readonly DATABASE_KEY_DB_KEY = 'system_database_key';
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@@ -95,6 +98,58 @@ class SystemCrypto {
|
|||||||
return this.jwtSecret!;
|
return this.jwtSecret!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database encryption key - same pattern as JWT but for database file encryption
|
||||||
|
*/
|
||||||
|
async initializeDatabaseKey(): Promise<void> {
|
||||||
|
try {
|
||||||
|
databaseLogger.info("Initializing database encryption key", {
|
||||||
|
operation: "db_key_init",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Environment variable priority (production best practice)
|
||||||
|
const envKey = process.env.DATABASE_KEY;
|
||||||
|
if (envKey && envKey.length >= 64) {
|
||||||
|
this.databaseKey = Buffer.from(envKey, 'hex');
|
||||||
|
databaseLogger.info("✅ Using database key from environment variable", {
|
||||||
|
operation: "db_key_env_loaded",
|
||||||
|
source: "environment"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check filesystem storage
|
||||||
|
const fileKey = await this.loadDatabaseKeyFromFile();
|
||||||
|
if (fileKey) {
|
||||||
|
this.databaseKey = fileKey;
|
||||||
|
databaseLogger.info("✅ Loaded database key from file", {
|
||||||
|
operation: "db_key_file_loaded",
|
||||||
|
source: "file"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Generate new key and persist (NO database storage to avoid circular dependency)
|
||||||
|
await this.generateAndStoreDatabaseKey();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
databaseLogger.error("Failed to initialize database key", error, {
|
||||||
|
operation: "db_key_init_failed",
|
||||||
|
});
|
||||||
|
throw new Error("Database key initialization failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database encryption key
|
||||||
|
*/
|
||||||
|
async getDatabaseKey(): Promise<Buffer> {
|
||||||
|
if (!this.databaseKey) {
|
||||||
|
await this.initializeDatabaseKey();
|
||||||
|
}
|
||||||
|
return this.databaseKey!;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate new key and persist storage
|
* Generate new key and persist storage
|
||||||
*/
|
*/
|
||||||
@@ -168,7 +223,77 @@ class SystemCrypto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Database storage methods =====
|
// ===== Database key generation and storage methods =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate new database key and persist to file storage only
|
||||||
|
* (avoid circular dependency with database)
|
||||||
|
*/
|
||||||
|
private async generateAndStoreDatabaseKey(): Promise<void> {
|
||||||
|
const newKey = crypto.randomBytes(32); // 256-bit key for AES-256
|
||||||
|
const instanceId = crypto.randomBytes(8).toString('hex');
|
||||||
|
|
||||||
|
databaseLogger.info("🔑 Generating new database encryption key for this Termix instance", {
|
||||||
|
operation: "db_key_generate",
|
||||||
|
instanceId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only try file storage (no database storage to avoid circular dependency)
|
||||||
|
try {
|
||||||
|
await this.saveDatabaseKeyToFile(newKey);
|
||||||
|
databaseLogger.info("✅ Database key saved to file", {
|
||||||
|
operation: "db_key_file_saved",
|
||||||
|
path: SystemCrypto.DATABASE_KEY_FILE
|
||||||
|
});
|
||||||
|
} catch (fileError) {
|
||||||
|
databaseLogger.error("❌ Failed to save database key to file", {
|
||||||
|
operation: "db_key_file_save_failed",
|
||||||
|
error: fileError instanceof Error ? fileError.message : "Unknown error",
|
||||||
|
note: "Database encryption cannot work without persistent key storage"
|
||||||
|
});
|
||||||
|
throw new Error("Database key file storage is required for database encryption");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.databaseKey = newKey;
|
||||||
|
|
||||||
|
databaseLogger.success("🔐 This Termix instance now has a unique database encryption key", {
|
||||||
|
operation: "db_key_generated_success",
|
||||||
|
instanceId,
|
||||||
|
note: "Database file is now encrypted at rest"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save database key to file (binary format)
|
||||||
|
*/
|
||||||
|
private async saveDatabaseKeyToFile(key: Buffer): Promise<void> {
|
||||||
|
const dir = path.dirname(SystemCrypto.DATABASE_KEY_FILE);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
await fs.writeFile(SystemCrypto.DATABASE_KEY_FILE, key.toString('hex'), {
|
||||||
|
mode: 0o600 // Only owner can read/write
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load database key from file
|
||||||
|
*/
|
||||||
|
private async loadDatabaseKeyFromFile(): Promise<Buffer | null> {
|
||||||
|
try {
|
||||||
|
const keyHex = await fs.readFile(SystemCrypto.DATABASE_KEY_FILE, 'utf8');
|
||||||
|
if (keyHex.trim().length >= 64) { // 32 bytes = 64 hex chars
|
||||||
|
return Buffer.from(keyHex.trim(), 'hex');
|
||||||
|
}
|
||||||
|
databaseLogger.warn("Database key file exists but too short", {
|
||||||
|
operation: "db_key_file_invalid",
|
||||||
|
length: keyHex.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// File doesn't exist or can't be read, this is normal
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== JWT Database storage methods =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save key to database (plaintext storage, don't pretend encryption helps)
|
* Save key to database (plaintext storage, don't pretend encryption helps)
|
||||||
|
|||||||
Reference in New Issue
Block a user
This is an excellent and critical piece of error handling. Failing fast when database decryption fails is the correct approach to prevent data corruption or loss. The detailed logging and explicit error message will be invaluable for debugging if such a critical failure occurs. This significantly improves the robustness of the database initialization process.