diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 0ef099be2..afbed2861 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -23,7 +23,6 @@ import io.dropwizard.auth.basic.BasicCredentialAuthFilter; import io.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; -import io.grpc.Server; import io.grpc.ServerBuilder; import io.lettuce.core.metrics.MicrometerCommandLatencyRecorder; import io.lettuce.core.metrics.MicrometerOptions; @@ -650,10 +649,9 @@ public class WhisperServerService extends Application dynamicConfigurationManager; private final TurnTokenGenerator turnTokenGenerator; - private final RegistrationCaptchaManager registrationCaptchaManager; - private final PushNotificationManager pushNotificationManager; - private final RegistrationLockVerificationManager registrationLockVerificationManager; private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; - private final ChangeNumberManager changeNumberManager; - private final Clock clock; private final UsernameHashZkProofVerifier usernameHashZkProofVerifier; - - @VisibleForTesting - static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15); - public AccountController( - StoredVerificationCodeManager pendingAccounts, AccountsManager accounts, RateLimiters rateLimiters, - RegistrationServiceClient registrationServiceClient, - DynamicConfigurationManager dynamicConfigurationManager, TurnTokenGenerator turnTokenGenerator, - RegistrationCaptchaManager registrationCaptchaManager, - PushNotificationManager pushNotificationManager, - ChangeNumberManager changeNumberManager, - RegistrationLockVerificationManager registrationLockVerificationManager, RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, - UsernameHashZkProofVerifier usernameHashZkProofVerifier, - Clock clock - ) { - this.pendingAccounts = pendingAccounts; + UsernameHashZkProofVerifier usernameHashZkProofVerifier) { this.accounts = accounts; this.rateLimiters = rateLimiters; - this.registrationServiceClient = registrationServiceClient; - this.dynamicConfigurationManager = dynamicConfigurationManager; this.turnTokenGenerator = turnTokenGenerator; - this.registrationCaptchaManager = registrationCaptchaManager; - this.pushNotificationManager = pushNotificationManager; - this.registrationLockVerificationManager = registrationLockVerificationManager; - this.changeNumberManager = changeNumberManager; this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; this.usernameHashZkProofVerifier = usernameHashZkProofVerifier; - this.clock = clock; - } - - @Timed - @GET - @Path("/{type}/preauth/{token}/{number}") - @Produces(MediaType.APPLICATION_JSON) - public Response getPreAuth(@PathParam("type") String pushType, - @PathParam("token") String pushToken, - @PathParam("number") String number, - @QueryParam("voip") @DefaultValue("true") boolean useVoip) - throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException, RateLimitExceededException { - - final PushNotification.TokenType tokenType = switch(pushType) { - case "apn" -> useVoip ? PushNotification.TokenType.APN_VOIP : PushNotification.TokenType.APN; - case "fcm" -> PushNotification.TokenType.FCM; - default -> throw new BadRequestException(); - }; - - Util.requireNormalizedNumber(number); - - final Phonenumber.PhoneNumber phoneNumber; - try { - phoneNumber = PhoneNumberUtil.getInstance().parse(number, null); - } catch (final NumberParseException e) { - // This should never happen since we just verified that the number is already normalized - throw new BadRequestException("Bad phone number"); - } - - final StoredVerificationCode storedVerificationCode; - { - final Optional maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number); - - if (maybeStoredVerificationCode.isPresent()) { - final StoredVerificationCode existingStoredVerificationCode = maybeStoredVerificationCode.get(); - - if (StringUtils.isBlank(existingStoredVerificationCode.pushCode())) { - storedVerificationCode = new StoredVerificationCode( - existingStoredVerificationCode.code(), - existingStoredVerificationCode.timestamp(), - generatePushChallenge(), - existingStoredVerificationCode.sessionId()); - } else { - storedVerificationCode = existingStoredVerificationCode; - } - } else { - final byte[] sessionId = createRegistrationSession(phoneNumber, accounts.getByE164(number).isPresent()); - storedVerificationCode = new StoredVerificationCode(null, clock.millis(), generatePushChallenge(), sessionId); - new StoredVerificationCode(null, clock.millis(), generatePushChallenge(), sessionId); - } - } - - pendingAccounts.store(number, storedVerificationCode); - pushNotificationManager.sendRegistrationChallengeNotification(pushToken, tokenType, storedVerificationCode.pushCode()); - - return Response.ok().build(); - } - - @Timed - @GET - @Path("/{transport}/code/{number}") - @FilterSpam - @Produces(MediaType.APPLICATION_JSON) - public Response createAccount(@PathParam("transport") String transport, - @PathParam("number") String number, - @HeaderParam(HttpHeaders.X_FORWARDED_FOR) String forwardedFor, - @HeaderParam(HttpHeaders.USER_AGENT) String userAgent, - @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) Optional acceptLanguage, - @QueryParam("client") Optional client, - @QueryParam("captcha") Optional captcha, - @QueryParam("challenge") Optional pushChallenge, - @Extract ScoreThreshold captchaScoreThreshold) - throws RateLimitExceededException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException, IOException { - - Util.requireNormalizedNumber(number); - - final String sourceHost = HeaderUtils.getMostRecentProxy(forwardedFor).orElseThrow(); - final Optional maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number); - - final String countryCode = Util.getCountryCode(number); - final String region = Util.getRegion(number); - - // if there's a captcha, assess it, otherwise check if we need a captcha - final Optional assessmentResult = registrationCaptchaManager.assessCaptcha(captcha, sourceHost); - - assessmentResult.ifPresent(result -> - Metrics.counter(CAPTCHA_ATTEMPT_COUNTER_NAME, Tags.of( - Tag.of("success", String.valueOf(result.isValid(captchaScoreThreshold.getScoreThreshold()))), - UserAgentTagUtil.getPlatformTag(userAgent), - Tag.of(COUNTRY_CODE_TAG_NAME, countryCode), - Tag.of(REGION_TAG_NAME, region), - Tag.of(REGION_CODE_TAG_NAME, region), - Tag.of(SCORE_TAG_NAME, result.getScoreString()))) - .increment()); - - final boolean pushChallengeMatch = pushChallengeMatches(number, pushChallenge, maybeStoredVerificationCode); - - if (pushChallenge.isPresent() && !pushChallengeMatch) { - throw new WebApplicationException(Response.status(403).build()); - } - - final boolean requiresCaptcha = assessmentResult - .map(result -> !result.isValid(captchaScoreThreshold.getScoreThreshold())) - .orElseGet( - () -> registrationCaptchaManager.requiresCaptcha(number, forwardedFor, sourceHost, pushChallengeMatch)); - - if (requiresCaptcha) { - captchaRequiredMeter.mark(); - Metrics.counter(CHALLENGE_ISSUED_COUNTER_NAME, Tags.of( - UserAgentTagUtil.getPlatformTag(userAgent), - Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)), - Tag.of(REGION_TAG_NAME, Util.getRegion(number)), - Tag.of(REGION_CODE_TAG_NAME, region))) - .increment(); - return Response.status(402).build(); - } - - switch (transport) { - case "sms" -> rateLimiters.getSmsDestinationLimiter().validate(number); - case "voice" -> { - rateLimiters.getVoiceDestinationLimiter().validate(number); - rateLimiters.getVoiceDestinationDailyLimiter().validate(number); - } - default -> throw new WebApplicationException(Response.status(422).build()); - } - - final Phonenumber.PhoneNumber phoneNumber; - - try { - phoneNumber = PhoneNumberUtil.getInstance().parse(number, null); - } catch (final NumberParseException e) { - throw new WebApplicationException(Response.status(422).build()); - } - - final MessageTransport messageTransport = switch (transport) { - case "sms" -> MessageTransport.SMS; - case "voice" -> MessageTransport.VOICE; - default -> throw new WebApplicationException(Response.status(422).build()); - }; - - final ClientType clientType = client.map(clientTypeString -> { - if ("ios".equalsIgnoreCase(clientTypeString)) { - return ClientType.IOS; - } else if ("android-2021-03".equalsIgnoreCase(clientTypeString)) { - return ClientType.ANDROID_WITH_FCM; - } else if (StringUtils.startsWithIgnoreCase(clientTypeString, "android")) { - return ClientType.ANDROID_WITHOUT_FCM; - } else { - return ClientType.UNKNOWN; - } - }).orElse(ClientType.UNKNOWN); - - // During the transition to explicit session creation, some previously-stored records may not have a session ID; - // after the transition, we can assume that any existing record has an associated session ID. - final byte[] sessionId = maybeStoredVerificationCode.isPresent() && maybeStoredVerificationCode.get().sessionId() != null - ? maybeStoredVerificationCode.get().sessionId() - : createRegistrationSession(phoneNumber, accounts.getByE164(number).isPresent()); - - sendVerificationCode(sessionId, messageTransport, clientType, acceptLanguage); - - final StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null, - clock.millis(), - maybeStoredVerificationCode.map(StoredVerificationCode::pushCode).orElse(null), sessionId); - - pendingAccounts.store(number, storedVerificationCode); - - Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, Tags.of( - UserAgentTagUtil.getPlatformTag(userAgent), - Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)), - Tag.of(REGION_TAG_NAME, Util.getRegion(number)), - Tag.of(VERIFICATION_TRANSPORT_TAG_NAME, transport))) - .increment(); - - return Response.ok().build(); - } - - @Timed - @PUT - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Path("/code/{verification_code}") - public AccountIdentityResponse verifyAccount(@PathParam("verification_code") String verificationCode, - @HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader, - @HeaderParam(HeaderUtils.X_SIGNAL_AGENT) String signalAgent, - @HeaderParam(HttpHeaders.USER_AGENT) String userAgent, - @QueryParam("transfer") Optional availableForTransfer, - @NotNull @Valid AccountAttributes accountAttributes) - throws RateLimitExceededException, InterruptedException { - - String number = authorizationHeader.getUsername(); - String password = authorizationHeader.getPassword(); - - rateLimiters.getVerifyLimiter().validate(number); - - if (!AccountsManager.validNewAccountAttributes(accountAttributes)) { - Metrics.counter(INVALID_ACCOUNT_ATTRS_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment(); - throw new WebApplicationException(Response.status(422, "account attributes invalid").build()); - } - - // Note that successful verification depends on being able to find a stored verification code for the given number. - // We check that numbers are normalized before we store verification codes, and so don't need to re-assert - // normalization here. - final boolean codeVerified; - final Optional maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number); - - if (maybeStoredVerificationCode.isPresent()) { - codeVerified = checkVerificationCode(maybeStoredVerificationCode.get().sessionId(), verificationCode); - } else { - codeVerified = false; - } - - if (!codeVerified) { - throw new WebApplicationException(Response.status(403).build()); - } - - Optional existingAccount = accounts.getByE164(number); - - existingAccount.ifPresent(account -> { - Instant accountLastSeen = Instant.ofEpochMilli(account.getLastSeen()); - Duration timeSinceLastSeen = Duration.between(accountLastSeen, Instant.now()); - REREGISTRATION_IDLE_DAYS_DISTRIBUTION.record(timeSinceLastSeen.toDays()); - }); - - if (existingAccount.isPresent()) { - registrationLockVerificationManager.verifyRegistrationLock(existingAccount.get(), - accountAttributes.getRegistrationLock(), - userAgent, RegistrationLockVerificationManager.Flow.REGISTRATION, - PhoneVerificationRequest.VerificationType.SESSION); - } - - if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) { - throw new WebApplicationException(Status.CONFLICT); - } - - rateLimiters.getVerifyLimiter().clear(number); - - Account account = accounts.create(number, password, signalAgent, accountAttributes, - existingAccount.map(Account::getBadges).orElseGet(ArrayList::new)); - - Metrics.counter(ACCOUNT_VERIFY_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), - Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)), - Tag.of(REGION_TAG_NAME, Util.getRegion(number)), - Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(number)))) - .increment(); - - return new AccountIdentityResponse(account.getUuid(), - account.getNumber(), - account.getPhoneNumberIdentifier(), - account.getUsernameHash().orElse(null), - existingAccount.map(Account::isStorageSupported).orElse(false)); - } - - @Timed - @PUT - @Path("/number") - @Produces(MediaType.APPLICATION_JSON) - public AccountIdentityResponse changeNumber(@Auth final AuthenticatedAccount authenticatedAccount, - @NotNull @Valid final ChangePhoneNumberRequest request, - @HeaderParam(HttpHeaders.USER_AGENT) String userAgent) - throws RateLimitExceededException, InterruptedException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException { - - if (!authenticatedAccount.getAuthenticatedDevice().isMaster()) { - throw new ForbiddenException(); - } - - final String number = request.number(); - - // Only "bill" for rate limiting if we think there's a change to be made... - if (!authenticatedAccount.getAccount().getNumber().equals(number)) { - Util.requireNormalizedNumber(number); - - rateLimiters.getVerifyLimiter().validate(number); - - final boolean codeVerified; - final Optional maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number); - - if (maybeStoredVerificationCode.isPresent()) { - codeVerified = checkVerificationCode(maybeStoredVerificationCode.get().sessionId(), request.code()); - } else { - codeVerified = false; - } - - if (!codeVerified) { - throw new ForbiddenException(); - } - - final Optional existingAccount = accounts.getByE164(number); - - if (existingAccount.isPresent()) { - registrationLockVerificationManager.verifyRegistrationLock(existingAccount.get(), request.registrationLock(), - userAgent, RegistrationLockVerificationManager.Flow.CHANGE_NUMBER, PhoneVerificationRequest.VerificationType.SESSION); - } - - rateLimiters.getVerifyLimiter().clear(number); - } - - // ...but always attempt to make the change in case a client retries and needs to re-send messages - try { - final Account updatedAccount = changeNumberManager.changeNumber( - authenticatedAccount.getAccount(), - request.number(), - request.pniIdentityKey(), - request.devicePniSignedPrekeys(), - request.devicePniPqLastResortPrekeys(), - request.deviceMessages(), - request.pniRegistrationIds()); - - return new AccountIdentityResponse( - updatedAccount.getUuid(), - updatedAccount.getNumber(), - updatedAccount.getPhoneNumberIdentifier(), - updatedAccount.getUsernameHash().orElse(null), - updatedAccount.isStorageSupported()); - } catch (MismatchedDevicesException e) { - throw new WebApplicationException(Response.status(409) - .type(MediaType.APPLICATION_JSON_TYPE) - .entity(new MismatchedDevices(e.getMissingDevices(), - e.getExtraDevices())) - .build()); - } catch (StaleDevicesException e) { - throw new WebApplicationException(Response.status(410) - .type(MediaType.APPLICATION_JSON) - .entity(new StaleDevices(e.getStaleDevices())) - .build()); - } catch (IllegalArgumentException e) { - throw new BadRequestException(e); - } } @Timed @@ -710,7 +271,7 @@ public class AccountController { @Operation( summary = "Delete username hash", description = """ - Authenticated endpoint. Deletes previously stored username for the account. + Authenticated endpoint. Deletes previously stored username for the account. """ ) @ApiResponse(responseCode = "204", description = "Username successfully deleted.", useReturnTypeSchema = true) @@ -729,7 +290,7 @@ public class AccountController { summary = "Reserve username hash", description = """ Authenticated endpoint. Takes in a list of hashes of potential username hashes, finds one that is not taken, - and reserves it for the current account. + and reserves it for the current account. """ ) @ApiResponse(responseCode = "200", description = "Username hash reserved successfully.", useReturnTypeSchema = true) @@ -768,8 +329,8 @@ public class AccountController { @Operation( summary = "Confirm username hash", description = """ - Authenticated endpoint. For a previously reserved username hash, confirm that this username hash is now taken - by this account. + Authenticated endpoint. For a previously reserved username hash, confirm that this username hash is now taken + by this account. """ ) @ApiResponse(responseCode = "200", description = "Username hash confirmed successfully.", useReturnTypeSchema = true) @@ -815,7 +376,7 @@ public class AccountController { @Operation( summary = "Lookup username hash", description = """ - Forced unauthenticated endpoint. For the given username hash, look up a user ID. + Forced unauthenticated endpoint. For the given username hash, look up a user ID. """ ) @ApiResponse(responseCode = "200", description = "Account found for the given username.", useReturnTypeSchema = true) @@ -855,7 +416,7 @@ public class AccountController { Authenticated endpoint. For the given encrypted username generates a username link handle. Username link handle could be used to lookup the encrypted username. An account can only have one username link at a time. Calling this endpoint will reset previously stored - encrypted username and deactivate previous link handle. + encrypted username and deactivate previous link handle. """ ) @ApiResponse(responseCode = "200", description = "Username Link updated successfully.", useReturnTypeSchema = true) @@ -886,7 +447,7 @@ public class AccountController { summary = "Delete username link", description = """ Authenticated endpoint. Deletes username link for the given account: previously store encrypted username is deleted - and username link handle is deactivated. + and username link handle is deactivated. """ ) @ApiResponse(responseCode = "204", description = "Username Link successfully deleted.", useReturnTypeSchema = true) @@ -943,29 +504,6 @@ public class AccountController { return Response.status(status).build(); } - @VisibleForTesting - static boolean pushChallengeMatches( - final String number, - final Optional pushChallenge, - final Optional storedVerificationCode) { - - final String countryCode = Util.getCountryCode(number); - final String region = Util.getRegion(number); - final Optional storedPushChallenge = storedVerificationCode.map(StoredVerificationCode::pushCode); - - final boolean match = Optionals.zipWith(pushChallenge, storedPushChallenge, String::equals).orElse(false); - - Metrics.counter(PUSH_CHALLENGE_COUNTER_NAME, - COUNTRY_CODE_TAG_NAME, countryCode, - REGION_TAG_NAME, region, - REGION_CODE_TAG_NAME, region, - CHALLENGE_PRESENT_TAG_NAME, Boolean.toString(pushChallenge.isPresent()), - CHALLENGE_MATCH_TAG_NAME, Boolean.toString(match)) - .increment(); - - return match; - } - @Timed @DELETE @Path("/me") @@ -973,63 +511,6 @@ public class AccountController { accounts.delete(auth.getAccount(), AccountsManager.DeletionReason.USER_REQUEST); } - private String generatePushChallenge() { - SecureRandom random = new SecureRandom(); - byte[] challenge = new byte[16]; - random.nextBytes(challenge); - - return HexFormat.of().formatHex(challenge); - } - - private byte[] createRegistrationSession(final Phonenumber.PhoneNumber phoneNumber, - final boolean accountExistsWithPhoneNumber) throws RateLimitExceededException { - - try { - return registrationServiceClient.createRegistrationSession(phoneNumber, accountExistsWithPhoneNumber, REGISTRATION_RPC_TIMEOUT).join(); - } catch (final CompletionException e) { - rethrowRateLimitException(e); - - logger.debug("Failed to create session", e); - - // Meet legacy client expectations by "swallowing" session creation exceptions and proceeding as if we had created - // a new session. Future operations on this "session" will always fail, but that's the legacy behavior. - return new byte[16]; - } - } - - private void sendVerificationCode(final byte[] sessionId, - final MessageTransport messageTransport, - final ClientType clientType, - final Optional acceptLanguage) throws RateLimitExceededException { - - try { - registrationServiceClient.sendRegistrationCode(sessionId, - messageTransport, - clientType, - acceptLanguage.orElse(null), - REGISTRATION_RPC_TIMEOUT).join(); - } catch (final CompletionException e) { - // Note that, to meet legacy client expectations, we'll ONLY rethrow rate limit exceptions. All others will be - // swallowed silently. - rethrowRateLimitException(e); - - logger.debug("Failed to send verification code", e); - } - } - - private boolean checkVerificationCode(final byte[] sessionId, final String verificationCode) - throws RateLimitExceededException { - - try { - return registrationServiceClient.checkVerificationCode(sessionId, verificationCode, REGISTRATION_RPC_TIMEOUT).join(); - } catch (final CompletionException e) { - rethrowRateLimitException(e); - - // For legacy API compatibility, funnel all errors into the same return value - return false; - } - } - private void clearUsernameLink(final Account account) { updateUsernameLink(account, null, null); } @@ -1044,20 +525,6 @@ public class AccountController { accounts.update(account, a -> a.setUsernameLinkDetails(usernameLinkHandle, encryptedUsername)); } - private void rethrowRateLimitException(final CompletionException completionException) - throws RateLimitExceededException { - - Throwable cause = completionException; - - while (cause instanceof CompletionException) { - cause = cause.getCause(); - } - - if (cause instanceof RateLimitExceededException rateLimitExceededException) { - throw rateLimitExceededException; - } - } - private void requireNotAuthenticated(final Optional authenticatedAccount) { if (authenticatedAccount.isPresent()) { throw new BadRequestException("Operation requires unauthenticated access"); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java index 54f41aaa5..f921dac88 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java @@ -9,54 +9,36 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes; import com.google.common.collect.ImmutableSet; import com.google.common.net.HttpHeaders; -import com.google.i18n.phonenumbers.NumberParseException; -import com.google.i18n.phonenumbers.PhoneNumberUtil; import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.time.Duration; import java.time.Instant; -import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.HexFormat; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.annotation.Nullable; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.commons.lang3.RandomUtils; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; @@ -66,41 +48,24 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; -import org.signal.libsignal.protocol.IdentityKey; -import org.signal.libsignal.protocol.ecc.Curve; -import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.signal.libsignal.usernames.BaseUsernameException; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; -import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; -import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; -import org.whispersystems.textsecuregcm.captcha.Action; -import org.whispersystems.textsecuregcm.captcha.AssessmentResult; -import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; -import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; -import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration; -import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse; import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; -import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest; import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest; import org.whispersystems.textsecuregcm.entities.EncryptedUsername; import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; -import org.whispersystems.textsecuregcm.entities.IncomingMessage; import org.whispersystems.textsecuregcm.entities.RegistrationLock; -import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest; import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse; import org.whispersystems.textsecuregcm.entities.UsernameHashResponse; @@ -110,31 +75,18 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper; import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper; import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper; -import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberResponse; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; -import org.whispersystems.textsecuregcm.push.ClientPresenceManager; -import org.whispersystems.textsecuregcm.push.PushNotification; -import org.whispersystems.textsecuregcm.push.PushNotificationManager; -import org.whispersystems.textsecuregcm.registration.ClientType; -import org.whispersystems.textsecuregcm.registration.MessageTransport; -import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; -import org.whispersystems.textsecuregcm.spam.Extract; import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; -import org.whispersystems.textsecuregcm.storage.ChangeNumberManager; -import org.whispersystems.textsecuregcm.storage.Device; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager; import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException; import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; -import org.whispersystems.textsecuregcm.tests.util.KeysHelper; import org.whispersystems.textsecuregcm.util.MockUtils; import org.whispersystems.textsecuregcm.util.SystemMapper; -import org.whispersystems.textsecuregcm.util.TestClock; import org.whispersystems.textsecuregcm.util.UsernameHashZkProofVerifier; @ExtendWith(DropwizardExtensionsSupport.class) @@ -148,8 +100,6 @@ class AccountControllerTest { private static final String SENDER_REG_LOCK = "+14158888888"; private static final String SENDER_HAS_STORAGE = "+14159999999"; private static final String SENDER_TRANSFER = "+14151111112"; - private static final String RESTRICTED_COUNTRY = "800"; - private static final String RESTRICTED_NUMBER = "+" + RESTRICTED_COUNTRY + "11111111"; private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE"; private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc"; @@ -166,67 +116,26 @@ class AccountControllerTest { private static final String NICE_HOST = "127.0.0.1"; private static final String RATE_LIMITED_IP_HOST = "10.0.0.1"; - private static final String RATE_LIMITED_PREFIX_HOST = "10.0.0.2"; - private static final String RATE_LIMITED_HOST2 = "10.0.0.3"; - - private static final String VALID_CAPTCHA_TOKEN = "valid_token"; - private static final String INVALID_CAPTCHA_TOKEN = "invalid_token"; - - private static final String TEST_NUMBER = "+14151111113"; private static StoredVerificationCodeManager pendingAccountsManager = mock(StoredVerificationCodeManager.class); private static AccountsManager accountsManager = mock(AccountsManager.class); private static RateLimiters rateLimiters = mock(RateLimiters.class); private static RateLimiter rateLimiter = mock(RateLimiter.class); - private static RateLimiter pinLimiter = mock(RateLimiter.class); - private static RateLimiter smsVoiceIpLimiter = mock(RateLimiter.class); - private static RateLimiter smsVoicePrefixLimiter = mock(RateLimiter.class); - private static RateLimiter autoBlockLimiter = mock(RateLimiter.class); private static RateLimiter usernameSetLimiter = mock(RateLimiter.class); private static RateLimiter usernameReserveLimiter = mock(RateLimiter.class); private static RateLimiter usernameLookupLimiter = mock(RateLimiter.class); private static RateLimiter checkAccountExistence = mock(RateLimiter.class); - private static RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); private static TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); private static Account senderPinAccount = mock(Account.class); private static Account senderRegLockAccount = mock(Account.class); private static Account senderHasStorage = mock(Account.class); private static Account senderTransfer = mock(Account.class); - private static CaptchaChecker captchaChecker = mock(CaptchaChecker.class); - private static PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class); - private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class); private static RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( RegistrationRecoveryPasswordsManager.class); - private static ClientPresenceManager clientPresenceManager = mock(ClientPresenceManager.class); private static final UsernameHashZkProofVerifier usernameZkProofVerifier = mock(UsernameHashZkProofVerifier.class); - private static TestClock testClock = TestClock.now(); - private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); private byte[] registration_lock_key = new byte[32]; - private static final SecureBackupServiceConfiguration SVR1_CFG = MockUtils.buildMock( - SecureBackupServiceConfiguration.class, - cfg -> when(cfg.userAuthenticationTokenSharedSecret()).thenReturn(randomSecretBytes(32))); - - private static final SecureValueRecovery2Configuration SVR2_CFG = MockUtils.buildMock( - SecureValueRecovery2Configuration.class, - cfg -> { - when(cfg.userAuthenticationTokenSharedSecret()).thenReturn(randomSecretBytes(32)); - when(cfg.userIdTokenSharedSecret()).thenReturn(randomSecretBytes(32)); - }); - - private static final ExternalServiceCredentialsGenerator svr1CredentialsGenerator = SecureBackupController.credentialsGenerator( - SVR1_CFG); - - private static final ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator( - SVR2_CFG); - - private static final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager( - accountsManager, clientPresenceManager, svr1CredentialsGenerator, svr2CredentialsGenerator, registrationRecoveryPasswordsManager, - pushNotificationManager, rateLimiters); - private static final RegistrationCaptchaManager registrationCaptchaManager = new RegistrationCaptchaManager( - captchaChecker, rateLimiters, Set.of(TEST_NUMBER), dynamicConfigurationManager); - private static final ResourceExtension resources = ResourceExtension.builder() .addProvider(AuthHelper.getAuthFilter()) .addProvider( @@ -241,19 +150,13 @@ class AccountControllerTest { .addProvider(ScoreThresholdProvider.ScoreThresholdFeature.class) .setMapper(SystemMapper.jsonMapper()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) - .addResource(new AccountController(pendingAccountsManager, - accountsManager, + .addResource(new AccountController( + accountsManager, rateLimiters, - registrationServiceClient, - dynamicConfigurationManager, turnTokenGenerator, - registrationCaptchaManager, - pushNotificationManager, - changeNumberManager, - registrationLockVerificationManager, registrationRecoveryPasswordsManager, - usernameZkProofVerifier, - testClock)) + usernameZkProofVerifier + )) .build(); @@ -267,13 +170,6 @@ class AccountControllerTest { AccountsHelper.setupMockUpdate(accountsManager); - when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter); - when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter); - when(rateLimiters.getVoiceDestinationDailyLimiter()).thenReturn(rateLimiter); - when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter); - when(rateLimiters.getPinLimiter()).thenReturn(pinLimiter); - when(rateLimiters.getSmsVoiceIpLimiter()).thenReturn(smsVoiceIpLimiter); - when(rateLimiters.getSmsVoicePrefixLimiter()).thenReturn(smsVoicePrefixLimiter); when(rateLimiters.getUsernameSetLimiter()).thenReturn(usernameSetLimiter); when(rateLimiters.getUsernameReserveLimiter()).thenReturn(usernameReserveLimiter); when(rateLimiters.getUsernameLookupLimiter()).thenReturn(usernameLookupLimiter); @@ -336,76 +232,6 @@ class AccountControllerTest { return account; }); - - when(changeNumberManager.changeNumber(any(), any(), any(), any(), any(), any(), any())).thenAnswer((Answer) invocation -> { - final Account account = invocation.getArgument(0); - final String number = invocation.getArgument(1); - final IdentityKey pniIdentityKey = invocation.getArgument(2); - - final UUID uuid = account.getUuid(); - final UUID pni = number.equals(account.getNumber()) ? account.getPhoneNumberIdentifier() : UUID.randomUUID(); - final List devices = account.getDevices(); - - final Account updatedAccount = mock(Account.class); - when(updatedAccount.getUuid()).thenReturn(uuid); - when(updatedAccount.getNumber()).thenReturn(number); - when(updatedAccount.getPhoneNumberIdentityKey()).thenReturn(pniIdentityKey); - when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(pni); - when(updatedAccount.getDevices()).thenReturn(devices); - - for (long i = 1; i <= 3; i++) { - final Optional d = account.getDevice(i); - when(updatedAccount.getDevice(i)).thenReturn(d); - } - - return updatedAccount; - }); - - when(changeNumberManager.updatePniKeys(any(), any(), any(), any(), any(), any())).thenAnswer((Answer) invocation -> { - final Account account = invocation.getArgument(0); - final IdentityKey pniIdentityKey = invocation.getArgument(1); - - final String number = account.getNumber(); - final UUID uuid = account.getUuid(); - final UUID pni = account.getPhoneNumberIdentifier(); - final List devices = account.getDevices(); - - final Account updatedAccount = mock(Account.class); - when(updatedAccount.getNumber()).thenReturn(number); - when(updatedAccount.getUuid()).thenReturn(uuid); - when(updatedAccount.getPhoneNumberIdentityKey()).thenReturn(pniIdentityKey); - when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(pni); - when(updatedAccount.getDevices()).thenReturn(devices); - - for (long i = 1; i <= 3; i++) { - final Optional d = account.getDevice(i); - when(updatedAccount.getDevice(i)).thenReturn(d); - } - - return updatedAccount; - }); - - { - DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); - when(dynamicConfigurationManager.getConfiguration()) - .thenReturn(dynamicConfiguration); - - DynamicCaptchaConfiguration signupCaptchaConfig = new DynamicCaptchaConfiguration(); - signupCaptchaConfig.setSignupCountryCodes(Set.of(RESTRICTED_COUNTRY)); - - when(dynamicConfiguration.getCaptchaConfiguration()).thenReturn(signupCaptchaConfig); - } - when(captchaChecker.verify(eq(Action.REGISTRATION), eq(INVALID_CAPTCHA_TOKEN), anyString())) - .thenReturn(AssessmentResult.invalid()); - when(captchaChecker.verify(eq(Action.REGISTRATION), eq(VALID_CAPTCHA_TOKEN), anyString())) - .thenReturn(AssessmentResult.alwaysValid()); - - doThrow(new RateLimitExceededException(Duration.ZERO, true)).when(pinLimiter).validate(eq(SENDER_OVER_PIN)); - - doThrow(new RateLimitExceededException(Duration.ZERO, true)).when(smsVoicePrefixLimiter) - .validate(SENDER_OVER_PREFIX.substring(0, 4 + 2)); - doThrow(new RateLimitExceededException(Duration.ZERO, true)).when(smsVoiceIpLimiter).validate(RATE_LIMITED_IP_HOST); - doThrow(new RateLimitExceededException(Duration.ZERO, true)).when(smsVoiceIpLimiter).validate(RATE_LIMITED_HOST2); } @AfterEach @@ -415,1345 +241,19 @@ class AccountControllerTest { accountsManager, rateLimiters, rateLimiter, - pinLimiter, - smsVoiceIpLimiter, - smsVoicePrefixLimiter, usernameSetLimiter, usernameReserveLimiter, usernameLookupLimiter, - registrationServiceClient, turnTokenGenerator, senderPinAccount, senderRegLockAccount, senderHasStorage, senderTransfer, - captchaChecker, - pushNotificationManager, - changeNumberManager, - clientPresenceManager, usernameZkProofVerifier); clearInvocations(AuthHelper.DISABLED_DEVICE); } - @Test - void testGetFcmPreauth() throws NumberParseException { - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(new byte[16])); - - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.empty()); - - Response response = resources.getJerseyTest() - .target("/v1/accounts/fcm/preauth/mytoken/" + SENDER) - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - final ArgumentCaptor challengeTokenCaptor = ArgumentCaptor.forClass(String.class); - - verify(registrationServiceClient).createRegistrationSession( - eq(PhoneNumberUtil.getInstance().parse(SENDER, null)), anyBoolean(), any()); - - verify(pushNotificationManager).sendRegistrationChallengeNotification( - eq("mytoken"), eq(PushNotification.TokenType.FCM), challengeTokenCaptor.capture()); - - assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32); - } - - @Test - void testGetFcmPreauthIvoryCoast() throws NumberParseException { - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(new byte[16])); - - Response response = resources.getJerseyTest() - .target("/v1/accounts/fcm/preauth/mytoken/+2250707312345") - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - final ArgumentCaptor challengeTokenCaptor = ArgumentCaptor.forClass(String.class); - - verify(registrationServiceClient).createRegistrationSession( - eq(PhoneNumberUtil.getInstance().parse("+2250707312345", null)), anyBoolean(), any()); - - verify(pushNotificationManager).sendRegistrationChallengeNotification( - eq("mytoken"), eq(PushNotification.TokenType.FCM), challengeTokenCaptor.capture()); - - assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32); - } - - @Test - void testGetApnPreauth() throws NumberParseException { - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(new byte[16])); - - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.empty()); - - Response response = resources.getJerseyTest() - .target("/v1/accounts/apn/preauth/mytoken/" + SENDER) - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - final ArgumentCaptor challengeTokenCaptor = ArgumentCaptor.forClass(String.class); - - verify(registrationServiceClient).createRegistrationSession( - eq(PhoneNumberUtil.getInstance().parse(SENDER, null)), anyBoolean(), any()); - - verify(pushNotificationManager).sendRegistrationChallengeNotification( - eq("mytoken"), eq(PushNotification.TokenType.APN_VOIP), challengeTokenCaptor.capture()); - - assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32); - } - - @Test - void testGetApnPreauthExplicitVoip() throws NumberParseException { - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(new byte[16])); - - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.empty()); - - Response response = resources.getJerseyTest() - .target("/v1/accounts/apn/preauth/mytoken/" + SENDER) - .queryParam("voip", "true") - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - final ArgumentCaptor challengeTokenCaptor = ArgumentCaptor.forClass(String.class); - - verify(registrationServiceClient).createRegistrationSession( - eq(PhoneNumberUtil.getInstance().parse(SENDER, null)), anyBoolean(), any()); - - verify(pushNotificationManager).sendRegistrationChallengeNotification( - eq("mytoken"), eq(PushNotification.TokenType.APN_VOIP), challengeTokenCaptor.capture()); - - assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32); - } - - @Test - void testGetApnPreauthExplicitNoVoip() throws NumberParseException { - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(new byte[16])); - - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.empty()); - - Response response = resources.getJerseyTest() - .target("/v1/accounts/apn/preauth/mytoken/" + SENDER) - .queryParam("voip", "false") - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - final ArgumentCaptor challengeTokenCaptor = ArgumentCaptor.forClass(String.class); - - verify(registrationServiceClient).createRegistrationSession( - eq(PhoneNumberUtil.getInstance().parse(SENDER, null)), anyBoolean(), any()); - - verify(pushNotificationManager).sendRegistrationChallengeNotification( - eq("mytoken"), eq(PushNotification.TokenType.APN), challengeTokenCaptor.capture()); - - assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32); - } - - @Test - void testGetPreauthImpossibleNumber() { - final Response response = resources.getJerseyTest() - .target("/v1/accounts/fcm/preauth/mytoken/BogusNumber") - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(400); - assertThat(response.readEntity(String.class)).isBlank(); - - verifyNoInteractions(registrationServiceClient); - verifyNoInteractions(pushNotificationManager); - } - - @Test - void testGetPreauthNonNormalized() { - final String number = "+4407700900111"; - - final Response response = resources.getJerseyTest() - .target("/v1/accounts/fcm/preauth/mytoken/" + number) - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(400); - - final NonNormalizedPhoneNumberResponse responseEntity = response.readEntity(NonNormalizedPhoneNumberResponse.class); - assertThat(responseEntity.getOriginalNumber()).isEqualTo(number); - assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111"); - - verifyNoInteractions(registrationServiceClient); - verifyNoInteractions(pushNotificationManager); - } - - @Test - void testGetPreauthExistingSession() throws NumberParseException { - final String existingPushCode = "existing-push-code"; - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(new byte[16])); - - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), existingPushCode, new byte[16]))); - - Response response = resources.getJerseyTest() - .target("/v1/accounts/apn/preauth/mytoken/" + SENDER) - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - final ArgumentCaptor challengeTokenCaptor = ArgumentCaptor.forClass(String.class); - - verify(registrationServiceClient, never()).createRegistrationSession(any(), anyBoolean(), any()); - - verify(pushNotificationManager).sendRegistrationChallengeNotification( - eq("mytoken"), eq(PushNotification.TokenType.APN_VOIP), challengeTokenCaptor.capture()); - - assertThat(challengeTokenCaptor.getValue()).isEqualTo(existingPushCode); - } - - @Test - void testGetPreauthExistingSessionWithoutPushCode() throws NumberParseException { - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(new byte[16])); - - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), null, new byte[16]))); - - Response response = resources.getJerseyTest() - .target("/v1/accounts/apn/preauth/mytoken/" + SENDER) - .request() - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - final ArgumentCaptor challengeTokenCaptor = ArgumentCaptor.forClass(String.class); - - verify(registrationServiceClient, never()).createRegistrationSession(any(), anyBoolean(), any()); - - verify(pushNotificationManager).sendRegistrationChallengeNotification( - eq("mytoken"), eq(PushNotification.TokenType.APN_VOIP), challengeTokenCaptor.capture()); - - assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32); - } - - @Test - void testSendCodeWithExistingSessionFromPreauth() { - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER)) - .thenReturn(Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "1234-push", sessionId))); - - when(registrationServiceClient.sendRegistrationCode(eq(sessionId), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - verify(pendingAccountsManager).store(eq(SENDER), argThat( - storedVerificationCode -> Arrays.equals(storedVerificationCode.sessionId(), sessionId) && - "1234-push".equals(storedVerificationCode.pushCode()))); - } - - @Test - void testSendCode() { - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - verify(pendingAccountsManager).store(eq(SENDER), argThat( - storedVerificationCode -> Arrays.equals(storedVerificationCode.sessionId(), sessionId) && - "1234-push".equals(storedVerificationCode.pushCode()))); - } - - @Test - void testSendCodeRateLimited() { - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.failedFuture(new RateLimitExceededException(Duration.ofMinutes(10), true))); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(413); - - verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any()); - } - - @Test - void testSendCodeImpossibleNumber() { - final Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", "Definitely not a real number")) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(400); - assertThat(response.readEntity(String.class)).isBlank(); - - verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any()); - } - - @Test - void testSendCodeNonNormalized() { - final String number = "+4407700900111"; - - final Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", number)) - .register(Extract.class) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(400); - - final NonNormalizedPhoneNumberResponse responseEntity = response.readEntity(NonNormalizedPhoneNumberResponse.class); - assertThat(responseEntity.getOriginalNumber()).isEqualTo(number); - assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111"); - - verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any()); - } - - @Test - public void testSendCodeVoiceNoLocale() { - - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/voice/code/%s", SENDER)) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.VOICE, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } - - @Test - void testSendCodeWithValidPreauth() { - - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) - .queryParam("challenge", "validchallenge") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } - - @Test - void testSendCodeWithInvalidPreauth() { - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) - .queryParam("challenge", "invalidchallenge") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(403); - - verifyNoInteractions(registrationServiceClient); - } - - @Test - void testSendCodeWithNoPreauth() { - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(402); - - verifyNoInteractions(registrationServiceClient); - } - - @Test - void testSendiOSCode() { - - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("client", "ios") - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.IOS, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } - - @Test - void testSendAndroidNgCode() { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("client", "android-ng") - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.ANDROID_WITHOUT_FCM, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } - - @Test - void testSendWithValidCaptcha() throws NumberParseException, IOException { - - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("captcha", VALID_CAPTCHA_TOKEN) - .request() - .header("X-Forwarded-For", NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - verify(captchaChecker).verify(eq(Action.REGISTRATION), eq(VALID_CAPTCHA_TOKEN), eq(NICE_HOST)); - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } - - @Test - void testSendWithInvalidCaptcha() throws IOException { - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("captcha", INVALID_CAPTCHA_TOKEN) - .request() - .header("X-Forwarded-For", NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(402); - - verify(captchaChecker).verify(eq(Action.REGISTRATION), eq(INVALID_CAPTCHA_TOKEN), eq(NICE_HOST)); - verifyNoInteractions(registrationServiceClient); - } - - @Test - void testSendRateLimitedHost() { - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, RATE_LIMITED_IP_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(402); - - verifyNoInteractions(captchaChecker); - verifyNoInteractions(registrationServiceClient); - } - - @Test - void testSendRateLimitedPrefixAutoBlock() { - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER_OVER_PREFIX)) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, RATE_LIMITED_PREFIX_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(402); - - verifyNoInteractions(captchaChecker); - verifyNoInteractions(registrationServiceClient); - } - - @Test - void testSendRestrictedHostOut() { - - final String challenge = "challenge"; - when(pendingAccountsManager.getCodeForNumber(RESTRICTED_NUMBER)) - .thenReturn(Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), challenge, null))); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", RESTRICTED_NUMBER)) - .queryParam("challenge", challenge) - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(402); - - verifyNoInteractions(registrationServiceClient); - } - - @ParameterizedTest - @CsvSource({ - "+12025550123, true", - "+12505550199, false", - }) - void testRestrictedRegion(final String number, final boolean expectSendCode) throws NumberParseException { - final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); - when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); - - final DynamicCaptchaConfiguration signupCaptchaConfig = new DynamicCaptchaConfiguration(); - signupCaptchaConfig.setSignupRegions(Set.of("CA")); - - when(dynamicConfiguration.getCaptchaConfiguration()).thenReturn(signupCaptchaConfig); - - final String challenge = "challenge"; - when(pendingAccountsManager.getCodeForNumber(number)) - .thenReturn(Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), challenge, null))); - - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", number)) - .queryParam("challenge", challenge) - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - if (expectSendCode) { - assertThat(response.getStatus()).isEqualTo(200); - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } else { - assertThat(response.getStatus()).isEqualTo(402); - verifyNoInteractions(registrationServiceClient); - } - } - - @Test - void testSendRestrictedIn() { - - final String challenge = "challenge"; - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of( - new StoredVerificationCode(null, System.currentTimeMillis(), challenge, null))); - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("challenge", challenge) - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } - - @Test - void testSendCodeTestDeviceNumber() { - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", TEST_NUMBER)) - .request() - .header("X-Forwarded-For", RATE_LIMITED_IP_HOST) - .get(); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(StoredVerificationCode.class); - verify(pendingAccountsManager).store(eq(TEST_NUMBER), captor.capture()); - assertThat(captor.getValue().code()).isNull(); - assertThat(captor.getValue().sessionId()).isEqualTo(sessionId); - assertThat(response.getStatus()).isEqualTo(200); - - // Even though no actual SMS will be sent, we leave that decision to the registration service - verify(registrationServiceClient).sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } - - @Test - void testVerifyCode() throws Exception { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER)) - .thenReturn(Optional.of( - new StoredVerificationCode(null, System.currentTimeMillis(), "1234-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - final AccountAttributes attrs = new AccountAttributes(true, 1, "test", "", true, new Device.DeviceCapabilities()); - - resources.getJerseyTest() - .target("/v1/accounts/code/1234") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER, "bar")) - .put(Entity.entity(attrs, MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList()); - - verify(registrationServiceClient).checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT); - } - - @Test - void testVerifyCodeBadCredentials() { - final Response response = resources.getJerseyTest() - .target("/v1/accounts/code/1234") - .request() - .header(HttpHeaders.AUTHORIZATION, "This is not a valid authorization header") - .put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(401); - } - - @Test - void testVerifyCodeOld() { - Response response = - resources.getJerseyTest() - .target("/v1/accounts/code/1234") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_OLD, "bar")) - .put(Entity.entity(new AccountAttributes(false, 2222, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(403); - - verifyNoInteractions(accountsManager); - } - - @Test - void testVerifyBadCode() { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER)) - .thenReturn(Optional.of( - new StoredVerificationCode(null, System.currentTimeMillis(), "1234-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(false)); - - Response response = - resources.getJerseyTest() - .target("/v1/accounts/code/1111") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER, "bar")) - .put(Entity.entity(new AccountAttributes(false, 3333, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(403); - - verify(registrationServiceClient).checkVerificationCode(sessionId, "1111", AccountController.REGISTRATION_RPC_TIMEOUT); - verifyNoInteractions(accountsManager); - } - - @Test - void testVerifyRegistrationLock() throws Exception { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)) - .thenReturn(Optional.of( - new StoredVerificationCode(null, System.currentTimeMillis(), "666666-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "666666", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - AccountIdentityResponse result = - resources.getJerseyTest() - .target("/v1/accounts/code/666666") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_REG_LOCK, "bar")) - .put(Entity.entity( - new AccountAttributes(false, 3333, null, HexFormat.of().formatHex(registration_lock_key), true, null), - MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - assertThat(result.uuid()).isNotNull(); - - verify(pinLimiter).validate(eq(SENDER_REG_LOCK)); - } - - @Test - void testVerifyRegistrationLockSetsRegistrationLockOnNewAccount() throws Exception { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)) - .thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "666666-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "666666", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - AccountIdentityResponse result = - resources.getJerseyTest() - .target("/v1/accounts/code/666666") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_REG_LOCK, "bar")) - .put(Entity.entity( - new AccountAttributes(false, 3333, null, HexFormat.of().formatHex(registration_lock_key), true, null), - MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - assertThat(result.uuid()).isNotNull(); - - verify(pinLimiter).validate(eq(SENDER_REG_LOCK)); - - verify(accountsManager).create(eq(SENDER_REG_LOCK), eq("bar"), any(), argThat( - attributes -> HexFormat.of().formatHex(registration_lock_key).equals(attributes.getRegistrationLock())), - argThat(List::isEmpty)); - } - - @Test - void testVerifyRegistrationLockOld() { - StoredRegistrationLock lock = senderRegLockAccount.getRegistrationLock(); - - try { - when(senderRegLockAccount.getRegistrationLock()).thenReturn(lock.forTime(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7))); - - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)) - .thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "666666-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "666666", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - AccountIdentityResponse result = - resources.getJerseyTest() - .target("/v1/accounts/code/666666") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_REG_LOCK, "bar")) - .put(Entity.entity(new AccountAttributes(false, 3333, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - assertThat(result.uuid()).isNotNull(); - - verifyNoInteractions(pinLimiter); - } finally { - when(senderRegLockAccount.getRegistrationLock()).thenReturn(lock); - } - } - - @Test - void testVerifyWrongRegistrationLock() throws Exception { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)) - .thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "666666-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "666666", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - Response response = - resources.getJerseyTest() - .target("/v1/accounts/code/666666") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_REG_LOCK, "bar")) - .put(Entity.entity(new AccountAttributes(false, 3333, null, - HexFormat.of().formatHex(new byte[32]), true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(423); - - // verify(senderRegLockAccount).lockAuthenticationCredentials(); - // verify(clientPresenceManager, times(1)).disconnectAllPresences(eq(SENDER_REG_LOCK_UUID), any()); - verify(pinLimiter).validate(eq(SENDER_REG_LOCK)); - } - - @Test - void testVerifyNoRegistrationLock() { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)) - .thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "666666-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "666666", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - Response response = - resources.getJerseyTest() - .target("/v1/accounts/code/666666") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_REG_LOCK, "bar")) - .put(Entity.entity(new AccountAttributes(false, 3333, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(423); - - RegistrationLockFailure failure = response.readEntity(RegistrationLockFailure.class); - assertThat(failure.backupCredentials()).isNotNull(); - assertThat(failure.backupCredentials().username()).isEqualTo(SENDER_REG_LOCK_UUID.toString()); - assertThat(failure.backupCredentials().password()).isNotEmpty(); - assertThat(failure.backupCredentials().password().startsWith(SENDER_REG_LOCK_UUID.toString())).isTrue(); - assertThat(failure.svr2Credentials()).isNotNull(); - assertThat(failure.svr2Credentials()).isEqualTo(svr2CredentialsGenerator.generateFor(SENDER_REG_LOCK_UUID.toString())); - assertThat(failure.timeRemaining()).isGreaterThan(0); - - // verify(senderRegLockAccount).lockAuthenticationCredentials(); - // verify(clientPresenceManager, atLeastOnce()).disconnectAllPresences(eq(SENDER_REG_LOCK_UUID), any()); - verifyNoInteractions(pinLimiter); - } - - @Test - void testVerifyTransferSupported() { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)) - .thenReturn(Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "1234-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - when(senderTransfer.isTransferSupported()).thenReturn(true); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/code/1234") - .queryParam("transfer", true) - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_TRANSFER, "bar")) - .put(Entity.entity(new AccountAttributes(false, 2222, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(409); - } - - @Test - void testVerifyTransferNotSupported() { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)) - .thenReturn(Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "1234-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - when(senderTransfer.isTransferSupported()).thenReturn(false); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/code/1234") - .queryParam("transfer", true) - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_TRANSFER, "bar")) - .put(Entity.entity(new AccountAttributes(false, 2222, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(200); - } - - @Test - void testVerifyTransferSupportedNotRequested() { - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)) - .thenReturn(Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "1234-push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT)) - .thenReturn(CompletableFuture.completedFuture(true)); - - when(senderTransfer.isTransferSupported()).thenReturn(true); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/code/1234") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(SENDER_TRANSFER, "bar")) - .put(Entity.entity(new AccountAttributes(false, 2222, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(200); - } - - @Test - void testChangePhoneNumber() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(true)); - - final AccountIdentityResponse accountIdentityResponse = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - verify(registrationServiceClient).checkVerificationCode(sessionId, code, AccountController.REGISTRATION_RPC_TIMEOUT); - - verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any(), any(), any(), any(), any()); - - assertThat(accountIdentityResponse.uuid()).isEqualTo(AuthHelper.VALID_UUID); - assertThat(accountIdentityResponse.number()).isEqualTo(number); - assertThat(accountIdentityResponse.pni()).isNotEqualTo(AuthHelper.VALID_PNI); - } - - @Test - void testChangePhoneNumberImpossibleNumber() throws Exception { - final String number = "This is not a real phone number"; - final String code = "987654"; - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(400); - assertThat(response.readEntity(String.class)).isBlank(); - verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberNonNormalized() throws Exception { - final String number = "+4407700900111"; - final String code = "987654"; - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(400); - - final NonNormalizedPhoneNumberResponse responseEntity = response.readEntity(NonNormalizedPhoneNumberResponse.class); - assertThat(responseEntity.getOriginalNumber()).isEqualTo(number); - assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111"); - - verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberSameNumber() throws Exception { - final AccountIdentityResponse accountIdentityResponse = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(AuthHelper.VALID_NUMBER, "567890", null, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberNoPendingCode() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.empty()); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(403); - verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberIncorrectCode() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn( - Optional.of(new StoredVerificationCode(code, System.currentTimeMillis(), "push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(false)); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - verify(registrationServiceClient).checkVerificationCode(sessionId, code, AccountController.REGISTRATION_RPC_TIMEOUT); - - assertThat(response.getStatus()).isEqualTo(403); - verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberExistingAccountReglockNotRequired() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn( - Optional.of(new StoredVerificationCode(code, System.currentTimeMillis(), "push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(true)); - - final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); - when(existingRegistrationLock.getStatus()).thenReturn(StoredRegistrationLock.Status.ABSENT); - - final Account existingAccount = mock(Account.class); - when(existingAccount.getNumber()).thenReturn(number); - when(existingAccount.getUuid()).thenReturn(UUID.randomUUID()); - when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); - - when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount)); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(200); - verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberExistingAccountReglockRequiredNotProvided() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn( - Optional.of(new StoredVerificationCode(code, System.currentTimeMillis(), "push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(true)); - - final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); - when(existingRegistrationLock.getStatus()).thenReturn(StoredRegistrationLock.Status.REQUIRED); - - final UUID existingUuid = UUID.randomUUID(); - final Account existingAccount = mock(Account.class); - when(existingAccount.getNumber()).thenReturn(number); - when(existingAccount.getUuid()).thenReturn(existingUuid); - when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); - - when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount)); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(423); - - // verify(existingAccount).lockAuthenticationCredentials(); - // verify(clientPresenceManager, atLeastOnce()).disconnectAllPresences(eq(existingUuid), any()); - verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberExistingAccountReglockRequiredIncorrect() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; - final String reglock = "setec-astronomy"; - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(null, System.currentTimeMillis(), "push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(true)); - - final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); - when(existingRegistrationLock.getStatus()).thenReturn(StoredRegistrationLock.Status.REQUIRED); - when(existingRegistrationLock.verify(anyString())).thenReturn(false); - - UUID existingUuid = UUID.randomUUID(); - final Account existingAccount = mock(Account.class); - when(existingAccount.getNumber()).thenReturn(number); - when(existingAccount.getUuid()).thenReturn(existingUuid); - when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); - - when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount)); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(423); - - // verify(existingAccount).lockAuthenticationCredentials(); - // verify(clientPresenceManager, atLeastOnce()).disconnectAllPresences(eq(existingUuid), any()); - verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberExistingAccountReglockRequiredCorrect() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; - final String reglock = "setec-astronomy"; - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(true)); - - final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); - when(existingRegistrationLock.getStatus()).thenReturn(StoredRegistrationLock.Status.REQUIRED); - when(existingRegistrationLock.verify(reglock)).thenReturn(true); - - final Account existingAccount = mock(Account.class); - when(existingAccount.getNumber()).thenReturn(number); - when(existingAccount.getUuid()).thenReturn(UUID.randomUUID()); - when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); - - when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount)); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(200); - verify(senderRegLockAccount, never()).lockAuthTokenHash(); - verify(clientPresenceManager, never()).disconnectAllPresences(eq(SENDER_REG_LOCK_UUID), any()); - verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberChangePrekeys() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; - final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); - final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - Device device2 = mock(Device.class); - when(device2.getId()).thenReturn(2L); - when(device2.isEnabled()).thenReturn(true); - when(device2.getRegistrationId()).thenReturn(2); - - Device device3 = mock(Device.class); - when(device3.getId()).thenReturn(3L); - when(device3.isEnabled()).thenReturn(true); - when(device3.getRegistrationId()).thenReturn(3); - - when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(AuthHelper.VALID_DEVICE, device2, device3)); - when(AuthHelper.VALID_ACCOUNT.getDevice(2L)).thenReturn(Optional.of(device2)); - when(AuthHelper.VALID_ACCOUNT.getDevice(3L)).thenReturn(Optional.of(device3)); - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(true)); - - var deviceMessages = List.of( - new IncomingMessage(1, 2, 2, "content2"), - new IncomingMessage(1, 3, 3, "content3")); - var deviceKeys = List.of(1L, 2L, 3L).stream().collect(Collectors.toMap(Function.identity(), n -> KeysHelper.signedECPreKey(n + 100, pniIdentityKeyPair))); - - final Map registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89); - - final AccountIdentityResponse accountIdentityResponse = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest( - number, code, null, - pniIdentityKey, deviceMessages, - deviceKeys, - null, - registrationIds), - MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any(), any(), any(), any(), any()); - - assertThat(accountIdentityResponse.uuid()).isEqualTo(AuthHelper.VALID_UUID); - assertThat(accountIdentityResponse.number()).isEqualTo(number); - assertThat(accountIdentityResponse.pni()).isNotEqualTo(AuthHelper.VALID_PNI); - } - - @Test - void testChangePhoneNumberSameNumberChangePrekeys() throws Exception { - final String code = "987654"; - final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); - final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey()); - final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); - - Device device2 = mock(Device.class); - when(device2.getId()).thenReturn(2L); - when(device2.isEnabled()).thenReturn(true); - when(device2.getRegistrationId()).thenReturn(2); - - Device device3 = mock(Device.class); - when(device3.getId()).thenReturn(3L); - when(device3.isEnabled()).thenReturn(true); - when(device3.getRegistrationId()).thenReturn(3); - - when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(AuthHelper.VALID_DEVICE, device2, device3)); - when(AuthHelper.VALID_ACCOUNT.getDevice(2L)).thenReturn(Optional.of(device2)); - when(AuthHelper.VALID_ACCOUNT.getDevice(3L)).thenReturn(Optional.of(device3)); - - when(pendingAccountsManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn( - Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "push", sessionId))); - - when(registrationServiceClient.checkVerificationCode(any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(true)); - - var deviceMessages = List.of( - new IncomingMessage(1, 2, 2, "content2"), - new IncomingMessage(1, 3, 3, "content3")); - var deviceKeys = List.of(1L, 2L, 3L).stream().collect(Collectors.toMap(Function.identity(), n -> KeysHelper.signedECPreKey(n + 100, pniIdentityKeyPair))); - - final Map registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89); - - final AccountIdentityResponse accountIdentityResponse = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest( - AuthHelper.VALID_NUMBER, code, null, - pniIdentityKey, deviceMessages, - deviceKeys, - null, - registrationIds), - MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - verify(changeNumberManager).changeNumber( - eq(AuthHelper.VALID_ACCOUNT), eq(AuthHelper.VALID_NUMBER), any(), any(), any(), any(), any()); - verifyNoInteractions(rateLimiter); - verifyNoInteractions(pendingAccountsManager); - - assertThat(accountIdentityResponse.uuid()).isEqualTo(AuthHelper.VALID_UUID); - assertThat(accountIdentityResponse.number()).isEqualTo(AuthHelper.VALID_NUMBER); - assertThat(accountIdentityResponse.pni()).isEqualTo(AuthHelper.VALID_PNI); - } - @Test void testSetRegistrationLock() { Response response = @@ -2330,48 +830,6 @@ class AccountControllerTest { verify(accountsManager).delete(AuthHelper.VALID_ACCOUNT, AccountsManager.DeletionReason.USER_REQUEST); } - @ParameterizedTest - @MethodSource - void testSignupCaptcha(final String message, final boolean enforced, final Set countryCodes, final int expectedResponseStatusCode) { - DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); - when(dynamicConfigurationManager.getConfiguration()) - .thenReturn(dynamicConfiguration); - - DynamicCaptchaConfiguration signupCaptchaConfig = new DynamicCaptchaConfiguration(); - signupCaptchaConfig.setSignupCountryCodes(countryCodes); - when(dynamicConfiguration.getCaptchaConfiguration()) - .thenReturn(signupCaptchaConfig); - - final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); - - when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any())) - .thenReturn(CompletableFuture.completedFuture(sessionId)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("challenge", "1234-push") - .request() - .header(HttpHeaders.X_FORWARDED_FOR, NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(expectedResponseStatusCode); - - verify(registrationServiceClient, 200 == expectedResponseStatusCode ? times(1) : never()) - .sendRegistrationCode(sessionId, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); - } - - static Stream testSignupCaptcha() { - return Stream.of( - Arguments.of("captcha not enforced", false, Collections.emptySet(), 200), - Arguments.of("no enforced country codes", true, Collections.emptySet(), 200), - Arguments.of("captcha enforced", true, Set.of("1"), 402) - ); - } - @Test void testAccountExists() { final Account account = mock(Account.class); @@ -2519,27 +977,4 @@ class AccountControllerTest { .get() .getStatus()).isEqualTo(422); } - - @ParameterizedTest - @MethodSource - void pushTokensMatch(@Nullable final String pushChallenge, @Nullable final StoredVerificationCode storedVerificationCode, final boolean expectMatch) { - final String number = "+18005550123"; - final Optional maybePushChallenge = Optional.ofNullable(pushChallenge); - final Optional maybeStoredVerificationCode = Optional.ofNullable(storedVerificationCode); - - assertEquals(expectMatch, AccountController.pushChallengeMatches(number, maybePushChallenge, maybeStoredVerificationCode)); - } - - private static Stream pushTokensMatch() { - return Stream.of( - Arguments.of(null, null, false), - Arguments.of("123456", null, false), - Arguments.of(null, new StoredVerificationCode(null, 0, null, null), false), - Arguments.of(null, new StoredVerificationCode(null, 0, "123456", null), false), - Arguments.of("654321", new StoredVerificationCode(null, 0, "123456", null), - false), - Arguments.of("123456", new StoredVerificationCode(null, 0, "123456", null), - true) - ); - } }