feat: enable rollback from argon2 to bcrypt

This commit is contained in:
Victor Parpoil
2025-01-31 15:14:37 +01:00
parent 107f2621ca
commit df0a2ef6ae
2 changed files with 69 additions and 11 deletions

View File

@@ -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);

View File

@@ -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)) {