mirror of
https://github.com/google/nomulus.git
synced 2025-08-01 23:42:12 +02:00
Remove SHA256 as a supported password hashing algorithm (#2310)
We introduced Scrypt as the default password hashing algorithm in November 2023 and have been auto-converting saved hashes whenever a successful EPP login or registry lock/unlock request is processed. We will send comms to registrars to inform them the upcoming removal of SHA256 support and urge them to log in at least once before the change. Otherwise, they will need to contact support to reset the password out of band after the change. This PR will NOT be submitted until comms are out and the effective date is immediate. Co-authored-by: Weimin Yu <weiminyu@google.com>
This commit is contained in:
parent
1eef260da9
commit
f72a0d2f16
9 changed files with 27 additions and 223 deletions
|
@ -47,7 +47,6 @@ import google.registry.model.eppinput.EppInput.Options;
|
|||
import google.registry.model.eppinput.EppInput.Services;
|
||||
import google.registry.model.eppoutput.EppResponse;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
|
@ -142,17 +141,8 @@ public class LoginFlow implements MutatingFlow {
|
|||
throw new RegistrarAccountNotActiveException();
|
||||
}
|
||||
|
||||
if (login.getNewPassword().isPresent()
|
||||
|| registrar.get().getCurrentHashAlgorithm(login.getPassword()).orElse(null)
|
||||
!= HashAlgorithm.SCRYPT) {
|
||||
String newPassword =
|
||||
login
|
||||
.getNewPassword()
|
||||
.orElseGet(
|
||||
() -> {
|
||||
logger.atInfo().log("Rehashing existing registrar password with Scrypt");
|
||||
return login.getPassword();
|
||||
});
|
||||
if (login.getNewPassword().isPresent()) {
|
||||
String newPassword = login.getNewPassword().get();
|
||||
// Load fresh from database (bypassing the cache) to ensure we don't save stale data.
|
||||
Optional<Registrar> freshRegistrar = Registrar.loadByRegistrarId(login.getClientId());
|
||||
if (freshRegistrar.isEmpty()) {
|
||||
|
|
|
@ -86,8 +86,7 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
|
|||
return false;
|
||||
}
|
||||
return PasswordUtils.verifyPassword(
|
||||
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt)
|
||||
.isPresent();
|
||||
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -62,7 +62,6 @@ import google.registry.model.tld.Tld.TldType;
|
|||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
@ -642,10 +641,6 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
|||
}
|
||||
|
||||
public boolean verifyPassword(String password) {
|
||||
return getCurrentHashAlgorithm(password).isPresent();
|
||||
}
|
||||
|
||||
public Optional<HashAlgorithm> getCurrentHashAlgorithm(String password) {
|
||||
return PasswordUtils.verifyPassword(password, passwordHash, salt);
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ import google.registry.model.UnsafeSerializable;
|
|||
import google.registry.model.registrar.RegistrarPoc.RegistrarPocId;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
@ -242,10 +241,6 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
|
|||
|| isNullOrEmpty(registryLockPasswordHash)) {
|
||||
return false;
|
||||
}
|
||||
return getCurrentHashAlgorithm(registryLockPassword).isPresent();
|
||||
}
|
||||
|
||||
public Optional<HashAlgorithm> getCurrentHashAlgorithm(String registryLockPassword) {
|
||||
return PasswordUtils.verifyPassword(
|
||||
registryLockPassword, registryLockPasswordHash, registryLockPasswordSalt);
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@ import google.registry.request.auth.UserAuthInfo;
|
|||
import google.registry.security.JsonResponseHelper;
|
||||
import google.registry.tools.DomainLockUtils;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.PasswordUtils.HashAlgorithm;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
@ -223,19 +222,6 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
|
|||
checkArgument(
|
||||
registrarPoc.verifyRegistryLockPassword(postInput.password),
|
||||
"Incorrect registry lock password for contact");
|
||||
if (registrarPoc.getCurrentHashAlgorithm(postInput.password).orElse(null)
|
||||
!= HashAlgorithm.SCRYPT) {
|
||||
logger.atInfo().log("Rehashing existing registry lock password with Scrypt.");
|
||||
tm().transact(
|
||||
() -> {
|
||||
tm().update(
|
||||
tm().loadByEntity(registrarPoc)
|
||||
.asBuilder()
|
||||
.setAllowedToSetRegistryLockPassword(true)
|
||||
.setRegistryLockPassword(postInput.password)
|
||||
.build());
|
||||
});
|
||||
}
|
||||
return registrarPoc
|
||||
.getRegistryLockEmailAddress()
|
||||
.orElseThrow(
|
||||
|
|
|
@ -14,15 +14,11 @@
|
|||
|
||||
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;
|
||||
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SCRYPT;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SHA256;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
@ -42,7 +38,6 @@ 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 org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
@ -191,33 +186,6 @@ public abstract class LoginFlowTestCase extends FlowTestCase<LoginFlow> {
|
|||
doFailingTest("login_valid.xml", RegistrarAccountNotActiveException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_sha256Password() 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, SHA256);
|
||||
// Set password directly, as the Java method would have used Scrypt.
|
||||
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(SHA256);
|
||||
doSuccessfulTest("login_valid.xml");
|
||||
// Verifies that after successfully login, the password is re-hased with Scrypt.
|
||||
assertThat(loadRegistrar("NewRegistrar").getCurrentHashAlgorithm(password).get())
|
||||
.isEqualTo(SCRYPT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_incorrectPassword() {
|
||||
persistResource(getRegistrarBuilder().setPassword("diff password").build());
|
||||
|
|
|
@ -15,10 +15,8 @@
|
|||
package google.registry.ui.server.registrar;
|
||||
|
||||
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
|
@ -27,8 +25,6 @@ import static google.registry.testing.SqlHelper.getRegistryLockByVerificationCod
|
|||
import static google.registry.testing.SqlHelper.saveRegistryLock;
|
||||
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
|
||||
import static google.registry.ui.server.registrar.RegistryLockGetActionTest.userFromRegistrarPoc;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SCRYPT;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SHA256;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -42,9 +38,6 @@ import google.registry.model.console.RegistrarRole;
|
|||
import google.registry.model.console.UserRoles;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.RegistryLock;
|
||||
import google.registry.model.registrar.RegistrarPoc;
|
||||
import google.registry.model.registrar.RegistrarPoc.RegistrarPocId;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
|
||||
|
@ -61,7 +54,6 @@ import google.registry.testing.DeterministicStringGenerator;
|
|||
import google.registry.testing.FakeClock;
|
||||
import google.registry.tools.DomainLockUtils;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.PasswordUtils;
|
||||
import google.registry.util.StringGenerator.Alphabets;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
@ -131,48 +123,6 @@ final class RegistryLockPostActionTest {
|
|||
assertSuccess(response, "lock", "Marla.Singer.RegistryLock@crr.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_lock_sha256Password() throws Exception {
|
||||
tm().transact(
|
||||
() -> {
|
||||
// The salt is not exposed by RegistrarPoc (nor should it be), so we query
|
||||
// it directly.
|
||||
String encodedSalt =
|
||||
tm().query(
|
||||
"SELECT registryLockPasswordSalt FROM RegistrarPoc "
|
||||
+ "WHERE emailAddress = :email "
|
||||
+ "AND registrarId = :registrarId",
|
||||
String.class)
|
||||
.setParameter("email", "Marla.Singer@crr.com")
|
||||
.setParameter("registrarId", "TheRegistrar")
|
||||
.getSingleResult();
|
||||
byte[] salt = base64().decode(encodedSalt);
|
||||
String newHash = PasswordUtils.hashPassword("hi", salt, SHA256);
|
||||
// Set password directly, as the Java method would have used Scrypt.
|
||||
tm().query("UPDATE RegistrarPoc SET registryLockPasswordHash = :hash")
|
||||
.setParameter("hash", newHash)
|
||||
.executeUpdate();
|
||||
});
|
||||
RegistrarPoc registrarPoc =
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().loadByKey(
|
||||
VKey.create(
|
||||
RegistrarPoc.class,
|
||||
new RegistrarPocId("Marla.Singer@crr.com", "TheRegistrar"))));
|
||||
assertThat(registrarPoc.getCurrentHashAlgorithm("hi").get()).isEqualTo(SHA256);
|
||||
Map<String, ?> response = action.handleJsonRequest(lockRequest());
|
||||
RegistrarPoc updatedRegistrarPoc =
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().loadByKey(
|
||||
VKey.create(
|
||||
RegistrarPoc.class,
|
||||
new RegistrarPocId("Marla.Singer@crr.com", "TheRegistrar"))));
|
||||
assertThat(updatedRegistrarPoc.getCurrentHashAlgorithm("hi").get()).isEqualTo(SCRYPT);
|
||||
assertSuccess(response, "lock", "Marla.Singer.RegistryLock@crr.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_unlock() throws Exception {
|
||||
saveRegistryLock(createLock().asBuilder().setLockCompletionTime(clock.nowUtc()).build());
|
||||
|
|
|
@ -15,82 +15,29 @@
|
|||
package google.registry.util;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SCRYPT;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SHA256;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
|
||||
/** Common utility class to handle password hashing and salting */
|
||||
/**
|
||||
* Common utility class to handle password hashing and salting /*
|
||||
*
|
||||
* <p>We use a memory-hard hashing algorithm (Scrypt) to prevent brute-force attacks on passwords.
|
||||
*
|
||||
* <p>Note that in tests, we simply concatenate the password and salt which is much faster and
|
||||
* reduces the overall test run time by a half. Our tests are not verifying that SCRYPT is
|
||||
* implemented correctly anyway.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Scrypt">Scrypt</a>
|
||||
*/
|
||||
public final class PasswordUtils {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Supplier<MessageDigest> SHA256_DIGEST_SUPPLIER =
|
||||
Suppliers.memoize(
|
||||
() -> {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// All implementations of MessageDigest are required to support SHA-256.
|
||||
throw new RuntimeException(
|
||||
"All MessageDigest implementations are required to support SHA-256 but this one"
|
||||
+ " didn't",
|
||||
e);
|
||||
}
|
||||
});
|
||||
|
||||
private PasswordUtils() {}
|
||||
|
||||
/**
|
||||
* Password hashing algorithm that takes a password and a salt (both as {@code byte[]}) and
|
||||
* returns a hash.
|
||||
*/
|
||||
public enum HashAlgorithm {
|
||||
/**
|
||||
* SHA-2 that returns a 256-bit digest.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/SHA-2">SHA-2</a>
|
||||
*/
|
||||
@Deprecated
|
||||
SHA256 {
|
||||
@Override
|
||||
byte[] hash(byte[] password, byte[] salt) {
|
||||
return SHA256_DIGEST_SUPPLIER
|
||||
.get()
|
||||
.digest((new String(password, US_ASCII) + base64().encode(salt)).getBytes(US_ASCII));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Memory-hard hashing algorithm, preferred over SHA-256.
|
||||
*
|
||||
* <p>Note that in tests, we simply concatenate the password and salt which is much faster and
|
||||
* reduces the overall test run time by a half. Our tests are not verifying that SCRYPT is
|
||||
* implemented correctly anyway.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Scrypt">Scrypt</a>
|
||||
*/
|
||||
SCRYPT {
|
||||
@Override
|
||||
byte[] hash(byte[] password, byte[] salt) {
|
||||
return RegistryEnvironment.get() == RegistryEnvironment.UNITTEST
|
||||
? Bytes.concat(password, salt)
|
||||
: SCrypt.generate(password, salt, 32768, 8, 1, 256);
|
||||
}
|
||||
};
|
||||
|
||||
abstract byte[] hash(byte[] password, byte[] salt);
|
||||
}
|
||||
|
||||
public static final Supplier<byte[]> SALT_SUPPLIER =
|
||||
() -> {
|
||||
// The generated hashes are 256 bits, and the salt should generally be of the same size.
|
||||
|
@ -99,38 +46,25 @@ public final class PasswordUtils {
|
|||
return salt;
|
||||
};
|
||||
|
||||
public static String hashPassword(String password, byte[] salt) {
|
||||
return hashPassword(password, salt, SCRYPT);
|
||||
private static byte[] hashPassword(byte[] password, byte[] salt) {
|
||||
return RegistryEnvironment.get() == RegistryEnvironment.UNITTEST
|
||||
? Bytes.concat(password, salt)
|
||||
: SCrypt.generate(password, salt, 32768, 8, 1, 256);
|
||||
}
|
||||
|
||||
/** Returns the hash of the password using the provided salt and {@link HashAlgorithm}. */
|
||||
public static String hashPassword(String password, byte[] salt, HashAlgorithm algorithm) {
|
||||
return base64().encode(algorithm.hash(password.getBytes(US_ASCII), salt));
|
||||
/** Returns the hash of the password using the provided salt. */
|
||||
public static String hashPassword(String password, byte[] salt) {
|
||||
return base64().encode(hashPassword(password.getBytes(US_ASCII), salt));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a password by regenerating the hash with the provided salt and comparing it to the
|
||||
* provided hash.
|
||||
*
|
||||
* <p>This method will first try to use {@link HashAlgorithm#SCRYPT} to verify the password, and
|
||||
* falls back to {@link HashAlgorithm#SHA256} if the former fails.
|
||||
*
|
||||
* @return the {@link HashAlgorithm} used to successfully verify the password, or {@link
|
||||
* Optional#empty()} if neither works.
|
||||
*/
|
||||
public static Optional<HashAlgorithm> verifyPassword(String password, String hash, String salt) {
|
||||
public static boolean verifyPassword(String password, String hash, String salt) {
|
||||
byte[] decodedHash = base64().decode(hash);
|
||||
byte[] decodedSalt = base64().decode(salt);
|
||||
byte[] calculatedHash = SCRYPT.hash(password.getBytes(US_ASCII), decodedSalt);
|
||||
if (Arrays.equals(decodedHash, calculatedHash)) {
|
||||
logger.atInfo().log("Scrypt hash verified.");
|
||||
return Optional.of(SCRYPT);
|
||||
}
|
||||
calculatedHash = SHA256.hash(password.getBytes(US_ASCII), decodedSalt);
|
||||
if (Arrays.equals(decodedHash, calculatedHash)) {
|
||||
logger.atInfo().log("SHA256 hash verified.");
|
||||
return Optional.of(SHA256);
|
||||
}
|
||||
return Optional.empty();
|
||||
byte[] calculatedHash = hashPassword(password.getBytes(US_ASCII), decodedSalt);
|
||||
return Arrays.equals(decodedHash, calculatedHash);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ package google.registry.util;
|
|||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SCRYPT;
|
||||
import static google.registry.util.PasswordUtils.HashAlgorithm.SHA256;
|
||||
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
|
||||
import static google.registry.util.PasswordUtils.hashPassword;
|
||||
import static google.registry.util.PasswordUtils.verifyPassword;
|
||||
|
@ -53,18 +51,8 @@ final class PasswordUtilsTest {
|
|||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt);
|
||||
assertThat(hashedPassword).isEqualTo(hashPassword(password, salt, SCRYPT));
|
||||
assertThat(verifyPassword(password, hashedPassword, base64().encode(salt)).get())
|
||||
.isEqualTo(SCRYPT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerify_sha256() {
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt, SHA256);
|
||||
assertThat(verifyPassword(password, hashedPassword, base64().encode(salt)).get())
|
||||
.isEqualTo(SHA256);
|
||||
assertThat(hashedPassword).isEqualTo(hashPassword(password, salt));
|
||||
assertThat(verifyPassword(password, hashedPassword, base64().encode(salt))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -72,7 +60,6 @@ final class PasswordUtilsTest {
|
|||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt);
|
||||
assertThat(verifyPassword(password + "a", hashedPassword, base64().encode(salt)).isPresent())
|
||||
.isFalse();
|
||||
assertThat(verifyPassword(password + "a", hashedPassword, base64().encode(salt))).isFalse();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue