Add support for capabilities

This commit is contained in:
Moxie Marlinspike 2019-09-23 12:01:12 -07:00
parent 62a10047ca
commit c623f70caa
12 changed files with 154 additions and 32 deletions

View File

@ -487,7 +487,7 @@ public class AccountController {
device.setFetchesMessages(attributes.getFetchesMessages());
device.setName(attributes.getName());
device.setLastSeen(Util.todayInMillis());
device.setUnauthenticatedDeliverySupported(attributes.getUnidentifiedAccessKey() != null);
device.setCapabilities(attributes.getCapabilities());
device.setRegistrationId(attributes.getRegistrationId());
device.setSignalingKey(attributes.getSignalingKey());
device.setUserAgent(userAgent);
@ -599,7 +599,7 @@ public class AccountController {
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setName(accountAttributes.getName());
device.setUnauthenticatedDeliverySupported(accountAttributes.getUnidentifiedAccessKey() != null);
device.setCapabilities(accountAttributes.getCapabilities());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(userAgent);

View File

@ -33,6 +33,7 @@ import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.Util;
@ -221,7 +222,15 @@ public class DeviceController {
@Path("/unauthenticated_delivery")
public void setUnauthenticatedDelivery(@Auth Account account) {
assert(account.getAuthenticatedDevice().isPresent());
account.getAuthenticatedDevice().get().setUnauthenticatedDeliverySupported(true);
// Deprecated
}
@Timed
@PUT
@Path("/capabilities")
public void setCapabiltities(@Auth Account account, @Valid DeviceCapabilities capabilities) {
assert(account.getAuthenticatedDevice().isPresent());
account.getAuthenticatedDevice().get().setCapabilities(capabilities);
accounts.update(account);
}

View File

@ -17,6 +17,7 @@ import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
@ -102,8 +103,9 @@ public class ProfileController {
return new Profile(accountProfile.get().getProfileName(),
accountProfile.get().getAvatar(),
accountProfile.get().getIdentityKey(),
accountProfile.get().isUnauthenticatedDeliverySupported() ? UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()) : null,
accountProfile.get().isUnrestrictedUnidentifiedAccess());
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
new UserCapabilities(accountProfile.get().isUuidAddressingSupported()));
}
@Timed

View File

@ -19,6 +19,8 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.Length;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
public class AccountAttributes {
@ -47,6 +49,9 @@ public class AccountAttributes {
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private DeviceCapabilities capabilities;
public AccountAttributes() {}
@VisibleForTesting
@ -95,4 +100,8 @@ public class AccountAttributes {
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
public DeviceCapabilities getCapabilities() {
return capabilities;
}
}

View File

@ -3,10 +3,6 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Max;
public class Profile {
@JsonProperty
@ -24,14 +20,21 @@ public class Profile {
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private UserCapabilities capabilities;
public Profile() {}
public Profile(String name, String avatar, String identityKey, String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess) {
public Profile(String name, String avatar, String identityKey,
String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess,
UserCapabilities capabilities)
{
this.name = name;
this.avatar = avatar;
this.identityKey = identityKey;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = capabilities;
}
@VisibleForTesting
@ -59,4 +62,9 @@ public class Profile {
return unrestrictedUnidentifiedAccess;
}
@VisibleForTesting
public UserCapabilities getCapabilities() {
return capabilities;
}
}

View File

@ -0,0 +1,18 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class UserCapabilities {
@JsonProperty
private boolean uuid;
public UserCapabilities() {}
public UserCapabilities(boolean uuid) {
this.uuid = uuid;
}
public boolean isUuid() {
return uuid;
}
}

View File

@ -20,7 +20,6 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
import javax.security.auth.Subject;
@ -33,8 +32,6 @@ import java.util.concurrent.TimeUnit;
public class Account implements Principal {
static final int MEMCACHE_VERION = 5;
@JsonIgnore
private UUID uuid;
@ -114,7 +111,7 @@ public class Account implements Principal {
}
public void removeDevice(long deviceId) {
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", false, 0));
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", 0, null));
}
public Set<Device> getDevices() {
@ -135,8 +132,8 @@ public class Account implements Principal {
return Optional.empty();
}
public boolean isUnauthenticatedDeliverySupported() {
return devices.stream().filter(Device::isEnabled).allMatch(Device::isUnauthenticatedDeliverySupported);
public boolean isUuidAddressingSupported() {
return devices.stream().filter(Device::isEnabled).allMatch(device -> device.getCapabilities().isUuid());
}
public boolean isEnabled() {

View File

@ -19,6 +19,7 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.Util;
@ -77,7 +78,7 @@ public class Device {
private String userAgent;
@JsonProperty
private boolean unauthenticatedDelivery;
private DeviceCapabilities capabilities;
public Device() {}
@ -86,7 +87,7 @@ public class Device {
String voipApnId, boolean fetchesMessages,
int registrationId, SignedPreKey signedPreKey,
long lastSeen, long created, String userAgent,
boolean unauthenticatedDelivery, long uninstalledFeedback)
long uninstalledFeedback, DeviceCapabilities capabilities)
{
this.id = id;
this.name = name;
@ -102,8 +103,8 @@ public class Device {
this.lastSeen = lastSeen;
this.created = created;
this.userAgent = userAgent;
this.unauthenticatedDelivery = unauthenticatedDelivery;
this.uninstalledFeedback = uninstalledFeedback;
this.capabilities = capabilities;
}
public String getApnId() {
@ -178,14 +179,6 @@ public class Device {
this.name = name;
}
public boolean isUnauthenticatedDeliverySupported() {
return unauthenticatedDelivery;
}
public void setUnauthenticatedDeliverySupported(boolean unauthenticatedDelivery) {
this.unauthenticatedDelivery = unauthenticatedDelivery;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.authToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();
@ -195,6 +188,14 @@ public class Device {
return new AuthenticationCredentials(authToken, salt);
}
public DeviceCapabilities getCapabilities() {
return capabilities;
}
public void setCapabilities(DeviceCapabilities capabilities) {
this.capabilities = capabilities;
}
public String getSignalingKey() {
return signalingKey;
}
@ -262,4 +263,20 @@ public class Device {
public int hashCode() {
return (int)this.id;
}
public static class DeviceCapabilities {
@JsonProperty
private boolean uuid;
public DeviceCapabilities() {}
public DeviceCapabilities(boolean uuid) {
this.uuid = uuid;
}
public boolean isUuid() {
return uuid;
}
}
}

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", true, 0));
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)));
}};
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", true, 0));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true, 0));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", true, 0));
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)));
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)));
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)));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, singleDeviceList, "1234".getBytes());

View File

@ -63,9 +63,22 @@ public class ProfileControllerTest {
when(profileAccount.getAvatar()).thenReturn("profiles/bang");
when(profileAccount.getAvatarDigest()).thenReturn("buh");
when(profileAccount.isEnabled()).thenReturn(true);
when(profileAccount.isUuidAddressingSupported()).thenReturn(false);
Account capabilitiesAccount = mock(Account.class);
when(capabilitiesAccount.getIdentityKey()).thenReturn("barz");
when(capabilitiesAccount.getProfileName()).thenReturn("bazz");
when(capabilitiesAccount.getAvatar()).thenReturn("profiles/bangz");
when(capabilitiesAccount.getAvatarDigest()).thenReturn("buz");
when(capabilitiesAccount.isEnabled()).thenReturn(true);
when(capabilitiesAccount.isUuidAddressingSupported()).thenReturn(true);
when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount));
when(accountsManager.get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER_TWO)))).thenReturn(Optional.of(profileAccount));
when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount));
when(accountsManager.get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER)))).thenReturn(Optional.of(capabilitiesAccount));
}
@ -80,6 +93,7 @@ public class ProfileControllerTest {
assertThat(profile.getIdentityKey()).isEqualTo("bar");
assertThat(profile.getName()).isEqualTo("baz");
assertThat(profile.getAvatar()).isEqualTo("profiles/bang");
assertThat(profile.getCapabilities().isUuid()).isFalse();
verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER_TWO)));
verify(rateLimiters, times(1)).getProfileLimiter();
@ -107,4 +121,15 @@ public class ProfileControllerTest {
assertThat(response.getStatus()).isEqualTo(401);
}
@Test
public void testProfileCapabilities() throws Exception {
Profile profile= resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_NUMBER)
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(Profile.class);
assertThat(profile.getCapabilities().isUuid()).isTrue();
}
}

View File

@ -23,6 +23,10 @@ public class AccountTest {
private final Device recentSecondaryDevice = mock(Device.class);
private final Device oldSecondaryDevice = mock(Device.class);
private final Device uuidCapableDevice = mock(Device.class);
private final Device uuidIncapableDevice = mock(Device.class);
private final Device uuidIncapableExpiredDevice = mock(Device.class);
@Before
public void setup() {
when(oldMasterDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(366));
@ -44,6 +48,18 @@ public class AccountTest {
when(oldSecondaryDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(366));
when(oldSecondaryDevice.isEnabled()).thenReturn(false);
when(oldSecondaryDevice.getId()).thenReturn(2L);
when(uuidCapableDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(true));
when(uuidCapableDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
when(uuidCapableDevice.isEnabled()).thenReturn(true);
when(uuidIncapableDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(false));
when(uuidIncapableDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
when(uuidIncapableDevice.isEnabled()).thenReturn(true);
when(uuidIncapableExpiredDevice.getCapabilities()).thenReturn(new Device.DeviceCapabilities(false));
when(uuidIncapableExpiredDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31));
when(uuidIncapableExpiredDevice.isEnabled()).thenReturn(false);
}
@Test
@ -80,4 +96,25 @@ public class AccountTest {
assertFalse(oldPrimaryAccount.isEnabled());
}
@Test
public void testCapabilities() {
Account uuidCapable = new Account("+14152222222", UUID.randomUUID(), new HashSet<Device>() {{
add(uuidCapableDevice);
}}, "1234".getBytes());
Account uuidIncapable = new Account("+14152222222", UUID.randomUUID(), new HashSet<Device>() {{
add(uuidCapableDevice);
add(uuidIncapableDevice);
}}, "1234".getBytes());
Account uuidCapableWithExpiredIncapable = new Account("+14152222222", UUID.randomUUID(), new HashSet<Device>() {{
add(uuidCapableDevice);
add(uuidIncapableExpiredDevice);
}}, "1234".getBytes());
assertTrue(uuidCapable.isUuidAddressingSupported());
assertFalse(uuidIncapable.isUuidAddressingSupported());
assertTrue(uuidCapableWithExpiredIncapable.isUuidAddressingSupported());
}
}

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(), random.nextBoolean(), 0);
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()));
}
private Account generateAccount(String number, UUID uuid) {