core: minor refactor + add test for password change (#10291)

This commit is contained in:
Abdullah Atta
2026-08-27 09:55:24 +05:00
committed by GitHub
parent 725df6f6f7
commit 4c702af1ca
2 changed files with 97 additions and 64 deletions

View File

@@ -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 () => { test("empty new password throws 'New password is required'", async () => {
await databaseTest().then(async (db) => { await databaseTest().then(async (db) => {
await setupLoggedInUser(db, "oldpassword"); await setupLoggedInUser(db, "oldpassword");

View File

@@ -675,15 +675,77 @@ class UserManager {
encryptionKey?: SerializedKey; 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 token = await this.tokenManager.getAccessToken();
const user = await this.getUser(); const user = await this.getUser();
if (!token || !user) throw new Error("You are not logged in."); if (!token || !user) throw new Error("You are not logged in.");
const { email, salt } = user; const { email, salt } = user;
const { new_password, old_password } = data;
if (old_password && !(await this.verifyPassword(old_password))) if (old_password && !(await this.verifyPassword(old_password)))
throw new Error("Incorrect 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<User> = {};
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 const oldPassword = old_password
? // we don't lowercase email here to allow user accounts with ? // we don't lowercase email here to allow user accounts with
@@ -694,69 +756,6 @@ class UserManager {
}) })
: null; : 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<User> = {};
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( await http.patch.json(
`${constants.API_HOST}/users/password/${type}`, `${constants.API_HOST}/users/password/${type}`,
{ {