Allow primary to set and provide new signed prekeys for linked devices (#950)

This commit is contained in:
gram-signal
2022-04-15 12:39:47 -06:00
committed by GitHub
parent 7b3703506b
commit 473ecbdf2d
11 changed files with 782 additions and 324 deletions

View File

@@ -85,6 +85,7 @@ import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.MessageValidation;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.SystemMapper;
@@ -775,196 +776,4 @@ class MessageControllerTest {
Arguments.of("fixtures/current_message_single_device_server_receipt_type.json", false)
);
}
static Account mockAccountWithDeviceAndRegId(Object... deviceAndRegistrationIds) {
Account account = mock(Account.class);
if (deviceAndRegistrationIds.length % 2 != 0) {
throw new IllegalArgumentException("invalid number of arguments specified; must be even");
}
for (int i = 0; i < deviceAndRegistrationIds.length; i+=2) {
if (!(deviceAndRegistrationIds[i] instanceof Long)) {
throw new IllegalArgumentException("device id is not instance of long at index " + i);
}
if (!(deviceAndRegistrationIds[i + 1] instanceof Integer)) {
throw new IllegalArgumentException("registration id is not instance of integer at index " + (i + 1));
}
Long deviceId = (Long) deviceAndRegistrationIds[i];
Integer registrationId = (Integer) deviceAndRegistrationIds[i + 1];
Device device = mock(Device.class);
when(device.getRegistrationId()).thenReturn(registrationId);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
}
return account;
}
static Collection<Pair<Long, Integer>> deviceAndRegistrationIds(Object... deviceAndRegistrationIds) {
final Collection<Pair<Long, Integer>> result = new HashSet<>(deviceAndRegistrationIds.length);
if (deviceAndRegistrationIds.length % 2 != 0) {
throw new IllegalArgumentException("invalid number of arguments specified; must be even");
}
for (int i = 0; i < deviceAndRegistrationIds.length; i += 2) {
if (!(deviceAndRegistrationIds[i] instanceof Long)) {
throw new IllegalArgumentException("device id is not instance of long at index " + i);
}
if (!(deviceAndRegistrationIds[i + 1] instanceof Integer)) {
throw new IllegalArgumentException("registration id is not instance of integer at index " + (i + 1));
}
Long deviceId = (Long) deviceAndRegistrationIds[i];
Integer registrationId = (Integer) deviceAndRegistrationIds[i + 1];
result.add(new Pair<>(deviceId, registrationId));
}
return result;
}
static Stream<Arguments> validateRegistrationIdsSource() {
return Stream.of(
arguments(
mockAccountWithDeviceAndRegId(1L, 0xFFFF, 2L, 0xDEAD, 3L, 0xBEEF),
deviceAndRegistrationIds(1L, 0xFFFF, 2L, 0xDEAD, 3L, 0xBEEF),
null),
arguments(
mockAccountWithDeviceAndRegId(1L, 42),
deviceAndRegistrationIds(1L, 1492),
Set.of(1L)),
arguments(
mockAccountWithDeviceAndRegId(1L, 42),
deviceAndRegistrationIds(1L, 42),
null),
arguments(
mockAccountWithDeviceAndRegId(1L, 42),
deviceAndRegistrationIds(1L, 0),
null),
arguments(
mockAccountWithDeviceAndRegId(1L, 42, 2L, 255),
deviceAndRegistrationIds(1L, 0, 2L, 42),
Set.of(2L)),
arguments(
mockAccountWithDeviceAndRegId(1L, 42, 2L, 256),
deviceAndRegistrationIds(1L, 41, 2L, 257),
Set.of(1L, 2L))
);
}
@ParameterizedTest
@MethodSource("validateRegistrationIdsSource")
void testValidateRegistrationIds(
Account account,
Collection<Pair<Long, Integer>> deviceAndRegistrationIds,
Set<Long> expectedStaleDeviceIds) throws Exception {
if (expectedStaleDeviceIds != null) {
Assertions.assertThat(assertThrows(StaleDevicesException.class, () -> {
MessageController.validateRegistrationIds(account, deviceAndRegistrationIds.stream());
}).getStaleDevices()).hasSameElementsAs(expectedStaleDeviceIds);
} else {
MessageController.validateRegistrationIds(account, deviceAndRegistrationIds.stream());
}
}
static Account mockAccountWithDeviceAndEnabled(Object... deviceIdAndEnabled) {
Account account = mock(Account.class);
if (deviceIdAndEnabled.length % 2 != 0) {
throw new IllegalArgumentException("invalid number of arguments specified; must be even");
}
final Set<Device> devices = new HashSet<>(deviceIdAndEnabled.length / 2);
for (int i = 0; i < deviceIdAndEnabled.length; i+=2) {
if (!(deviceIdAndEnabled[i] instanceof Long)) {
throw new IllegalArgumentException("device id is not instance of long at index " + i);
}
if (!(deviceIdAndEnabled[i + 1] instanceof Boolean)) {
throw new IllegalArgumentException("enabled is not instance of boolean at index " + (i + 1));
}
Long deviceId = (Long) deviceIdAndEnabled[i];
Boolean enabled = (Boolean) deviceIdAndEnabled[i + 1];
Device device = mock(Device.class);
when(device.isEnabled()).thenReturn(enabled);
when(device.getId()).thenReturn(deviceId);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
devices.add(device);
}
when(account.getDevices()).thenReturn(devices);
return account;
}
static Stream<Arguments> validateCompleteDeviceListSource() {
return Stream.of(
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L, 3L),
null,
null,
false,
null),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L, 2L, 3L),
null,
Set.of(2L),
false,
null),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L),
Set.of(3L),
null,
false,
null),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L, 2L),
Set.of(3L),
Set.of(2L),
false,
null),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L),
Set.of(3L),
Set.of(1L),
true,
1L
),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(2L),
Set.of(3L),
Set.of(2L),
true,
1L
),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(3L),
null,
null,
true,
1L
)
);
}
@ParameterizedTest
@MethodSource("validateCompleteDeviceListSource")
void testValidateCompleteDeviceList(
Account account,
Set<Long> deviceIds,
Collection<Long> expectedMissingDeviceIds,
Collection<Long> expectedExtraDeviceIds,
boolean isSyncMessage,
Long authenticatedDeviceId) throws Exception {
if (expectedMissingDeviceIds != null || expectedExtraDeviceIds != null) {
final MismatchedDevicesException mismatchedDevicesException = assertThrows(MismatchedDevicesException.class,
() -> MessageController.validateCompleteDeviceList(account, deviceIds, isSyncMessage,
Optional.ofNullable(authenticatedDeviceId)));
if (expectedMissingDeviceIds != null) {
Assertions.assertThat(mismatchedDevicesException.getMissingDevices())
.hasSameElementsAs(expectedMissingDeviceIds);
}
if (expectedExtraDeviceIds != null) {
Assertions.assertThat(mismatchedDevicesException.getExtraDevices()).hasSameElementsAs(expectedExtraDeviceIds);
}
} else {
MessageController.validateCompleteDeviceList(account, deviceIds, isSyncMessage,
Optional.ofNullable(authenticatedDeviceId));
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import org.apache.commons.codec.binary.Base64;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.push.MessageSender;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ChangeNumberManagerTest {
private static AccountsManager accountsManager = mock(AccountsManager.class);
private static MessageSender messageSender = mock(MessageSender.class);
private ChangeNumberManager changeNumberManager = new ChangeNumberManager(messageSender, accountsManager);
@BeforeEach
void reset() throws Exception {
Mockito.reset(accountsManager, messageSender);
when(accountsManager.changeNumber(any(), any())).thenAnswer((Answer<Account>) invocation -> {
final Account account = invocation.getArgument(0, Account.class);
final String number = invocation.getArgument(1, String.class);
final UUID uuid = account.getUuid();
final Set<Device> devices = account.getDevices();
final Account updatedAccount = mock(Account.class);
when(updatedAccount.getUuid()).thenReturn(uuid);
when(updatedAccount.getNumber()).thenReturn(number);
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(UUID.randomUUID());
when(updatedAccount.getDevices()).thenReturn(devices);
for (long i = 1; i <= 3; i++) {
final Optional<Device> d = account.getDevice(i);
when(updatedAccount.getDevice(i)).thenReturn(d);
}
return updatedAccount;
});
}
@Test
void changeNumberNoMessages() throws Exception {
Account account = mock(Account.class);
when(account.getNumber()).thenReturn("+18005551234");
changeNumberManager.changeNumber(account, "+18025551234", Collections.EMPTY_MAP, Collections.EMPTY_LIST);
verify(accountsManager).changeNumber(account, "+18025551234");
verify(accountsManager, never()).updateDevice(any(), eq(1L), any());
verify(messageSender, never()).sendMessage(eq(account), any(), any(), eq(false));
}
@Test
void changeNumberSetPrimaryDevicePrekey() throws Exception {
Account account = mock(Account.class);
when(account.getNumber()).thenReturn("+18005551234");
var prekeys = Map.of(1L, new SignedPreKey());
changeNumberManager.changeNumber(account, "+18025551234", prekeys, Collections.EMPTY_LIST);
verify(accountsManager).changeNumber(account, "+18025551234");
verify(accountsManager).updateDevice(any(), eq(1L), any());
verify(messageSender, never()).sendMessage(eq(account), any(), any(), eq(false));
}
@Test
void changeNumberSetPrimaryDevicePrekeyAndSendMessages() throws Exception {
Account account = mock(Account.class);
when(account.getNumber()).thenReturn("+18005551234");
when(account.getUuid()).thenReturn(UUID.randomUUID());
Device d2 = mock(Device.class);
when(account.getDevice(2L)).thenReturn(Optional.of(d2));
var prekeys = Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey());
IncomingMessage msg = mock(IncomingMessage.class);
when(msg.getDestinationDeviceId()).thenReturn(2L);
when(msg.getContent()).thenReturn(Base64.encodeBase64String(new byte[]{1}));
changeNumberManager.changeNumber(account, "+18025551234", prekeys, List.of(msg));
verify(accountsManager).changeNumber(account, "+18025551234");
verify(accountsManager).updateDevice(any(), eq(1L), any());
verify(accountsManager).updateDevice(any(), eq(2L), any());
verify(messageSender).sendMessage(any(), eq(d2), any(), eq(false));
}
}

View File

@@ -33,6 +33,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -69,8 +70,10 @@ import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest;
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.SignedPreKey;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
@@ -88,6 +91,8 @@ import org.whispersystems.textsecuregcm.storage.AbusiveHostRule;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
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.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
@@ -141,6 +146,7 @@ class AccountControllerTest {
private static RecaptchaClient recaptchaClient = mock(RecaptchaClient.class);
private static GCMSender gcmSender = mock(GCMSender.class);
private static APNSender apnSender = mock(APNSender.class);
private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class);
private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
@@ -172,8 +178,9 @@ class AccountControllerTest {
recaptchaClient,
gcmSender,
apnSender,
storageCredentialGenerator,
verifyExperimentEnrollmentManager))
verifyExperimentEnrollmentManager,
changeNumberManager,
storageCredentialGenerator))
.build();
@@ -243,16 +250,22 @@ class AccountControllerTest {
when(accountsManager.setUsername(AuthHelper.VALID_ACCOUNT, "takenusername"))
.thenThrow(new UsernameNotAvailableException());
when(accountsManager.changeNumber(any(), any())).thenAnswer((Answer<Account>) invocation -> {
when(changeNumberManager.changeNumber(any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
final Account account = invocation.getArgument(0, Account.class);
final String number = invocation.getArgument(1, String.class);
final UUID uuid = account.getUuid();
final Set<Device> devices = account.getDevices();
final Account updatedAccount = mock(Account.class);
when(updatedAccount.getUuid()).thenReturn(uuid);
when(updatedAccount.getNumber()).thenReturn(number);
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(UUID.randomUUID());
when(updatedAccount.getDevices()).thenReturn(devices);
for (long i = 1; i <= 3; i++) {
final Optional<Device> d = account.getDevice(i);
when(updatedAccount.getDevice(i)).thenReturn(d);
}
return updatedAccount;
});
@@ -305,7 +318,8 @@ class AccountControllerTest {
recaptchaClient,
gcmSender,
apnSender,
verifyExperimentEnrollmentManager);
verifyExperimentEnrollmentManager,
changeNumberManager);
clearInvocations(AuthHelper.DISABLED_DEVICE);
}
@@ -1221,7 +1235,7 @@ class AccountControllerTest {
}
@Test
void testChangePhoneNumber() throws InterruptedException {
void testChangePhoneNumber() throws Exception {
final String number = "+18005559876";
final String code = "987654";
@@ -1233,10 +1247,10 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(accountsManager).changeNumber(AuthHelper.VALID_ACCOUNT, number);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any(), any());
assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(accountIdentityResponse.getNumber()).isEqualTo(number);
@@ -1244,7 +1258,7 @@ class AccountControllerTest {
}
@Test
void testChangePhoneNumberImpossibleNumber() throws InterruptedException {
void testChangePhoneNumberImpossibleNumber() throws Exception {
final String number = "This is not a real phone number";
final String code = "987654";
@@ -1253,16 +1267,16 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.readEntity(String.class)).isBlank();
verify(accountsManager, never()).changeNumber(any(), any());
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any());
}
@Test
void testChangePhoneNumberNonNormalized() throws InterruptedException {
void testChangePhoneNumberNonNormalized() throws Exception {
final String number = "+4407700900111";
final String code = "987654";
@@ -1271,7 +1285,7 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(400);
@@ -1280,28 +1294,24 @@ class AccountControllerTest {
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
verify(accountsManager, never()).changeNumber(any(), any());
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any());
}
@Test
void testChangePhoneNumberSameNumber() throws InterruptedException {
void testChangePhoneNumberSameNumber() throws Exception {
final AccountIdentityResponse accountIdentityResponse =
resources.getJerseyTest()
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(AuthHelper.VALID_NUMBER, "567890", null),
.put(Entity.entity(new ChangePhoneNumberRequest(AuthHelper.VALID_NUMBER, "567890", null, null, null),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(accountsManager, never()).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any());
assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(accountIdentityResponse.getNumber()).isEqualTo(AuthHelper.VALID_NUMBER);
assertThat(accountIdentityResponse.getPni()).isEqualTo(AuthHelper.VALID_PNI);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any());
}
@Test
void testChangePhoneNumberNoPendingCode() throws InterruptedException {
void testChangePhoneNumberNoPendingCode() throws Exception {
final String number = "+18005559876";
final String code = "987654";
@@ -1312,15 +1322,15 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(403);
verify(accountsManager, never()).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any());
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any());
}
@Test
void testChangePhoneNumberIncorrectCode() throws InterruptedException {
void testChangePhoneNumberIncorrectCode() throws Exception {
final String number = "+18005559876";
final String code = "987654";
@@ -1332,15 +1342,15 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code + "-incorrect", null),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code + "-incorrect", null, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(403);
verify(accountsManager, never()).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any());
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any());
}
@Test
void testChangePhoneNumberExistingAccountReglockNotRequired() throws InterruptedException {
void testChangePhoneNumberExistingAccountReglockNotRequired() throws Exception {
final String number = "+18005559876";
final String code = "987654";
@@ -1362,15 +1372,15 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200);
verify(accountsManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any());
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any());
}
@Test
void testChangePhoneNumberExistingAccountReglockRequiredNotProvided() throws InterruptedException {
void testChangePhoneNumberExistingAccountReglockRequiredNotProvided() throws Exception {
final String number = "+18005559876";
final String code = "987654";
@@ -1392,15 +1402,15 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(423);
verify(accountsManager, never()).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any());
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any());
}
@Test
void testChangePhoneNumberExistingAccountReglockRequiredIncorrect() throws InterruptedException {
void testChangePhoneNumberExistingAccountReglockRequiredIncorrect() throws Exception {
final String number = "+18005559876";
final String code = "987654";
final String reglock = "setec-astronomy";
@@ -1424,15 +1434,15 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(423);
verify(accountsManager, never()).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any());
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any());
}
@Test
void testChangePhoneNumberExistingAccountReglockRequiredCorrect() throws InterruptedException {
void testChangePhoneNumberExistingAccountReglockRequiredCorrect() throws Exception {
final String number = "+18005559876";
final String code = "987654";
final String reglock = "setec-astronomy";
@@ -1456,11 +1466,142 @@ class AccountControllerTest {
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock),
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, reglock, null, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200);
verify(accountsManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any());
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any());
}
@Test
void testChangePhoneNumberDeviceMessagesWithoutPrekeys() throws Exception {
final String number = "+18005559876";
final String code = "987654";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
final Response response =
resources.getJerseyTest()
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(number, code, null,
List.of(new IncomingMessage(1, null, 1, 1, "foo")), null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(400);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any());
}
@Test
void testChangePhoneNumberChangePrekeysDeviceMessagesMismatchDeviceIDs() throws Exception {
final String number = "+18005559876";
final String code = "987654";
Device device2 = mock(Device.class);
when(device2.getId()).thenReturn(2L);
when(device2.isEnabled()).thenReturn(true);
Device device3 = mock(Device.class);
when(device3.getId()).thenReturn(3L);
when(device3.isEnabled()).thenReturn(true);
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(Set.of(AuthHelper.VALID_DEVICE, device2, device3));
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
final Response response =
resources.getJerseyTest()
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(
number, code, null,
List.of(
new IncomingMessage(1, null, 2, 1, "foo"),
new IncomingMessage(1, null, 4, 1, "foo")),
Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey())),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(409);
verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any());
}
@Test
void testChangePhoneNumberChangePrekeys() throws Exception {
final String number = "+18005559876";
final String code = "987654";
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(Set.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(code, System.currentTimeMillis(), "push", null)));
var deviceMessages = List.of(
new IncomingMessage(1, null, 2, 2, "content2"),
new IncomingMessage(1, null, 3, 3, "content3"));
var deviceKeys = Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey());
final AccountIdentityResponse accountIdentityResponse =
resources.getJerseyTest()
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(
number, code, null,
deviceMessages,
deviceKeys),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any(), any());
assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(accountIdentityResponse.getNumber()).isEqualTo(number);
assertThat(accountIdentityResponse.getPni()).isNotEqualTo(AuthHelper.VALID_PNI);
}
@Test
void testChangePhoneNumberChangePrekeysDeviceMessagesMismatchRegistrationID() throws Exception {
final String number = "+18005559876";
final String code = "987654";
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(Set.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(code, System.currentTimeMillis(), "push", null)));
final Response response =
resources.getJerseyTest()
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(
number, code, null,
List.of(
new IncomingMessage(1, null, 2, 1, "foo"),
new IncomingMessage(1, null, 3, 1, "foo")),
Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey())),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(410);
verify(accountsManager, never()).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any());
}
@Test

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.util;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.assertj.core.api.Assertions;
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.MethodSource;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.controllers.StaleDevicesException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.MessageValidation;
import org.whispersystems.textsecuregcm.util.Pair;
@ExtendWith(DropwizardExtensionsSupport.class)
class MessageValidationTest {
static Account mockAccountWithDeviceAndRegId(Object... deviceAndRegistrationIds) {
Account account = mock(Account.class);
if (deviceAndRegistrationIds.length % 2 != 0) {
throw new IllegalArgumentException("invalid number of arguments specified; must be even");
}
for (int i = 0; i < deviceAndRegistrationIds.length; i+=2) {
if (!(deviceAndRegistrationIds[i] instanceof Long)) {
throw new IllegalArgumentException("device id is not instance of long at index " + i);
}
if (!(deviceAndRegistrationIds[i + 1] instanceof Integer)) {
throw new IllegalArgumentException("registration id is not instance of integer at index " + (i + 1));
}
Long deviceId = (Long) deviceAndRegistrationIds[i];
Integer registrationId = (Integer) deviceAndRegistrationIds[i + 1];
Device device = mock(Device.class);
when(device.getRegistrationId()).thenReturn(registrationId);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
}
return account;
}
static Collection<Pair<Long, Integer>> deviceAndRegistrationIds(Object... deviceAndRegistrationIds) {
final Collection<Pair<Long, Integer>> result = new HashSet<>(deviceAndRegistrationIds.length);
if (deviceAndRegistrationIds.length % 2 != 0) {
throw new IllegalArgumentException("invalid number of arguments specified; must be even");
}
for (int i = 0; i < deviceAndRegistrationIds.length; i += 2) {
if (!(deviceAndRegistrationIds[i] instanceof Long)) {
throw new IllegalArgumentException("device id is not instance of long at index " + i);
}
if (!(deviceAndRegistrationIds[i + 1] instanceof Integer)) {
throw new IllegalArgumentException("registration id is not instance of integer at index " + (i + 1));
}
Long deviceId = (Long) deviceAndRegistrationIds[i];
Integer registrationId = (Integer) deviceAndRegistrationIds[i + 1];
result.add(new Pair<>(deviceId, registrationId));
}
return result;
}
static Stream<Arguments> validateRegistrationIdsSource() {
return Stream.of(
arguments(
mockAccountWithDeviceAndRegId(1L, 0xFFFF, 2L, 0xDEAD, 3L, 0xBEEF),
deviceAndRegistrationIds(1L, 0xFFFF, 2L, 0xDEAD, 3L, 0xBEEF),
null),
arguments(
mockAccountWithDeviceAndRegId(1L, 42),
deviceAndRegistrationIds(1L, 1492),
Set.of(1L)),
arguments(
mockAccountWithDeviceAndRegId(1L, 42),
deviceAndRegistrationIds(1L, 42),
null),
arguments(
mockAccountWithDeviceAndRegId(1L, 42),
deviceAndRegistrationIds(1L, 0),
null),
arguments(
mockAccountWithDeviceAndRegId(1L, 42, 2L, 255),
deviceAndRegistrationIds(1L, 0, 2L, 42),
Set.of(2L)),
arguments(
mockAccountWithDeviceAndRegId(1L, 42, 2L, 256),
deviceAndRegistrationIds(1L, 41, 2L, 257),
Set.of(1L, 2L))
);
}
@ParameterizedTest
@MethodSource("validateRegistrationIdsSource")
void testValidateRegistrationIds(
Account account,
Collection<Pair<Long, Integer>> deviceAndRegistrationIds,
Set<Long> expectedStaleDeviceIds) throws Exception {
if (expectedStaleDeviceIds != null) {
Assertions.assertThat(assertThrows(StaleDevicesException.class, () -> {
MessageValidation.validateRegistrationIds(account, deviceAndRegistrationIds.stream());
}).getStaleDevices()).hasSameElementsAs(expectedStaleDeviceIds);
} else {
MessageValidation.validateRegistrationIds(account, deviceAndRegistrationIds.stream());
}
}
static Account mockAccountWithDeviceAndEnabled(Object... deviceIdAndEnabled) {
Account account = mock(Account.class);
if (deviceIdAndEnabled.length % 2 != 0) {
throw new IllegalArgumentException("invalid number of arguments specified; must be even");
}
final Set<Device> devices = new HashSet<>(deviceIdAndEnabled.length / 2);
for (int i = 0; i < deviceIdAndEnabled.length; i+=2) {
if (!(deviceIdAndEnabled[i] instanceof Long)) {
throw new IllegalArgumentException("device id is not instance of long at index " + i);
}
if (!(deviceIdAndEnabled[i + 1] instanceof Boolean)) {
throw new IllegalArgumentException("enabled is not instance of boolean at index " + (i + 1));
}
Long deviceId = (Long) deviceIdAndEnabled[i];
Boolean enabled = (Boolean) deviceIdAndEnabled[i + 1];
Device device = mock(Device.class);
when(device.isEnabled()).thenReturn(enabled);
when(device.getId()).thenReturn(deviceId);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
devices.add(device);
}
when(account.getDevices()).thenReturn(devices);
return account;
}
static Stream<Arguments> validateCompleteDeviceListSource() {
return Stream.of(
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L, 3L),
null,
null,
false,
null),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L, 2L, 3L),
null,
Set.of(2L),
false,
null),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L),
Set.of(3L),
null,
false,
null),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L, 2L),
Set.of(3L),
Set.of(2L),
false,
null),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(1L),
Set.of(3L),
Set.of(1L),
true,
1L
),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(2L),
Set.of(3L),
Set.of(2L),
true,
1L
),
arguments(
mockAccountWithDeviceAndEnabled(1L, true, 2L, false, 3L, true),
Set.of(3L),
null,
null,
true,
1L
)
);
}
@ParameterizedTest
@MethodSource("validateCompleteDeviceListSource")
void testValidateCompleteDeviceList(
Account account,
Set<Long> deviceIds,
Collection<Long> expectedMissingDeviceIds,
Collection<Long> expectedExtraDeviceIds,
boolean isSyncMessage,
Long authenticatedDeviceId) throws Exception {
if (expectedMissingDeviceIds != null || expectedExtraDeviceIds != null) {
final MismatchedDevicesException mismatchedDevicesException = assertThrows(MismatchedDevicesException.class,
() -> MessageValidation.validateCompleteDeviceList(account, deviceIds, isSyncMessage,
Optional.ofNullable(authenticatedDeviceId)));
if (expectedMissingDeviceIds != null) {
Assertions.assertThat(mismatchedDevicesException.getMissingDevices())
.hasSameElementsAs(expectedMissingDeviceIds);
}
if (expectedExtraDeviceIds != null) {
Assertions.assertThat(mismatchedDevicesException.getExtraDevices()).hasSameElementsAs(expectedExtraDeviceIds);
}
} else {
MessageValidation.validateCompleteDeviceList(account, deviceIds, isSyncMessage,
Optional.ofNullable(authenticatedDeviceId));
}
}
}