diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java index a0cd2358d..8b42f8efb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -234,13 +234,9 @@ public class DeviceController { private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities, String userAgent) { boolean isDowngrade = false; - if (account.isSenderKeySupported() && !capabilities.isSenderKey()) { - isDowngrade = true; - } - - if (account.isGv1MigrationSupported() && !capabilities.isGv1Migration()) { - isDowngrade = true; - } + isDowngrade |= account.isAnnouncementGroupSupported() && !capabilities.isAnnouncementGroup(); + isDowngrade |= account.isSenderKeySupported() && !capabilities.isSenderKey(); + isDowngrade |= account.isGv1MigrationSupported() && !capabilities.isGv1Migration(); if (account.isGroupsV2Supported()) { try { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserCapabilities.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserCapabilities.java index cce35e8ac..4431d856e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserCapabilities.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserCapabilities.java @@ -14,7 +14,8 @@ public class UserCapabilities { return new UserCapabilities( account.isGroupsV2Supported(), account.isGv1MigrationSupported(), - account.isSenderKeySupported()); + account.isSenderKeySupported(), + account.isAnnouncementGroupSupported()); } @JsonProperty @@ -26,12 +27,16 @@ public class UserCapabilities { @JsonProperty private boolean senderKey; + @JsonProperty + private boolean announcementGroup; + public UserCapabilities() {} - public UserCapabilities(boolean gv2, boolean gv1Migration, final boolean senderKey) { + public UserCapabilities(boolean gv2, boolean gv1Migration, final boolean senderKey, final boolean announcementGroup) { this.gv2 = gv2; this.gv1Migration = gv1Migration; this.senderKey = senderKey; + this.announcementGroup = announcementGroup; } public boolean isGv2() { @@ -45,4 +50,8 @@ public class UserCapabilities { public boolean isSenderKey() { return senderKey; } + + public boolean isAnnouncementGroup() { + return announcementGroup; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 3dbb3c3d5..fdebdf5ad 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -151,6 +151,12 @@ public class Account implements Principal { .allMatch(device -> device.getCapabilities() != null && device.getCapabilities().isSenderKey()); } + public boolean isAnnouncementGroupSupported() { + return devices.stream() + .filter(Device::isEnabled) + .allMatch(device -> device.getCapabilities() != null && device.getCapabilities().isAnnouncementGroup()); + } + public boolean isEnabled() { return getMasterDevice().map(Device::isEnabled).orElse(false); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java index 0ef120634..1e63ee40f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java @@ -276,10 +276,13 @@ public class Device { @JsonProperty private boolean senderKey; + @JsonProperty + private boolean announcementGroup; + public DeviceCapabilities() {} public DeviceCapabilities(boolean gv2, final boolean gv2_2, final boolean gv2_3, boolean storage, boolean transfer, - boolean gv1Migration, final boolean senderKey) { + boolean gv1Migration, final boolean senderKey, final boolean announcementGroup) { this.gv2 = gv2; this.gv2_2 = gv2_2; this.gv2_3 = gv2_3; @@ -287,6 +290,7 @@ public class Device { this.transfer = transfer; this.gv1Migration = gv1Migration; this.senderKey = senderKey; + this.announcementGroup = announcementGroup; } public boolean isGv2() { @@ -316,5 +320,9 @@ public class Device { public boolean isSenderKey() { return senderKey; } + + public boolean isAnnouncementGroup() { + return announcementGroup; + } } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsDynamoDbTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsDynamoDbTest.java index 42e2fb8a3..6780e42ab 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsDynamoDbTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsDynamoDbTest.java @@ -423,7 +423,7 @@ class AccountsDynamoDbTest { 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(), "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(), random.nextBoolean(), random.nextBoolean(), - false)); + false, false)); } private Account generateAccount(String number, UUID uuid) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/DeviceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/DeviceTest.java index 4ba27cb09..2a123d0d9 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/DeviceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/DeviceTest.java @@ -70,7 +70,7 @@ public class DeviceTest { @Parameters(method = "argumentsForTestIsGroupsV2Supported") public void testIsGroupsV2Supported(final boolean master, final String apnId, final boolean gv2Capability, final boolean gv2_2Capability, final boolean gv2_3Capability, final boolean expectGv2Supported) { final Device.DeviceCapabilities capabilities = new Device.DeviceCapabilities(gv2Capability, gv2_2Capability, gv2_3Capability, false, false, false, - false); + false, false); final Device device = new Device(master ? 1 : 2, "test", "auth-token", "salt", null, apnId, null, false, 1, null, 0, 0, "user-agent", 0, capabilities); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java index af871d26c..ba2afd294 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -116,6 +116,7 @@ public class DeviceControllerTest { when(account.isGroupsV2Supported()).thenReturn(true); when(account.isGv1MigrationSupported()).thenReturn(true); when(account.isSenderKeySupported()).thenReturn(true); + when(account.isAnnouncementGroupSupported()).thenReturn(true); when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis(), null))); when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(new StoredVerificationCode("1112223", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31), null))); @@ -223,7 +224,7 @@ public class DeviceControllerTest { @Test @Parameters(method = "argumentsForDeviceDowngradeCapabilitiesTest") public void deviceDowngradeCapabilitiesTest(final String userAgent, final boolean gv2, final boolean gv2_2, final boolean gv2_3, final int expectedStatus) throws Exception { - DeviceCapabilities deviceCapabilities = new DeviceCapabilities(gv2, gv2_2, gv2_3, true, false, true, true); + DeviceCapabilities deviceCapabilities = new DeviceCapabilities(gv2, gv2_2, gv2_3, true, false, true, true, true); AccountAttributes accountAttributes = new AccountAttributes(false, 1234, null, null, null, true, deviceCapabilities); Response response = resources.getJerseyTest() .target("/v1/devices/5678901") @@ -263,7 +264,7 @@ public class DeviceControllerTest { @Test public void deviceDowngradeGv1MigrationTest() { - DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, false, false, true); + DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, false, false, true, true); AccountAttributes accountAttributes = new AccountAttributes(false, 1234, null, null, null, true, deviceCapabilities); Response response = resources.getJerseyTest() .target("/v1/devices/5678901") @@ -274,7 +275,7 @@ public class DeviceControllerTest { assertThat(response.getStatus()).isEqualTo(409); - deviceCapabilities = new DeviceCapabilities(true, true, true, true, false, true, true); + deviceCapabilities = new DeviceCapabilities(true, true, true, true, false, true, true, true); accountAttributes = new AccountAttributes(false, 1234, null, null, null, true, deviceCapabilities); response = resources.getJerseyTest() .target("/v1/devices/5678901") @@ -288,7 +289,7 @@ public class DeviceControllerTest { @Test public void deviceDowngradeSenderKeyTest() { - DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, false); + DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, false, true); AccountAttributes accountAttributes = new AccountAttributes(false, 1234, null, null, null, true, deviceCapabilities); Response response = resources @@ -300,7 +301,33 @@ public class DeviceControllerTest { .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(409); - deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true); + deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true); + accountAttributes = new AccountAttributes(false, 1234, null, null, null, true, deviceCapabilities); + response = resources + .getJerseyTest() + .target("/v1/devices/5678901") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .header("User-Agent", "Signal-Android/5.42.8675309 Android/30") + .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void deviceDowngradeAnnouncementGroupTest() { + DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, false); + AccountAttributes accountAttributes = + new AccountAttributes(false, 1234, null, null, null, true, deviceCapabilities); + Response response = resources + .getJerseyTest() + .target("/v1/devices/5678901") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .header("User-Agent", "Signal-Android/5.42.8675309 Android/30") + .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(409); + + deviceCapabilities = new DeviceCapabilities(true, true, true, true, true, true, true, true); accountAttributes = new AccountAttributes(false, 1234, null, null, null, true, deviceCapabilities); response = resources .getJerseyTest() diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java index 71b336f10..b0933f429 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java @@ -157,19 +157,19 @@ class MessageControllerTest { Set singleDeviceList = new HashSet() {{ add(new Device(1, null, "foo", "bar", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, false, false, true, true, false, - false))); + false, false))); }}; Set multiDeviceList = new HashSet() {{ add(new Device(1, null, "foo", "bar", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, false, false, true, false, false, - false))); + false, false))); add(new Device(2, null, "foo", "bar", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(true, false, false, true, false, false, - false))); + false, false))); add(new Device(3, null, "foo", "bar", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", 0, new Device.DeviceCapabilities(false, false, false, false, false, false, - false))); + false, false))); }}; Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, singleDeviceList, "1234".getBytes()); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java index a9217bec9..34898c3d1 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java @@ -125,6 +125,7 @@ public class ProfileControllerTest { when(profileAccount.isGroupsV2Supported()).thenReturn(false); when(profileAccount.isGv1MigrationSupported()).thenReturn(false); when(profileAccount.isSenderKeySupported()).thenReturn(false); + when(profileAccount.isAnnouncementGroupSupported()).thenReturn(false); when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.empty()); Account capabilitiesAccount = mock(Account.class); @@ -136,6 +137,7 @@ public class ProfileControllerTest { when(capabilitiesAccount.isGroupsV2Supported()).thenReturn(true); when(capabilitiesAccount.isGv1MigrationSupported()).thenReturn(true); when(capabilitiesAccount.isSenderKeySupported()).thenReturn(true); + when(capabilitiesAccount.isAnnouncementGroupSupported()).thenReturn(true); when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount)); when(accountsManager.get(AuthHelper.VALID_UUID_TWO)).thenReturn(Optional.of(profileAccount)); @@ -274,6 +276,7 @@ public class ProfileControllerTest { assertThat(profile.getCapabilities().isGv2()).isTrue(); assertThat(profile.getCapabilities().isGv1Migration()).isTrue(); assertThat(profile.getCapabilities().isSenderKey()).isTrue(); + assertThat(profile.getCapabilities().isAnnouncementGroup()).isTrue(); profile = resources .getJerseyTest() @@ -285,6 +288,7 @@ public class ProfileControllerTest { assertThat(profile.getCapabilities().isGv2()).isFalse(); assertThat(profile.getCapabilities().isGv1Migration()).isFalse(); assertThat(profile.getCapabilities().isSenderKey()).isFalse(); + assertThat(profile.getCapabilities().isAnnouncementGroup()).isFalse(); } @Test diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountTest.java index ebdc3659e..e3601f4a2 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountTest.java @@ -43,6 +43,10 @@ public class AccountTest { private final Device senderKeyIncapableDevice = mock(Device.class); private final Device senderKeyIncapableExpiredDevice = mock(Device.class); + private final Device announcementGroupCapableDevice = mock(Device.class); + private final Device announcementGroupIncapableDevice = mock(Device.class); + private final Device announcementGroupIncapableExpiredDevice = mock(Device.class); + @Before public void setup() { when(oldMasterDevice.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(366)); @@ -78,28 +82,40 @@ public class AccountTest { when(gv2IncapableExpiredDevice.isEnabled()).thenReturn(false); when(gv1MigrationCapableDevice.getCapabilities()).thenReturn( - new DeviceCapabilities(true, true, true, true, true, true, false)); + new DeviceCapabilities(true, true, true, true, true, true, false, false)); when(gv1MigrationCapableDevice.isEnabled()).thenReturn(true); when(gv1MigrationIncapableDevice.getCapabilities()).thenReturn( - new DeviceCapabilities(true, true, true, true, true, false, false)); + new DeviceCapabilities(true, true, true, true, true, false, false, false)); when(gv1MigrationIncapableDevice.isEnabled()).thenReturn(true); when(gv1MigrationIncapableExpiredDevice.getCapabilities()).thenReturn( - new DeviceCapabilities(true, true, true, true, true, false, false)); + new DeviceCapabilities(true, true, true, true, true, false, false, false)); when(gv1MigrationIncapableExpiredDevice.isEnabled()).thenReturn(false); when(senderKeyCapableDevice.getCapabilities()).thenReturn( - new DeviceCapabilities(true, true, true, true, true, true, true)); + new DeviceCapabilities(true, true, true, true, true, true, true, false)); when(senderKeyCapableDevice.isEnabled()).thenReturn(true); when(senderKeyIncapableDevice.getCapabilities()).thenReturn( - new DeviceCapabilities(true, true, true, true, true, true, false)); + new DeviceCapabilities(true, true, true, true, true, true, false, false)); when(senderKeyIncapableDevice.isEnabled()).thenReturn(true); when(senderKeyIncapableExpiredDevice.getCapabilities()).thenReturn( - new DeviceCapabilities(true, true, true, true, true, true, false)); + new DeviceCapabilities(true, true, true, true, true, true, false, false)); when(senderKeyIncapableExpiredDevice.isEnabled()).thenReturn(false); + + when(announcementGroupCapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, true, true, true, true, true)); + when(announcementGroupCapableDevice.isEnabled()).thenReturn(true); + + when(announcementGroupIncapableDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, true, true, true, true, false)); + when(announcementGroupIncapableDevice.isEnabled()).thenReturn(true); + + when(announcementGroupIncapableExpiredDevice.getCapabilities()).thenReturn( + new DeviceCapabilities(true, true, true, true, true, true, true, false)); + when(announcementGroupIncapableExpiredDevice.isEnabled()).thenReturn(false); } @Test @@ -233,4 +249,17 @@ public class AccountTest { Set.of(senderKeyCapableDevice, senderKeyIncapableExpiredDevice), "1234".getBytes(StandardCharsets.UTF_8)).isSenderKeySupported()).isTrue(); } + + @Test + public void isAnnouncementGroupSupported() { + assertThat(new Account("+18005551234", UUID.randomUUID(), + Set.of(announcementGroupCapableDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isTrue(); + assertThat(new Account("+18005551234", UUID.randomUUID(), + Set.of(announcementGroupCapableDevice, announcementGroupIncapableDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isFalse(); + assertThat(new Account("+18005551234", UUID.randomUUID(), + Set.of(announcementGroupCapableDevice, announcementGroupIncapableExpiredDevice), + "1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isTrue(); + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountsTest.java index 9c70c3b69..3b91acbc8 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountsTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/AccountsTest.java @@ -318,7 +318,7 @@ public class AccountsTest { 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(), "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(), random.nextBoolean(), random.nextBoolean(), - false)); + false, false)); } private Account generateAccount(String number, UUID uuid) {