SIMPLIFY: Delete fake migration system and implement honest legacy user handling

This commit removes 500+ lines of fake "migration" code that admitted it couldn't
do what it claimed to do. Following Linus principles: if code can't deliver on
its promise, delete it rather than pretend.

Changes:
- DELETE: security-migration.ts (448 lines of fake migration logic)
- DELETE: SECURITY_REFACTOR_PLAN.md (outdated documentation)
- DELETE: /encryption/migrate API endpoint (non-functional)
- REPLACE: Complex "migration" with simple 3-line legacy user setup
- CLEAN: Remove all migration imports and references

The new approach is honest: legacy users get encryption setup on first login.
No fake progress bars, no false promises, no broken complexity.

Good code doesn't pretend to do things it can't do.
This commit is contained in:
ZacharyZcR
2025-09-21 21:23:00 +08:00
parent b9caa82ad4
commit cc5f1fd25a
4 changed files with 16 additions and 599 deletions

View File

@@ -1,94 +0,0 @@
# Termix 安全重构计划
## 现状分析
- 当前所有密钥都用base64编码存储在数据库
- JWT Secret和数据加密密钥混合管理
- 没有真正的KEK-DEK分离
- 数据库文件泄露 = 完全沦陷
## 目标架构
### 密钥层次
```
用户密码 → KEK → DEK → 字段加密密钥 → 数据
系统启动 → JWT Secret → JWT Token → API认证
```
### 存储分离
```
系统级settings.system_jwt_secret (base64保护)
用户级settings.user_kek_salt_${userId}
用户级settings.user_encrypted_dek_${userId} (KEK保护)
```
## 修复步骤
### 第1步新建分离的密钥管理类
- [ ] 创建 SystemKeyManager (JWT密钥)
- [ ] 创建 UserKeyManager (用户数据密钥)
- [ ] 创建 SecuritySession (会话管理)
### 第2步重构认证流程
- [ ] 修改用户注册生成用户专属KEK salt和DEK
- [ ] 修改用户登录:验证密码 + 解锁数据密钥
- [ ] 修改JWT验证系统密钥验证 + 用户会话检查
### 第3步重构数据加密
- [ ] 分离数据加密和JWT密钥初始化
- [ ] 修改EncryptedDBOperations使用用户会话密钥
- [ ] 添加会话过期处理
### 第4步数据库迁移
- [ ] 创建迁移脚本:现有数据 → KEK保护
- [ ] 向后兼容处理
- [ ] 安全删除旧密钥
### 第5步API修改
- [ ] 添加用户密码验证接口
- [ ] 修改所有加密相关接口
- [ ] 添加会话管理接口
## 文件修改清单
### 新建文件
- src/backend/utils/system-key-manager.ts
- src/backend/utils/user-key-manager.ts
- src/backend/utils/security-session.ts
- src/backend/utils/security-migration.ts
### 修改文件
- src/backend/utils/encryption-key-manager.ts (简化或删除)
- src/backend/utils/database-encryption.ts
- src/backend/utils/encrypted-db-operations.ts
- src/backend/database/routes/users.ts
- src/backend/database/database.ts
### 数据库Schema
- 新增user_kek_salt_${userId}
- 新增user_encrypted_dek_${userId}
- 修改system_jwt_secret (从current混合模式分离)
## 安全考虑
### 密钥生命周期
- JWT Secret: 应用生命周期
- 用户KEK: 永不存储,从密码推导
- 用户DEK: 会话期间,内存存储
- 字段密钥: 临时推导,立即销毁
### 会话管理
- 数据会话独立于JWT有效期
- 非活跃自动过期
- 用户登出立即清理
### 向后兼容
- 检测旧格式数据
- 用户登录时自动迁移
- 迁移完成后删除旧密钥
## 测试计划
- [ ] 密钥生成和推导测试
- [ ] 加密解密正确性测试
- [ ] 会话管理测试
- [ ] 迁移流程测试
- [ ] 性能影响评估

View File

@@ -13,7 +13,6 @@ import "dotenv/config";
import { databaseLogger, apiLogger } from "../utils/logger.js";
import { SecuritySession } from "../utils/security-session.js";
import { DatabaseEncryption } from "../utils/database-encryption.js";
import { SecurityMigration } from "../utils/security-migration.js";
import { DatabaseFileEncryption } from "../utils/database-file-encryption.js";
const app = express();
@@ -294,11 +293,9 @@ app.get("/encryption/status", async (req, res) => {
try {
const securitySession = SecuritySession.getInstance();
const securityStatus = await securitySession.getSecurityStatus();
const migrationStatus = await SecurityMigration.checkMigrationStatus();
res.json({
security: securityStatus,
migration: migrationStatus,
version: "v2-kek-dek",
});
} catch (error) {
@@ -337,47 +334,6 @@ app.post("/encryption/initialize", async (req, res) => {
}
});
app.post("/encryption/migrate", async (req, res) => {
try {
const { dryRun = false } = req.body;
const migration = new SecurityMigration({
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 {
@@ -654,7 +610,6 @@ app.listen(PORT, async () => {
"/releases/rss",
"/encryption/status",
"/encryption/initialize",
"/encryption/migrate",
"/encryption/regenerate",
"/database/export",
"/database/import",

View File

@@ -7,6 +7,7 @@ import {
fileManagerPinned,
fileManagerShortcuts,
dismissedAlerts,
settings,
} from "../db/schema.js";
import { eq, and } from "drizzle-orm";
import bcrypt from "bcryptjs";
@@ -18,7 +19,6 @@ import type { Request, Response, NextFunction } from "express";
import { authLogger, apiLogger } from "../../utils/logger.js";
import { SecuritySession } from "../../utils/security-session.js";
import { UserKeyManager } from "../../utils/user-key-manager.js";
import { SecurityMigration } from "../../utils/security-migration.js";
// Get security session instance
const securitySession = SecuritySession.getInstance();
@@ -785,24 +785,29 @@ router.post("/login", async (req, res) => {
return res.status(401).json({ error: "Incorrect password" });
}
// Check and handle user migration (from old encryption system)
let migrationPerformed = false;
// Check if legacy user needs encryption setup
try {
migrationPerformed = await SecurityMigration.handleUserLoginMigration(userRecord.id, password);
if (migrationPerformed) {
authLogger.success("User encryption migrated during login", {
operation: "login_migration_success",
const kekSalt = await db
.select()
.from(settings)
.where(eq(settings.key, `user_kek_salt_${userRecord.id}`));
if (kekSalt.length === 0) {
// Legacy user first login - set up new encryption
await securitySession.registerUser(userRecord.id, password);
authLogger.success("Legacy user encryption initialized", {
operation: "legacy_user_setup",
username,
userId: userRecord.id,
});
}
} catch (migrationError) {
authLogger.error("Failed to migrate user during login", migrationError, {
operation: "login_migration_failed",
} catch (setupError) {
authLogger.error("Failed to initialize user encryption", setupError, {
operation: "user_encryption_setup_failed",
username,
userId: userRecord.id,
});
// Migration failure should not block login, but needs to be logged
// Encryption setup failure should not block login for existing users
}
// Unlock user data keys

View File

@@ -1,449 +0,0 @@
#!/usr/bin/env node
import { db } from "../database/db/index.js";
import { settings, users, sshData, sshCredentials } from "../database/db/schema.js";
import { eq, sql } from "drizzle-orm";
import { SecuritySession } from "./security-session.js";
import { UserKeyManager } from "./user-key-manager.js";
import { DatabaseEncryption } from "./database-encryption.js";
import { EncryptedDBOperations } from "./encrypted-db-operations.js";
import { FieldEncryption } from "./encryption.js";
import { databaseLogger } from "./logger.js";
interface MigrationConfig {
dryRun?: boolean;
backupEnabled?: boolean;
forceRegeneration?: boolean;
}
interface MigrationResult {
success: boolean;
usersProcessed: number;
recordsMigrated: number;
errors: string[];
warnings: string[];
}
/**
* SecurityMigration - Migrate from old encryption system to KEK-DEK architecture
*
* Migration steps:
* 1. Detect existing system state
* 2. Backup existing data
* 3. Initialize new security system
* 4. Set up KEK-DEK for existing users
* 5. Migrate encrypted data
* 6. Clean up old keys
*/
class SecurityMigration {
private config: MigrationConfig;
private securitySession: SecuritySession;
private userKeyManager: UserKeyManager;
constructor(config: MigrationConfig = {}) {
this.config = {
dryRun: config.dryRun ?? false,
backupEnabled: config.backupEnabled ?? true,
forceRegeneration: config.forceRegeneration ?? false,
};
this.securitySession = SecuritySession.getInstance();
this.userKeyManager = UserKeyManager.getInstance();
}
/**
* Run complete migration
*/
async runMigration(): Promise<MigrationResult> {
const result: MigrationResult = {
success: false,
usersProcessed: 0,
recordsMigrated: 0,
errors: [],
warnings: [],
};
try {
databaseLogger.info("Starting security migration to KEK-DEK architecture", {
operation: "security_migration_start",
dryRun: this.config.dryRun,
backupEnabled: this.config.backupEnabled,
});
// 1. Check migration prerequisites
await this.validatePrerequisites();
// 2. Create backup
if (this.config.backupEnabled && !this.config.dryRun) {
await this.createBackup();
}
// 3. Initialize new security system
await this.initializeNewSecurity();
// 4. Detect users needing migration
const usersToMigrate = await this.detectUsersNeedingMigration();
result.warnings.push(`Found ${usersToMigrate.length} users that need migration`);
// 5. Process each user
for (const user of usersToMigrate) {
try {
await this.migrateUser(user, result);
result.usersProcessed++;
} catch (error) {
const errorMsg = `Failed to migrate user ${user.username}: ${error instanceof Error ? error.message : 'Unknown error'}`;
result.errors.push(errorMsg);
databaseLogger.error("User migration failed", error, {
operation: "user_migration_failed",
userId: user.id,
username: user.username,
});
}
}
// 6. Clean up old system (if all users migrated successfully)
if (result.errors.length === 0 && !this.config.dryRun) {
await this.cleanupOldSystem();
}
result.success = result.errors.length === 0;
databaseLogger.success("Security migration completed", {
operation: "security_migration_complete",
result,
});
return result;
} catch (error) {
const errorMsg = `Migration failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
result.errors.push(errorMsg);
databaseLogger.error("Security migration failed", error, {
operation: "security_migration_failed",
});
return result;
}
}
/**
* Validate migration prerequisites
*/
private async validatePrerequisites(): Promise<void> {
databaseLogger.info("Validating migration prerequisites", {
operation: "migration_validation",
});
// Check database connection
try {
await db.select().from(settings).limit(1);
} catch (error) {
throw new Error("Database connection failed");
}
// Check for old encryption keys
const oldEncryptionKey = await db
.select()
.from(settings)
.where(eq(settings.key, "db_encryption_key"));
if (oldEncryptionKey.length === 0) {
databaseLogger.info("No old encryption key found - fresh installation", {
operation: "migration_validation",
});
} else {
databaseLogger.info("Old encryption key detected - migration needed", {
operation: "migration_validation",
});
}
databaseLogger.success("Prerequisites validation passed", {
operation: "migration_validation_complete",
});
}
/**
* Create pre-migration backup
*/
private async createBackup(): Promise<void> {
databaseLogger.info("Creating migration backup", {
operation: "migration_backup",
});
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, `migration-backup-${Date.now()}.sqlite`);
if (fs.existsSync(dbPath)) {
fs.copyFileSync(dbPath, backupPath);
databaseLogger.success(`Migration backup created: ${backupPath}`, {
operation: "migration_backup_complete",
backupPath,
});
}
} catch (error) {
databaseLogger.error("Failed to create migration backup", error, {
operation: "migration_backup_failed",
});
throw error;
}
}
/**
* Initialize new security system
*/
private async initializeNewSecurity(): Promise<void> {
databaseLogger.info("Initializing new security system", {
operation: "new_security_init",
});
await this.securitySession.initialize();
DatabaseEncryption.initialize();
const isValid = await this.securitySession.validateSecuritySystem();
if (!isValid) {
throw new Error("New security system validation failed");
}
databaseLogger.success("New security system initialized", {
operation: "new_security_init_complete",
});
}
/**
* Detect users needing migration
*/
private async detectUsersNeedingMigration(): Promise<any[]> {
const allUsers = await db.select().from(users);
const usersNeedingMigration = [];
for (const user of allUsers) {
// Check if user already has KEK salt (new system)
const kekSalt = await db
.select()
.from(settings)
.where(eq(settings.key, `user_kek_salt_${user.id}`));
if (kekSalt.length === 0) {
usersNeedingMigration.push(user);
}
}
databaseLogger.info(`Found ${usersNeedingMigration.length} users needing migration`, {
operation: "migration_user_detection",
totalUsers: allUsers.length,
needingMigration: usersNeedingMigration.length,
});
return usersNeedingMigration;
}
/**
* Migrate single user
*/
private async migrateUser(user: any, result: MigrationResult): Promise<void> {
databaseLogger.info(`Migrating user: ${user.username}`, {
operation: "user_migration_start",
userId: user.id,
username: user.username,
});
if (this.config.dryRun) {
databaseLogger.info(`[DRY RUN] Would migrate user: ${user.username}`, {
operation: "user_migration_dry_run",
userId: user.id,
});
return;
}
// Issue: We need user's plaintext password to set up KEK
// but we only have password hash. Solutions:
// 1. Require user to re-enter password on first login
// 2. Generate temporary password and require user to change it
//
// For demonstration, we skip actual KEK setup and just mark user for password reset
try {
// Mark user needing encryption reset
await db.insert(settings).values({
key: `user_migration_required_${user.id}`,
value: JSON.stringify({
userId: user.id,
username: user.username,
migrationTime: new Date().toISOString(),
reason: "Security system upgrade - password re-entry required",
}),
});
result.warnings.push(`User ${user.username} marked for password re-entry on next login`);
databaseLogger.success(`User migration prepared: ${user.username}`, {
operation: "user_migration_prepared",
userId: user.id,
username: user.username,
});
} catch (error) {
databaseLogger.error(`Failed to prepare user migration: ${user.username}`, error, {
operation: "user_migration_prepare_failed",
userId: user.id,
username: user.username,
});
throw error;
}
}
/**
* Clean up old encryption system
*/
private async cleanupOldSystem(): Promise<void> {
databaseLogger.info("Cleaning up old encryption system", {
operation: "old_system_cleanup",
});
try {
// Delete old encryption keys
await db.delete(settings).where(eq(settings.key, "db_encryption_key"));
await db.delete(settings).where(eq(settings.key, "encryption_key_created"));
// Keep JWT key (now managed by new system)
// Delete old jwt_secret, let new system take over
await db.delete(settings).where(eq(settings.key, "jwt_secret"));
await db.delete(settings).where(eq(settings.key, "jwt_secret_created"));
databaseLogger.success("Old encryption system cleaned up", {
operation: "old_system_cleanup_complete",
});
} catch (error) {
databaseLogger.error("Failed to cleanup old system", error, {
operation: "old_system_cleanup_failed",
});
throw error;
}
}
/**
* Check migration status
*/
static async checkMigrationStatus(): Promise<{
migrationRequired: boolean;
usersNeedingMigration: number;
hasOldSystem: boolean;
hasNewSystem: boolean;
}> {
try {
// Check for old system
const oldEncryptionKey = await db
.select()
.from(settings)
.where(eq(settings.key, "db_encryption_key"));
// Check for new system
const newSystemJWT = await db
.select()
.from(settings)
.where(eq(settings.key, "system_jwt_secret"));
// Check users needing migration
const allUsers = await db.select().from(users);
let usersNeedingMigration = 0;
for (const user of allUsers) {
const kekSalt = await db
.select()
.from(settings)
.where(eq(settings.key, `user_kek_salt_${user.id}`));
if (kekSalt.length === 0) {
usersNeedingMigration++;
}
}
const hasOldSystem = oldEncryptionKey.length > 0;
const hasNewSystem = newSystemJWT.length > 0;
const migrationRequired = hasOldSystem || usersNeedingMigration > 0;
return {
migrationRequired,
usersNeedingMigration,
hasOldSystem,
hasNewSystem,
};
} catch (error) {
databaseLogger.error("Failed to check migration status", error, {
operation: "migration_status_check_failed",
});
throw error;
}
}
/**
* Handle user login migration (when user enters password)
*/
static async handleUserLoginMigration(userId: string, password: string): Promise<boolean> {
try {
// Check if user needs migration
const migrationRequired = await db
.select()
.from(settings)
.where(eq(settings.key, `user_migration_required_${userId}`));
if (migrationRequired.length === 0) {
return false; // No migration needed
}
databaseLogger.info("Performing user migration during login", {
operation: "login_migration_start",
userId,
});
// Initialize user encryption
const securitySession = SecuritySession.getInstance();
await securitySession.registerUser(userId, password);
// Delete migration marker
await db.delete(settings).where(eq(settings.key, `user_migration_required_${userId}`));
databaseLogger.success("User migration completed during login", {
operation: "login_migration_complete",
userId,
});
return true; // Migration completed
} catch (error) {
databaseLogger.error("Login migration failed", error, {
operation: "login_migration_failed",
userId,
});
throw error;
}
}
}
// CLI execution
if (import.meta.url === `file://${process.argv[1]}`) {
const config: MigrationConfig = {
dryRun: process.env.DRY_RUN === "true",
backupEnabled: process.env.BACKUP_ENABLED !== "false",
forceRegeneration: process.env.FORCE_REGENERATION === "true",
};
const migration = new SecurityMigration(config);
migration
.runMigration()
.then((result) => {
console.log("Migration completed:", result);
process.exit(result.success ? 0 : 1);
})
.catch((error) => {
console.error("Migration failed:", error.message);
process.exit(1);
});
}
export { SecurityMigration, type MigrationConfig, type MigrationResult };