diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 3c2303cc..11d600f6 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -18,7 +18,6 @@ import QRCode from "qrcode"; import type { Request, Response } from "express"; import { authLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; -import { UserCrypto } from "../../utils/user-crypto.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js"; @@ -1318,8 +1317,48 @@ router.post("/complete-reset", async (req, res) => { .where(eq(users.username, username)); try { - await authManager.registerUser(userId, newPassword); - authManager.logoutUser(userId); + const hasActiveSession = authManager.isUserUnlocked(userId); + + if (hasActiveSession) { + const success = await authManager.resetUserPasswordWithPreservedDEK( + userId, + newPassword, + ); + + if (!success) { + authLogger.warn( + `Failed to preserve DEK during password reset for ${username}. Creating new DEK - data will be lost.`, + { + operation: "password_reset_preserve_failed", + userId, + username, + }, + ); + await authManager.registerUser(userId, newPassword); + authManager.logoutUser(userId); + } else { + authLogger.success( + `Password reset completed for user: ${username}. Data preserved using existing session.`, + { + operation: "password_reset_data_preserved", + userId, + username, + }, + ); + } + } else { + await authManager.registerUser(userId, newPassword); + authManager.logoutUser(userId); + + authLogger.warn( + `Password reset completed for user: ${username}. Existing encrypted data is now inaccessible and will need to be re-entered.`, + { + operation: "password_reset_data_inaccessible", + userId, + username, + }, + ); + } await db .update(users) @@ -1329,15 +1368,6 @@ router.post("/complete-reset", async (req, res) => { totp_backup_codes: null, }) .where(eq(users.id, userId)); - - authLogger.warn( - `Password reset completed for user: ${username}. Existing encrypted data is now inaccessible and will need to be re-entered.`, - { - operation: "password_reset_data_inaccessible", - userId, - username, - }, - ); } catch (encryptionError) { authLogger.error( "Failed to re-encrypt user data after password reset", @@ -2014,6 +2044,9 @@ router.post("/change-password", authenticateJWT, async (req, res) => { .set({ password_hash: newPasswordHash }) .where(eq(users.id, userId)); + const { saveMemoryDatabaseToFile } = await import("../db/index.js"); + await saveMemoryDatabaseToFile(); + authLogger.success("User password changed successfully", { operation: "password_change_success", userId, diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 541d889f..a13db6cd 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -295,6 +295,16 @@ class AuthManager { newPassword, ); } + + async resetUserPasswordWithPreservedDEK( + userId: string, + newPassword: string, + ): Promise { + return await this.userCrypto.resetUserPasswordWithPreservedDEK( + userId, + newPassword, + ); + } } export { AuthManager, type AuthenticationResult, type JWTPayload }; diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts index 73a2e39f..8e12778d 100644 --- a/src/backend/utils/user-crypto.ts +++ b/src/backend/utils/user-crypto.ts @@ -1,6 +1,6 @@ import crypto from "crypto"; import { getDb } from "../database/db/index.js"; -import { settings, users } from "../database/db/schema.js"; +import { settings } from "../database/db/schema.js"; import { eq } from "drizzle-orm"; import { databaseLogger } from "./logger.js"; @@ -263,19 +263,82 @@ class UserCrypto { const newKekSalt = await this.generateKEKSalt(); const newKEK = this.deriveKEK(newPassword, newKekSalt); + const newEncryptedDEK = this.encryptDEK(DEK, newKEK); await this.storeKEKSalt(userId, newKekSalt); await this.storeEncryptedDEK(userId, newEncryptedDEK); + const { saveMemoryDatabaseToFile } = await import("../database/db/index.js"); + await saveMemoryDatabaseToFile(); + oldKEK.fill(0); newKEK.fill(0); - DEK.fill(0); - this.logoutUser(userId); + const dekCopy = Buffer.from(DEK); + + const now = Date.now(); + const oldSession = this.userSessions.get(userId); + if (oldSession) { + oldSession.dataKey.fill(0); + } + + this.userSessions.set(userId, { + dataKey: dekCopy, + lastActivity: now, + expiresAt: now + UserCrypto.SESSION_DURATION, + }); + + DEK.fill(0); return true; } catch (error) { + databaseLogger.error("Password change failed", error, { + operation: "password_change_error", + userId, + error: error instanceof Error ? error.message : "Unknown error", + }); + return false; + } + } + + async resetUserPasswordWithPreservedDEK( + userId: string, + newPassword: string, + ): Promise { + try { + const existingDEK = this.getUserDataKey(userId); + if (!existingDEK) { + return false; + } + + const newKekSalt = await this.generateKEKSalt(); + const newKEK = this.deriveKEK(newPassword, newKekSalt); + + const newEncryptedDEK = this.encryptDEK(existingDEK, newKEK); + + await this.storeKEKSalt(userId, newKekSalt); + await this.storeEncryptedDEK(userId, newEncryptedDEK); + + const { saveMemoryDatabaseToFile } = await import( + "../database/db/index.js" + ); + await saveMemoryDatabaseToFile(); + + newKEK.fill(0); + + const session = this.userSessions.get(userId); + if (session) { + session.lastActivity = Date.now(); + } + + return true; + } catch (error) { + databaseLogger.error("Password reset with preserved DEK failed", error, { + operation: "password_reset_preserve_error", + userId, + error: error instanceof Error ? error.message : "Unknown error", + }); return false; } }