mirror of
https://github.com/google/nomulus
synced 2026-10-01 04:05:49 +00:00
Add and default to Argon2 hashing (#2877)
We've previously been using Scrypt since PR #2191 which, while being a memory-hard slow function, isn't the optimal solution according to the OWASP recommendations. While we could get away with increasing the parallelization parameter to 3, it's better to just switch to the most-recommended solution if we're switching things up anyway. For the transition, we do something similar to PR #2191 where if the previous-algorithm's hash is successful, we re-hash with Argo2id and store that version. By doing this, we should not need any intervention for registrars who log in at any point during the transition period. Much of this PR, especially the parts where we re-hash the passwords in Argon2 instead of Scrypt upon login, is based on the code that was eventually removed in #2310.
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
|
||||
package google.registry.flows.session;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.deleteResource;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
@@ -38,6 +40,8 @@ import google.registry.model.eppoutput.EppOutput;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.Registrar.State;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -186,6 +190,32 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
|
||||
doFailingTest("login_valid.xml", RegistrarAccountNotActiveException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_scryptPasswordToArgon2() throws Exception {
|
||||
String password = "foo-BAR2";
|
||||
tm().transact(
|
||||
() -> {
|
||||
// The salt is not exposed by Registrar (nor should it be), so we query it directly.
|
||||
String encodedSalt =
|
||||
tm().query("SELECT salt FROM Registrar WHERE registrarId = :id", String.class)
|
||||
.setParameter("id", registrar.getRegistrarId())
|
||||
.getSingleResult();
|
||||
byte[] salt = base64().decode(encodedSalt);
|
||||
String newHash = PasswordUtils.hashPassword(password, salt, HashAlgorithm.SCRYPT_P_1);
|
||||
// Set password directly, as the Java method would have used Argon2.
|
||||
tm().query("UPDATE Registrar SET passwordHash = :hash WHERE registrarId = :id")
|
||||
.setParameter("id", registrar.getRegistrarId())
|
||||
.setParameter("hash", newHash)
|
||||
.executeUpdate();
|
||||
});
|
||||
assertThat(loadRegistrar("NewRegistrar").getCurrentHashAlgorithm(password).get())
|
||||
.isEqualTo(HashAlgorithm.SCRYPT_P_1);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
// Verifies that after successfully login, the password is re-hased with Scrypt.
|
||||
assertThat(loadRegistrar("NewRegistrar").getCurrentHashAlgorithm(password).get())
|
||||
.isEqualTo(HashAlgorithm.ARGON_2_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_incorrectPassword() {
|
||||
persistResource(getRegistrarBuilder().setPassword("diff password").build());
|
||||
|
||||
+49
-10
@@ -14,7 +14,9 @@
|
||||
|
||||
package google.registry.ui.server.console;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
@@ -49,6 +51,8 @@ import google.registry.testing.DeterministicStringGenerator;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.tools.DomainLockUtils;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import google.registry.util.StringGenerator;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import java.io.IOException;
|
||||
@@ -88,16 +92,17 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
|
||||
createTld("test");
|
||||
defaultDomain = persistActiveDomain("example.test");
|
||||
user =
|
||||
new User.Builder()
|
||||
.setEmailAddress("user@theregistrar.com")
|
||||
.setRegistryLockEmailAddress("registrylock@theregistrar.com")
|
||||
.setUserRoles(
|
||||
new UserRoles.Builder()
|
||||
.setRegistrarRoles(
|
||||
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
|
||||
.build())
|
||||
.setRegistryLockPassword("registryLockPassword")
|
||||
.build();
|
||||
persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("user@theregistrar.com")
|
||||
.setRegistryLockEmailAddress("registrylock@theregistrar.com")
|
||||
.setUserRoles(
|
||||
new UserRoles.Builder()
|
||||
.setRegistrarRoles(
|
||||
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
|
||||
.build())
|
||||
.setRegistryLockPassword("registryLockPassword")
|
||||
.build());
|
||||
action = createGetAction();
|
||||
}
|
||||
|
||||
@@ -277,6 +282,40 @@ public class ConsoleRegistryLockActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPost_lock_scryptPasswordToArgon2() throws Exception {
|
||||
tm().transact(
|
||||
() -> {
|
||||
// The salt is not exposed by User (nor should it be), so we query it directly.
|
||||
String encodedSalt =
|
||||
tm().query(
|
||||
"SELECT registryLockPasswordSalt FROM User WHERE emailAddress ="
|
||||
+ " :emailAddress",
|
||||
String.class)
|
||||
.setParameter("emailAddress", user.getEmailAddress())
|
||||
.getSingleResult();
|
||||
byte[] salt = base64().decode(encodedSalt);
|
||||
String newHash =
|
||||
PasswordUtils.hashPassword(
|
||||
"registryLockPassword", salt, HashAlgorithm.SCRYPT_P_1);
|
||||
// Set password directly, as the Java method would have used Argon2.
|
||||
tm().query("UPDATE User SET registryLockPasswordHash = :hash")
|
||||
.setParameter("hash", newHash)
|
||||
.executeUpdate();
|
||||
});
|
||||
user = loadByEntity(user);
|
||||
assertThat(user.getCurrentHashAlgorithm("registryLockPassword").get())
|
||||
.isEqualTo(HashAlgorithm.SCRYPT_P_1);
|
||||
action = createDefaultPostAction(true);
|
||||
action.run();
|
||||
verifyEmail();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(getMostRecentRegistryLockByRepoId(defaultDomain.getRepoId())).isPresent();
|
||||
user = loadByEntity(user);
|
||||
assertThat(user.getCurrentHashAlgorithm("registryLockPassword").get())
|
||||
.isEqualTo(HashAlgorithm.ARGON_2_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPost_unlock() throws Exception {
|
||||
saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(clock.nowUtc()).build());
|
||||
|
||||
Reference in New Issue
Block a user