From 8ec16dca8d8ad7ec46d52baaa6d2a3e1d906f19f Mon Sep 17 00:00:00 2001 From: gbrodman Date: Fri, 23 Aug 2019 22:34:43 -0400 Subject: [PATCH] Add a registry lock password to contacts (#226) * Add a registry lock password to contacts * enabled -> allowed * Simple CR responses, still need to add tests * Add a very simple hashing test file * Allow setting of RL password rather than directly setting it * Round out pw tests * Include 'allowedToSet...' in registrar contact JSON * Responses to CR * fix the hardcoded tests * Use null or empty rather than just null --- .../PasswordOnlyTransportCredentials.java | 2 +- .../google/registry/flows/TlsCredentials.java | 2 +- .../registry/model/registrar/Registrar.java | 34 ++------ .../model/registrar/RegistrarContact.java | 81 ++++++++++++++++--- .../tools/RegistrarContactCommand.java | 15 ++++ .../registry/model/OteAccountBuilderTest.java | 8 +- .../tools/CreateRegistrarCommandTest.java | 4 +- .../tools/RegistrarContactCommandTest.java | 61 ++++++++++++++ .../registry/tools/SetupOteCommandTest.java | 2 +- .../tools/UpdateRegistrarCommandTest.java | 8 +- .../registrar/ConsoleOteSetupActionTest.java | 2 +- .../ConsoleRegistrarCreatorActionTest.java | 4 +- .../google/registry/model/schema.txt | 3 + .../registrar/update_registrar_email.txt | 8 +- .../google/registry/util/PasswordUtils.java | 47 +++++++++++ .../registry/util/PasswordUtilsTest.java | 50 ++++++++++++ 16 files changed, 274 insertions(+), 57 deletions(-) create mode 100644 util/src/main/java/google/registry/util/PasswordUtils.java create mode 100644 util/src/test/java/google/registry/util/PasswordUtilsTest.java diff --git a/core/src/main/java/google/registry/flows/PasswordOnlyTransportCredentials.java b/core/src/main/java/google/registry/flows/PasswordOnlyTransportCredentials.java index a9607ffa7..a60cec81d 100644 --- a/core/src/main/java/google/registry/flows/PasswordOnlyTransportCredentials.java +++ b/core/src/main/java/google/registry/flows/PasswordOnlyTransportCredentials.java @@ -23,7 +23,7 @@ import google.registry.model.registrar.Registrar; public class PasswordOnlyTransportCredentials implements TransportCredentials { @Override public void validate(Registrar r, String password) throws AuthenticationErrorException { - if (!r.testPassword(password)) { + if (!r.verifyPassword(password)) { throw new BadRegistrarPasswordException(); } } diff --git a/core/src/main/java/google/registry/flows/TlsCredentials.java b/core/src/main/java/google/registry/flows/TlsCredentials.java index efd949322..8b8ff974d 100644 --- a/core/src/main/java/google/registry/flows/TlsCredentials.java +++ b/core/src/main/java/google/registry/flows/TlsCredentials.java @@ -145,7 +145,7 @@ public class TlsCredentials implements TransportCredentials { private void validatePassword(Registrar registrar, String password) throws BadRegistrarPasswordException { - if (!registrar.testPassword(password)) { + if (!registrar.verifyPassword(password)) { throw new BadRegistrarPasswordException(); } } diff --git a/core/src/main/java/google/registry/model/registrar/Registrar.java b/core/src/main/java/google/registry/model/registrar/Registrar.java index 602a84b1e..40662793e 100644 --- a/core/src/main/java/google/registry/model/registrar/Registrar.java +++ b/core/src/main/java/google/registry/model/registrar/Registrar.java @@ -34,10 +34,11 @@ import static google.registry.model.registry.Registries.assertTldsExist; import static google.registry.model.transaction.TransactionManagerFactory.tm; import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy; +import static google.registry.util.PasswordUtils.SALT_SUPPLIER; +import static google.registry.util.PasswordUtils.hashPassword; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import static google.registry.util.X509Utils.getCertificateHash; import static google.registry.util.X509Utils.loadCertificate; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Comparator.comparing; import static java.util.function.Predicate.isEqual; @@ -73,10 +74,6 @@ import google.registry.model.common.EntityGroupRoot; import google.registry.model.registrar.Registrar.BillingAccountEntry.CurrencyMapper; import google.registry.model.registry.Registry; import google.registry.util.CidrAddressBlock; -import google.registry.util.NonFinalForTesting; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.security.cert.CertificateParsingException; import java.util.Comparator; import java.util.List; @@ -412,15 +409,6 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable /** Whether or not registry lock is allowed for this registrar. */ boolean registryLockAllowed = false; - @NonFinalForTesting - private static Supplier saltSupplier = - () -> { - // There are 32 bytes in a sha-256 hash, and the salt should generally be the same size. - byte[] salt = new byte[32]; - new SecureRandom().nextBytes(salt); - return salt; - }; - public String getClientId() { return clientIdentifier; } @@ -634,16 +622,6 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable .build(); } - private String hashPassword(String password) { - try { - return base64() - .encode(MessageDigest.getInstance("SHA-256").digest((password + salt).getBytes(UTF_8))); - } catch (NoSuchAlgorithmException e) { - // All implementations of MessageDigest are required to support SHA-256. - throw new RuntimeException(e); - } - } - private static String checkValidPhoneNumber(String phoneNumber) { checkArgument( E164_PATTERN.matcher(phoneNumber).matches(), @@ -652,8 +630,8 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable return phoneNumber; } - public boolean testPassword(String password) { - return hashPassword(password).equals(passwordHash); + public boolean verifyPassword(String password) { + return hashPassword(password, salt).equals(passwordHash); } public String getPhonePasscode() { @@ -888,8 +866,8 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable checkArgument( Range.closed(6, 16).contains(nullToEmpty(password).length()), "Password must be 6-16 characters long."); - getInstance().salt = base64().encode(saltSupplier.get()); - getInstance().passwordHash = getInstance().hashPassword(password); + getInstance().salt = base64().encode(SALT_SUPPLIER.get()); + getInstance().passwordHash = hashPassword(password, getInstance().salt); return this; } diff --git a/core/src/main/java/google/registry/model/registrar/RegistrarContact.java b/core/src/main/java/google/registry/model/registrar/RegistrarContact.java index c660b724d..40279f84a 100644 --- a/core/src/main/java/google/registry/model/registrar/RegistrarContact.java +++ b/core/src/main/java/google/registry/model/registrar/RegistrarContact.java @@ -14,13 +14,18 @@ package google.registry.model.registrar; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.Sets.difference; +import static com.google.common.io.BaseEncoding.base64; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.registrar.Registrar.checkValidEmail; import static google.registry.model.transaction.TransactionManagerFactory.tm; import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy; +import static google.registry.util.PasswordUtils.SALT_SUPPLIER; +import static google.registry.util.PasswordUtils.hashPassword; import static java.util.stream.Collectors.joining; import com.google.common.base.Enums; @@ -135,6 +140,21 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable { */ boolean visibleInDomainWhoisAsAbuse = false; + /** + * Whether or not the contact is allowed to set their registry lock password through the registrar + * console. This will be set to false on contact creation and when the user sets a password. + */ + boolean allowedToSetRegistryLockPassword = false; + + /** + * A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64 + * encoded SHA256 string. + */ + String registryLockPasswordHash; + + /** Randomly generated hash salt. */ + String registryLockPasswordSalt; + public static ImmutableSet typesFromCSV(String csv) { return typesFromStrings(Arrays.asList(csv.split(","))); } @@ -213,19 +233,39 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable { return new Builder(clone(this)); } + public boolean isAllowedToSetRegistryLockPassword() { + return allowedToSetRegistryLockPassword; + } + + public boolean isRegistryLockAllowed() { + return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt); + } + + public boolean verifyRegistryLockPassword(String registryLockPassword) { + if (isNullOrEmpty(registryLockPassword) + || isNullOrEmpty(registryLockPasswordSalt) + || isNullOrEmpty(registryLockPasswordHash)) { + return false; + } + return hashPassword(registryLockPassword, registryLockPasswordSalt) + .equals(registryLockPasswordHash); + } + /** * Returns a string representation that's human friendly. * - *

The output will look something like this:

   {@code
+   * 

The output will look something like this: * - * Some Person - * person@example.com - * Tel: +1.2125650666 - * Types: [ADMIN, WHOIS] - * Visible in WHOIS as Admin contact: Yes - * Visible in WHOIS as Technical contact: No - * GAE-UserID: 1234567890 - * Registrar-Console access: Yes}

+ *
{@code
+   * Some Person
+   * person@example.com
+   * Tel: +1.2125650666
+   * Types: [ADMIN, WHOIS]
+   * Visible in WHOIS as Admin contact: Yes
+   * Visible in WHOIS as Technical contact: No
+   * GAE-UserID: 1234567890
+   * Registrar-Console access: Yes
+   * }
*/ public String toStringMultilinePlainText() { StringBuilder result = new StringBuilder(256); @@ -273,6 +313,7 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable { .put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin) .put("visibleInWhoisAsTech", visibleInWhoisAsTech) .put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse) + .put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword) .put("gaeUserId", gaeUserId) .build(); } @@ -346,5 +387,27 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable { getInstance().gaeUserId = gaeUserId; return this; } + + public Builder setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) { + if (allowedToSetRegistryLockPassword) { + getInstance().registryLockPasswordSalt = null; + getInstance().registryLockPasswordHash = null; + } + getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword; + return this; + } + + public Builder setRegistryLockPassword(String registryLockPassword) { + checkArgument( + getInstance().allowedToSetRegistryLockPassword, + "Not allowed to set registry lock password for this contact"); + checkArgument( + !isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty"); + getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get()); + getInstance().registryLockPasswordHash = + hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt); + getInstance().allowedToSetRegistryLockPassword = false; + return this; + } } } diff --git a/core/src/main/java/google/registry/tools/RegistrarContactCommand.java b/core/src/main/java/google/registry/tools/RegistrarContactCommand.java index ab8c7f359..7a74596ab 100644 --- a/core/src/main/java/google/registry/tools/RegistrarContactCommand.java +++ b/core/src/main/java/google/registry/tools/RegistrarContactCommand.java @@ -131,6 +131,15 @@ final class RegistrarContactCommand extends MutatingCommand { arity = 1) private Boolean visibleInDomainWhoisAsAbuse; + @Nullable + @Parameter( + names = "--allowed_to_set_registry_lock_password", + description = + "Allow this contact to set their registry lock password in the console," + + " enabling registry lock", + arity = 1) + private Boolean allowedToSetRegistryLockPassword; + @Parameter( names = {"-o", "--output"}, description = "Output file when --mode=LIST", @@ -260,6 +269,9 @@ final class RegistrarContactCommand extends MutatingCommand { if (visibleInDomainWhoisAsAbuse != null) { builder.setVisibleInDomainWhoisAsAbuse(visibleInDomainWhoisAsAbuse); } + if (allowedToSetRegistryLockPassword != null) { + builder.setAllowedToSetRegistryLockPassword(allowedToSetRegistryLockPassword); + } return builder.build(); } @@ -301,6 +313,9 @@ final class RegistrarContactCommand extends MutatingCommand { builder.setGaeUserId(null); } } + if (allowedToSetRegistryLockPassword != null) { + builder.setAllowedToSetRegistryLockPassword(allowedToSetRegistryLockPassword); + } return builder.build(); } diff --git a/core/src/test/java/google/registry/model/OteAccountBuilderTest.java b/core/src/test/java/google/registry/model/OteAccountBuilderTest.java index 94347a62d..6ea412fe9 100644 --- a/core/src/test/java/google/registry/model/OteAccountBuilderTest.java +++ b/core/src/test/java/google/registry/model/OteAccountBuilderTest.java @@ -153,7 +153,7 @@ public final class OteAccountBuilderTest { public void testCreateOteEntities_setPassword() { OteAccountBuilder.forClientId("myclientid").setPassword("myPassword").buildAndPersist(); - assertThat(Registrar.loadByClientId("myclientid-3").get().testPassword("myPassword")).isTrue(); + assertThat(Registrar.loadByClientId("myclientid-3").get().verifyPassword("myPassword")).isTrue(); } @Test @@ -268,7 +268,7 @@ public final class OteAccountBuilderTest { .addContact("email@example.com") .buildAndPersist(); - assertThat(Registrar.loadByClientId("myclientid-3").get().testPassword("oldPassword")).isTrue(); + assertThat(Registrar.loadByClientId("myclientid-3").get().verifyPassword("oldPassword")).isTrue(); OteAccountBuilder.forClientId("myclientid") .setPassword("newPassword") @@ -276,9 +276,9 @@ public final class OteAccountBuilderTest { .setReplaceExisting(true) .buildAndPersist(); - assertThat(Registrar.loadByClientId("myclientid-3").get().testPassword("oldPassword")) + assertThat(Registrar.loadByClientId("myclientid-3").get().verifyPassword("oldPassword")) .isFalse(); - assertThat(Registrar.loadByClientId("myclientid-3").get().testPassword("newPassword")).isTrue(); + assertThat(Registrar.loadByClientId("myclientid-3").get().verifyPassword("newPassword")).isTrue(); } @Test diff --git a/core/src/test/java/google/registry/tools/CreateRegistrarCommandTest.java b/core/src/test/java/google/registry/tools/CreateRegistrarCommandTest.java index f013001b3..0659b9cfe 100644 --- a/core/src/test/java/google/registry/tools/CreateRegistrarCommandTest.java +++ b/core/src/test/java/google/registry/tools/CreateRegistrarCommandTest.java @@ -77,7 +77,7 @@ public class CreateRegistrarCommandTest extends CommandTestCase registrarOptional = Registrar.loadByClientId("clientz"); assertThat(registrarOptional).isPresent(); Registrar registrar = registrarOptional.get(); - assertThat(registrar.testPassword("some_password")).isTrue(); + assertThat(registrar.verifyPassword("some_password")).isTrue(); assertThat(registrar.getType()).isEqualTo(Registrar.Type.REAL); assertThat(registrar.getIanaIdentifier()).isEqualTo(8); assertThat(registrar.getState()).isEqualTo(Registrar.State.ACTIVE); @@ -118,7 +118,7 @@ public class CreateRegistrarCommandTest extends CommandTestCase registrar = Registrar.loadByClientId("clientz"); assertThat(registrar).isPresent(); - assertThat(registrar.get().testPassword("some_password")).isTrue(); + assertThat(registrar.get().verifyPassword("some_password")).isTrue(); } @Test diff --git a/core/src/test/java/google/registry/tools/RegistrarContactCommandTest.java b/core/src/test/java/google/registry/tools/RegistrarContactCommandTest.java index 12b04b8ea..27f6fd9f7 100644 --- a/core/src/test/java/google/registry/tools/RegistrarContactCommandTest.java +++ b/core/src/test/java/google/registry/tools/RegistrarContactCommandTest.java @@ -368,6 +368,67 @@ public class RegistrarContactCommandTest extends CommandTestCase registrarContact.asBuilder().setRegistryLockPassword("foo")); + runCommandForced( + "--mode=UPDATE", + "--email=jim.doe@example.com", + "--allowed_to_set_registry_lock_password=true", + "NewRegistrar"); + RegistrarContact newContact = reloadResource(registrarContact); + assertThat(newContact.isAllowedToSetRegistryLockPassword()).isTrue(); + // should be allowed to set the password now + newContact.asBuilder().setRegistryLockPassword("foo"); + } + + @Test + public void testUpdate_setAllowedToSetRegistryLockPassword_removesOldPassword() throws Exception { + Registrar registrar = loadRegistrar("NewRegistrar"); + RegistrarContact registrarContact = + persistSimpleResource( + new RegistrarContact.Builder() + .setParent(registrar) + .setName("Jim Doe") + .setEmailAddress("jim.doe@example.com") + .setAllowedToSetRegistryLockPassword(true) + .setRegistryLockPassword("hi") + .build()); + assertThat(registrarContact.verifyRegistryLockPassword("hi")).isTrue(); + assertThat(registrarContact.verifyRegistryLockPassword("hello")).isFalse(); + runCommandForced( + "--mode=UPDATE", + "--email=jim.doe@example.com", + "--allowed_to_set_registry_lock_password=true", + "NewRegistrar"); + registrarContact = reloadResource(registrarContact); + assertThat(registrarContact.verifyRegistryLockPassword("hi")).isFalse(); + } + @Test public void testCreate_failure_badEmail() { IllegalArgumentException thrown = diff --git a/core/src/test/java/google/registry/tools/SetupOteCommandTest.java b/core/src/test/java/google/registry/tools/SetupOteCommandTest.java index f544930ef..92e267cd0 100644 --- a/core/src/test/java/google/registry/tools/SetupOteCommandTest.java +++ b/core/src/test/java/google/registry/tools/SetupOteCommandTest.java @@ -105,7 +105,7 @@ public class SetupOteCommandTest extends CommandTestCase { assertThat(registrar.getAllowedTlds()).containsExactlyElementsIn(ImmutableSet.of(allowedTld)); assertThat(registrar.getRegistrarName()).isEqualTo(registrarName); assertThat(registrar.getState()).isEqualTo(ACTIVE); - assertThat(registrar.testPassword(password)).isTrue(); + assertThat(registrar.verifyPassword(password)).isTrue(); assertThat(registrar.getIpAddressWhitelist()).isEqualTo(ipWhitelist); assertThat(registrar.getClientCertificateHash()).isEqualTo(SAMPLE_CERT_HASH); // If certificate hash is provided, there's no certificate file stored with the registrar. diff --git a/core/src/test/java/google/registry/tools/UpdateRegistrarCommandTest.java b/core/src/test/java/google/registry/tools/UpdateRegistrarCommandTest.java index 642e63dd0..a937c7c57 100644 --- a/core/src/test/java/google/registry/tools/UpdateRegistrarCommandTest.java +++ b/core/src/test/java/google/registry/tools/UpdateRegistrarCommandTest.java @@ -44,9 +44,9 @@ public class UpdateRegistrarCommandTest extends CommandTestCaseOT&E successfully created for registrar myclientid!"); diff --git a/core/src/test/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorActionTest.java index 5014bb44c..5706990ea 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorActionTest.java @@ -206,7 +206,7 @@ public final class ConsoleRegistrarCreatorActionTest { assertThat(registrar.getIanaIdentifier()).isEqualTo(12321L); assertThat(registrar.getIcannReferralEmail()).isEqualTo("icann@example.com"); assertThat(registrar.getEmailAddress()).isEqualTo("icann@example.com"); - assertThat(registrar.testPassword("abcdefghijklmnop")).isTrue(); + assertThat(registrar.verifyPassword("abcdefghijklmnop")).isTrue(); assertThat(registrar.getPhonePasscode()).isEqualTo("31415"); assertThat(registrar.getState()).isEqualTo(Registrar.State.PENDING); assertThat(registrar.getType()).isEqualTo(Registrar.Type.REAL); @@ -411,7 +411,7 @@ public final class ConsoleRegistrarCreatorActionTest { Registrar registrar = loadByClientId("myclientid").orElse(null); assertThat(registrar).isNotNull(); - assertThat(registrar.testPassword("SomePassword")).isTrue(); + assertThat(registrar.verifyPassword("SomePassword")).isTrue(); assertThat(registrar.getPhonePasscode()).isEqualTo("10203"); } diff --git a/core/src/test/resources/google/registry/model/schema.txt b/core/src/test/resources/google/registry/model/schema.txt index ea72bab4f..f67fa50eb 100644 --- a/core/src/test/resources/google/registry/model/schema.txt +++ b/core/src/test/resources/google/registry/model/schema.txt @@ -469,6 +469,7 @@ class google.registry.model.registrar.RegistrarAddress { class google.registry.model.registrar.RegistrarContact { @Id java.lang.String emailAddress; @Parent com.googlecode.objectify.Key parent; + boolean allowedToSetRegistryLockPassword; boolean visibleInDomainWhoisAsAbuse; boolean visibleInWhoisAsAdmin; boolean visibleInWhoisAsTech; @@ -476,6 +477,8 @@ class google.registry.model.registrar.RegistrarContact { java.lang.String gaeUserId; java.lang.String name; java.lang.String phoneNumber; + java.lang.String registryLockPasswordHash; + java.lang.String registryLockPasswordSalt; java.util.Set types; } enum google.registry.model.registrar.RegistrarContact$Type { diff --git a/core/src/test/resources/google/registry/ui/server/registrar/update_registrar_email.txt b/core/src/test/resources/google/registry/ui/server/registrar/update_registrar_email.txt index 630877de9..0fe61c218 100644 --- a/core/src/test/resources/google/registry/ui/server/registrar/update_registrar_email.txt +++ b/core/src/test/resources/google/registry/ui/server/registrar/update_registrar_email.txt @@ -11,9 +11,9 @@ emailAddress: the.registrar@example.com -> thase@the.registrar url: http://my.fake.url -> http://my.new.url contacts: ADDED: - {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Extra Terrestrial, emailAddress=etphonehome@example.com, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], gaeUserId=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false} + {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Extra Terrestrial, emailAddress=etphonehome@example.com, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], gaeUserId=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false, registryLockPasswordHash=null, registryLockPasswordSalt=null} REMOVED: - {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=John Doe, emailAddress=johndoe@theregistrar.com, phoneNumber=+1.1234567890, faxNumber=null, types=[ADMIN], gaeUserId=31337, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false}, - {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Jian-Yang, emailAddress=jyang@bachman.accelerator, phoneNumber=+1.1234567890, faxNumber=null, types=[TECH], gaeUserId=null, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false} + {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=John Doe, emailAddress=johndoe@theregistrar.com, phoneNumber=+1.1234567890, faxNumber=null, types=[ADMIN], gaeUserId=31337, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false, registryLockPasswordHash=null, registryLockPasswordSalt=null}, + {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Jian-Yang, emailAddress=jyang@bachman.accelerator, phoneNumber=+1.1234567890, faxNumber=null, types=[TECH], gaeUserId=null, visibleInWhoisAsAdmin=false, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false, registryLockPasswordHash=null, registryLockPasswordSalt=null} FINAL CONTENTS: - {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Extra Terrestrial, emailAddress=etphonehome@example.com, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], gaeUserId=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false} + {parent=Key(EntityGroupRoot("cross-tld")/Registrar("TheRegistrar")), name=Extra Terrestrial, emailAddress=etphonehome@example.com, phoneNumber=+1.2345678901, faxNumber=null, types=[ADMIN, BILLING, TECH, WHOIS], gaeUserId=null, visibleInWhoisAsAdmin=true, visibleInWhoisAsTech=false, visibleInDomainWhoisAsAbuse=false, allowedToSetRegistryLockPassword=false, registryLockPasswordHash=null, registryLockPasswordSalt=null} diff --git a/util/src/main/java/google/registry/util/PasswordUtils.java b/util/src/main/java/google/registry/util/PasswordUtils.java new file mode 100644 index 000000000..2855f7afc --- /dev/null +++ b/util/src/main/java/google/registry/util/PasswordUtils.java @@ -0,0 +1,47 @@ +// Copyright 2019 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.util; + +import static com.google.common.io.BaseEncoding.base64; +import static java.nio.charset.StandardCharsets.US_ASCII; + +import com.google.common.base.Supplier; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** Common utility class to handle password hashing and salting */ +public final class PasswordUtils { + + public static final Supplier SALT_SUPPLIER = + () -> { + // There are 32 bytes in a SHA-256 hash, and the salt should generally be 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); + } + } +} diff --git a/util/src/test/java/google/registry/util/PasswordUtilsTest.java b/util/src/test/java/google/registry/util/PasswordUtilsTest.java new file mode 100644 index 000000000..0c523945d --- /dev/null +++ b/util/src/test/java/google/registry/util/PasswordUtilsTest.java @@ -0,0 +1,50 @@ +// Copyright 2019 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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.SALT_SUPPLIER; +import static google.registry.util.PasswordUtils.hashPassword; + +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link google.registry.util.PasswordUtils}. */ +@RunWith(JUnit4.class) +public final class PasswordUtilsTest { + + @Test + public void testDifferentSalts() { + byte[] first = SALT_SUPPLIER.get(); + byte[] second = SALT_SUPPLIER.get(); + assertThat(first.length).isEqualTo(32); + assertThat(second.length).isEqualTo(32); + assertThat(Arrays.equals(first, second)).isFalse(); + } + + @Test + public void testHash() { + String salt = base64().encode(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()); + assertThat(hashedPassword).isNotEqualTo(hashPassword(password, secondSalt)); + } +}