From df0a2ef6aeb78b9ffcebdb692e0b02c870027ab5 Mon Sep 17 00:00:00 2001 From: Victor Parpoil Date: Fri, 31 Jan 2025 15:14:37 +0100 Subject: [PATCH] feat: enable rollback from argon2 to bcrypt --- .../accounts-password/password_argon_tests.js | 36 +++++++++++++++ packages/accounts-password/password_server.js | 44 ++++++++++++++----- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/packages/accounts-password/password_argon_tests.js b/packages/accounts-password/password_argon_tests.js index 51eefc66e7..d580cf0512 100644 --- a/packages/accounts-password/password_argon_tests.js +++ b/packages/accounts-password/password_argon_tests.js @@ -92,8 +92,44 @@ if (Meteor.isServer) { await Meteor.users.removeAsync(userId); }); + Tinytest.addAsync("passwords Argon - migration from argon2 encryption to bcrypt", async (test) => { + Accounts._options.argon2Enabled = true; + const username = Random.id(); + const email = `${username}@bcrypt.com`; + const password = "password"; + const userId = await Accounts.createUser( + { + username: username, + email: email, + password: password + } + ); + Accounts._options.argon2Enabled = false; + let user = await Meteor.users.findOneAsync(userId); + const isValidArgon = await Accounts._checkPasswordAsync(user, password); + test.equal(isValidArgon.userId, userId, "checkPassword with argon2 - User ID should be returned"); + test.equal(typeof isValidArgon.error, "undefined", "checkPassword with argon2 - No error should be returned"); + // wait for defered execution of user update inside _checkPasswordAsync + await new Promise((resolve) => { + Meteor.setTimeout(async () => { + user = await Meteor.users.findOneAsync(userId); + // bcrypt has been unset and argon2 set + test.equal(typeof user.services.password.argon2, "undefined", "argon2 should be unset"); + test.equal(typeof user.services.password.bcrypt, "string", "bcrypt should be set"); + // password is still valid using argon2 + const isValidBcrypt = await Accounts._checkPasswordAsync(user, password); + test.equal(isValidBcrypt.userId, userId, "checkPassword with argon2 - User ID should be returned"); + test.equal(typeof isValidBcrypt.error, "undefined", "checkPassword with argon2 - No error should be returned"); + resolve(); + }, 100); + }); + + // cleanup + await Meteor.users.removeAsync(userId); + }); + const getUserHashArgon2Params = function (user) { const hash = user?.services?.password?.argon2; return Accounts._getArgon2Params(hash); diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 8d738b7fd5..b6fd1c2348 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -134,10 +134,14 @@ Accounts._checkPasswordUserFields = { _id: 1, services: 1 }; const isBcrypt = (hash) => { // bcrypt hashes start with $2a$ or $2b$ - // argon2 hashes start with $argon2i$, $argon2d$ or $argon2id$ return hash.startsWith("$2"); }; +const isArgon = (hash) => { + // argon2 hashes start with $argon2i$, $argon2d$ or $argon2id$ + return hash.startsWith("$argon2"); +} + const updateUserPasswordDefered = (user, formattedPassword) => { Meteor.defer(async () => { await updateUserPassword(user, formattedPassword); @@ -155,6 +159,9 @@ const getUpdatorForUserPassword = async (formattedPassword) => { return { $set: { "services.password.bcrypt": encryptedPassword + }, + $unset: { + "services.password.argon2": 1 } }; } @@ -208,19 +215,34 @@ const checkPasswordAsync = async (user, password) => { const argon2Enabled = Accounts._argon2Enabled(); if (argon2Enabled === false) { - const hashRounds = getRoundsFromBcryptHash(hash); - const match = await bcryptCompare(formattedPassword, hash); - if (!match) { - result.error = Accounts._handleError("Incorrect password", false); - } - else if (hash) { - const paramsChanged = hashRounds !== Accounts._bcryptRounds(); - // The password checks out, but the user's bcrypt hash needs to be updated - // to match current bcrypt settings - if (paramsChanged === true) { + if (isArgon(hash)) { + // this is a rollback feature, enabling to switch back from argon2 to bcrypt if needed + // TODO : deprecate this + console.warn("User has an argon2 password and argon2 is not enabled, rolling back to bcrypt encryption"); + const match = await argon2.verify(hash, formattedPassword); + if (!match) { + result.error = Accounts._handleError("Incorrect password", false); + } + else{ + // The password checks out, but the user's stored password needs to be updated to argon2 updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" }); } } + else { + const hashRounds = getRoundsFromBcryptHash(hash); + const match = await bcryptCompare(formattedPassword, hash); + if (!match) { + result.error = Accounts._handleError("Incorrect password", false); + } + else if (hash) { + const paramsChanged = hashRounds !== Accounts._bcryptRounds(); + // The password checks out, but the user's bcrypt hash needs to be updated + // to match current bcrypt settings + if (paramsChanged === true) { + updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" }); + } + } + } } else if (argon2Enabled === true) { if (isBcrypt(hash)) {