Support device transfers (SERVER-41, SERVER-42) (#32)

This change introduces a `transfer` device capability and account creation argument in support of the iOS device transfer effort.
This commit is contained in:
Jon Chambers 2020-05-12 12:23:18 -04:00 committed by GitHub
parent 4cea9023f2
commit 001a9310c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 12 deletions

View File

@ -254,6 +254,7 @@ public class AccountController {
public AccountCreationResult verifyAccount(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String userAgent,
@QueryParam("transfer") Optional<Boolean> availableForTransfer,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
@ -296,6 +297,10 @@ public class AccountController {
rateLimiters.getPinLimiter().clear(number);
}
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
throw new WebApplicationException(Response.status(409).build());
}
Account account = createAccount(number, password, userAgent, accountAttributes);
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();

View File

@ -150,6 +150,10 @@ public class Account implements Principal {
return devices.stream().anyMatch(device -> device.getCapabilities() != null && device.getCapabilities().isStorage());
}
public boolean isTransferSupported() {
return getMasterDevice().map(Device::getCapabilities).map(Device.DeviceCapabilities::isTransfer).orElse(false);
}
public boolean isEnabled() {
return
getMasterDevice().isPresent() &&

View File

@ -276,12 +276,16 @@ public class Device {
@JsonProperty
private boolean storage;
@JsonProperty
private boolean transfer;
public DeviceCapabilities() {}
public DeviceCapabilities(boolean uuid, boolean gv2, boolean storage) {
this.uuid = uuid;
this.gv2 = gv2;
this.storage = storage;
public DeviceCapabilities(boolean uuid, boolean gv2, boolean storage, boolean transfer) {
this.uuid = uuid;
this.gv2 = gv2;
this.storage = storage;
this.transfer = transfer;
}
public boolean isUuid() {
@ -295,6 +299,10 @@ public class Device {
public boolean isStorage() {
return storage;
}
public boolean isTransfer() {
return transfer;
}
}
}

View File

@ -39,6 +39,7 @@ 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.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PaymentAddress;
import org.whispersystems.textsecuregcm.storage.PaymentAddressList;
@ -53,14 +54,18 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doThrow;
@ -83,6 +88,7 @@ public class AccountControllerTest {
private static final String SENDER_PREAUTH = "+14157777777";
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 UUID SENDER_REG_LOCK_UUID = UUID.randomUUID();
@ -114,6 +120,7 @@ public class AccountControllerTest {
private Account senderPinAccount = mock(Account.class);
private Account senderRegLockAccount = mock(Account.class);
private Account senderHasStorage = mock(Account.class);
private Account senderTransfer = mock(Account.class);
private RecaptchaClient recaptchaClient = mock(RecaptchaClient.class);
private GCMSender gcmSender = mock(GCMSender.class);
private APNSender apnSender = mock(APNSender.class);
@ -180,6 +187,7 @@ public class AccountControllerTest {
when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis(), null)));
when(pendingAccountsManager.getCodeForNumber(SENDER_PREAUTH)).thenReturn(Optional.of(new StoredVerificationCode("555555", System.currentTimeMillis(), "validchallenge")));
when(pendingAccountsManager.getCodeForNumber(SENDER_HAS_STORAGE)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null)));
when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null)));
when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.get(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount));
@ -188,6 +196,7 @@ public class AccountControllerTest {
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_PREAUTH))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_HAS_STORAGE))).thenReturn(Optional.of(senderHasStorage));
when(accountsManager.get(eq(SENDER_TRANSFER))).thenReturn(Optional.of(senderTransfer));
when(usernamesManager.put(eq(AuthHelper.VALID_UUID), eq("n00bkiller"))).thenReturn(true);
when(usernamesManager.put(eq(AuthHelper.VALID_UUID), eq("takenusername"))).thenReturn(false);
@ -747,6 +756,52 @@ public class AccountControllerTest {
}
}
@Test
public void testVerifyTransferSupported() {
when(senderTransfer.isTransferSupported()).thenReturn(true);
final Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/code/%s", "1234"))
.queryParam("transfer", true)
.request()
.header("Authorization", AuthHelper.getAuthHeader(SENDER_TRANSFER, "bar"))
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(409);
}
@Test
public void testVerifyTransferNotSupported() {
when(senderTransfer.isTransferSupported()).thenReturn(false);
final Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/code/%s", "1234"))
.queryParam("transfer", true)
.request()
.header("Authorization", AuthHelper.getAuthHeader(SENDER_TRANSFER, "bar"))
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
public void testVerifyTransferSupportedNotRequested() {
when(senderTransfer.isTransferSupported()).thenReturn(true);
final Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/code/%s", "1234"))
.request()
.header("Authorization", AuthHelper.getAuthHeader(SENDER_TRANSFER, "bar"))
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222, null),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
public void testSetPin() throws Exception {

View File

@ -85,13 +85,13 @@ public class MessageControllerTest {
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, true, true)));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, true, true, true)));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, true, true)));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, true, true)));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(false, false, false)));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, true, true, false)));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, true, true, false)));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(false, false, false, false)));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, singleDeviceList, "1234".getBytes());

View File

@ -5,6 +5,7 @@ import org.junit.Test;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.UUID;
@ -49,15 +50,15 @@ public class AccountTest {
when(oldSecondaryDevice.isEnabled()).thenReturn(false);
when(oldSecondaryDevice.getId()).thenReturn(2L);
when(uuidCapableDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(true, true, true));
when(uuidCapableDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(true, true, true, true));
when(uuidCapableDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
when(uuidCapableDevice.isEnabled()).thenReturn(true);
when(uuidIncapableDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(false, false, false));
when(uuidIncapableDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(false, false, false, false));
when(uuidIncapableDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
when(uuidIncapableDevice.isEnabled()).thenReturn(true);
when(uuidIncapableExpiredDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(false, false, false));
when(uuidIncapableExpiredDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(false, false, false, false));
when(uuidIncapableExpiredDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31));
when(uuidIncapableExpiredDevice.isEnabled()).thenReturn(false);
}
@ -117,4 +118,52 @@ public class AccountTest {
assertTrue(uuidCapableWithExpiredIncapable.isUuidAddressingSupported());
}
@Test
public void testIsTransferSupported() {
final Device transferCapableMasterDevice = mock(Device.class);
final Device nonTransferCapableMasterDevice = mock(Device.class);
final Device transferCapableLinkedDevice = mock(Device.class);
final Device.DeviceCapabilities transferCapabilities = mock(Device.DeviceCapabilities.class);
final Device.DeviceCapabilities nonTransferCapabilities = mock(Device.DeviceCapabilities.class);
when(transferCapableMasterDevice.getId()).thenReturn(1L);
when(transferCapableMasterDevice.isMaster()).thenReturn(true);
when(transferCapableMasterDevice.getCapabilities()).thenReturn(transferCapabilities);
when(nonTransferCapableMasterDevice.getId()).thenReturn(1L);
when(nonTransferCapableMasterDevice.isMaster()).thenReturn(true);
when(nonTransferCapableMasterDevice.getCapabilities()).thenReturn(nonTransferCapabilities);
when(transferCapableLinkedDevice.getId()).thenReturn(2L);
when(transferCapableLinkedDevice.isMaster()).thenReturn(false);
when(transferCapableLinkedDevice.getCapabilities()).thenReturn(transferCapabilities);
when(transferCapabilities.isTransfer()).thenReturn(true);
when(nonTransferCapabilities.isTransfer()).thenReturn(false);
{
final Account transferableMasterAccount =
new Account("+14152222222", UUID.randomUUID(), Collections.singleton(transferCapableMasterDevice), "1234".getBytes());
assertTrue(transferableMasterAccount.isTransferSupported());
}
{
final Account nonTransferableMasterAccount =
new Account("+14152222222", UUID.randomUUID(), Collections.singleton(nonTransferCapableMasterDevice), "1234".getBytes());
assertFalse(nonTransferableMasterAccount.isTransferSupported());
}
{
final Account transferableLinkedAccount = new Account("+14152222222", UUID.randomUUID(), new HashSet<>() {{
add(nonTransferCapableMasterDevice);
add(transferCapableLinkedDevice);
}}, "1234".getBytes());
assertFalse(transferableLinkedAccount.isTransferSupported());
}
}
}

View File

@ -270,7 +270,7 @@ public class AccountsTest {
private Device generateDevice(long id) {
Random random = new Random(System.currentTimeMillis());
SignedPreKey signedPreKey = new SignedPreKey(random.nextInt(), "testPublicKey-" + random.nextInt(), "testSignature-" + random.nextInt());
return new Device(id, "testName-" + random.nextInt(), "testAuthToken-" + random.nextInt(), "testSalt-" + random.nextInt(), null, "testGcmId-" + random.nextInt(), "testApnId-" + random.nextInt(), "testVoipApnId-" + random.nextInt(), random.nextBoolean(), random.nextInt(), signedPreKey, random.nextInt(), random.nextInt(), "testUserAgent-" + random.nextInt() , 0, new Device.DeviceCapabilities(random.nextBoolean(), random.nextBoolean(), random.nextBoolean()));
return new Device(id, "testName-" + random.nextInt(), "testAuthToken-" + random.nextInt(), "testSalt-" + random.nextInt(), null, "testGcmId-" + random.nextInt(), "testApnId-" + random.nextInt(), "testVoipApnId-" + random.nextInt(), random.nextBoolean(), random.nextInt(), signedPreKey, random.nextInt(), random.nextInt(), "testUserAgent-" + random.nextInt() , 0, new Device.DeviceCapabilities(random.nextBoolean(), random.nextBoolean(), random.nextBoolean(), random.nextBoolean()));
}
private Account generateAccount(String number, UUID uuid) {