From 6c7a3df5aefa716fd26758689c77f6b18bf03c5c Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Fri, 8 Dec 2023 13:18:20 -0500 Subject: [PATCH] Retire non-atomic device-linking pathways --- .../textsecuregcm/WhisperServerService.java | 3 +- .../controllers/DeviceController.java | 221 +++++--------- .../controllers/DeviceControllerTest.java | 279 +++++------------- 3 files changed, 151 insertions(+), 352 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index cca742b50..c1c3e9cb9 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -802,8 +802,7 @@ public class WhisperServerService extends Application maxDeviceConfiguration; @@ -94,15 +88,11 @@ public class DeviceController { public DeviceController(byte[] linkDeviceSecret, AccountsManager accounts, - MessagesManager messages, - KeysManager keys, RateLimiters rateLimiters, FaultTolerantRedisCluster usedTokenCluster, Map maxDeviceConfiguration, final Clock clock) { this.verificationTokenKey = new SecretKeySpec(linkDeviceSecret, VERIFICATION_TOKEN_ALGORITHM); this.accounts = accounts; - this.messages = messages; - this.keys = keys; this.rateLimiters = rateLimiters; this.usedTokenCluster = usedTokenCluster; this.maxDeviceConfiguration = maxDeviceConfiguration; @@ -175,34 +165,6 @@ public class DeviceController { return new VerificationCode(generateVerificationToken(account.getUuid())); } - /** - * @deprecated callers should use {@link #linkDevice(BasicAuthorizationHeader, LinkDeviceRequest, ContainerRequest)} - * instead - */ - @PUT - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - @Path("/{verification_code}") - @ChangesDeviceEnabledState - @Deprecated(forRemoval = true) - public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode, - @HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader, - @NotNull @Valid AccountAttributes accountAttributes, - @Context ContainerRequest containerRequest) - throws RateLimitExceededException, DeviceLimitExceededException { - - final Pair accountAndDevice = createDevice(authorizationHeader.getPassword(), - verificationCode, - accountAttributes, - containerRequest, - Optional.empty()); - - final Account account = accountAndDevice.first(); - final Device device = accountAndDevice.second(); - - return new DeviceResponse(account.getUuid(), account.getPhoneNumberIdentifier(), device.getId()); - } - @PUT @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @@ -219,21 +181,81 @@ public class DeviceController { @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( name = "Retry-After", description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed")) - public DeviceResponse linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader, + public CompletableFuture linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader, @NotNull @Valid LinkDeviceRequest linkDeviceRequest, @Context ContainerRequest containerRequest) throws RateLimitExceededException, DeviceLimitExceededException { - final Pair accountAndDevice = createDevice(authorizationHeader.getPassword(), - linkDeviceRequest.verificationCode(), - linkDeviceRequest.accountAttributes(), - containerRequest, - Optional.of(linkDeviceRequest.deviceActivationRequest())); + final Optional maybeAciFromToken = checkVerificationToken(linkDeviceRequest.verificationCode()); - final Account account = accountAndDevice.first(); - final Device device = accountAndDevice.second(); + final Account account = maybeAciFromToken.flatMap(accounts::getByAccountIdentifier) + .orElseThrow(ForbiddenException::new); - return new DeviceResponse(account.getUuid(), account.getPhoneNumberIdentifier(), device.getId()); + final DeviceActivationRequest deviceActivationRequest = linkDeviceRequest.deviceActivationRequest(); + final AccountAttributes accountAttributes = linkDeviceRequest.accountAttributes(); + + rateLimiters.getVerifyDeviceLimiter().validate(account.getUuid()); + + final boolean allKeysValid = + PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.ACI), + List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey())) + && PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI), + List.of(deviceActivationRequest.pniSignedPreKey(), deviceActivationRequest.pniPqLastResortPreKey())); + + if (!allKeysValid) { + throw new WebApplicationException(Response.status(422).build()); + } + + // Normally, the "do we need to refresh somebody's websockets" listener can do this on its own. In this case, + // we're not using the conventional authentication system, and so we need to give it a hint so it knows who the + // active user is and what their device states look like. + AuthEnablementRefreshRequirementProvider.setAccount(containerRequest, account); + + int maxDeviceLimit = MAX_DEVICES; + + if (maxDeviceConfiguration.containsKey(account.getNumber())) { + maxDeviceLimit = maxDeviceConfiguration.get(account.getNumber()); + } + + if (account.getDevices().size() >= maxDeviceLimit) { + throw new DeviceLimitExceededException(account.getDevices().size(), maxDeviceLimit); + } + + final DeviceCapabilities capabilities = accountAttributes.getCapabilities(); + if (capabilities != null && isCapabilityDowngrade(account, capabilities)) { + throw new WebApplicationException(Response.status(409).build()); + } + + final String signalAgent; + + if (deviceActivationRequest.apnToken().isPresent()) { + signalAgent = "OWP"; + } else if (deviceActivationRequest.gcmToken().isPresent()) { + signalAgent = "OWA"; + } else { + signalAgent = "OWD"; + } + + return accounts.addDevice(account, new DeviceSpec(accountAttributes.getName(), + authorizationHeader.getPassword(), + signalAgent, + capabilities, + accountAttributes.getRegistrationId(), + accountAttributes.getPhoneNumberIdentityRegistrationId(), + accountAttributes.getFetchesMessages(), + deviceActivationRequest.apnToken(), + deviceActivationRequest.gcmToken(), + deviceActivationRequest.aciSignedPreKey(), + deviceActivationRequest.pniSignedPreKey(), + deviceActivationRequest.aciPqLastResortPreKey(), + deviceActivationRequest.pniPqLastResortPreKey())) + .thenCompose(a -> usedTokenCluster.withCluster(connection -> connection.async() + .set(getUsedTokenKey(linkDeviceRequest.verificationCode()), "", new SetArgs().ex(TOKEN_EXPIRATION_DURATION))) + .thenApply(ignored -> a)) + .thenApply(accountAndDevice -> new DeviceResponse( + accountAndDevice.first().getIdentifier(IdentityType.ACI), + accountAndDevice.first().getIdentifier(IdentityType.PNI), + accountAndDevice.second().getId())); } @PUT @@ -338,111 +360,6 @@ public class DeviceController { return account.isPniSupported() && !capabilities.pni(); } - private Pair createDevice(final String password, - final String verificationCode, - final AccountAttributes accountAttributes, - final ContainerRequest containerRequest, - final Optional maybeDeviceActivationRequest) - throws RateLimitExceededException, DeviceLimitExceededException { - - final Optional maybeAciFromToken = checkVerificationToken(verificationCode); - - final Account account = maybeAciFromToken.flatMap(accounts::getByAccountIdentifier) - .orElseThrow(ForbiddenException::new); - - rateLimiters.getVerifyDeviceLimiter().validate(account.getUuid()); - - maybeDeviceActivationRequest.ifPresent(deviceActivationRequest -> { - final boolean allKeysValid = - PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.ACI), - List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey())) - && PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI), - List.of(deviceActivationRequest.pniSignedPreKey(), deviceActivationRequest.pniPqLastResortPreKey())); - - if (!allKeysValid) { - throw new WebApplicationException(Response.status(422).build()); - } - }); - - // Normally, the "do we need to refresh somebody's websockets" listener can do this on its own. In this case, - // we're not using the conventional authentication system, and so we need to give it a hint so it knows who the - // active user is and what their device states look like. - AuthEnablementRefreshRequirementProvider.setAccount(containerRequest, account); - - int maxDeviceLimit = MAX_DEVICES; - - if (maxDeviceConfiguration.containsKey(account.getNumber())) { - maxDeviceLimit = maxDeviceConfiguration.get(account.getNumber()); - } - - if (account.getDevices().size() >= maxDeviceLimit) { - throw new DeviceLimitExceededException(account.getDevices().size(), maxDeviceLimit); - } - - final DeviceCapabilities capabilities = accountAttributes.getCapabilities(); - if (capabilities != null && isCapabilityDowngrade(account, capabilities)) { - throw new WebApplicationException(Response.status(409).build()); - } - - return maybeDeviceActivationRequest.map(deviceActivationRequest -> { - final String signalAgent; - - if (deviceActivationRequest.apnToken().isPresent()) { - signalAgent = "OWP"; - } else if (deviceActivationRequest.gcmToken().isPresent()) { - signalAgent = "OWA"; - } else { - signalAgent = "OWD"; - } - - return accounts.addDevice(account, new DeviceSpec(accountAttributes.getName(), - password, - signalAgent, - capabilities, - accountAttributes.getRegistrationId(), - accountAttributes.getPhoneNumberIdentityRegistrationId(), - accountAttributes.getFetchesMessages(), - deviceActivationRequest.apnToken(), - deviceActivationRequest.gcmToken(), - deviceActivationRequest.aciSignedPreKey(), - deviceActivationRequest.pniSignedPreKey(), - deviceActivationRequest.aciPqLastResortPreKey(), - deviceActivationRequest.pniPqLastResortPreKey())) - .thenCompose(a -> usedTokenCluster.withCluster(connection -> connection.async() - .set(getUsedTokenKey(verificationCode), "", new SetArgs().ex(TOKEN_EXPIRATION_DURATION))) - .thenApply(ignored -> a)) - .join(); - }) - .orElseGet(() -> { - final Device device = new Device(); - device.setName(accountAttributes.getName()); - device.setAuthTokenHash(SaltedTokenHash.generateFor(password)); - device.setFetchesMessages(accountAttributes.getFetchesMessages()); - device.setRegistrationId(accountAttributes.getRegistrationId()); - device.setPhoneNumberIdentityRegistrationId(accountAttributes.getPhoneNumberIdentityRegistrationId()); - device.setLastSeen(Util.todayInMillis()); - device.setCreated(System.currentTimeMillis()); - device.setCapabilities(accountAttributes.getCapabilities()); - - final Account updatedAccount = accounts.update(account, a -> { - device.setId(a.getNextDeviceId()); - - CompletableFuture.allOf( - keys.delete(a.getUuid(), device.getId()), - keys.delete(a.getPhoneNumberIdentifier(), device.getId()), - messages.clear(a.getUuid(), device.getId())) - .join(); - - a.addDevice(device); - }); - - usedTokenCluster.useCluster(connection -> - connection.sync().set(getUsedTokenKey(verificationCode), "", new SetArgs().ex(TOKEN_EXPIRATION_DURATION))); - - return new Pair<>(updatedAccount, device); - }); - } - private static String getUsedTokenKey(final String token) { return "usedToken::" + token; } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java index 559268b3d..69cad4136 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java @@ -16,7 +16,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableSet; @@ -49,7 +48,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.ecc.Curve; @@ -75,8 +73,6 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; import org.whispersystems.textsecuregcm.storage.DeviceSpec; -import org.whispersystems.textsecuregcm.storage.KeysManager; -import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.KeysHelper; @@ -91,8 +87,6 @@ import org.whispersystems.textsecuregcm.util.VerificationCode; class DeviceControllerTest { private static AccountsManager accountsManager = mock(AccountsManager.class); - private static MessagesManager messagesManager = mock(MessagesManager.class); - private static KeysManager keysManager = mock(KeysManager.class); private static RateLimiters rateLimiters = mock(RateLimiters.class); private static RateLimiter rateLimiter = mock(RateLimiter.class); private static RedisAdvancedClusterCommands commands = mock(RedisAdvancedClusterCommands.class); @@ -109,8 +103,6 @@ class DeviceControllerTest { private static DeviceController deviceController = new DeviceController( generateLinkDeviceSecret(), accountsManager, - messagesManager, - keysManager, rateLimiters, RedisClusterHelper.builder() .stringCommands(commands) @@ -160,19 +152,12 @@ class DeviceControllerTest { when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount)); AccountsHelper.setupMockUpdate(accountsManager); - - when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(keysManager.delete(any(), anyByte())).thenReturn(CompletableFuture.completedFuture(null)); - - when(messagesManager.clear(any(), anyByte())).thenReturn(CompletableFuture.completedFuture(null)); } @AfterEach void teardown() { reset( accountsManager, - messagesManager, - keysManager, rateLimiters, rateLimiter, commands, @@ -186,95 +171,6 @@ class DeviceControllerTest { testClock.unpin(); } - @Test - void validDeviceRegisterTest() { - final Device existingDevice = mock(Device.class); - when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); - when(existingDevice.isEnabled()).thenReturn(true); - when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); - - VerificationCode deviceCode = resources.getJerseyTest() - .target("/v1/devices/provisioning/code") - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .get(VerificationCode.class); - - DeviceResponse response = resources.getJerseyTest() - .target("/v1/devices/" + deviceCode.verificationCode()) - .request() - .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes(false, 1234, 5678, - null, null, true, null), - MediaType.APPLICATION_JSON_TYPE), - DeviceResponse.class); - - assertThat(response.getDeviceId()).isEqualTo(NEXT_DEVICE_ID); - - verify(messagesManager).clear(eq(AuthHelper.VALID_UUID), eq(NEXT_DEVICE_ID)); - verify(commands).set(anyString(), anyString(), any()); - } - - @Test - void validDeviceRegisterTestSignedTokenUsed() { - when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); - - final Device existingDevice = mock(Device.class); - when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); - when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); - - final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); - - when(commands.get(anyString())).thenReturn(""); - - final Response response = resources.getJerseyTest() - .target("/v1/devices/" + verificationToken) - .request() - .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes(false, 1234, 5678, - null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); - } - - @Test - void verifyDeviceWithNullAccountAttributes() { - when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT)); - - final Device existingDevice = mock(Device.class); - when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); - when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); - - VerificationCode deviceCode = resources.getJerseyTest() - .target("/v1/devices/provisioning/code") - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .get(VerificationCode.class); - - final Response response = resources.getJerseyTest() - .target("/v1/devices/" + deviceCode.verificationCode()) - .request() - .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.json("")); - - assertThat(response.getStatus()).isNotEqualTo(500); - } - - @Test - void verifyDeviceTokenBadCredentials() { - final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); - - final Response response = resources.getJerseyTest() - .target("/v1/devices/" + verificationToken) - .request() - .header("Authorization", "This is not a valid authorization header") - .put(Entity.entity(new AccountAttributes(false, 1234, 5678, - null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertEquals(401, response.getStatus()); - } - @ParameterizedTest @MethodSource @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @@ -318,9 +214,6 @@ class DeviceControllerTest { return CompletableFuture.completedFuture(new Pair<>(a, deviceSpec.toDevice(NEXT_DEVICE_ID, testClock))); }); - when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null)); final AccountAttributes accountAttributes = new AccountAttributes(fetchesMessages, 1234, 5678, null, null, true, null); @@ -371,6 +264,44 @@ class DeviceControllerTest { ); } + @Test + void linkDeviceAtomicBadCredentials() { + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + + final Device primaryDevice = mock(Device.class); + when(primaryDevice.getId()).thenReturn(Device.PRIMARY_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(primaryDevice)); + + final ECSignedPreKey aciSignedPreKey; + final ECSignedPreKey pniSignedPreKey; + final KEMSignedPreKey aciPqLastResortPreKey; + final KEMSignedPreKey pniPqLastResortPreKey; + + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair); + pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair); + aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); + pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); + + when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + + final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID), + new AccountAttributes(false, 1234, 5678, null, null, true, null), + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id")))); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", "This is not a valid authorization header") + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); + } + } + @Test void linkDeviceAtomicWithVerificationTokenUsed() { @@ -396,9 +327,6 @@ class DeviceControllerTest { when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); - when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(commands.get(anyString())).thenReturn(""); final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID), @@ -474,7 +402,6 @@ class DeviceControllerTest { @ParameterizedTest @MethodSource - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") void linkDeviceAtomicMissingProperty(final IdentityKey aciIdentityKey, final IdentityKey pniIdentityKey, final ECSignedPreKey aciSignedPreKey, @@ -589,6 +516,45 @@ class DeviceControllerTest { ); } + @Test + void linkDeviceAtomicExcessiveDeviceName() { + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); + when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice)); + + final ECSignedPreKey aciSignedPreKey; + final ECSignedPreKey pniSignedPreKey; + final KEMSignedPreKey aciPqLastResortPreKey; + final KEMSignedPreKey pniPqLastResortPreKey; + + final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); + final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); + + aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair); + pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair); + aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); + pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); + + when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + + final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID), + new AccountAttributes(false, 1234, 5678, TestRandomUtil.nextBytes(512), null, true, null), + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id")))); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(422, response.getStatus()); + } + } + @ParameterizedTest @MethodSource void linkDeviceRegistrationId(final int registrationId, final int pniRegistrationId, final int expectedStatusCode) { @@ -620,9 +586,6 @@ class DeviceControllerTest { return CompletableFuture.completedFuture(new Pair<>(a, deviceSpec.toDevice(NEXT_DEVICE_ID, testClock))); }); - when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null)); final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(), @@ -675,41 +638,6 @@ class DeviceControllerTest { assertThat(response.getStatus()).isEqualTo(401); } - @Test - void invalidDeviceRegisterTest() { - VerificationCode deviceCode = resources.getJerseyTest() - .target("/v1/devices/provisioning/code") - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .get(VerificationCode.class); - - Response response = resources.getJerseyTest() - .target("/v1/devices/" + deviceCode.verificationCode() + "-incorrect") - .request() - .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes(false, 1234, 5678, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(403); - - verifyNoMoreInteractions(messagesManager); - } - - @Test - void oldDeviceRegisterTest() { - Response response = resources.getJerseyTest() - .target("/v1/devices/1112223") - .request() - .header("Authorization", - AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO)) - .put(Entity.entity(new AccountAttributes(false, 1234, 5678, null, null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(403); - - verifyNoMoreInteractions(messagesManager); - } - @Test void maxDevicesTest() { final AuthHelper.TestAccount testAccount = AUTH_FILTER_EXTENSION.createTestAccount(); @@ -726,29 +654,15 @@ class DeviceControllerTest { .get(); assertEquals(411, response.getStatus()); - verifyNoMoreInteractions(messagesManager); - } - - @Test - void longNameTest() { - final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); - - Response response = resources.getJerseyTest() - .target("/v1/devices/" + verificationToken) - .request() - .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes(false, 1234, 5678, - TestRandomUtil.nextBytes(226), null, true, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertEquals(422, response.getStatus()); - verifyNoMoreInteractions(messagesManager); + verify(accountsManager, never()).addDevice(any(), any()); } @ParameterizedTest @MethodSource void deviceDowngradePniTest(final boolean accountSupportsPni, final boolean deviceSupportsPni, final int expectedStatus) { when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + when(accountsManager.addDevice(any(), any())) + .thenReturn(CompletableFuture.completedFuture(new Pair<>(mock(Account.class), mock(Device.class)))); final Device primaryDevice = mock(Device.class); when(primaryDevice.getId()).thenReturn(Device.PRIMARY_ID); @@ -771,22 +685,10 @@ class DeviceControllerTest { when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); when(account.isPniSupported()).thenReturn(accountSupportsPni); - when(accountsManager.addDevice(any(), any())).thenAnswer(invocation -> { - final Account a = invocation.getArgument(0); - final DeviceSpec deviceSpec = invocation.getArgument(1); - - return CompletableFuture.completedFuture(new Pair<>(a, deviceSpec.toDevice(NEXT_DEVICE_ID, testClock))); - }); - - when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null)); - final AccountAttributes accountAttributes = new AccountAttributes(false, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, deviceSupportsPni, true)); - final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID), - accountAttributes, + new AccountAttributes(false, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, deviceSupportsPni, true)), new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id")))); try (final Response response = resources.getJerseyTest() @@ -833,25 +735,6 @@ class DeviceControllerTest { assertThat(response.getStatus()).isEqualTo(422); } - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void deviceDowngradePaymentActivationTest(boolean paymentActivation) { - // Update when we start returning true value of capability & restricting downgrades - DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, paymentActivation); - AccountAttributes accountAttributes = new AccountAttributes(false, 1234, 5678, null, null, true, deviceCapabilities); - - final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID); - - Response response = resources - .getJerseyTest() - .target("/v1/devices/" + verificationToken) - .request() - .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) - .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30") - .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE)); - assertThat(response.getStatus()).isEqualTo(200); - } - @Test void removeDevice() {