mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 12:07:51 +02:00
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
This commit is contained in:
parent
584f887099
commit
a5f27c693f
16 changed files with 274 additions and 57 deletions
|
@ -23,7 +23,7 @@ import google.registry.model.registrar.Registrar;
|
||||||
public class PasswordOnlyTransportCredentials implements TransportCredentials {
|
public class PasswordOnlyTransportCredentials implements TransportCredentials {
|
||||||
@Override
|
@Override
|
||||||
public void validate(Registrar r, String password) throws AuthenticationErrorException {
|
public void validate(Registrar r, String password) throws AuthenticationErrorException {
|
||||||
if (!r.testPassword(password)) {
|
if (!r.verifyPassword(password)) {
|
||||||
throw new BadRegistrarPasswordException();
|
throw new BadRegistrarPasswordException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,7 +145,7 @@ public class TlsCredentials implements TransportCredentials {
|
||||||
|
|
||||||
private void validatePassword(Registrar registrar, String password)
|
private void validatePassword(Registrar registrar, String password)
|
||||||
throws BadRegistrarPasswordException {
|
throws BadRegistrarPasswordException {
|
||||||
if (!registrar.testPassword(password)) {
|
if (!registrar.verifyPassword(password)) {
|
||||||
throw new BadRegistrarPasswordException();
|
throw new BadRegistrarPasswordException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,11 @@ import static google.registry.model.registry.Registries.assertTldsExist;
|
||||||
import static google.registry.model.transaction.TransactionManagerFactory.tm;
|
import static google.registry.model.transaction.TransactionManagerFactory.tm;
|
||||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
|
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.PreconditionsUtils.checkArgumentNotNull;
|
||||||
import static google.registry.util.X509Utils.getCertificateHash;
|
import static google.registry.util.X509Utils.getCertificateHash;
|
||||||
import static google.registry.util.X509Utils.loadCertificate;
|
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.Comparator.comparing;
|
||||||
import static java.util.function.Predicate.isEqual;
|
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.registrar.Registrar.BillingAccountEntry.CurrencyMapper;
|
||||||
import google.registry.model.registry.Registry;
|
import google.registry.model.registry.Registry;
|
||||||
import google.registry.util.CidrAddressBlock;
|
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.security.cert.CertificateParsingException;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
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. */
|
/** Whether or not registry lock is allowed for this registrar. */
|
||||||
boolean registryLockAllowed = false;
|
boolean registryLockAllowed = false;
|
||||||
|
|
||||||
@NonFinalForTesting
|
|
||||||
private static Supplier<byte[]> 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() {
|
public String getClientId() {
|
||||||
return clientIdentifier;
|
return clientIdentifier;
|
||||||
}
|
}
|
||||||
|
@ -634,16 +622,6 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
|
||||||
.build();
|
.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) {
|
private static String checkValidPhoneNumber(String phoneNumber) {
|
||||||
checkArgument(
|
checkArgument(
|
||||||
E164_PATTERN.matcher(phoneNumber).matches(),
|
E164_PATTERN.matcher(phoneNumber).matches(),
|
||||||
|
@ -652,8 +630,8 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
|
||||||
return phoneNumber;
|
return phoneNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean testPassword(String password) {
|
public boolean verifyPassword(String password) {
|
||||||
return hashPassword(password).equals(passwordHash);
|
return hashPassword(password, salt).equals(passwordHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPhonePasscode() {
|
public String getPhonePasscode() {
|
||||||
|
@ -888,8 +866,8 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
|
||||||
checkArgument(
|
checkArgument(
|
||||||
Range.closed(6, 16).contains(nullToEmpty(password).length()),
|
Range.closed(6, 16).contains(nullToEmpty(password).length()),
|
||||||
"Password must be 6-16 characters long.");
|
"Password must be 6-16 characters long.");
|
||||||
getInstance().salt = base64().encode(saltSupplier.get());
|
getInstance().salt = base64().encode(SALT_SUPPLIER.get());
|
||||||
getInstance().passwordHash = getInstance().hashPassword(password);
|
getInstance().passwordHash = hashPassword(password, getInstance().salt);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,18 @@
|
||||||
|
|
||||||
package google.registry.model.registrar;
|
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.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||||
import static com.google.common.collect.Sets.difference;
|
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.ofy.ObjectifyService.ofy;
|
||||||
import static google.registry.model.registrar.Registrar.checkValidEmail;
|
import static google.registry.model.registrar.Registrar.checkValidEmail;
|
||||||
import static google.registry.model.transaction.TransactionManagerFactory.tm;
|
import static google.registry.model.transaction.TransactionManagerFactory.tm;
|
||||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
|
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 static java.util.stream.Collectors.joining;
|
||||||
|
|
||||||
import com.google.common.base.Enums;
|
import com.google.common.base.Enums;
|
||||||
|
@ -135,6 +140,21 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable {
|
||||||
*/
|
*/
|
||||||
boolean visibleInDomainWhoisAsAbuse = false;
|
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<Type> typesFromCSV(String csv) {
|
public static ImmutableSet<Type> typesFromCSV(String csv) {
|
||||||
return typesFromStrings(Arrays.asList(csv.split(",")));
|
return typesFromStrings(Arrays.asList(csv.split(",")));
|
||||||
}
|
}
|
||||||
|
@ -213,11 +233,30 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable {
|
||||||
return new Builder(clone(this));
|
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.
|
* Returns a string representation that's human friendly.
|
||||||
*
|
*
|
||||||
* <p>The output will look something like this:<pre> {@code
|
* <p>The output will look something like this:
|
||||||
*
|
*
|
||||||
|
* <pre>{@code
|
||||||
* Some Person
|
* Some Person
|
||||||
* person@example.com
|
* person@example.com
|
||||||
* Tel: +1.2125650666
|
* Tel: +1.2125650666
|
||||||
|
@ -225,7 +264,8 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable {
|
||||||
* Visible in WHOIS as Admin contact: Yes
|
* Visible in WHOIS as Admin contact: Yes
|
||||||
* Visible in WHOIS as Technical contact: No
|
* Visible in WHOIS as Technical contact: No
|
||||||
* GAE-UserID: 1234567890
|
* GAE-UserID: 1234567890
|
||||||
* Registrar-Console access: Yes}</pre>
|
* Registrar-Console access: Yes
|
||||||
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
public String toStringMultilinePlainText() {
|
public String toStringMultilinePlainText() {
|
||||||
StringBuilder result = new StringBuilder(256);
|
StringBuilder result = new StringBuilder(256);
|
||||||
|
@ -273,6 +313,7 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable {
|
||||||
.put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin)
|
.put("visibleInWhoisAsAdmin", visibleInWhoisAsAdmin)
|
||||||
.put("visibleInWhoisAsTech", visibleInWhoisAsTech)
|
.put("visibleInWhoisAsTech", visibleInWhoisAsTech)
|
||||||
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
|
.put("visibleInDomainWhoisAsAbuse", visibleInDomainWhoisAsAbuse)
|
||||||
|
.put("allowedToSetRegistryLockPassword", allowedToSetRegistryLockPassword)
|
||||||
.put("gaeUserId", gaeUserId)
|
.put("gaeUserId", gaeUserId)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -346,5 +387,27 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable {
|
||||||
getInstance().gaeUserId = gaeUserId;
|
getInstance().gaeUserId = gaeUserId;
|
||||||
return this;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,6 +131,15 @@ final class RegistrarContactCommand extends MutatingCommand {
|
||||||
arity = 1)
|
arity = 1)
|
||||||
private Boolean visibleInDomainWhoisAsAbuse;
|
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(
|
@Parameter(
|
||||||
names = {"-o", "--output"},
|
names = {"-o", "--output"},
|
||||||
description = "Output file when --mode=LIST",
|
description = "Output file when --mode=LIST",
|
||||||
|
@ -260,6 +269,9 @@ final class RegistrarContactCommand extends MutatingCommand {
|
||||||
if (visibleInDomainWhoisAsAbuse != null) {
|
if (visibleInDomainWhoisAsAbuse != null) {
|
||||||
builder.setVisibleInDomainWhoisAsAbuse(visibleInDomainWhoisAsAbuse);
|
builder.setVisibleInDomainWhoisAsAbuse(visibleInDomainWhoisAsAbuse);
|
||||||
}
|
}
|
||||||
|
if (allowedToSetRegistryLockPassword != null) {
|
||||||
|
builder.setAllowedToSetRegistryLockPassword(allowedToSetRegistryLockPassword);
|
||||||
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,6 +313,9 @@ final class RegistrarContactCommand extends MutatingCommand {
|
||||||
builder.setGaeUserId(null);
|
builder.setGaeUserId(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (allowedToSetRegistryLockPassword != null) {
|
||||||
|
builder.setAllowedToSetRegistryLockPassword(allowedToSetRegistryLockPassword);
|
||||||
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -153,7 +153,7 @@ public final class OteAccountBuilderTest {
|
||||||
public void testCreateOteEntities_setPassword() {
|
public void testCreateOteEntities_setPassword() {
|
||||||
OteAccountBuilder.forClientId("myclientid").setPassword("myPassword").buildAndPersist();
|
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
|
@Test
|
||||||
|
@ -268,7 +268,7 @@ public final class OteAccountBuilderTest {
|
||||||
.addContact("email@example.com")
|
.addContact("email@example.com")
|
||||||
.buildAndPersist();
|
.buildAndPersist();
|
||||||
|
|
||||||
assertThat(Registrar.loadByClientId("myclientid-3").get().testPassword("oldPassword")).isTrue();
|
assertThat(Registrar.loadByClientId("myclientid-3").get().verifyPassword("oldPassword")).isTrue();
|
||||||
|
|
||||||
OteAccountBuilder.forClientId("myclientid")
|
OteAccountBuilder.forClientId("myclientid")
|
||||||
.setPassword("newPassword")
|
.setPassword("newPassword")
|
||||||
|
@ -276,9 +276,9 @@ public final class OteAccountBuilderTest {
|
||||||
.setReplaceExisting(true)
|
.setReplaceExisting(true)
|
||||||
.buildAndPersist();
|
.buildAndPersist();
|
||||||
|
|
||||||
assertThat(Registrar.loadByClientId("myclientid-3").get().testPassword("oldPassword"))
|
assertThat(Registrar.loadByClientId("myclientid-3").get().verifyPassword("oldPassword"))
|
||||||
.isFalse();
|
.isFalse();
|
||||||
assertThat(Registrar.loadByClientId("myclientid-3").get().testPassword("newPassword")).isTrue();
|
assertThat(Registrar.loadByClientId("myclientid-3").get().verifyPassword("newPassword")).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -77,7 +77,7 @@ public class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarC
|
||||||
Optional<Registrar> registrarOptional = Registrar.loadByClientId("clientz");
|
Optional<Registrar> registrarOptional = Registrar.loadByClientId("clientz");
|
||||||
assertThat(registrarOptional).isPresent();
|
assertThat(registrarOptional).isPresent();
|
||||||
Registrar registrar = registrarOptional.get();
|
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.getType()).isEqualTo(Registrar.Type.REAL);
|
||||||
assertThat(registrar.getIanaIdentifier()).isEqualTo(8);
|
assertThat(registrar.getIanaIdentifier()).isEqualTo(8);
|
||||||
assertThat(registrar.getState()).isEqualTo(Registrar.State.ACTIVE);
|
assertThat(registrar.getState()).isEqualTo(Registrar.State.ACTIVE);
|
||||||
|
@ -118,7 +118,7 @@ public class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarC
|
||||||
|
|
||||||
Optional<Registrar> registrar = Registrar.loadByClientId("clientz");
|
Optional<Registrar> registrar = Registrar.loadByClientId("clientz");
|
||||||
assertThat(registrar).isPresent();
|
assertThat(registrar).isPresent();
|
||||||
assertThat(registrar.get().testPassword("some_password")).isTrue();
|
assertThat(registrar.get().verifyPassword("some_password")).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -368,6 +368,67 @@ public class RegistrarContactCommandTest extends CommandTestCase<RegistrarContac
|
||||||
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isTrue();
|
assertThat(loadRegistrar("NewRegistrar").getContactsRequireSyncing()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreate_setAllowedToSetRegistryLockPassword() throws Exception {
|
||||||
|
runCommandForced(
|
||||||
|
"--mode=CREATE",
|
||||||
|
"--name=Jim Doe",
|
||||||
|
"--email=jim.doe@example.com",
|
||||||
|
"--allowed_to_set_registry_lock_password=true",
|
||||||
|
"NewRegistrar");
|
||||||
|
RegistrarContact registrarContact = loadRegistrar("NewRegistrar").getContacts().asList().get(1);
|
||||||
|
assertThat(registrarContact.isAllowedToSetRegistryLockPassword()).isTrue();
|
||||||
|
registrarContact.asBuilder().setRegistryLockPassword("foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdate_setAllowedToSetRegistryLockPassword() throws Exception {
|
||||||
|
Registrar registrar = loadRegistrar("NewRegistrar");
|
||||||
|
RegistrarContact registrarContact =
|
||||||
|
persistSimpleResource(
|
||||||
|
new RegistrarContact.Builder()
|
||||||
|
.setParent(registrar)
|
||||||
|
.setName("Jim Doe")
|
||||||
|
.setEmailAddress("jim.doe@example.com")
|
||||||
|
.build());
|
||||||
|
assertThat(registrarContact.isAllowedToSetRegistryLockPassword()).isFalse();
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> 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
|
@Test
|
||||||
public void testCreate_failure_badEmail() {
|
public void testCreate_failure_badEmail() {
|
||||||
IllegalArgumentException thrown =
|
IllegalArgumentException thrown =
|
||||||
|
|
|
@ -105,7 +105,7 @@ public class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
|
||||||
assertThat(registrar.getAllowedTlds()).containsExactlyElementsIn(ImmutableSet.of(allowedTld));
|
assertThat(registrar.getAllowedTlds()).containsExactlyElementsIn(ImmutableSet.of(allowedTld));
|
||||||
assertThat(registrar.getRegistrarName()).isEqualTo(registrarName);
|
assertThat(registrar.getRegistrarName()).isEqualTo(registrarName);
|
||||||
assertThat(registrar.getState()).isEqualTo(ACTIVE);
|
assertThat(registrar.getState()).isEqualTo(ACTIVE);
|
||||||
assertThat(registrar.testPassword(password)).isTrue();
|
assertThat(registrar.verifyPassword(password)).isTrue();
|
||||||
assertThat(registrar.getIpAddressWhitelist()).isEqualTo(ipWhitelist);
|
assertThat(registrar.getIpAddressWhitelist()).isEqualTo(ipWhitelist);
|
||||||
assertThat(registrar.getClientCertificateHash()).isEqualTo(SAMPLE_CERT_HASH);
|
assertThat(registrar.getClientCertificateHash()).isEqualTo(SAMPLE_CERT_HASH);
|
||||||
// If certificate hash is provided, there's no certificate file stored with the registrar.
|
// If certificate hash is provided, there's no certificate file stored with the registrar.
|
||||||
|
|
|
@ -44,9 +44,9 @@ public class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarC
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSuccess_password() throws Exception {
|
public void testSuccess_password() throws Exception {
|
||||||
assertThat(loadRegistrar("NewRegistrar").testPassword("some_password")).isFalse();
|
assertThat(loadRegistrar("NewRegistrar").verifyPassword("some_password")).isFalse();
|
||||||
runCommand("--password=some_password", "--force", "NewRegistrar");
|
runCommand("--password=some_password", "--force", "NewRegistrar");
|
||||||
assertThat(loadRegistrar("NewRegistrar").testPassword("some_password")).isTrue();
|
assertThat(loadRegistrar("NewRegistrar").verifyPassword("some_password")).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -814,10 +814,10 @@ public class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarC
|
||||||
Registrar registrar =
|
Registrar registrar =
|
||||||
persistResource(
|
persistResource(
|
||||||
loadRegistrar("NewRegistrar").asBuilder().setPoNumber(Optional.of("1664")).build());
|
loadRegistrar("NewRegistrar").asBuilder().setPoNumber(Optional.of("1664")).build());
|
||||||
assertThat(registrar.testPassword("some_password")).isFalse();
|
assertThat(registrar.verifyPassword("some_password")).isFalse();
|
||||||
runCommand("--password=some_password", "--force", "NewRegistrar");
|
runCommand("--password=some_password", "--force", "NewRegistrar");
|
||||||
Registrar reloadedRegistrar = loadRegistrar("NewRegistrar");
|
Registrar reloadedRegistrar = loadRegistrar("NewRegistrar");
|
||||||
assertThat(reloadedRegistrar.testPassword("some_password")).isTrue();
|
assertThat(reloadedRegistrar.verifyPassword("some_password")).isTrue();
|
||||||
assertThat(reloadedRegistrar.getPoNumber()).hasValue("1664");
|
assertThat(reloadedRegistrar.getPoNumber()).hasValue("1664");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,7 @@ public final class ConsoleOteSetupActionTest {
|
||||||
|
|
||||||
// We just check some samples to make sure OteAccountBuilder was called successfully. We aren't
|
// We just check some samples to make sure OteAccountBuilder was called successfully. We aren't
|
||||||
// checking that all the entities are there or that they have the correct values.
|
// checking that all the entities are there or that they have the correct values.
|
||||||
assertThat(loadByClientId("myclientid-4").get().testPassword("SomePassword"))
|
assertThat(loadByClientId("myclientid-4").get().verifyPassword("SomePassword"))
|
||||||
.isTrue();
|
.isTrue();
|
||||||
assertThat(response.getPayload())
|
assertThat(response.getPayload())
|
||||||
.contains("<h1>OT&E successfully created for registrar myclientid!</h1>");
|
.contains("<h1>OT&E successfully created for registrar myclientid!</h1>");
|
||||||
|
|
|
@ -206,7 +206,7 @@ public final class ConsoleRegistrarCreatorActionTest {
|
||||||
assertThat(registrar.getIanaIdentifier()).isEqualTo(12321L);
|
assertThat(registrar.getIanaIdentifier()).isEqualTo(12321L);
|
||||||
assertThat(registrar.getIcannReferralEmail()).isEqualTo("icann@example.com");
|
assertThat(registrar.getIcannReferralEmail()).isEqualTo("icann@example.com");
|
||||||
assertThat(registrar.getEmailAddress()).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.getPhonePasscode()).isEqualTo("31415");
|
||||||
assertThat(registrar.getState()).isEqualTo(Registrar.State.PENDING);
|
assertThat(registrar.getState()).isEqualTo(Registrar.State.PENDING);
|
||||||
assertThat(registrar.getType()).isEqualTo(Registrar.Type.REAL);
|
assertThat(registrar.getType()).isEqualTo(Registrar.Type.REAL);
|
||||||
|
@ -411,7 +411,7 @@ public final class ConsoleRegistrarCreatorActionTest {
|
||||||
|
|
||||||
Registrar registrar = loadByClientId("myclientid").orElse(null);
|
Registrar registrar = loadByClientId("myclientid").orElse(null);
|
||||||
assertThat(registrar).isNotNull();
|
assertThat(registrar).isNotNull();
|
||||||
assertThat(registrar.testPassword("SomePassword")).isTrue();
|
assertThat(registrar.verifyPassword("SomePassword")).isTrue();
|
||||||
assertThat(registrar.getPhonePasscode()).isEqualTo("10203");
|
assertThat(registrar.getPhonePasscode()).isEqualTo("10203");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -469,6 +469,7 @@ class google.registry.model.registrar.RegistrarAddress {
|
||||||
class google.registry.model.registrar.RegistrarContact {
|
class google.registry.model.registrar.RegistrarContact {
|
||||||
@Id java.lang.String emailAddress;
|
@Id java.lang.String emailAddress;
|
||||||
@Parent com.googlecode.objectify.Key<google.registry.model.registrar.Registrar> parent;
|
@Parent com.googlecode.objectify.Key<google.registry.model.registrar.Registrar> parent;
|
||||||
|
boolean allowedToSetRegistryLockPassword;
|
||||||
boolean visibleInDomainWhoisAsAbuse;
|
boolean visibleInDomainWhoisAsAbuse;
|
||||||
boolean visibleInWhoisAsAdmin;
|
boolean visibleInWhoisAsAdmin;
|
||||||
boolean visibleInWhoisAsTech;
|
boolean visibleInWhoisAsTech;
|
||||||
|
@ -476,6 +477,8 @@ class google.registry.model.registrar.RegistrarContact {
|
||||||
java.lang.String gaeUserId;
|
java.lang.String gaeUserId;
|
||||||
java.lang.String name;
|
java.lang.String name;
|
||||||
java.lang.String phoneNumber;
|
java.lang.String phoneNumber;
|
||||||
|
java.lang.String registryLockPasswordHash;
|
||||||
|
java.lang.String registryLockPasswordSalt;
|
||||||
java.util.Set<google.registry.model.registrar.RegistrarContact$Type> types;
|
java.util.Set<google.registry.model.registrar.RegistrarContact$Type> types;
|
||||||
}
|
}
|
||||||
enum google.registry.model.registrar.RegistrarContact$Type {
|
enum google.registry.model.registrar.RegistrarContact$Type {
|
||||||
|
|
|
@ -11,9 +11,9 @@ emailAddress: the.registrar@example.com -> thase@the.registrar
|
||||||
url: http://my.fake.url -> http://my.new.url
|
url: http://my.fake.url -> http://my.new.url
|
||||||
contacts:
|
contacts:
|
||||||
ADDED:
|
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:
|
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=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}
|
{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:
|
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}
|
||||||
|
|
47
util/src/main/java/google/registry/util/PasswordUtils.java
Normal file
47
util/src/main/java/google/registry/util/PasswordUtils.java
Normal file
|
@ -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<byte[]> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue