SECURITY: Implement SystemCrypto database key auto-generation

Replace fixed seed database encryption with per-instance unique keys:

- Add database key management to SystemCrypto alongside JWT keys
- Remove hardcoded default seed security vulnerability
- Implement auto-generation of unique database encryption keys
- Add backward compatibility for legacy v1 encrypted files
- Update DatabaseFileEncryption to use SystemCrypto keys
- Refactor database initialization to async architecture

Security improvements:
- Each Termix instance gets unique database encryption key
- Keys stored in .termix/db.key with 600 permissions
- Environment variable DATABASE_KEY support for production
- Eliminated fixed seed "termix-database-file-encryption-seed-v1"

Architecture: SystemCrypto (database) + UserCrypto (KEK-DEK) dual-layer
This commit is contained in:
ZacharyZcR
2025-09-22 07:50:01 +08:00
parent dca4a89a1a
commit ed11b309f4
4 changed files with 320 additions and 172 deletions

View File

@@ -5,6 +5,7 @@ import fs from "fs";
import path from "path";
import { databaseLogger } from "../../utils/logger.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 dbDir = path.resolve(dataDir);
@@ -25,105 +26,116 @@ const encryptedDbPath = `${dbPath}.encrypted`;
let actualDbPath = ":memory:"; // Always use memory database
let memoryDatabase: Database.Database;
let isNewDatabase = false;
let sqlite: Database.Database; // Module-level sqlite instance
if (enableFileEncryption) {
try {
// Check if encrypted database exists
if (DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)) {
databaseLogger.info(
"Found encrypted database file, loading into memory...",
{
operation: "db_memory_load",
encryptedPath: encryptedDbPath,
},
);
// 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();
// Hardware compatibility check removed - using fixed seed encryption
if (enableFileEncryption) {
try {
// Check if encrypted database exists
if (DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)) {
databaseLogger.info(
"Found encrypted database file, loading into memory...",
{
operation: "db_memory_load",
encryptedPath: encryptedDbPath,
},
);
// Decrypt database content to memory buffer
const decryptedBuffer =
DatabaseFileEncryption.decryptDatabaseToBuffer(encryptedDbPath);
// Decrypt database content to memory buffer (now async)
const decryptedBuffer =
await DatabaseFileEncryption.decryptDatabaseToBuffer(encryptedDbPath);
// Create in-memory database from decrypted buffer
memoryDatabase = new Database(decryptedBuffer);
} else {
memoryDatabase = new Database(":memory:");
isNewDatabase = true;
// Create in-memory database from decrypted buffer
memoryDatabase = new Database(decryptedBuffer);
} else {
memoryDatabase = new Database(":memory:");
isNewDatabase = true;
// Check if there's an old unencrypted database to migrate
if (fs.existsSync(dbPath)) {
// Load old database and copy its content to memory database
const oldDb = new Database(dbPath, { readonly: true });
// Check if there's an old unencrypted database to migrate
if (fs.existsSync(dbPath)) {
// Load old database and copy its content to memory database
const oldDb = new Database(dbPath, { readonly: true });
// Get all table schemas and data from old database
const tables = oldDb
.prepare(
`
SELECT name, sql FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`,
)
.all() as { name: string; sql: string }[];
// Get all table schemas and data from old database
const tables = oldDb
.prepare(
`
SELECT name, sql FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`,
)
.all() as { name: string; sql: string }[];
// Create tables in memory database
for (const table of tables) {
memoryDatabase.exec(table.sql);
}
// Create tables in memory database
for (const table of tables) {
memoryDatabase.exec(table.sql);
}
// Copy data for each table
for (const table of tables) {
const rows = oldDb.prepare(`SELECT * FROM ${table.name}`).all();
if (rows.length > 0) {
const columns = Object.keys(rows[0]);
const placeholders = columns.map(() => "?").join(", ");
const insertStmt = memoryDatabase.prepare(
`INSERT INTO ${table.name} (${columns.join(", ")}) VALUES (${placeholders})`,
);
// Copy data for each table
for (const table of tables) {
const rows = oldDb.prepare(`SELECT * FROM ${table.name}`).all();
if (rows.length > 0) {
const columns = Object.keys(rows[0]);
const placeholders = columns.map(() => "?").join(", ");
const insertStmt = memoryDatabase.prepare(
`INSERT INTO ${table.name} (${columns.join(", ")}) VALUES (${placeholders})`,
);
for (const row of rows) {
const values = columns.map((col) => (row as any)[col]);
insertStmt.run(values);
for (const row of rows) {
const values = columns.map((col) => (row as any)[col]);
insertStmt.run(values);
}
}
}
oldDb.close();
isNewDatabase = false;
}
oldDb.close();
isNewDatabase = false;
} else {
}
}
} catch (error) {
databaseLogger.error("Failed to initialize memory database", error, {
operation: "db_memory_init_failed",
});
} catch (error) {
databaseLogger.error("Failed to initialize memory database", error, {
operation: "db_memory_init_failed",
});
// If file encryption is critical, fail fast
if (process.env.DB_FILE_ENCRYPTION_REQUIRED === "true") {
throw error;
}
// If file encryption is critical, fail fast
if (process.env.DB_FILE_ENCRYPTION_REQUIRED === "true") {
throw error;
}
memoryDatabase = new Database(":memory:");
isNewDatabase = true;
}
} else {
memoryDatabase = new Database(":memory:");
isNewDatabase = true;
}
} else {
memoryDatabase = new Database(":memory:");
isNewDatabase = true;
}
databaseLogger.info(`Initializing SQLite database`, {
operation: "db_init",
path: actualDbPath,
encrypted:
enableFileEncryption &&
DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath),
inMemory: true,
isNewDatabase,
});
// Main async initialization function that combines database setup with schema creation
async function initializeCompleteDatabase(): Promise<void> {
// First initialize the database and SystemCrypto
await initializeDatabaseAsync();
const sqlite = memoryDatabase;
databaseLogger.info(`Initializing SQLite database`, {
operation: "db_init",
path: actualDbPath,
encrypted:
enableFileEncryption &&
DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath),
inMemory: true,
isNewDatabase,
});
sqlite.exec(`
// Create module-level sqlite instance after database is initialized
sqlite = memoryDatabase;
sqlite.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
@@ -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 = (
table: 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
async function saveMemoryDatabaseToFile() {
if (!memoryDatabase || !enableFileEncryption) return;
@@ -401,8 +413,8 @@ async function saveMemoryDatabaseToFile() {
// Export in-memory database to buffer
const buffer = memoryDatabase.serialize();
// Encrypt and save to file
DatabaseFileEncryption.encryptDatabaseFromBuffer(buffer, encryptedDbPath);
// Encrypt and save to file (now async)
await DatabaseFileEncryption.encryptDatabaseFromBuffer(buffer, encryptedDbPath);
databaseLogger.debug("In-memory database saved to encrypted file", {
operation: "memory_db_save",
@@ -498,7 +510,7 @@ async function handlePostInitFileEncryption() {
}
}
initializeDatabase()
initializeCompleteDatabase()
.then(() => handlePostInitFileEncryption())
.catch((error) => {
databaseLogger.error("Failed to initialize database", error, {