dev-1.7.0 #294

Merged
ZacharyZcR merged 73 commits from main into dev-1.7.0 2025-09-25 04:56:32 +00:00
4 changed files with 16 additions and 599 deletions
Showing only changes of commit cc5f1fd25a - Show all commits
-94
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有效期
- 非活跃自动过期
- 用户登出立即清理
### 向后兼容
- 检测旧格式数据
- 用户登录时自动迁移
- 迁移完成后删除旧密钥
## 测试计划
- [ ] 密钥生成和推导测试
- [ ] 加密解密正确性测试
- [ ] 会话管理测试
- [ ] 迁移流程测试
- [ ] 性能影响评估
-45
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",
+16 -11
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
-449
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 };