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, public AccountCreationResult verifyAccount(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader, @HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String userAgent, @HeaderParam("X-Signal-Agent") String userAgent,
@QueryParam("transfer") Optional<Boolean> availableForTransfer,
@Valid AccountAttributes accountAttributes) @Valid AccountAttributes accountAttributes)
throws RateLimitExceededException throws RateLimitExceededException
{ {
@ -296,6 +297,10 @@ public class AccountController {
rateLimiters.getPinLimiter().clear(number); 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); Account account = createAccount(number, password, userAgent, accountAttributes);
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark(); 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()); 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() { public boolean isEnabled() {
return return
getMasterDevice().isPresent() && getMasterDevice().isPresent() &&

View File

@ -276,12 +276,16 @@ public class Device {
@JsonProperty @JsonProperty
private boolean storage; private boolean storage;
@JsonProperty
private boolean transfer;
public DeviceCapabilities() {} public DeviceCapabilities() {}
public DeviceCapabilities(boolean uuid, boolean gv2, boolean storage) { public DeviceCapabilities(boolean uuid, boolean gv2, boolean storage, boolean transfer) {
this.uuid = uuid; this.uuid = uuid;
this.gv2 = gv2; this.gv2 = gv2;
this.storage = storage; this.storage = storage;
this.transfer = transfer;
} }
public boolean isUuid() { public boolean isUuid() {
@ -295,6 +299,10 @@ public class Device {
public boolean isStorage() { public boolean isStorage() {
return storage; 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.AbusiveHostRules;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PaymentAddress; import org.whispersystems.textsecuregcm.storage.PaymentAddress;
import org.whispersystems.textsecuregcm.storage.PaymentAddressList; import org.whispersystems.textsecuregcm.storage.PaymentAddressList;
@ -53,14 +54,18 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; 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.ArgumentMatchers.argThat;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doThrow; 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_PREAUTH = "+14157777777";
private static final String SENDER_REG_LOCK = "+14158888888"; private static final String SENDER_REG_LOCK = "+14158888888";
private static final String SENDER_HAS_STORAGE = "+14159999999"; 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(); 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 senderPinAccount = mock(Account.class);
private Account senderRegLockAccount = mock(Account.class); private Account senderRegLockAccount = mock(Account.class);
private Account senderHasStorage = mock(Account.class); private Account senderHasStorage = mock(Account.class);
private Account senderTransfer = mock(Account.class);
private RecaptchaClient recaptchaClient = mock(RecaptchaClient.class); private RecaptchaClient recaptchaClient = mock(RecaptchaClient.class);
private GCMSender gcmSender = mock(GCMSender.class); private GCMSender gcmSender = mock(GCMSender.class);
private APNSender apnSender = mock(APNSender.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_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_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_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_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.get(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount)); 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_OLD))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_PREAUTH))).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_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("n00bkiller"))).thenReturn(true);
when(usernamesManager.put(eq(AuthHelper.VALID_UUID), eq("takenusername"))).thenReturn(false); 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 @Test
public void testSetPin() throws Exception { public void testSetPin() throws Exception {

View File

@ -85,13 +85,13 @@ public class MessageControllerTest {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{ 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>() {{ 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(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))); 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))); 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()); 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.Account;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -49,15 +50,15 @@ public class AccountTest {
when(oldSecondaryDevice.isEnabled()).thenReturn(false); when(oldSecondaryDevice.isEnabled()).thenReturn(false);
when(oldSecondaryDevice.getId()).thenReturn(2L); 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.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
when(uuidCapableDevice.isEnabled()).thenReturn(true); 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.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
when(uuidIncapableDevice.isEnabled()).thenReturn(true); 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.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31));
when(uuidIncapableExpiredDevice.isEnabled()).thenReturn(false); when(uuidIncapableExpiredDevice.isEnabled()).thenReturn(false);
} }
@ -117,4 +118,52 @@ public class AccountTest {
assertTrue(uuidCapableWithExpiredIncapable.isUuidAddressingSupported()); 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) { private Device generateDevice(long id) {
Random random = new Random(System.currentTimeMillis()); Random random = new Random(System.currentTimeMillis());
SignedPreKey signedPreKey = new SignedPreKey(random.nextInt(), "testPublicKey-" + random.nextInt(), "testSignature-" + random.nextInt()); 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) { private Account generateAccount(String number, UUID uuid) {