diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/recaptcha/EnterpriseRecaptchaClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/recaptcha/EnterpriseRecaptchaClient.java index 32a09fa0f..6bb5416b2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/recaptcha/EnterpriseRecaptchaClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/recaptcha/EnterpriseRecaptchaClient.java @@ -11,13 +11,14 @@ import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceClient; import com.google.cloud.recaptchaenterprise.v1.RecaptchaEnterpriseServiceSettings; import com.google.recaptchaenterprise.v1.Assessment; import com.google.recaptchaenterprise.v1.Event; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Objects; -import javax.annotation.Nonnull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class EnterpriseRecaptchaClient implements RecaptchaClient { private static final Logger logger = LoggerFactory.getLogger(EnterpriseRecaptchaClient.class); @@ -47,12 +48,20 @@ public class EnterpriseRecaptchaClient implements RecaptchaClient { @Override public boolean verify(final String token, final String ip) { - Event event = Event.newBuilder() - .setExpectedAction("challenge") + return verify(token, ip, null); + } + + public boolean verify(final String token, final String ip, @Nullable final String expectedAction) { + Event.Builder eventBuilder = Event.newBuilder() .setSiteKey(siteKey) .setToken(token) - .setUserIpAddress(ip) - .build(); + .setUserIpAddress(ip); + + if (expectedAction != null) { + eventBuilder.setExpectedAction(expectedAction); + } + + final Event event = eventBuilder.build(); final Assessment assessment = client.createAssessment(projectPath, Assessment.newBuilder().setEvent(event).build()); return assessment.getTokenProperties().getValid() && assessment.getRiskAnalysis().getScore() >= scoreFloor; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/recaptcha/TransitionalRecaptchaClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/recaptcha/TransitionalRecaptchaClient.java index 9def44cb9..4ca8a8158 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/recaptcha/TransitionalRecaptchaClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/recaptcha/TransitionalRecaptchaClient.java @@ -5,12 +5,16 @@ package org.whispersystems.textsecuregcm.recaptcha; +import com.google.common.annotations.VisibleForTesting; import java.util.Objects; import javax.annotation.Nonnull; public class TransitionalRecaptchaClient implements RecaptchaClient { - private static final String V2_PREFIX = "signal-recaptcha-v2:"; + @VisibleForTesting + static final String SEPARATOR = "."; + @VisibleForTesting + static final String V2_PREFIX = "signal-recaptcha-v2" + SEPARATOR; private final LegacyRecaptchaClient legacyRecaptchaClient; private final EnterpriseRecaptchaClient enterpriseRecaptchaClient; @@ -25,9 +29,28 @@ public class TransitionalRecaptchaClient implements RecaptchaClient { @Override public boolean verify(@Nonnull final String token, @Nonnull final String ip) { if (token.startsWith(V2_PREFIX)) { - return enterpriseRecaptchaClient.verify(token.substring(V2_PREFIX.length()), ip); + final String[] actionAndToken = parseV2ActionAndToken(token.substring(V2_PREFIX.length())); + return enterpriseRecaptchaClient.verify(actionAndToken[1], ip, actionAndToken[0]); } else { return legacyRecaptchaClient.verify(token, ip); } } + + /** + * Parses the token and action (if any) from {@code input}. The expected input format is: {@code [action:]token}. + *
+ * For action to be optional, there is a strong assumption that the token will never contain a {@value SEPARATOR}.
+ * Observation suggests {@code token} is base-64 encoded. In practice, an action should always be present, but we
+ * don’t need to be strict.
+ */
+ static String[] parseV2ActionAndToken(final String input) {
+ String[] actionAndToken = input.split("\\" + SEPARATOR, 2);
+
+ if (actionAndToken.length == 1) {
+ // there was no ":" delimiter; assume we only have a token
+ return new String[]{null, actionAndToken[0]};
+ }
+
+ return actionAndToken;
+ }
}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/recaptcha/TransitionalRecaptchaClientTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/recaptcha/TransitionalRecaptchaClientTest.java
new file mode 100644
index 000000000..1e2862da9
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/recaptcha/TransitionalRecaptchaClientTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.recaptcha;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.whispersystems.textsecuregcm.recaptcha.TransitionalRecaptchaClient.SEPARATOR;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class TransitionalRecaptchaClientTest {
+
+ private TransitionalRecaptchaClient transitionalRecaptchaClient;
+ private EnterpriseRecaptchaClient enterpriseRecaptchaClient;
+ private LegacyRecaptchaClient legacyRecaptchaClient;
+
+ private static final String PREFIX = TransitionalRecaptchaClient.V2_PREFIX.substring(0,
+ TransitionalRecaptchaClient.V2_PREFIX.lastIndexOf(SEPARATOR));
+ private static final String TOKEN = "some-token";
+ private static final String IP_ADDRESS = "127.0.0.1";
+
+ @BeforeEach
+ void setup() {
+ enterpriseRecaptchaClient = mock(EnterpriseRecaptchaClient.class);
+ legacyRecaptchaClient = mock(LegacyRecaptchaClient.class);
+ transitionalRecaptchaClient = new TransitionalRecaptchaClient(legacyRecaptchaClient, enterpriseRecaptchaClient);
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testVerify(final String inputToken, final boolean expectLegacy, final String expectedToken,
+ final String expectedAction) {
+
+ transitionalRecaptchaClient.verify(inputToken, IP_ADDRESS);
+
+ if (expectLegacy) {
+ verifyNoInteractions(enterpriseRecaptchaClient);
+ verify(legacyRecaptchaClient).verify(expectedToken, IP_ADDRESS);
+ } else {
+ verifyNoInteractions(legacyRecaptchaClient);
+ verify(enterpriseRecaptchaClient).verify(expectedToken, IP_ADDRESS, expectedAction);
+ }
+
+ }
+
+ static Stream