mirror of
https://github.com/google/nomulus.git
synced 2025-08-23 17:51:07 +02:00
Switch to a stronger algorithm for password hashing (#2191)
We have been using SHA256 to hash passwords (for both EPP and registry lock), which is now considered too weak. This PR switches to using Scrypt, a memory-hard slow hash function, with recommended parameters per go/crypto-password-hash. To ease the transition, when a password is being verified, both Scrypt and SHA256 are tried. If SHA256 verification is successful, we re-hash the verified password with Scrypt and replace the stored SHA256 hash with the new one. This way, as long as a user uses the password once before the transition period ends (when Scrypt becomes the only valid algorithm), there would be no need for manual intervention from them. We will send out notifications to users to remind them of the transition and urge them to use the password (which should not be a problem with EPP, but less so with the registry lock). After the transition, out-of-band reset for EPP password, or remove-and-add on the console for registry lock password, would be required for the hashes that have not been re-saved. Note that the re-save logic is not present for console user's registry lock password, as there is no production data for console users yet. Only legacy GAE user's password requires re-save.
This commit is contained in:
parent
ba54208dad
commit
cd23fea698
9 changed files with 286 additions and 43 deletions
|
@ -15,33 +15,114 @@
|
|||
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 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 */
|
||||
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.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Scrypt">Scrypt</a>
|
||||
*/
|
||||
SCRYPT {
|
||||
@Override
|
||||
byte[] hash(byte[] password, byte[] salt) {
|
||||
return SCrypt.generate(password, salt, 32768, 8, 1, 256);
|
||||
}
|
||||
};
|
||||
|
||||
abstract byte[] hash(byte[] password, byte[] salt);
|
||||
}
|
||||
|
||||
public static final Supplier<byte[]> SALT_SUPPLIER =
|
||||
() -> {
|
||||
// There are 32 bytes in a SHA-256 hash, and the salt should generally be the same size.
|
||||
// The generated hashes are 256 bits, and the salt should generally be of the same size.
|
||||
byte[] salt = new byte[32];
|
||||
new SecureRandom().nextBytes(salt);
|
||||
return salt;
|
||||
};
|
||||
|
||||
public static String hashPassword(String password, String salt) {
|
||||
try {
|
||||
return base64()
|
||||
.encode(
|
||||
MessageDigest.getInstance("SHA-256").digest((password + salt).getBytes(US_ASCII)));
|
||||
} 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 didn't", e);
|
||||
public static String hashPassword(String password, byte[] salt) {
|
||||
return hashPassword(password, salt, SCRYPT);
|
||||
}
|
||||
|
||||
/** 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,16 @@ 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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link google.registry.util.PasswordUtils}. */
|
||||
/** Unit tests for {@link PasswordUtils}. */
|
||||
final class PasswordUtilsTest {
|
||||
|
||||
@Test
|
||||
|
@ -36,12 +39,40 @@ final class PasswordUtilsTest {
|
|||
|
||||
@Test
|
||||
void testHash() {
|
||||
String salt = base64().encode(SALT_SUPPLIER.get());
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt);
|
||||
assertThat(hashedPassword).isEqualTo(hashPassword(password, salt));
|
||||
assertThat(hashedPassword).isNotEqualTo(hashPassword(password + "a", salt));
|
||||
String secondSalt = base64().encode(SALT_SUPPLIER.get());
|
||||
byte[] secondSalt = SALT_SUPPLIER.get();
|
||||
assertThat(hashedPassword).isNotEqualTo(hashPassword(password, secondSalt));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerify_scrypt_default() {
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerify_failure() {
|
||||
byte[] salt = SALT_SUPPLIER.get();
|
||||
String password = "mySuperSecurePassword";
|
||||
String hashedPassword = hashPassword(password, salt);
|
||||
assertThat(verifyPassword(password + "a", hashedPassword, base64().encode(salt)).isEmpty())
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue