diff --git a/packages/core/__tests__/password-change.test.ts b/packages/core/__tests__/password-change.test.ts index 618146079..41f715ca4 100644 --- a/packages/core/__tests__/password-change.test.ts +++ b/packages/core/__tests__/password-change.test.ts @@ -97,6 +97,40 @@ describe("UserManager._updatePassword (change)", () => { }); }); + test("new master key decrypts dataEncryptionKey and old master key throws (regression)", async () => { + await databaseTest().then(async (db) => { + const { salt } = await setupLoggedInUser(db, "oldpassword"); + + // Simulate a full password change with old -> new passwords. + const result = await db.user.changePassword("oldpassword", "newpassword"); + expect(result).toBe(true); + + const user = await db.user.getUser(); + + // The stored master key must now be the one derived from the NEW password. + const newMasterKey = await db.user.getMasterKey(); + expect(newMasterKey).toBeDefined(); + expect(newMasterKey!.key).toBe( + (await db.storage().generateCryptoKey("newpassword", salt)).key + ); + + // (a) The NEW master key must decrypt the rewrapped dataEncryptionKey. + await expect( + db.storage().decrypt(newMasterKey!, user!.dataEncryptionKey!) + ).resolves.toBeDefined(); + + // (b) The OLD master key must NOT be able to decrypt it anymore. + // We derive it without persisting it to the key store. + const oldMasterKey = await db.storage().generateCryptoKey( + "oldpassword", + salt + ); + await expect( + db.storage().decrypt(oldMasterKey, user!.dataEncryptionKey!) + ).rejects.toThrow(); + }); + }); + test("empty new password throws 'New password is required'", async () => { await databaseTest().then(async (db) => { await setupLoggedInUser(db, "oldpassword"); diff --git a/packages/core/src/api/user-manager.ts b/packages/core/src/api/user-manager.ts index 9862dcc6a..a63622b9c 100644 --- a/packages/core/src/api/user-manager.ts +++ b/packages/core/src/api/user-manager.ts @@ -675,15 +675,77 @@ class UserManager { encryptionKey?: SerializedKey; } ) { + const { new_password, old_password } = data; + if (!new_password) throw new Error("New password is required."); + const token = await this.tokenManager.getAccessToken(); const user = await this.getUser(); if (!token || !user) throw new Error("You are not logged in."); const { email, salt } = user; - const { new_password, old_password } = data; if (old_password && !(await this.verifyPassword(old_password))) throw new Error("Incorrect old password."); + if (old_password && !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 = {}; + const newMasterKey = await this.db + .storage() + .generateCryptoKey(new_password, salt); + + if (user.attachmentsKey) { + updateUserPayload.attachmentsKey = await this.keyManager.rewrapKey( + user.attachmentsKey, + data.encryptionKey, + newMasterKey + ); + } + if (user.monographPasswordsKey) { + updateUserPayload.monographPasswordsKey = await this.keyManager.rewrapKey( + user.monographPasswordsKey, + data.encryptionKey, + newMasterKey + ); + } + if (user.inboxKeys) { + updateUserPayload.inboxKeys = await this.keyManager.rewrapKey( + user.inboxKeys, + data.encryptionKey, + newMasterKey + ); + } + + if (user.legacyDataEncryptionKey) + updateUserPayload.legacyDataEncryptionKey = + await this.keyManager.rewrapKey( + user.legacyDataEncryptionKey, + data.encryptionKey, + newMasterKey + ); + if (user.dataEncryptionKey) + updateUserPayload.dataEncryptionKey = await this.keyManager.rewrapKey( + user.dataEncryptionKey, + data.encryptionKey, + newMasterKey + ); + + if (!user.legacyDataEncryptionKey && !user.dataEncryptionKey) { + updateUserPayload.dataEncryptionKey = await this.keyManager.wrapKey( + await this.db.crypto().generateRandomKey(), + newMasterKey + ); + updateUserPayload.legacyDataEncryptionKey = await this.keyManager.wrapKey( + data.encryptionKey, + newMasterKey + ); + } const oldPassword = old_password ? // we don't lowercase email here to allow user accounts with @@ -694,69 +756,6 @@ class UserManager { }) : null; - if (!new_password) throw new Error("New password is required."); - - 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) { - const newMasterKey = await this.db - .storage() - .generateCryptoKey(new_password, salt); - if (user.attachmentsKey) { - updateUserPayload.attachmentsKey = await this.keyManager.rewrapKey( - user.attachmentsKey, - data.encryptionKey, - newMasterKey - ); - } - if (user.monographPasswordsKey) { - updateUserPayload.monographPasswordsKey = - await this.keyManager.rewrapKey( - user.monographPasswordsKey, - data.encryptionKey, - newMasterKey - ); - } - if (user.inboxKeys) { - updateUserPayload.inboxKeys = await this.keyManager.rewrapKey( - user.inboxKeys, - data.encryptionKey, - newMasterKey - ); - } - - if (user.legacyDataEncryptionKey) - updateUserPayload.legacyDataEncryptionKey = - await this.keyManager.rewrapKey( - user.legacyDataEncryptionKey, - data.encryptionKey, - newMasterKey - ); - - if (user.dataEncryptionKey) - updateUserPayload.dataEncryptionKey = await this.keyManager.rewrapKey( - user.dataEncryptionKey, - data.encryptionKey, - newMasterKey - ); - - if (!user.legacyDataEncryptionKey && !user.dataEncryptionKey) { - updateUserPayload.dataEncryptionKey = await this.keyManager.wrapKey( - await this.db.crypto().generateRandomKey(), - newMasterKey - ); - updateUserPayload.legacyDataEncryptionKey = - await this.keyManager.wrapKey(data.encryptionKey, newMasterKey); - } - } - await http.patch.json( `${constants.API_HOST}/users/password/${type}`, {