From 54c97584cf430f057a35e1aa9f92dc133b7c4a87 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 17 Aug 2026 19:12:07 +0500 Subject: [PATCH 1/4] core: add encryption key verification on password change --- packages/core/src/api/user-manager.ts | 57 ++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/core/src/api/user-manager.ts b/packages/core/src/api/user-manager.ts index 13c835a0e..165adcaaa 100644 --- a/packages/core/src/api/user-manager.ts +++ b/packages/core/src/api/user-manager.ts @@ -24,7 +24,7 @@ import TokenManager from "./token-manager.js"; import { EV, EVENTS } from "../common.js"; import { HealthCheck } from "./healthcheck.js"; import Database from "./index.js"; -import { SerializedKeyPair, SerializedKey } from "@notesnook/crypto"; +import { SerializedKeyPair, SerializedKey, Cipher } from "@notesnook/crypto"; import { logger } from "../logger.js"; import { KEY_VERSION, KeyVersion } from "./sync/types.js"; import { @@ -394,9 +394,13 @@ class UserManager { ); } - resetPassword(newPassword: string) { + resetPassword(options: { + newPassword: string; + encryptionKey: SerializedKey; + }) { return this._updatePassword("reset", { - new_password: newPassword + new_password: options.newPassword, + encryptionKey: options.encryptionKey }); } @@ -622,6 +626,41 @@ class UserManager { } } + private async fetchEncryptionVerifier(): Promise< + Cipher<"base64"> | undefined + > { + const token = await this.tokenManager.getAccessToken(); + return http.get(`${constants.API_HOST}/users/verifier`, token); + } + + async verifyEncryptionKey(key: SerializedKey) { + const user = await this.getUser(); + if (!user) throw new Error("User not found."); + + if (user.legacyDataEncryptionKey && user.dataEncryptionKey) + await this.keyManager.unwrapKey(user.legacyDataEncryptionKey, key); + else if (!user.legacyDataEncryptionKey && !user.dataEncryptionKey) { + const verifier = + user.attachmentsKey ?? + user.monographPasswordsKey ?? + (await this.fetchEncryptionVerifier()); + if (!verifier) throw new Error("Failed to fetch encryption verifier."); + const decryptedData = await this.db + .storage() + .decrypt(key, verifier) + .then(() => true) + .catch(() => false); + + if (!decryptedData) + throw new Error( + "Your data cannot be decrypted using the provided encryption key." + ); + } else + throw new Error( + "Cannot verify the provided encryption key as user has only a single encryption key." + ); + } + async _updatePassword( type: "change" | "reset", data: { @@ -651,7 +690,13 @@ class UserManager { if (!new_password) throw new Error("New password is required."); - data.encryptionKey = data.encryptionKey || (await this.getMasterKey()); + if (oldPassword && !data.encryptionKey) + data.encryptionKey = await this.getMasterKey(); + if (!data.encryptionKey) throw new Error("Encryption key is required."); + + // we must be 100% sure that the provided encryption key is valid before + // proceeding + await this.verifyEncryptionKey(data.encryptionKey); const updateUserPayload: Partial = {}; if (data.encryptionKey) { @@ -688,13 +733,15 @@ class UserManager { data.encryptionKey, newMasterKey ); + if (user.dataEncryptionKey) updateUserPayload.dataEncryptionKey = await this.keyManager.rewrapKey( user.dataEncryptionKey, data.encryptionKey, newMasterKey ); - else { + + if (!user.legacyDataEncryptionKey && !user.dataEncryptionKey) { updateUserPayload.dataEncryptionKey = await this.keyManager.wrapKey( await this.db.crypto().generateRandomKey(), newMasterKey From 04f9ff41a09c0fcab823aabf54923da1193ac754 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 17 Aug 2026 19:12:28 +0500 Subject: [PATCH 2/4] web: verify encryption key during password reset --- apps/web/src/views/recovery.tsx | 70 ++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/apps/web/src/views/recovery.tsx b/apps/web/src/views/recovery.tsx index aaff67bec..e2be98631 100644 --- a/apps/web/src/views/recovery.tsx +++ b/apps/web/src/views/recovery.tsx @@ -30,7 +30,6 @@ import { ErrorText } from "../components/error-text"; import { EVENTS, User } from "@notesnook/core"; import { RecoveryKeyDialog } from "../dialogs/recovery-key-dialog"; import { strings } from "@notesnook/intl"; -import { useKeyStore } from "../interfaces/key-store"; type RecoveryMethodType = "key" | "reset"; type RecoveryMethodsFormData = Record; @@ -105,8 +104,8 @@ function useAuthenticateUser({ code, userId }: { - code: string; - userId: string; + code?: string; + userId?: string; }) { const [isAuthenticating, setIsAuthenticating] = useState(true); const [user, setUser] = useState(); @@ -114,6 +113,10 @@ function useAuthenticateUser({ async function authenticateUser() { setIsAuthenticating(true); try { + if (!code || !userId) { + throw new Error("Missing code or userId in query params."); + } + const accessToken = await db.tokenManager.getAccessToken(); if (!accessToken) { await db.tokenManager.getAccessTokenFromAuthorizationCode( @@ -181,7 +184,7 @@ function Recovery(props: RecoveryProps) { }} variant={"body"} > - {strings.authenticatedAs(user?.email)} + {user?.email ? strings.authenticatedAs(user.email) : ""} - +