Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions core/src/main/java/google/registry/flows/session/LoginFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import google.registry.model.eppinput.EppInput.Services;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.registrar.Registrar;
import google.registry.util.PasswordUtils;
import google.registry.util.StopwatchLogger;
import jakarta.inject.Inject;
import java.util.Optional;
Expand Down Expand Up @@ -150,8 +151,17 @@ private EppResponse runWithoutLogging() throws EppException {
throw new RegistrarAccountNotActiveException();
}

if (login.getNewPassword().isPresent()) {
String newPassword = login.getNewPassword().get();
if (login.getNewPassword().isPresent()
|| registrar.get().getCurrentHashAlgorithm(login.getPassword()).orElse(null)
!= PasswordUtils.HashAlgorithm.ARGON_2_ID) {
String newPassword =
login
.getNewPassword()
.orElseGet(
() -> {
logger.atInfo().log("Rehashing existing registrar password with ARGON_2_ID");
return login.getPassword();
});
// Load fresh from database (bypassing the cache) to ensure we don't save stale data.
Optional<Registrar> freshRegistrar = Registrar.loadByRegistrarId(login.getClientId());
stopwatch.tick("LoginFlow reload freshRegistrar");
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/google/registry/model/console/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import google.registry.tools.ServiceConnection;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.util.PasswordUtils;
import google.registry.util.PasswordUtils.HashAlgorithm;
import google.registry.util.RegistryEnvironment;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
Expand Down Expand Up @@ -229,6 +230,10 @@ public boolean verifyRegistryLockPassword(String registryLockPassword) {
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return getCurrentHashAlgorithm(registryLockPassword).isPresent();
}

public Optional<HashAlgorithm> getCurrentHashAlgorithm(String registryLockPassword) {
return PasswordUtils.verifyPassword(
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import google.registry.persistence.transaction.TransactionManager;
import google.registry.util.CidrAddressBlock;
import google.registry.util.PasswordUtils;
import google.registry.util.PasswordUtils.HashAlgorithm;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.persistence.AttributeOverride;
Expand Down Expand Up @@ -672,6 +673,10 @@ private static String checkValidPhoneNumber(String phoneNumber) {
}

public boolean verifyPassword(String password) {
return getCurrentHashAlgorithm(password).isPresent();
}

public Optional<HashAlgorithm> getCurrentHashAlgorithm(String password) {
return PasswordUtils.verifyPassword(password, passwordHash, salt);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;

import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gson.annotations.Expose;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
Expand All @@ -40,6 +41,7 @@
import google.registry.request.auth.Auth;
import google.registry.tools.DomainLockUtils;
import google.registry.util.EmailMessage;
import google.registry.util.PasswordUtils.HashAlgorithm;
import jakarta.inject.Inject;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
Expand All @@ -61,13 +63,16 @@
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleRegistryLockAction extends ConsoleApiAction {

private static final FluentLogger logger = FluentLogger.forEnclosingClass();

static final String PATH = "/console-api/registry-lock";
static final String VERIFICATION_EMAIL_TEMPLATE =
"""
Please click the link below to perform the lock / unlock action on domain %s. Note: this\
code will expire in one hour.

%s""";
%s\
""";

private final DomainLockUtils domainLockUtils;
private final GmailClient gmailClient;
Expand Down Expand Up @@ -114,7 +119,6 @@ protected void postHandler(User user) {
optionalPostInput.orElseThrow(() -> new IllegalArgumentException("No POST input provided"));
String domainName = postInput.domainName();
boolean isLock = postInput.isLock();
Optional<String> maybePassword = Optional.ofNullable(postInput.password());
Optional<Long> relockDurationMillis = Optional.ofNullable(postInput.relockDurationMillis());

try {
Expand All @@ -126,11 +130,23 @@ protected void postHandler(User user) {
// Passwords aren't required for admin users, otherwise we need to validate it
boolean isAdmin = user.getUserRoles().isAdmin();
if (!isAdmin) {
checkArgument(maybePassword.isPresent(), "No password provided");
if (!user.verifyRegistryLockPassword(maybePassword.get())) {
checkArgument(postInput.password != null, "No password provided");
Optional<HashAlgorithm> hashAlgorithm = user.getCurrentHashAlgorithm(postInput.password);
if (hashAlgorithm.isEmpty()) {
setFailedResponse("Incorrect registry lock password", SC_UNAUTHORIZED);
return;
}
if (hashAlgorithm.get() != HashAlgorithm.ARGON_2_ID) {
logger.atInfo().log("Rehashing existing registry lock password with ARGON_2_ID.");
tm().transact(
() ->
tm().update(
tm().loadByEntity(user)
.asBuilder()
.removeRegistryLockPassword()
.setRegistryLockPassword(postInput.password)
.build()));
}
}

String registryLockEmail =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +40,8 @@
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;

Expand Down Expand Up @@ -186,6 +190,32 @@ void testFailure_disabledRegistrar() {
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,6 +51,8 @@
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;
Expand Down Expand Up @@ -88,16 +92,17 @@ void beforeEach() throws Exception {
createTld("test");
defaultDomain = persistActiveDomain("example.test");
user =
new User.Builder()
.setEmailAddress("[email protected]")
.setRegistryLockEmailAddress("[email protected]")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
.build())
.setRegistryLockPassword("registryLockPassword")
.build();
persistResource(
new User.Builder()
.setEmailAddress("[email protected]")
.setRegistryLockEmailAddress("[email protected]")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
.build())
.setRegistryLockPassword("registryLockPassword")
.build());
action = createGetAction();
}

Expand Down Expand Up @@ -277,6 +282,40 @@ void testPost_lock() throws Exception {
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());
Expand Down
Loading