From 11902dec3c2adc382619309090a4ec438eb3aab1 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Fri, 7 Jun 2019 15:19:11 -0700 Subject: [PATCH] Support for v2 registration lock --- .../WhisperServerConfiguration.java | 9 ++ .../textsecuregcm/WhisperServerService.java | 15 +- .../auth/AuthenticationCredentials.java | 22 +-- ...> ExternalServiceCredentialGenerator.java} | 26 ++-- ...s.java => ExternalServiceCredentials.java} | 6 +- .../SecureStorageServiceConfiguration.java | 18 +++ .../controllers/AccountController.java | 127 +++++++++++----- .../controllers/DirectoryController.java | 18 +-- .../controllers/SecureStorageController.java | 31 ++++ .../entities/AccountAttributes.java | 39 ++--- .../textsecuregcm/entities/DeprecatedPin.java | 26 ++++ .../entities/RegistrationLock.java | 12 +- .../entities/RegistrationLockFailure.java | 14 +- .../textsecuregcm/storage/Account.java | 23 +++ .../auth/AuthenticationCredentialsTest.java | 35 +++++ ...ternalServiceCredentialsGeneratorTest.java | 29 ++++ .../controllers/AccountControllerTest.java | 138 +++++++++++++++++- .../controllers/DeviceControllerTest.java | 2 +- .../controllers/DirectoryControllerTest.java | 12 +- .../SecureStorageControllerTest.java | 59 ++++++++ .../fixtures/transparent_account.json | 2 +- .../fixtures/transparent_account2.json | 2 +- 22 files changed, 538 insertions(+), 127 deletions(-) rename service/src/main/java/org/whispersystems/textsecuregcm/auth/{DirectoryCredentialsGenerator.java => ExternalServiceCredentialGenerator.java} (72%) rename service/src/main/java/org/whispersystems/textsecuregcm/auth/{DirectoryCredentials.java => ExternalServiceCredentials.java} (70%) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/entities/DeprecatedPin.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/AuthenticationCredentialsTest.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/ExternalServiceCredentialsGeneratorTest.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/SecureStorageControllerTest.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index 0e881cdee..5f2084c37 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -156,6 +156,11 @@ public class WhisperServerConfiguration extends Configuration { @JsonProperty private RecaptchaConfiguration recaptcha; + @Valid + @NotNull + @JsonProperty + private SecureStorageServiceConfiguration storageService; + private Map transparentDataIndex = new HashMap<>(); public RecaptchaConfiguration getRecaptchaConfiguration() { @@ -194,6 +199,10 @@ public class WhisperServerConfiguration extends Configuration { return directory; } + public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() { + return storageService; + } + public AccountDatabaseCrawlerConfiguration getAccountDatabaseCrawlerConfiguration() { return accountDatabaseCrawler; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index b8a0cb670..323ef521e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -29,7 +29,7 @@ import org.jdbi.v3.core.Jdbi; import org.whispersystems.dispatch.DispatchManager; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.CertificateGenerator; -import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; @@ -44,6 +44,7 @@ import org.whispersystems.textsecuregcm.controllers.KeysController; import org.whispersystems.textsecuregcm.controllers.MessageController; import org.whispersystems.textsecuregcm.controllers.ProfileController; import org.whispersystems.textsecuregcm.controllers.ProvisioningController; +import org.whispersystems.textsecuregcm.controllers.SecureStorageController; import org.whispersystems.textsecuregcm.controllers.TransparentDataController; import org.whispersystems.textsecuregcm.controllers.VoiceVerificationController; import org.whispersystems.textsecuregcm.limits.RateLimiters; @@ -204,6 +205,12 @@ public class WhisperServerService extends Application(ImmutableSet.of(Account.class, DisabledPermittedAccount.class))); - environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender)); + environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, storageCredentialsGenerator)); environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices())); environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator)); environment.jersey().register(new ProvisioningController(rateLimiters, pushSender)); environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()))); environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales())); environment.jersey().register(new TransparentDataController(accountsManager, config.getTransparentDataIndex())); + environment.jersey().register(new SecureStorageController(storageCredentialsGenerator)); environment.jersey().register(attachmentControllerV1); environment.jersey().register(attachmentControllerV2); environment.jersey().register(keysController); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticationCredentials.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticationCredentials.java index da772b607..c63a9e158 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticationCredentials.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticationCredentials.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify @@ -17,18 +17,14 @@ package org.whispersystems.textsecuregcm.auth; import org.apache.commons.codec.binary.Hex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public class AuthenticationCredentials { - private final Logger logger = LoggerFactory.getLogger(AuthenticationCredentials.class); - private final String hashedAuthenticationToken; private final String salt; @@ -38,7 +34,7 @@ public class AuthenticationCredentials { } public AuthenticationCredentials(String authenticationToken) { - this.salt = Math.abs(new SecureRandom().nextInt()) + ""; + this.salt = String.valueOf(Math.abs(new SecureRandom().nextInt())); this.hashedAuthenticationToken = getHashedValue(salt, authenticationToken); } @@ -52,19 +48,13 @@ public class AuthenticationCredentials { public boolean verify(String authenticationToken) { String theirValue = getHashedValue(salt, authenticationToken); - - logger.debug("Comparing: " + theirValue + " , " + this.hashedAuthenticationToken); - - return theirValue.equals(this.hashedAuthenticationToken); + return MessageDigest.isEqual(theirValue.getBytes(StandardCharsets.UTF_8), this.hashedAuthenticationToken.getBytes(StandardCharsets.UTF_8)); } private static String getHashedValue(String salt, String token) { - Logger logger = LoggerFactory.getLogger(AuthenticationCredentials.class); - logger.debug("Getting hashed token: " + salt + " , " + token); - try { - return new String(Hex.encodeHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes("UTF-8")))); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { + return new String(Hex.encodeHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes(StandardCharsets.UTF_8)))); + } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/DirectoryCredentialsGenerator.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialGenerator.java similarity index 72% rename from service/src/main/java/org/whispersystems/textsecuregcm/auth/DirectoryCredentialsGenerator.java rename to service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialGenerator.java index 595c1bdf8..22b670108 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/DirectoryCredentialsGenerator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentialGenerator.java @@ -1,6 +1,5 @@ package org.whispersystems.textsecuregcm.auth; -import com.google.common.base.Optional; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.slf4j.Logger; @@ -14,27 +13,29 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.concurrent.TimeUnit; -public class DirectoryCredentialsGenerator { +public class ExternalServiceCredentialGenerator { - private final Logger logger = LoggerFactory.getLogger(DirectoryCredentialsGenerator.class); + private final Logger logger = LoggerFactory.getLogger(ExternalServiceCredentialGenerator.class); private final byte[] key; private final byte[] userIdKey; + private final boolean usernameDerivation; - public DirectoryCredentialsGenerator(byte[] key, byte[] userIdKey) { - this.key = key; - this.userIdKey = userIdKey; + public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) { + this.key = key; + this.userIdKey = userIdKey; + this.usernameDerivation = usernameDerivation; } - public DirectoryCredentials generateFor(String number) { + public ExternalServiceCredentials generateFor(String number) { Mac mac = getMacInstance(); - String username = getUserId(number, mac); + String username = getUserId(number, mac, usernameDerivation); long currentTimeSeconds = System.currentTimeMillis() / 1000; String prefix = username + ":" + currentTimeSeconds; String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10)); String token = prefix + ":" + output; - return new DirectoryCredentials(username, token); + return new ExternalServiceCredentials(username, token); } @@ -46,7 +47,7 @@ public class DirectoryCredentialsGenerator { return false; } - if (!getUserId(number, mac).equals(parts[0])) { + if (!getUserId(number, mac, usernameDerivation).equals(parts[0])) { return false; } @@ -57,8 +58,9 @@ public class DirectoryCredentialsGenerator { return isValidSignature(parts[0] + ":" + parts[1], parts[2], mac); } - private String getUserId(String number, Mac mac) { - return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10)); + private String getUserId(String number, Mac mac, boolean usernameDerivation) { + if (usernameDerivation) return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10)); + else return number; } private boolean isValidTime(String timeString, long currentTimeMillis) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/DirectoryCredentials.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentials.java similarity index 70% rename from service/src/main/java/org/whispersystems/textsecuregcm/auth/DirectoryCredentials.java rename to service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentials.java index cd11d2e01..910b9c258 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/DirectoryCredentials.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/ExternalServiceCredentials.java @@ -3,7 +3,7 @@ package org.whispersystems.textsecuregcm.auth; import com.fasterxml.jackson.annotation.JsonProperty; -public class DirectoryCredentials { +public class ExternalServiceCredentials { @JsonProperty private String username; @@ -11,12 +11,12 @@ public class DirectoryCredentials { @JsonProperty private String password; - public DirectoryCredentials(String username, String password) { + public ExternalServiceCredentials(String username, String password) { this.username = username; this.password = password; } - public DirectoryCredentials() {} + public ExternalServiceCredentials() {} public String getUsername() { return username; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java new file mode 100644 index 000000000..613e61b72 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SecureStorageServiceConfiguration.java @@ -0,0 +1,18 @@ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.hibernate.validator.constraints.NotEmpty; + +public class SecureStorageServiceConfiguration { + + @NotEmpty + @JsonProperty + private String userAuthenticationTokenSharedSecret; + + public byte[] getUserAuthenticationTokenSharedSecret() throws DecoderException { + return Hex.decodeHex(userAuthenticationTokenSharedSecret.toCharArray()); + } + +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index ab3de1537..4a4e45727 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -26,6 +26,8 @@ import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthorizationHeader; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.TurnToken; @@ -34,6 +36,7 @@ import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.DeviceName; import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; +import org.whispersystems.textsecuregcm.entities.DeprecatedPin; import org.whispersystems.textsecuregcm.entities.RegistrationLock; import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; import org.whispersystems.textsecuregcm.limits.RateLimiters; @@ -95,18 +98,19 @@ public class AccountController { private final Meter captchaFailureMeter = metricRegistry.meter(name(AccountController.class, "captcha_failure" )); - private final PendingAccountsManager pendingAccounts; - private final AccountsManager accounts; - private final AbusiveHostRules abusiveHostRules; - private final RateLimiters rateLimiters; - private final SmsSender smsSender; - private final DirectoryQueue directoryQueue; - private final MessagesManager messagesManager; - private final TurnTokenGenerator turnTokenGenerator; - private final Map testDevices; - private final RecaptchaClient recaptchaClient; - private final GCMSender gcmSender; - private final APNSender apnSender; + private final PendingAccountsManager pendingAccounts; + private final AccountsManager accounts; + private final AbusiveHostRules abusiveHostRules; + private final RateLimiters rateLimiters; + private final SmsSender smsSender; + private final DirectoryQueue directoryQueue; + private final MessagesManager messagesManager; + private final TurnTokenGenerator turnTokenGenerator; + private final Map testDevices; + private final RecaptchaClient recaptchaClient; + private final GCMSender gcmSender; + private final APNSender apnSender; + private final ExternalServiceCredentialGenerator storageServiceCredentialGenerator; public AccountController(PendingAccountsManager pendingAccounts, AccountsManager accounts, @@ -119,20 +123,22 @@ public class AccountController { Map testDevices, RecaptchaClient recaptchaClient, GCMSender gcmSender, - APNSender apnSender) + APNSender apnSender, + ExternalServiceCredentialGenerator storageServiceCredentialGenerator) { - this.pendingAccounts = pendingAccounts; - this.accounts = accounts; - this.abusiveHostRules = abusiveHostRules; - this.rateLimiters = rateLimiters; - this.smsSender = smsSenderFactory; - this.directoryQueue = directoryQueue; - this.messagesManager = messagesManager; - this.testDevices = testDevices; - this.turnTokenGenerator = turnTokenGenerator; - this.recaptchaClient = recaptchaClient; - this.gcmSender = gcmSender; - this.apnSender = apnSender; + this.pendingAccounts = pendingAccounts; + this.accounts = accounts; + this.abusiveHostRules = abusiveHostRules; + this.rateLimiters = rateLimiters; + this.smsSender = smsSenderFactory; + this.directoryQueue = directoryQueue; + this.messagesManager = messagesManager; + this.testDevices = testDevices; + this.turnTokenGenerator = turnTokenGenerator; + this.recaptchaClient = recaptchaClient; + this.gcmSender = gcmSender; + this.apnSender = apnSender; + this.storageServiceCredentialGenerator = storageServiceCredentialGenerator; } @Timed @@ -260,25 +266,42 @@ public class AccountController { Optional existingAccount = accounts.get(number); - if (existingAccount.isPresent() && - existingAccount.get().getPin().isPresent() && + if (existingAccount.isPresent() && + (existingAccount.get().getPin().isPresent() || existingAccount.get().getRegistrationLock().isPresent()) && System.currentTimeMillis() - existingAccount.get().getLastSeen() < TimeUnit.DAYS.toMillis(7)) { rateLimiters.getVerifyLimiter().clear(number); - long timeRemaining = TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - existingAccount.get().getLastSeen()); + long timeRemaining = TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - existingAccount.get().getLastSeen()); + Optional credentials = existingAccount.get().getRegistrationLock().isPresent() && + existingAccount.get().getRegistrationLockSalt().isPresent() ? + Optional.of(storageServiceCredentialGenerator.generateFor(number)) : + Optional.empty(); - if (accountAttributes.getPin() == null) { + if (Util.isEmpty(accountAttributes.getPin()) && + Util.isEmpty(accountAttributes.getRegistrationLock())) + { throw new WebApplicationException(Response.status(423) - .entity(new RegistrationLockFailure(timeRemaining)) + .entity(new RegistrationLockFailure(timeRemaining, credentials.orElse(null))) .build()); } rateLimiters.getPinLimiter().validate(number); - if (!MessageDigest.isEqual(existingAccount.get().getPin().get().getBytes(), accountAttributes.getPin().getBytes())) { + boolean pinMatches; + + if (existingAccount.get().getRegistrationLock().isPresent() && existingAccount.get().getRegistrationLockSalt().isPresent()) { + pinMatches = new AuthenticationCredentials(existingAccount.get().getRegistrationLock().get(), + existingAccount.get().getRegistrationLockSalt().get()).verify(accountAttributes.getRegistrationLock()); + } else if (existingAccount.get().getPin().isPresent()) { + pinMatches = MessageDigest.isEqual(existingAccount.get().getPin().get().getBytes(), accountAttributes.getPin().getBytes()); + } else { + throw new AssertionError("Invalid registration lock state"); + } + + if (!pinMatches) { throw new WebApplicationException(Response.status(423) - .entity(new RegistrationLockFailure(timeRemaining)) + .entity(new RegistrationLockFailure(timeRemaining, credentials.orElse(null))) .build()); } @@ -382,12 +405,37 @@ public class AccountController { } } + @Timed + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Path("/registration_lock") + public void setRegistrationLock(@Auth Account account, @Valid RegistrationLock accountLock) { + AuthenticationCredentials credentials = new AuthenticationCredentials(accountLock.getRegistrationLock()); + account.setRegistrationLock(credentials.getHashedAuthenticationToken()); + account.setRegistrationLockSalt(credentials.getSalt()); + account.setPin(null); + + accounts.update(account); + } + + @Timed + @DELETE + @Path("/registration_lock") + public void removeRegistrationLock(@Auth Account account) { + account.setRegistrationLock(null); + account.setRegistrationLockSalt(null); + accounts.update(account); + } + @Timed @PUT @Produces(MediaType.APPLICATION_JSON) @Path("/pin/") - public void setPin(@Auth Account account, @Valid RegistrationLock accountLock) { + public void setPin(@Auth Account account, @Valid DeprecatedPin accountLock) { account.setPin(accountLock.getPin()); + account.setRegistrationLock(null); + account.setRegistrationLockSalt(null); + accounts.update(account); } @@ -436,7 +484,18 @@ public class AccountController { device.setSignalingKey(attributes.getSignalingKey()); device.setUserAgent(userAgent); - account.setPin(attributes.getPin()); + if (!Util.isEmpty(attributes.getPin())) { + account.setPin(attributes.getPin()); + } else if (!Util.isEmpty(attributes.getRegistrationLock())) { + AuthenticationCredentials credentials = new AuthenticationCredentials(attributes.getRegistrationLock()); + account.setRegistrationLock(credentials.getHashedAuthenticationToken()); + account.setRegistrationLockSalt(credentials.getSalt()); + } else { + account.setPin(null); + account.setRegistrationLock(null); + account.setRegistrationLockSalt(null); + } + account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey()); account.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess()); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryController.java index ce757ea48..e37642fa5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DirectoryController.java @@ -23,7 +23,7 @@ import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.annotation.Timed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContactTokens; import org.whispersystems.textsecuregcm.entities.ClientContacts; @@ -86,17 +86,17 @@ public class DirectoryController { } }}; - private final RateLimiters rateLimiters; - private final DirectoryManager directory; - private final DirectoryCredentialsGenerator userTokenGenerator; + private final RateLimiters rateLimiters; + private final DirectoryManager directory; + private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator; public DirectoryController(RateLimiters rateLimiters, DirectoryManager directory, - DirectoryCredentialsGenerator userTokenGenerator) + ExternalServiceCredentialGenerator userTokenGenerator) { - this.directory = directory; - this.rateLimiters = rateLimiters; - this.userTokenGenerator = userTokenGenerator; + this.directory = directory; + this.rateLimiters = rateLimiters; + this.directoryServiceTokenGenerator = userTokenGenerator; } @Timed @@ -104,7 +104,7 @@ public class DirectoryController { @Path("/auth") @Produces(MediaType.APPLICATION_JSON) public Response getAuthToken(@Auth Account account) { - return Response.ok().entity(userTokenGenerator.generateFor(account.getNumber())).build(); + return Response.ok().entity(directoryServiceTokenGenerator.generateFor(account.getNumber())).build(); } @PUT diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java new file mode 100644 index 000000000..bbe0dc440 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureStorageController.java @@ -0,0 +1,31 @@ +package org.whispersystems.textsecuregcm.controllers; + +import com.codahale.metrics.annotation.Timed; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.storage.Account; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.dropwizard.auth.Auth; + +@Path("/v1/storage") +public class SecureStorageController { + + private final ExternalServiceCredentialGenerator storageServiceCredentialGenerator; + + public SecureStorageController(ExternalServiceCredentialGenerator storageServiceCredentialGenerator) { + this.storageServiceCredentialGenerator = storageServiceCredentialGenerator; + } + + @Timed + @GET + @Path("/auth") + @Produces(MediaType.APPLICATION_JSON) + public ExternalServiceCredentials getAuth(@Auth Account account) { + return storageServiceCredentialGenerator.generateFor(account.getNumber()); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java index c104c472c..9359fab3a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -19,7 +19,6 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; import org.hibernate.validator.constraints.Length; -import org.hibernate.validator.constraints.NotEmpty; public class AccountAttributes { @@ -36,15 +35,12 @@ public class AccountAttributes { @Length(max = 204, message = "This field must be less than 50 characters") private String name; - @JsonProperty - private boolean voice; - - @JsonProperty - private boolean video; - @JsonProperty private String pin; + @JsonProperty + private String registrationLock; + @JsonProperty private byte[] unidentifiedAccessKey; @@ -55,18 +51,17 @@ public class AccountAttributes { @VisibleForTesting public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String pin) { - this(signalingKey, fetchesMessages, registrationId, null, false, false, pin); + this(signalingKey, fetchesMessages, registrationId, null, pin, null); } @VisibleForTesting - public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice, boolean video, String pin) { - this.signalingKey = signalingKey; - this.fetchesMessages = fetchesMessages; - this.registrationId = registrationId; - this.name = name; - this.voice = voice; - this.video = video; - this.pin = pin; + public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, String pin, String registrationLock) { + this.signalingKey = signalingKey; + this.fetchesMessages = fetchesMessages; + this.registrationId = registrationId; + this.name = name; + this.pin = pin; + this.registrationLock = registrationLock; } public String getSignalingKey() { @@ -85,18 +80,14 @@ public class AccountAttributes { return name; } - public boolean getVoice() { - return voice; - } - - public boolean getVideo() { - return video; - } - public String getPin() { return pin; } + public String getRegistrationLock() { + return registrationLock; + } + public byte[] getUnidentifiedAccessKey() { return unidentifiedAccessKey; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeprecatedPin.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeprecatedPin.java new file mode 100644 index 000000000..5a7c33446 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeprecatedPin.java @@ -0,0 +1,26 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.NotEmpty; + +public class DeprecatedPin { + + @JsonProperty + @NotEmpty + @Length(min=4,max=20) + private String pin; + + public DeprecatedPin() {} + + @VisibleForTesting + public DeprecatedPin(String pin) { + this.pin = pin; + } + + public String getPin() { + return pin; + } + +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java index 6410a19a2..3384d70c6 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java @@ -8,19 +8,19 @@ import org.hibernate.validator.constraints.NotEmpty; public class RegistrationLock { @JsonProperty + @Length(min=64,max=64) @NotEmpty - @Length(min=4,max=20) - private String pin; + private String registrationLock; public RegistrationLock() {} @VisibleForTesting - public RegistrationLock(String pin) { - this.pin = pin; + public RegistrationLock(String registrationLock) { + this.registrationLock = registrationLock; } - public String getPin() { - return pin; + public String getRegistrationLock() { + return registrationLock; } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java index b57ed6e2a..28dea2f05 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java @@ -2,20 +2,30 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; public class RegistrationLockFailure { @JsonProperty private long timeRemaining; + @JsonProperty + private ExternalServiceCredentials storageCredentials; + public RegistrationLockFailure() {} - public RegistrationLockFailure(long timeRemaining) { - this.timeRemaining = timeRemaining; + public RegistrationLockFailure(long timeRemaining, ExternalServiceCredentials storageCredentials) { + this.timeRemaining = timeRemaining; + this.storageCredentials = storageCredentials; } @JsonIgnore public long getTimeRemaining() { return timeRemaining; } + + @JsonIgnore + public ExternalServiceCredentials getStorageCredentials() { + return storageCredentials; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index a1bc5a13c..ddbf225ac 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -53,6 +53,12 @@ public class Account implements Principal { @JsonProperty private String pin; + @JsonProperty + private String registrationLock; + + @JsonProperty + private String registrationLockSalt; + @JsonProperty("uak") private byte[] unidentifiedAccessKey; @@ -209,6 +215,22 @@ public class Account implements Principal { this.pin = pin; } + public void setRegistrationLock(String registrationLock) { + this.registrationLock = registrationLock; + } + + public Optional getRegistrationLock() { + return Optional.ofNullable(registrationLock); + } + + public void setRegistrationLockSalt(String registrationLockSalt) { + this.registrationLockSalt = registrationLockSalt; + } + + public Optional getRegistrationLockSalt() { + return Optional.ofNullable(registrationLockSalt); + } + public Optional getUnidentifiedAccessKey() { return Optional.ofNullable(unidentifiedAccessKey); } @@ -238,4 +260,5 @@ public class Account implements Principal { public boolean implies(Subject subject) { return false; } + } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/AuthenticationCredentialsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/AuthenticationCredentialsTest.java new file mode 100644 index 000000000..aefabec65 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/AuthenticationCredentialsTest.java @@ -0,0 +1,35 @@ +package org.whispersystems.textsecuregcm.tests.auth; + +import org.junit.Test; +import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class AuthenticationCredentialsTest { + + @Test + public void testCreating() { + AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword"); + assertThat(credentials.getSalt()).isNotEmpty(); + assertThat(credentials.getHashedAuthenticationToken()).isNotEmpty(); + assertThat(credentials.getHashedAuthenticationToken().length()).isEqualTo(40); + } + + @Test + public void testMatching() { + AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword"); + + AuthenticationCredentials provided = new AuthenticationCredentials(credentials.getHashedAuthenticationToken(), credentials.getSalt()); + assertThat(provided.verify("mypassword")).isTrue(); + } + + @Test + public void testMisMatching() { + AuthenticationCredentials credentials = new AuthenticationCredentials("mypassword"); + + AuthenticationCredentials provided = new AuthenticationCredentials(credentials.getHashedAuthenticationToken(), credentials.getSalt()); + assertThat(provided.verify("wrong")).isFalse(); + } + + +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/ExternalServiceCredentialsGeneratorTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/ExternalServiceCredentialsGeneratorTest.java new file mode 100644 index 000000000..570ccc408 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/ExternalServiceCredentialsGeneratorTest.java @@ -0,0 +1,29 @@ +package org.whispersystems.textsecuregcm.tests.auth; + +import org.junit.Test; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class ExternalServiceCredentialsGeneratorTest { + + @Test + public void testGenerateDerivedUsername() { + ExternalServiceCredentialGenerator generator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], true); + ExternalServiceCredentials credentials = generator.generateFor("+14152222222"); + + assertThat(credentials.getUsername()).isNotEqualTo("+14152222222"); + assertThat(credentials.getPassword().startsWith("+14152222222")).isFalse(); + } + + @Test + public void testGenerateNoDerivedUsername() { + ExternalServiceCredentialGenerator generator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false); + ExternalServiceCredentials credentials = generator.generateFor("+14152222222"); + + assertThat(credentials.getUsername()).isEqualTo("+14152222222"); + assertThat(credentials.getPassword().startsWith("+14152222222")).isTrue(); + } + +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index d1baa6f37..c1401608a 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -1,13 +1,14 @@ package org.whispersystems.textsecuregcm.tests.controllers; import com.google.common.collect.ImmutableSet; -import net.sourceforge.argparse4j.inf.Argument; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.controllers.AccountController; @@ -15,6 +16,7 @@ import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; +import org.whispersystems.textsecuregcm.entities.DeprecatedPin; import org.whispersystems.textsecuregcm.entities.RegistrationLock; import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; import org.whispersystems.textsecuregcm.limits.RateLimiter; @@ -35,12 +37,14 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.Hex; import org.whispersystems.textsecuregcm.util.SystemMapper; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; +import java.security.SecureRandom; import java.util.Collections; import java.util.HashMap; import java.util.Optional; @@ -60,6 +64,7 @@ public class AccountControllerTest { private static final String SENDER_OVER_PIN = "+14154444444"; private static final String SENDER_OVER_PREFIX = "+14156666666"; private static final String SENDER_PREAUTH = "+14157777777"; + private static final String SENDER_REG_LOCK = "+14158888888"; private static final String ABUSIVE_HOST = "192.168.1.1"; private static final String RESTRICTED_HOST = "192.168.1.2"; @@ -86,10 +91,14 @@ public class AccountControllerTest { private TimeProvider timeProvider = mock(TimeProvider.class ); private TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); private Account senderPinAccount = mock(Account.class); + private Account senderRegLockAccount = mock(Account.class); private RecaptchaClient recaptchaClient = mock(RecaptchaClient.class); private GCMSender gcmSender = mock(GCMSender.class); private APNSender apnSender = mock(APNSender.class); + private byte[] registration_lock_key = new byte[32]; + private ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false); + @Rule public final ResourceTestRule resources = ResourceTestRule.builder() .addProvider(AuthHelper.getAuthFilter()) @@ -108,12 +117,16 @@ public class AccountControllerTest { new HashMap<>(), recaptchaClient, gcmSender, - apnSender)) + apnSender, + storageCredentialGenerator)) .build(); @Before public void setup() throws Exception { + new SecureRandom().nextBytes(registration_lock_key); + AuthenticationCredentials registrationLockCredentials = new AuthenticationCredentials(Hex.toStringCondensed(registration_lock_key)); + when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter); @@ -127,13 +140,20 @@ public class AccountControllerTest { when(senderPinAccount.getPin()).thenReturn(Optional.of("31337")); when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis()); + when(senderRegLockAccount.getPin()).thenReturn(Optional.empty()); + when(senderRegLockAccount.getRegistrationLock()).thenReturn(Optional.of(registrationLockCredentials.getHashedAuthenticationToken())); + when(senderRegLockAccount.getRegistrationLockSalt()).thenReturn(Optional.of(registrationLockCredentials.getSalt())); + when(senderRegLockAccount.getLastSeen()).thenReturn(System.currentTimeMillis()); + when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null))); when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31), null))); when(pendingAccountsManager.getCodeForNumber(SENDER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("333333", System.currentTimeMillis(), null))); + when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null))); when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis(), null))); when(pendingAccountsManager.getCodeForNumber(SENDER_PREAUTH)).thenReturn(Optional.of(new StoredVerificationCode("555555", System.currentTimeMillis(), "validchallenge"))); when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount)); + when(accountsManager.get(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount)); when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount)); when(accountsManager.get(eq(SENDER))).thenReturn(Optional.empty()); when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty()); @@ -502,6 +522,21 @@ public class AccountControllerTest { verify(pinLimiter).validate(eq(SENDER_PIN)); } + @Test + public void testVerifyRegistrationLock() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/code/%s", "666666")) + .request() + .header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null, null, Hex.toStringCondensed(registration_lock_key)), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(pinLimiter).validate(eq(SENDER_REG_LOCK)); + } + @Test public void testVerifyWrongPin() throws Exception { Response response = @@ -517,6 +552,21 @@ public class AccountControllerTest { verify(pinLimiter).validate(eq(SENDER_PIN)); } + @Test + public void testVerifyWrongRegistrationLock() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/code/%s", "666666")) + .request() + .header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, Hex.toStringCondensed(new byte[32])), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(423); + + verify(pinLimiter).validate(eq(SENDER_REG_LOCK)); + } + @Test public void testVerifyNoPin() throws Exception { Response response = @@ -530,10 +580,34 @@ public class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(423); RegistrationLockFailure failure = response.readEntity(RegistrationLockFailure.class); + assertThat(failure.getStorageCredentials()).isNull(); verifyNoMoreInteractions(pinLimiter); } + @Test + public void testVerifyNoRegistrationLock() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/code/%s", "666666")) + .request() + .header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(423); + + RegistrationLockFailure failure = response.readEntity(RegistrationLockFailure.class); + assertThat(failure.getStorageCredentials()).isNotNull(); + assertThat(failure.getStorageCredentials().getUsername()).isEqualTo(SENDER_REG_LOCK); + assertThat(failure.getStorageCredentials().getPassword()).isNotEmpty(); + assertThat(failure.getStorageCredentials().getPassword().startsWith(SENDER_REG_LOCK)).isTrue(); + assertThat(failure.getTimeRemaining()).isGreaterThan(0); + + verifyNoMoreInteractions(pinLimiter); + } + + @Test public void testVerifyLimitPin() throws Exception { Response response = @@ -577,20 +651,47 @@ public class AccountControllerTest { .target("/v1/accounts/pin/") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) - .put(Entity.json(new RegistrationLock("31337"))); + .put(Entity.json(new DeprecatedPin("31337"))); assertThat(response.getStatus()).isEqualTo(204); verify(AuthHelper.VALID_ACCOUNT).setPin(eq("31337")); + verify(AuthHelper.VALID_ACCOUNT).setRegistrationLock(eq(null)); + verify(AuthHelper.VALID_ACCOUNT).setRegistrationLockSalt(eq(null)); } + @Test + public void testSetRegistrationLock() throws Exception { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/registration_lock/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new RegistrationLock("1234567890123456789012345678901234567890123456789012345678901234"))); + + assertThat(response.getStatus()).isEqualTo(204); + + ArgumentCaptor pinCapture = ArgumentCaptor.forClass(String.class); + ArgumentCaptor pinSaltCapture = ArgumentCaptor.forClass(String.class); + + verify(AuthHelper.VALID_ACCOUNT, times(1)).setPin(eq(null)); + verify(AuthHelper.VALID_ACCOUNT, times(1)).setRegistrationLock(pinCapture.capture()); + verify(AuthHelper.VALID_ACCOUNT, times(1)).setRegistrationLockSalt(pinSaltCapture.capture()); + + assertThat(pinCapture.getValue()).isNotEmpty(); + assertThat(pinSaltCapture.getValue()).isNotEmpty(); + + assertThat(pinCapture.getValue().length()).isEqualTo(40); + } + + @Test public void testSetPinUnauthorized() throws Exception { Response response = resources.getJerseyTest() .target("/v1/accounts/pin/") .request() - .put(Entity.json(new RegistrationLock("31337"))); + .put(Entity.json(new DeprecatedPin("31337"))); assertThat(response.getStatus()).isEqualTo(401); } @@ -602,13 +703,24 @@ public class AccountControllerTest { .target("/v1/accounts/pin/") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new DeprecatedPin("313"))); + + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + public void testSetShortRegistrationLock() throws Exception { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/registration_lock/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .put(Entity.json(new RegistrationLock("313"))); assertThat(response.getStatus()).isEqualTo(422); - - verify(AuthHelper.VALID_ACCOUNT, never()).setPin(anyString()); } + @Test public void testSetPinDisabled() throws Exception { Response response = @@ -616,11 +728,21 @@ public class AccountControllerTest { .target("/v1/accounts/pin/") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_NUMBER, AuthHelper.DISABLED_PASSWORD)) - .put(Entity.json(new RegistrationLock("31337"))); + .put(Entity.json(new DeprecatedPin("31337"))); assertThat(response.getStatus()).isEqualTo(401); + } - verify(AuthHelper.VALID_ACCOUNT, never()).setPin(anyString()); + @Test + public void testSetRegistrationLockDisabled() throws Exception { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/registration_lock/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.DISABLED_NUMBER, AuthHelper.DISABLED_PASSWORD)) + .put(Entity.json(new RegistrationLock("1234567890123456789012345678901234567890123456789012345678901234"))); + + assertThat(response.getStatus()).isEqualTo(401); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java index 362192897..e74b3682e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -212,7 +212,7 @@ public class DeviceControllerTest { .target("/v1/devices/5678901") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters it's so long that it's even longer than 204 characters. that's a lot of characters. we're talking lots and lots and lots of characters. 12345678", true, true, null), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters it's so long that it's even longer than 204 characters. that's a lot of characters. we're talking lots and lots and lots of characters. 12345678", null, null), MediaType.APPLICATION_JSON_TYPE)); assertEquals(response.getStatus(), 422); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DirectoryControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DirectoryControllerTest.java index 009a418a5..033587de6 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DirectoryControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DirectoryControllerTest.java @@ -7,8 +7,8 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import org.whispersystems.textsecuregcm.auth.DirectoryCredentials; -import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; import org.whispersystems.textsecuregcm.controllers.DirectoryController; import org.whispersystems.textsecuregcm.entities.ClientContactTokens; @@ -42,9 +42,9 @@ public class DirectoryControllerTest { private final RateLimiters rateLimiters = mock(RateLimiters.class); private final RateLimiter rateLimiter = mock(RateLimiter.class); private final DirectoryManager directoryManager = mock(DirectoryManager.class); - private final DirectoryCredentialsGenerator directoryCredentialsGenerator = mock(DirectoryCredentialsGenerator.class); + private final ExternalServiceCredentialGenerator directoryCredentialsGenerator = mock(ExternalServiceCredentialGenerator.class); - private final DirectoryCredentials validCredentials = new DirectoryCredentials("username", "password"); + private final ExternalServiceCredentials validCredentials = new ExternalServiceCredentials("username", "password"); @Rule public final ResourceTestRule resources = ResourceTestRule.builder() @@ -140,12 +140,12 @@ public class DirectoryControllerTest { @Test public void testGetAuthToken() { - DirectoryCredentials token = + ExternalServiceCredentials token = resources.getJerseyTest() .target("/v1/directory/auth") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) - .get(DirectoryCredentials.class); + .get(ExternalServiceCredentials.class); assertThat(token.getUsername()).isEqualTo(validCredentials.getUsername()); assertThat(token.getPassword()).isEqualTo(validCredentials.getPassword()); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/SecureStorageControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/SecureStorageControllerTest.java new file mode 100644 index 000000000..d7e08f86f --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/SecureStorageControllerTest.java @@ -0,0 +1,59 @@ +package org.whispersystems.textsecuregcm.tests.controllers; + +import com.google.common.collect.ImmutableSet; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.ClassRule; +import org.junit.Test; +import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; +import org.whispersystems.textsecuregcm.controllers.SecureStorageController; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +import javax.ws.rs.core.Response; + +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.testing.junit.ResourceTestRule; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class SecureStorageControllerTest { + + private static final ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false); + + @ClassRule + public static final ResourceTestRule resources = ResourceTestRule.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class))) + .setMapper(SystemMapper.getMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new SecureStorageController(storageCredentialGenerator)) + .build(); + + + @Test + public void testGetCredentials() throws Exception { + ExternalServiceCredentials credentials = resources.getJerseyTest() + .target("/v1/storage/auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(ExternalServiceCredentials.class); + + assertThat(credentials.getPassword()).isNotEmpty(); + assertThat(credentials.getUsername()).isNotEmpty(); + } + + @Test + public void testGetCredentialsBadAuth() throws Exception { + Response response = resources.getJerseyTest() + .target("/v1/storage/auth") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVVALID_NUMBER, AuthHelper.INVALID_PASSWORD)) + .get(); + + assertThat(response.getStatus()).isEqualTo(401); + } + + +} diff --git a/service/src/test/resources/fixtures/transparent_account.json b/service/src/test/resources/fixtures/transparent_account.json index 5ba7f0a4b..d2133ee23 100644 --- a/service/src/test/resources/fixtures/transparent_account.json +++ b/service/src/test/resources/fixtures/transparent_account.json @@ -1 +1 @@ -{"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false} +{"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","registrationLock":null, "registrationLockSalt":null,"uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false} diff --git a/service/src/test/resources/fixtures/transparent_account2.json b/service/src/test/resources/fixtures/transparent_account2.json index 0b4deab63..c37801591 100644 --- a/service/src/test/resources/fixtures/transparent_account2.json +++ b/service/src/test/resources/fixtures/transparent_account2.json @@ -1 +1 @@ -{"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false} +{"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"uninstalledFeedback":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","registrationLock":null,"registrationLockSalt":null,"uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}