v1.7.3 #390

Merged
LukeGus merged 22 commits from dev-1.7.3 into main 2025-10-09 04:55:33 +00:00
3 changed files with 121 additions and 15 deletions
Showing only changes of commit 79a2d3c91b - Show all commits

View File

@@ -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,

View File

@@ -295,6 +295,16 @@ class AuthManager {
newPassword,
);
}
async resetUserPasswordWithPreservedDEK(
userId: string,
newPassword: string,
): Promise<boolean> {
return await this.userCrypto.resetUserPasswordWithPreservedDEK(
userId,
newPassword,
);
}
}
export { AuthManager, type AuthenticationResult, type JWTPayload };

View File

@@ -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<boolean> {
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;
}
}