From e6d4620af1da1f4b209c79c17573a4a951a8d7dd Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Fri, 25 Sep 2020 16:34:21 -0400 Subject: [PATCH] Only allow linking desktop clients if they support the third-generation GV2 capability. --- .../controllers/DeviceController.java | 48 +++++++++++++++++-- .../controllers/DeviceControllerTest.java | 37 ++++++++++++-- 2 files changed, 76 insertions(+), 9 deletions(-) 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 3e2f06736..4ce754da3 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -58,6 +58,10 @@ import java.util.Map; import java.util.Optional; import io.dropwizard.auth.Auth; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; @Path("/v1/devices") public class DeviceController { @@ -156,6 +160,7 @@ public class DeviceController { @Path("/{verification_code}") public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode, @HeaderParam("Authorization") String authorizationHeader, + @HeaderParam("User-Agent") String userAgent, @Valid AccountAttributes accountAttributes) throws RateLimitExceededException, DeviceLimitExceededException { @@ -191,7 +196,7 @@ public class DeviceController { } final DeviceCapabilities capabilities = accountAttributes.getCapabilities(); - if (capabilities != null && isCapabilityDowngrade(account.get(), capabilities)) { + if (capabilities != null && isCapabilityDowngrade(account.get(), capabilities, userAgent)) { throw new WebApplicationException(Response.status(409).build()); } @@ -241,9 +246,42 @@ public class DeviceController { return new VerificationCode(randomInt); } - private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities) { - // Only iOS and desktop clients can be linked devices right now, and both require the second-gen GV2 capability to - // support GV2. - return (!capabilities.isGv2_2() && account.isGroupsV2Supported()); + private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities, String userAgent) { + boolean isDowngrade = false; + + if (account.isGroupsV2Supported()) { + try { + switch (UserAgentUtil.parseUserAgentString(userAgent).getPlatform()) { + case ANDROID: { + if (!capabilities.isGv2() && !capabilities.isGv2_2() && !capabilities.isGv2_3()) { + isDowngrade = true; + } + + break; + } + + case IOS: { + if (!capabilities.isGv2_2() && !capabilities.isGv2_3()) { + isDowngrade = true; + } + + break; + } + + case DESKTOP: { + if (!capabilities.isGv2_3()) { + isDowngrade = true; + } + + break; + } + } + } catch (final UnrecognizedUserAgentException e) { + // If we can't parse the UA string, the client is for sure too old to support groups V2 + isDowngrade = true; + } + } + + return isDowngrade; } } 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 46c1be94c..011a165c8 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 @@ -17,10 +17,13 @@ package org.whispersystems.textsecuregcm.tests.controllers; import com.google.common.collect.ImmutableSet; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.controllers.DeviceController; @@ -53,6 +56,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; +@RunWith(JUnitParamsRunner.class) public class DeviceControllerTest { @Path("/v1/devices") static class DumbVerificationDeviceController extends DeviceController { @@ -222,17 +226,42 @@ public class DeviceControllerTest { } @Test - public void deviceDowngradeCapabilitiesTest() throws Exception { - Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(false, false, false, true, false); + @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 { + Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(gv2, gv2_2, gv2_3, true, false); AccountAttributes accountAttributes = new AccountAttributes("keykeykeykey", false, 1234, null, null, null, null, true, deviceCapabilities); Response response = resources.getJerseyTest() .target("/v1/devices/5678901") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .header("User-Agent", userAgent) .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE)); - assertThat(response.getStatus()).isEqualTo(409); + assertThat(response.getStatus()).isEqualTo(expectedStatus); - verifyNoMoreInteractions(messagesManager); + if (expectedStatus >= 300) { + verifyNoMoreInteractions(messagesManager); + } + } + + private static Object argumentsForDeviceDowngradeCapabilitiesTest() { + return new Object[] { + new Object[] { "Signal-Android/4.68.3 Android/25", false, false, false, 409 }, + new Object[] { "Signal-Android/4.68.3 Android/25", true, false, false, 200 }, + new Object[] { "Signal-Android/4.68.3 Android/25", false, true, false, 200 }, + new Object[] { "Signal-Android/4.68.3 Android/25", false, false, true, 200 }, + new Object[] { "Signal-iOS/3.9.0", false, false, false, 409 }, + new Object[] { "Signal-iOS/3.9.0", true, false, false, 409 }, + new Object[] { "Signal-iOS/3.9.0", false, true, false, 200 }, + new Object[] { "Signal-iOS/3.9.0", false, false, true, 200 }, + new Object[] { "Signal-Desktop/1.32.0-beta.3", false, false, false, 409 }, + new Object[] { "Signal-Desktop/1.32.0-beta.3", true, false, false, 409 }, + new Object[] { "Signal-Desktop/1.32.0-beta.3", false, true, false, 409 }, + new Object[] { "Signal-Desktop/1.32.0-beta.3", false, false, true, 200 }, + new Object[] { "Old client with unparsable UA", false, false, false, 409 }, + new Object[] { "Old client with unparsable UA", true, false, false, 409 }, + new Object[] { "Old client with unparsable UA", false, true, false, 409 }, + new Object[] { "Old client with unparsable UA", false, false, true, 409 } + }; } }