mirror of
https://github.com/google/nomulus
synced 2026-01-03 11:45:39 +00: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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user