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:
parent
4cea9023f2
commit
001a9310c3
|
@ -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();
|
||||
|
|
|
@ -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() &&
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue