Remove ChallengeRequired variants from sealed-sender gRPC responses

This commit is contained in:
ravi-signal
2026-02-27 16:01:44 -06:00
committed by GitHub
parent 80893152c9
commit f872ade347
9 changed files with 213 additions and 236 deletions

View File

@@ -27,6 +27,7 @@ import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.InvalidVersionException;
import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.controllers.MultiRecipientMismatchedDevicesException;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
@@ -37,7 +38,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
import org.whispersystems.textsecuregcm.push.MessageUtil;
import org.whispersystems.textsecuregcm.spam.GrpcResponse;
import org.whispersystems.textsecuregcm.spam.GrpcChallengeResponse;
import org.whispersystems.textsecuregcm.spam.MessageType;
import org.whispersystems.textsecuregcm.spam.SpamCheckResult;
import org.whispersystems.textsecuregcm.spam.SpamChecker;
@@ -155,16 +156,15 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
final boolean urgent,
final boolean story) throws RateLimitExceededException {
final SpamCheckResult<GrpcResponse<SendMessageResponse>> spamCheckResult =
final SpamCheckResult<GrpcChallengeResponse> spamCheckResult =
spamChecker.checkForIndividualRecipientSpamGrpc(
story ? MessageType.INDIVIDUAL_STORY : MessageType.INDIVIDUAL_SEALED_SENDER,
Optional.empty(),
Optional.of(destination),
destinationServiceIdentifier);
if (spamCheckResult.response().isPresent()) {
return spamCheckResult.response().get().getResponseOrThrowStatus();
}
spamCheckResult.response().ifPresent(grpcResponse ->
grpcResponse.throwStatusOr(_ -> GrpcExceptions.rateLimitExceeded(null)));
try {
final int totalPayloadLength = messages.getMessagesMap().values().stream()
@@ -208,12 +208,22 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
entry -> entry.getKey().byteValue(),
entry -> entry.getValue().getRegistrationId()));
return MessagesGrpcHelper.sendMessage(messageSender,
destination,
destinationServiceIdentifier,
messagesByDeviceId,
registrationIdsByDeviceId,
Optional.empty());
try {
messageSender.sendMessages(destination,
destinationServiceIdentifier,
messagesByDeviceId,
registrationIdsByDeviceId,
Optional.empty(),
RequestAttributesUtil.getUserAgent().orElse(null));
return SEND_MESSAGE_SUCCESS_RESPONSE;
} catch (final MismatchedDevicesException e) {
return SendMessageResponse.newBuilder()
.setMismatchedDevices(MessagesGrpcHelper.buildMismatchedDevices(destinationServiceIdentifier, e.getMismatchedDevices()))
.build();
} catch (final MessageTooLargeException e) {
throw GrpcExceptions.invalidArguments("message too large");
}
}
@Override
@@ -265,14 +275,13 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
final boolean urgent,
final boolean story) {
final SpamCheckResult<GrpcResponse<SendMultiRecipientMessageResponse>> spamCheckResult =
final SpamCheckResult<GrpcChallengeResponse> spamCheckResult =
spamChecker.checkForMultiRecipientSpamGrpc(story
? MessageType.MULTI_RECIPIENT_STORY
: MessageType.MULTI_RECIPIENT_SEALED_SENDER);
if (spamCheckResult.response().isPresent()) {
return spamCheckResult.response().get().getResponseOrThrowStatus();
}
spamCheckResult.response().ifPresent(response ->
response.throwStatusOr(_ -> GrpcExceptions.rateLimitExceeded(null)));
// At this point, the caller has at least superficially provided the information needed to send a multi-recipient
// message. Attempt to resolve the destination service identifiers to Signal accounts.

View File

@@ -5,71 +5,11 @@
package org.whispersystems.textsecuregcm.grpc;
import com.google.protobuf.Empty;
import io.grpc.Status;
import io.grpc.StatusException;
import java.util.Map;
import java.util.Optional;
import org.signal.chat.messages.MismatchedDevices;
import org.signal.chat.messages.SendMessageResponse;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
import org.whispersystems.textsecuregcm.storage.Account;
public class MessagesGrpcHelper {
private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse
.newBuilder()
.setSuccess(Empty.getDefaultInstance())
.build();
/**
* Sends a "bundle" of messages to an individual destination account, mapping common exceptions to appropriate gRPC
* statuses.
*
* @param messageSender the {@code MessageSender} instance to use to send the messages
* @param destination the destination account for the messages
* @param destinationServiceIdentifier the service identifier for the destination account
* @param messagesByDeviceId a map of device IDs to message payloads
* @param registrationIdsByDeviceId a map of device IDs to device registration IDs
* @param syncMessageSenderDeviceId if the message is a sync message (i.e. a message to other devices linked to the
* caller's own account), contains the ID of the device that sent the message
*
* @return a response object to send to callers
*
* @throws RateLimitExceededException if the message bundle could not be sent due to a violated rated limit
* @throws io.grpc.StatusRuntimeException for invalid arguments if the message is too large to send
*/
public static SendMessageResponse sendMessage(final MessageSender messageSender,
final Account destination,
final ServiceIdentifier destinationServiceIdentifier,
final Map<Byte, MessageProtos.Envelope> messagesByDeviceId,
final Map<Byte, Integer> registrationIdsByDeviceId,
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<Byte> syncMessageSenderDeviceId)
throws RateLimitExceededException {
try {
messageSender.sendMessages(destination,
destinationServiceIdentifier,
messagesByDeviceId,
registrationIdsByDeviceId,
syncMessageSenderDeviceId,
RequestAttributesUtil.getUserAgent().orElse(null));
return SEND_MESSAGE_SUCCESS_RESPONSE;
} catch (final MismatchedDevicesException e) {
return SendMessageResponse.newBuilder()
.setMismatchedDevices(buildMismatchedDevices(destinationServiceIdentifier, e.getMismatchedDevices()))
.build();
} catch (final MessageTooLargeException e) {
throw GrpcExceptions.invalidArguments("message too large");
}
}
/**
* Translates an internal {@link org.whispersystems.textsecuregcm.controllers.MismatchedDevices} entity to a gRPC
* {@link MismatchedDevices} entity.

View File

@@ -10,15 +10,17 @@ import java.time.Clock;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import com.google.protobuf.Empty;
import org.signal.chat.errors.NotFound;
import org.signal.chat.messages.AuthenticatedSenderMessageType;
import org.signal.chat.messages.IndividualRecipientMessageBundle;
import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest;
import org.signal.chat.messages.SendMessageResponse;
import org.signal.chat.messages.SendMessageAuthenticatedSenderResponse;
import org.signal.chat.messages.SendSyncMessageRequest;
import org.signal.chat.messages.SimpleMessagesGrpc;
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
@@ -26,13 +28,16 @@ import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.limits.CardinalityEstimator;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.spam.GrpcResponse;
import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
import org.whispersystems.textsecuregcm.spam.GrpcChallengeResponse;
import org.whispersystems.textsecuregcm.spam.MessageType;
import org.whispersystems.textsecuregcm.spam.SpamCheckResult;
import org.whispersystems.textsecuregcm.spam.SpamChecker;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import static org.whispersystems.textsecuregcm.grpc.MessagesGrpcHelper.buildMismatchedDevices;
public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
private final AccountsManager accountsManager;
@@ -42,6 +47,9 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
private final SpamChecker spamChecker;
private final Clock clock;
private static final SendMessageAuthenticatedSenderResponse SEND_MESSAGE_SUCCESS_RESPONSE =
SendMessageAuthenticatedSenderResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build();
public MessagesGrpcService(final AccountsManager accountsManager,
final RateLimiters rateLimiters,
final MessageSender messageSender,
@@ -58,7 +66,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
}
@Override
public SendMessageResponse sendMessage(final SendAuthenticatedSenderMessageRequest request)
public SendMessageAuthenticatedSenderResponse sendMessage(final SendAuthenticatedSenderMessageRequest request)
throws RateLimitExceededException {
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
@@ -75,7 +83,9 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
final Optional<Account> maybeDestination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier);
if (maybeDestination.isEmpty()) {
return SendMessageResponse.newBuilder().setDestinationNotFound(NotFound.getDefaultInstance()).build();
return SendMessageAuthenticatedSenderResponse.newBuilder()
.setDestinationNotFound(NotFound.getDefaultInstance())
.build();
}
final Account destination = maybeDestination.get();
@@ -92,7 +102,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
}
@Override
public SendMessageResponse sendSyncMessage(final SendSyncMessageRequest request)
public SendMessageAuthenticatedSenderResponse sendSyncMessage(final SendSyncMessageRequest request)
throws RateLimitExceededException {
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
@@ -110,7 +120,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
request.getUrgent());
}
private SendMessageResponse sendMessage(final Account destination,
private SendMessageAuthenticatedSenderResponse sendMessage(final Account destination,
final ServiceIdentifier destinationServiceIdentifier,
final AuthenticatedDevice sender,
final AuthenticatedSenderMessageType envelopeType,
@@ -130,14 +140,16 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
throw e;
}
final SpamCheckResult<GrpcResponse<SendMessageResponse>> spamCheckResult =
final SpamCheckResult<GrpcChallengeResponse> spamCheckResult =
spamChecker.checkForIndividualRecipientSpamGrpc(messageType,
Optional.of(sender),
Optional.of(destination),
destinationServiceIdentifier);
if (spamCheckResult.response().isPresent()) {
return spamCheckResult.response().get().getResponseOrThrowStatus();
return SendMessageAuthenticatedSenderResponse.newBuilder()
.setChallengeRequired(spamCheckResult.response().get().getResponseOrThrowStatus())
.build();
}
final Map<Byte, MessageProtos.Envelope> messagesByDeviceId = messages.getMessagesMap().entrySet()
@@ -168,12 +180,22 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
entry -> entry.getKey().byteValue(),
entry -> entry.getValue().getRegistrationId()));
return MessagesGrpcHelper.sendMessage(messageSender,
destination,
destinationServiceIdentifier,
messagesByDeviceId,
registrationIdsByDeviceId,
messageType == MessageType.SYNC ? Optional.of(sender.deviceId()) : Optional.empty());
try {
messageSender.sendMessages(destination,
destinationServiceIdentifier,
messagesByDeviceId,
registrationIdsByDeviceId,
messageType == MessageType.SYNC ? Optional.of(sender.deviceId()) : Optional.empty(),
RequestAttributesUtil.getUserAgent().orElse(null));
return SEND_MESSAGE_SUCCESS_RESPONSE;
} catch (final MismatchedDevicesException e) {
return SendMessageAuthenticatedSenderResponse.newBuilder()
.setMismatchedDevices(buildMismatchedDevices(destinationServiceIdentifier, e.getMismatchedDevices()))
.build();
} catch (final MessageTooLargeException e) {
throw GrpcExceptions.invalidArguments("message too large");
}
}
private static MessageProtos.Envelope.Type getEnvelopeType(final AuthenticatedSenderMessageType type) {

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.spam;
import io.grpc.StatusRuntimeException;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.signal.chat.messages.ChallengeRequired;
/// A gRPC status or a challenge message to communicate to callers that a message has been flagged as potential spam.
public class GrpcChallengeResponse {
@Nullable
private final StatusRuntimeException statusException;
@Nullable
private final ChallengeRequired response;
private GrpcChallengeResponse(final @Nullable StatusRuntimeException statusException,
@Nullable final ChallengeRequired response) {
this.statusException = statusException;
this.response = response;
if (!((statusException == null) ^ (response == null))) {
throw new IllegalArgumentException("exactly one of statusException and response must be non-null");
}
}
/// Constructs a new response object with the given status and no challenge
///
/// @param status the status to send to callers
/// @return a new response object with the given status and no challenge
public static GrpcChallengeResponse withStatusException(final StatusRuntimeException status) {
return new GrpcChallengeResponse(status, null);
}
/// Constructs a new response object with the given challenge message.
///
/// @param response the challenge message to send to the caller
/// @return a new response object with the given challenge message
public static GrpcChallengeResponse withResponse(final ChallengeRequired response) {
return new GrpcChallengeResponse(null, response);
}
/// Returns the challenge message contained within this response or throws the contained status as a
/// [StatusRuntimeException] if no challenge message is specified.
///
/// @return the [ChallengeRequired] message
/// @throws StatusRuntimeException if no challenge message is specified
public ChallengeRequired getResponseOrThrowStatus() throws StatusRuntimeException {
if (statusException != null) {
throw statusException;
}
return response;
}
/// If this response contains a challenge message, throw a status generated using statusMapper. Otherwise, throw the
/// status.
///
/// @param statusMapper A function that converts a [ChallengeRequired] message into a
/// [StatusRuntimeException]
/// @throws StatusRuntimeException the contained or mapped status exception
public void throwStatusOr(Function<ChallengeRequired, StatusRuntimeException> statusMapper)
throws StatusRuntimeException {
if (statusException != null) {
throw statusException;
}
throw statusMapper.apply(response);
}
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.spam;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.StatusRuntimeException;
import javax.annotation.Nullable;
import java.util.Optional;
/**
* A combination of a gRPC status and response message to communicate to callers that a message has been flagged as
* potential spam.
*
* @param <R> the type of response object
*/
public class GrpcResponse<R> {
@Nullable
private final StatusRuntimeException statusException;
@Nullable
private final R response;
private GrpcResponse(final @Nullable StatusRuntimeException statusException, @Nullable final R response) {
this.statusException = statusException;
this.response = response;
if (!((statusException == null) ^ (response == null))) {
throw new IllegalArgumentException("exactly one of statusException and response must be non-null");
}
}
/**
* Constructs a new response object with the given status and no response message.
*
* @param status the status to send to callers
*
* @return a new response object with the given status and no response message
*
* @param <R> the type of response object
*/
public static <R> GrpcResponse<R> withStatusException(final StatusRuntimeException status) {
return new GrpcResponse<>(status, null);
}
/**
* Constructs a new response object with a status of {@link Status#OK} and the given response message.
*
* @param response the response to send to the caller
*
* @return a new response object with a status of {@link Status#OK} and the given response message
*
* @param <R> the type of response object
*/
public static <R> GrpcResponse<R> withResponse(final R response) {
return new GrpcResponse<>(null, response);
}
/**
* Returns the message body contained within this response or throws the contained status as a {@link StatusException}
* if no message body is specified.
*
* @return the message body contained within this response
*
* @throws StatusException if no message body is specified
*/
public R getResponseOrThrowStatus() throws StatusRuntimeException {
if (statusException != null) {
throw statusException;
}
return response;
}
}

View File

@@ -5,10 +5,8 @@
package org.whispersystems.textsecuregcm.spam;
import jakarta.ws.rs.container.ContainerRequestContext;
import java.util.Optional;
import jakarta.ws.rs.core.Response;
import org.signal.chat.messages.SendMessageResponse;
import org.signal.chat.messages.SendMultiRecipientMessageResponse;
import java.util.Optional;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -55,7 +53,7 @@ public interface SpamChecker {
* @return A {@link SpamCheckResult}
*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
SpamCheckResult<GrpcResponse<SendMessageResponse>> checkForIndividualRecipientSpamGrpc(
SpamCheckResult<GrpcChallengeResponse> checkForIndividualRecipientSpamGrpc(
final MessageType messageType,
final Optional<org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice> maybeSource,
final Optional<Account> maybeDestination,
@@ -68,7 +66,7 @@ public interface SpamChecker {
*
* @return A {@link SpamCheckResult}
*/
SpamCheckResult<GrpcResponse<SendMultiRecipientMessageResponse>> checkForMultiRecipientSpamGrpc(final MessageType messageType);
SpamCheckResult<GrpcChallengeResponse> checkForMultiRecipientSpamGrpc(final MessageType messageType);
static SpamChecker noop() {
@@ -92,7 +90,7 @@ public interface SpamChecker {
}
@Override
public SpamCheckResult<GrpcResponse<SendMessageResponse>> checkForIndividualRecipientSpamGrpc(final MessageType messageType,
public SpamCheckResult<GrpcChallengeResponse> checkForIndividualRecipientSpamGrpc(final MessageType messageType,
final Optional<org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice> maybeSource,
final Optional<Account> maybeDestination,
final ServiceIdentifier destinationIdentifier) {
@@ -101,7 +99,7 @@ public interface SpamChecker {
}
@Override
public SpamCheckResult<GrpcResponse<SendMultiRecipientMessageResponse>> checkForMultiRecipientSpamGrpc(
public SpamCheckResult<GrpcChallengeResponse> checkForMultiRecipientSpamGrpc(
final MessageType messageType) {
return new SpamCheckResult<>(Optional.empty(), Optional.empty());

View File

@@ -25,11 +25,11 @@ service Messages {
//
// The destination account must not be the same as the authenticated caller.
// Callers should use `SendSyncMessage` to send messages to themselves.
rpc SendMessage(SendAuthenticatedSenderMessageRequest) returns (SendMessageResponse) {}
rpc SendMessage(SendAuthenticatedSenderMessageRequest) returns (SendMessageAuthenticatedSenderResponse) {}
// Sends a "sync" message to all other devices linked to the authenticated
// sender's account.
rpc SendSyncMessage(SendSyncMessageRequest) returns (SendMessageResponse) {}
rpc SendSyncMessage(SendSyncMessageRequest) returns (SendMessageAuthenticatedSenderResponse) {}
}
// Provides methods for sending "sealed sender" messages.
@@ -133,6 +133,30 @@ message SendAuthenticatedSenderMessageRequest {
IndividualRecipientMessageBundle messages = 5;
}
message SendMessageAuthenticatedSenderResponse {
// The outcome of the message delivery
oneof response {
// The message was successfully delivered to all destination devices
google.protobuf.Empty success = 1;
// A list of discrepancies between the destination devices identified in a
// request to send a message and the devices that are actually linked to an
// account.
MismatchedDevices mismatched_devices = 2;
// A description of a challenge callers must complete before sending
// additional messages.
ChallengeRequired challenge_required = 3;
// The destination account did not exist
errors.NotFound destination_not_found = 4;
}
}
message SendSyncMessageRequest {
// The type identifier for this message.
@@ -202,15 +226,11 @@ message SendMessageResponse {
// account.
MismatchedDevices mismatched_devices = 2;
// A description of a challenge callers must complete before sending
// additional messages.
ChallengeRequired challenge_required = 3;
// The provided unidentified authorization credential was invalid
errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 4;
errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3;
// The destination account did not exist
errors.NotFound destination_not_found = 5;
errors.NotFound destination_not_found = 4;
}
}
@@ -280,12 +300,8 @@ message SendMultiRecipientMessageResponse {
// actually linked to a destination account.
MultiRecipientMismatchedDevices mismatched_devices = 2;
// A description of a challenge callers must complete before sending
// additional messages.
ChallengeRequired challenge_required = 3;
// The provided unidentified authorization credential was invalid
errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 4;
errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3;
}
}

View File

@@ -65,7 +65,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
import org.whispersystems.textsecuregcm.spam.GrpcResponse;
import org.whispersystems.textsecuregcm.spam.GrpcChallengeResponse;
import org.whispersystems.textsecuregcm.spam.MessageType;
import org.whispersystems.textsecuregcm.spam.SpamCheckResult;
import org.whispersystems.textsecuregcm.spam.SpamChecker;
@@ -427,7 +427,7 @@ class MessagesAnonymousGrpcServiceTest extends
when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any()))
.thenReturn(new SpamCheckResult<>(
Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.empty()));
//noinspection ResultOfMethodCallIgnored
@@ -464,16 +464,16 @@ class MessagesAnonymousGrpcServiceTest extends
.setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128)))
.build());
final SendMessageResponse response = SendMessageResponse.newBuilder()
.setChallengeRequired(ChallengeRequired.newBuilder()
.addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA))
.build();
final ChallengeRequired challengeResponse =
ChallengeRequired.newBuilder().addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA).build();
when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any()))
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty()));
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty()));
assertEquals(response, unauthenticatedServiceStub().sendSingleRecipientMessage(
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null)));
final SendSealedSenderMessageRequest request =
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null);
GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () ->
unauthenticatedServiceStub().sendSingleRecipientMessage(request));
verify(spamChecker).checkForIndividualRecipientSpamGrpc(MessageType.INDIVIDUAL_SEALED_SENDER,
Optional.empty(),
@@ -793,7 +793,7 @@ class MessagesAnonymousGrpcServiceTest extends
when(spamChecker.checkForMultiRecipientSpamGrpc(any()))
.thenReturn(new SpamCheckResult<>(
Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.empty()));
//noinspection ResultOfMethodCallIgnored
@@ -837,15 +837,14 @@ class MessagesAnonymousGrpcServiceTest extends
.setUrgent(true)
.build();
final SendMultiRecipientMessageResponse response = SendMultiRecipientMessageResponse.newBuilder()
.setChallengeRequired(ChallengeRequired.newBuilder()
.addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA))
.build();
final ChallengeRequired challengeResponse =
ChallengeRequired.newBuilder().addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA).build();
when(spamChecker.checkForMultiRecipientSpamGrpc(any()))
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty()));
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty()));
assertEquals(response, unauthenticatedServiceStub().sendMultiRecipientMessage(request));
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED,
() -> unauthenticatedServiceStub().sendMultiRecipientMessage(request));
verify(spamChecker).checkForMultiRecipientSpamGrpc(MessageType.MULTI_RECIPIENT_SEALED_SENDER);
@@ -1052,7 +1051,7 @@ class MessagesAnonymousGrpcServiceTest extends
when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any()))
.thenReturn(new SpamCheckResult<>(
Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.empty()));
//noinspection ResultOfMethodCallIgnored
@@ -1087,16 +1086,13 @@ class MessagesAnonymousGrpcServiceTest extends
.setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128)))
.build());
final SendMessageResponse response = SendMessageResponse.newBuilder()
.setChallengeRequired(ChallengeRequired.newBuilder()
.addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA))
.build();
final ChallengeRequired challengeResponse =
ChallengeRequired.newBuilder().addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA).build();
when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any()))
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty()));
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty()));
assertEquals(response, unauthenticatedServiceStub().sendStory(
generateRequest(serviceIdentifier, true, messages)));
GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () ->
unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, true, messages)));
verify(spamChecker).checkForIndividualRecipientSpamGrpc(MessageType.INDIVIDUAL_STORY,
Optional.empty(),
@@ -1335,7 +1331,7 @@ class MessagesAnonymousGrpcServiceTest extends
when(spamChecker.checkForMultiRecipientSpamGrpc(any()))
.thenReturn(new SpamCheckResult<>(
Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.empty()));
//noinspection ResultOfMethodCallIgnored
@@ -1377,15 +1373,13 @@ class MessagesAnonymousGrpcServiceTest extends
.setUrgent(true)
.build();
final SendMultiRecipientMessageResponse response = SendMultiRecipientMessageResponse.newBuilder()
.setChallengeRequired(ChallengeRequired.newBuilder()
.addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA))
.build();
final ChallengeRequired challengeResponse =
ChallengeRequired.newBuilder().addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA).build();
when(spamChecker.checkForMultiRecipientSpamGrpc(any()))
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty()));
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty()));
assertEquals(response, unauthenticatedServiceStub().sendMultiRecipientStory(request));
GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () ->
unauthenticatedServiceStub().sendMultiRecipientStory(request));
verify(spamChecker).checkForMultiRecipientSpamGrpc(MessageType.MULTI_RECIPIENT_STORY);

View File

@@ -39,7 +39,7 @@ import org.signal.chat.messages.IndividualRecipientMessageBundle;
import org.signal.chat.messages.MessagesGrpc;
import org.signal.chat.messages.MismatchedDevices;
import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest;
import org.signal.chat.messages.SendMessageResponse;
import org.signal.chat.messages.SendMessageAuthenticatedSenderResponse;
import org.signal.chat.messages.SendSyncMessageRequest;
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
@@ -53,7 +53,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
import org.whispersystems.textsecuregcm.spam.GrpcResponse;
import org.whispersystems.textsecuregcm.spam.GrpcChallengeResponse;
import org.whispersystems.textsecuregcm.spam.MessageType;
import org.whispersystems.textsecuregcm.spam.SpamCheckResult;
import org.whispersystems.textsecuregcm.spam.SpamChecker;
@@ -184,10 +184,10 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
.setPayload(ByteString.copyFrom(payload))
.build());
final SendMessageResponse response = authenticatedServiceStub().sendMessage(
final SendMessageAuthenticatedSenderResponse response = authenticatedServiceStub().sendMessage(
generateRequest(serviceIdentifier, messageType, ephemeral, urgent, messages));
assertEquals(SendMessageResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response);
assertEquals(SendMessageAuthenticatedSenderResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response);
final MessageProtos.Envelope.Type expectedEnvelopeType = switch (messageType) {
case DOUBLE_RATCHET -> MessageProtos.Envelope.Type.CIPHERTEXT;
@@ -245,10 +245,10 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
final SendMessageResponse response = authenticatedServiceStub().sendMessage(
final SendMessageAuthenticatedSenderResponse response = authenticatedServiceStub().sendMessage(
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages));
final SendMessageResponse expectedResponse = SendMessageResponse.newBuilder()
final SendMessageAuthenticatedSenderResponse expectedResponse = SendMessageAuthenticatedSenderResponse.newBuilder()
.setMismatchedDevices(MismatchedDevices.newBuilder()
.setServiceIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(serviceIdentifier))
.addMissingDevices(missingDeviceId)
@@ -270,7 +270,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
.setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128)))
.build());
final SendMessageResponse response = authenticatedServiceStub().sendMessage(
final SendMessageAuthenticatedSenderResponse response = authenticatedServiceStub().sendMessage(
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages));
assertTrue(response.hasDestinationNotFound());
@@ -359,7 +359,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any()))
.thenReturn(new SpamCheckResult<>(
Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))),
Optional.empty()));
//noinspection ResultOfMethodCallIgnored
@@ -395,15 +395,17 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
.setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128)))
.build());
final SendMessageResponse response = SendMessageResponse.newBuilder()
.setChallengeRequired(ChallengeRequired.newBuilder()
.addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA))
final ChallengeRequired challengeRequired = ChallengeRequired.newBuilder()
.addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA)
.build();
final SendMessageAuthenticatedSenderResponse expectedResponse = SendMessageAuthenticatedSenderResponse.newBuilder()
.setChallengeRequired(challengeRequired)
.build();
when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any()))
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty()));
.thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeRequired)), Optional.empty()));
assertEquals(response, authenticatedServiceStub().sendMessage(
assertEquals(expectedResponse, authenticatedServiceStub().sendMessage(
generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages)));
verify(spamChecker).checkForIndividualRecipientSpamGrpc(MessageType.INDIVIDUAL_IDENTIFIED_SENDER,
@@ -466,10 +468,10 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
.thenReturn(new SpamCheckResult<>(Optional.empty(), Optional.of(reportSpamToken)));
}
final SendMessageResponse response =
final SendMessageAuthenticatedSenderResponse response =
authenticatedServiceStub().sendSyncMessage(generateRequest(messageType, urgent, messages));
assertEquals(SendMessageResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response);
assertEquals(SendMessageAuthenticatedSenderResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response);
final MessageProtos.Envelope.Type expectedEnvelopeType = switch (messageType) {
case DOUBLE_RATCHET -> MessageProtos.Envelope.Type.CIPHERTEXT;
@@ -539,10 +541,10 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
Set.of(missingDeviceId), Set.of(extraDeviceId), Set.of(staleDeviceId))))
.when(messageSender).sendMessages(any(), any(), any(), any(), any(), any());
final SendMessageResponse response = authenticatedServiceStub().sendSyncMessage(
final SendMessageAuthenticatedSenderResponse response = authenticatedServiceStub().sendSyncMessage(
generateRequest(AuthenticatedSenderMessageType.DOUBLE_RATCHET, true, messages));
final SendMessageResponse expectedResponse = SendMessageResponse.newBuilder()
final SendMessageAuthenticatedSenderResponse expectedResponse = SendMessageAuthenticatedSenderResponse.newBuilder()
.setMismatchedDevices(MismatchedDevices.newBuilder()
.setServiceIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(new AciServiceIdentifier(AUTHENTICATED_ACI)))
.addMissingDevices(missingDeviceId)