From 79a2d3c91b655d3a5aa6ce80a6456f34d6c7fe1a Mon Sep 17 00:00:00 2001 From: Ved Prakash <54140516+thorved@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:53:44 +0530 Subject: [PATCH] Fix: Password Change/Reset Credential Preservation (#383) * Improve password reset to preserve encrypted data during active sessions Enhances password reset logic to preserve encrypted user data when the user is logged in and has an active session. Introduces a fallback mechanism to create a new data encryption key (DEK) if preservation fails, ensuring user data integrity. Adds a dedicated method for preserving the DEK during password reset and updates related session management. Includes improved logging for better tracking of password reset operations and potential data loss scenarios. Fixes inefficiencies in password change and reset workflows by persisting encryption key changes promptly and cleaning up sensitive data from memory. * fix(auth): preserve user credentials during password change/reset - Maintain session during password change to prevent credential loss - Add intelligent password reset that preserves data when logged in - Improve Buffer handling and session cleanup - Remove dead code that could fail for OIDC users The DEK is now properly maintained in session memory when password changes, preventing apparent data loss. Password reset intelligently detects active sessions and preserves credentials when possible. * Removes redundant comments to improve code readability --- src/backend/database/routes/users.ts | 57 ++++++++++++++++++----- src/backend/utils/auth-manager.ts | 10 ++++ src/backend/utils/user-crypto.ts | 69 ++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 15 deletions(-) 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; } }