From ca47a7b663d8fcab646fb8067d7a41ad139ccff7 Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Tue, 19 Dec 2023 13:37:04 -0600 Subject: [PATCH] handle new RegistrationService proto error --- .../textsecuregcm/WhisperServerService.java | 2 +- .../dynamic/DynamicConfiguration.java | 9 +++ .../DynamicRegistrationConfiguration.java | 7 +++ .../controllers/VerificationController.java | 14 ++++- .../RegistrationFraudException.java | 12 ++++ .../RegistrationServiceClient.java | 3 + .../src/main/proto/RegistrationService.proto | 6 ++ .../VerificationControllerTest.java | 55 ++++++++++++++++++- 8 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRegistrationConfiguration.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationFraudException.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 2c40a222f..d39c9c82d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -844,7 +844,7 @@ public class WhisperServerService extends Application getExperimentEnrollmentConfiguration( final String experimentName) { return Optional.ofNullable(experiments.get(experimentName)); @@ -104,4 +109,8 @@ public class DynamicConfiguration { public DynamicInboundMessageByteLimitConfiguration getInboundMessageByteLimitConfiguration() { return inboundMessageByteLimit; } + + public DynamicRegistrationConfiguration getRegistrationConfiguration() { + return registrationConfiguration; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRegistrationConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRegistrationConfiguration.java new file mode 100644 index 000000000..e3ce05b1f --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicRegistrationConfiguration.java @@ -0,0 +1,7 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.configuration.dynamic; + +public record DynamicRegistrationConfiguration(boolean squashDeclinedAttemptErrors) {} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java index f81980afb..2e41c175b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java @@ -59,6 +59,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.captcha.AssessmentResult; import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest; import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; import org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest; @@ -72,6 +73,7 @@ import org.whispersystems.textsecuregcm.push.PushNotification; import org.whispersystems.textsecuregcm.push.PushNotificationManager; import org.whispersystems.textsecuregcm.registration.ClientType; import org.whispersystems.textsecuregcm.registration.MessageTransport; +import org.whispersystems.textsecuregcm.registration.RegistrationFraudException; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; import org.whispersystems.textsecuregcm.registration.RegistrationServiceException; import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException; @@ -81,6 +83,7 @@ import org.whispersystems.textsecuregcm.spam.Extract; import org.whispersystems.textsecuregcm.spam.FilterSpam; import org.whispersystems.textsecuregcm.spam.ScoreThreshold; import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; import org.whispersystems.textsecuregcm.util.ExceptionUtils; @@ -119,6 +122,7 @@ public class VerificationController { private final AccountsManager accountsManager; private final boolean useRemoteAddress; + private final DynamicConfigurationManager dynamicConfigurationManager; private final Clock clock; public VerificationController(final RegistrationServiceClient registrationServiceClient, @@ -129,6 +133,7 @@ public class VerificationController { final RateLimiters rateLimiters, final AccountsManager accountsManager, final boolean useRemoteAddress, + final DynamicConfigurationManager dynamicConfigurationManager, final Clock clock) { this.registrationServiceClient = registrationServiceClient; this.verificationSessionManager = verificationSessionManager; @@ -138,6 +143,7 @@ public class VerificationController { this.rateLimiters = rateLimiters; this.accountsManager = accountsManager; this.useRemoteAddress = useRemoteAddress; + this.dynamicConfigurationManager = dynamicConfigurationManager; this.clock = clock; } @@ -501,10 +507,14 @@ public class VerificationController { }) .orElseGet(NotFoundException::new); + } else if (unwrappedException instanceof RegistrationFraudException) { + if (dynamicConfigurationManager.getConfiguration().getRegistrationConfiguration().squashDeclinedAttemptErrors()) { + return buildResponse(registrationServiceSession, verificationSession); + } else { + throw unwrappedException.getCause(); + } } else if (unwrappedException instanceof RegistrationServiceSenderException) { - throw unwrappedException; - } else { logger.error("Registration service failure", unwrappedException); throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationFraudException.java b/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationFraudException.java new file mode 100644 index 000000000..8c596c573 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationFraudException.java @@ -0,0 +1,12 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.registration; + +public class RegistrationFraudException extends Exception { + public RegistrationFraudException(final RegistrationServiceSenderException cause) { + super(null, cause, true, false); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java index bf848b4a6..e9333b85b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java @@ -146,6 +146,9 @@ public class RegistrationServiceClient implements Managed { case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_REJECTED -> throw new CompletionException( RegistrationServiceSenderException.rejected(response.getError().getMayRetry())); + case SEND_VERIFICATION_CODE_ERROR_TYPE_SUSPECTED_FRAUD -> + throw new CompletionException(new RegistrationFraudException( + RegistrationServiceSenderException.rejected(response.getError().getMayRetry()))); case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_ILLEGAL_ARGUMENT -> throw new CompletionException( RegistrationServiceSenderException.illegalArgument(response.getError().getMayRetry())); case SEND_VERIFICATION_CODE_ERROR_TYPE_UNSPECIFIED -> throw new CompletionException( diff --git a/service/src/main/proto/RegistrationService.proto b/service/src/main/proto/RegistrationService.proto index 5208059ac..b08aaad0e 100644 --- a/service/src/main/proto/RegistrationService.proto +++ b/service/src/main/proto/RegistrationService.proto @@ -323,6 +323,12 @@ enum SendVerificationCodeErrorType { * transport. */ SEND_VERIFICATION_CODE_ERROR_TYPE_TRANSPORT_NOT_ALLOWED = 6; + + /** + * The sender declined to send the verification code due to suspected fraud + */ + SEND_VERIFICATION_CODE_ERROR_TYPE_SUSPECTED_FRAUD = 7; + } message CheckVerificationCodeRequest { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/VerificationControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/VerificationControllerTest.java index e37623283..6f6c46051 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/VerificationControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/VerificationControllerTest.java @@ -56,6 +56,8 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.whispersystems.textsecuregcm.captcha.AssessmentResult; import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRegistrationConfiguration; import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse; import org.whispersystems.textsecuregcm.limits.RateLimiter; @@ -65,6 +67,7 @@ import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptio import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper; import org.whispersystems.textsecuregcm.push.PushNotificationManager; +import org.whispersystems.textsecuregcm.registration.RegistrationFraudException; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; import org.whispersystems.textsecuregcm.registration.RegistrationServiceException; import org.whispersystems.textsecuregcm.registration.RegistrationServiceSenderException; @@ -73,6 +76,7 @@ import org.whispersystems.textsecuregcm.registration.VerificationSession; import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; import org.whispersystems.textsecuregcm.util.SystemMapper; @@ -97,6 +101,9 @@ class VerificationControllerTest { private final RateLimiter captchaLimiter = mock(RateLimiter.class); private final RateLimiter pushChallengeLimiter = mock(RateLimiter.class); + private final DynamicConfigurationManager dynamicConfigurationManager = mock( + DynamicConfigurationManager.class); + private final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); private final ResourceExtension resources = ResourceExtension.builder() .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) @@ -110,7 +117,7 @@ class VerificationControllerTest { .addResource( new VerificationController(registrationServiceClient, verificationSessionManager, pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, accountsManager, true, - clock)) + dynamicConfigurationManager, clock)) .build(); @BeforeEach @@ -119,8 +126,12 @@ class VerificationControllerTest { .thenReturn(captchaLimiter); when(rateLimiters.getVerificationPushChallengeLimiter()) .thenReturn(pushChallengeLimiter); - - when(accountsManager.getByE164(any())).thenReturn(Optional.empty()); + when(accountsManager.getByE164(any())) + .thenReturn(Optional.empty()); + when(dynamicConfiguration.getRegistrationConfiguration()) + .thenReturn(new DynamicRegistrationConfiguration(false)); + when(dynamicConfigurationManager.getConfiguration()) + .thenReturn(dynamicConfiguration); } @ParameterizedTest @@ -1089,6 +1100,44 @@ class VerificationControllerTest { ); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void fraudError(boolean shadowFailure) { + if (shadowFailure) { + when(this.dynamicConfiguration.getRegistrationConfiguration()) + .thenReturn(new DynamicRegistrationConfiguration(true)); + } + final String encodedSessionId = encodeSessionId(SESSION_ID); + final RegistrationServiceSession registrationServiceSession = new RegistrationServiceSession(SESSION_ID, NUMBER, + false, null, null, 0L, + SESSION_EXPIRATION_SECONDS); + when(registrationServiceClient.getSession(any(), any())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(registrationServiceSession))); + when(verificationSessionManager.findForId(any())) + .thenReturn(CompletableFuture.completedFuture( + Optional.of(new VerificationSession(null, Collections.emptyList(), Collections.emptyList(), true, + clock.millis(), clock.millis(), registrationServiceSession.expiration())))); + + when(registrationServiceClient.sendVerificationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.failedFuture(new CompletionException( + new RegistrationFraudException(RegistrationServiceSenderException.rejected(true))))); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/verification/session/" + encodedSessionId + "/code") + .request() + .header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1"); + try (Response response = request.post(Entity.json(requestVerificationCodeJson("voice", "ios")))) { + if (shadowFailure) { + assertEquals(200, response.getStatus()); + } else { + assertEquals(RegistrationServiceSenderExceptionMapper.REMOTE_SERVICE_REJECTED_REQUEST_STATUS, response.getStatus()); + final Map responseMap = response.readEntity(Map.class); + assertEquals("providerRejected", responseMap.get("reason")); + } + } + } + + @Test void verifyCodeServerError() { final String encodedSessionId = encodeSessionId(SESSION_ID);