From 809ba29ce811d148e9a36d402f5ec76f552222a3 Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Fri, 6 Feb 2026 16:03:57 -0600 Subject: [PATCH] Update error model in messages.proto --- .../grpc/GroupSendTokenUtil.java | 20 +-- .../grpc/KeysAnonymousGrpcService.java | 34 ++--- .../grpc/MessagesAnonymousGrpcService.java | 108 ++++++++++------ .../grpc/MessagesGrpcHelper.java | 13 +- .../grpc/MessagesGrpcService.java | 30 ++--- .../grpc/ProfileAnonymousGrpcService.java | 5 +- .../textsecuregcm/spam/GrpcResponse.java | 24 ++-- .../src/main/proto/org/signal/chat/keys.proto | 2 +- .../main/proto/org/signal/chat/messages.proto | 122 +++++++----------- .../MessagesAnonymousGrpcServiceTest.java | 116 ++++++++++++----- .../grpc/MessagesGrpcServiceTest.java | 17 ++- 11 files changed, 272 insertions(+), 219 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GroupSendTokenUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GroupSendTokenUtil.java index 3ae56c94d..89aeebc53 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GroupSendTokenUtil.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/GroupSendTokenUtil.java @@ -29,22 +29,22 @@ public class GroupSendTokenUtil { this.clock = clock; } - public void checkGroupSendToken(final ByteString serializedGroupSendToken, - final ServiceIdentifier serviceIdentifier) throws StatusException { - checkGroupSendToken(serializedGroupSendToken, List.of(serviceIdentifier.toLibsignal())); + public boolean checkGroupSendToken(final ByteString groupSendToken, final ServiceIdentifier serviceIdentifier) { + return checkGroupSendToken(groupSendToken, List.of(serviceIdentifier.toLibsignal())); } - public void checkGroupSendToken(final ByteString serializedGroupSendToken, - final Collection serviceIds) throws StatusException { - + public boolean checkGroupSendToken(final ByteString groupSendToken, final Collection serviceIds) { try { - final GroupSendFullToken token = new GroupSendFullToken(serializedGroupSendToken.toByteArray()); - token.verify(serviceIds, clock.instant(), GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams)); + final GroupSendFullToken token = new GroupSendFullToken(groupSendToken.toByteArray()); + final GroupSendDerivedKeyPair groupSendKeyPair = + GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams); + token.verify(serviceIds, clock.instant(), groupSendKeyPair); + return true; } catch (final InvalidInputException e) { - throw Status.INVALID_ARGUMENT.asException(); + throw GrpcExceptions.fieldViolation("group_send_token", "malformed group send token"); } catch (VerificationFailedException e) { - throw Status.UNAUTHENTICATED.asException(); + return false; } } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java index 924671a9e..29783ed5e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java @@ -10,7 +10,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Clock; import java.util.Arrays; -import java.util.List; import org.signal.chat.errors.FailedUnidentifiedAuthorization; import org.signal.chat.errors.NotFound; import org.signal.chat.keys.CheckIdentityKeyRequest; @@ -19,11 +18,7 @@ import org.signal.chat.keys.GetPreKeysAnonymousRequest; import org.signal.chat.keys.GetPreKeysAnonymousResponse; import org.signal.chat.keys.ReactorKeysAnonymousGrpc; import org.signal.libsignal.protocol.IdentityKey; -import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.ServerSecretParams; -import org.signal.libsignal.zkgroup.VerificationFailedException; -import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair; -import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.storage.Account; @@ -37,15 +32,13 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony private final AccountsManager accountsManager; private final KeysManager keysManager; - private final ServerSecretParams serverSecretParams; - private final Clock clock; + private final GroupSendTokenUtil groupSendTokenUtil; public KeysAnonymousGrpcService( final AccountsManager accountsManager, final KeysManager keysManager, final ServerSecretParams serverSecretParams, final Clock clock) { this.accountsManager = accountsManager; this.keysManager = keysManager; - this.serverSecretParams = serverSecretParams; - this.clock = clock; + groupSendTokenUtil = new GroupSendTokenUtil(serverSecretParams, clock); } @Override @@ -59,25 +52,18 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony return switch (request.getAuthorizationCase()) { case GROUP_SEND_TOKEN -> { - try { - final GroupSendFullToken token = new GroupSendFullToken(request.getGroupSendToken().toByteArray()); - token.verify(List.of(serviceIdentifier.toLibsignal()), clock.instant(), - GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams)); - - yield lookUpAccount(serviceIdentifier) - .flatMap(targetAccount -> KeysGrpcHelper - .getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager)) - .map(preKeys -> GetPreKeysAnonymousResponse.newBuilder().setPreKeys(preKeys).build()) - .switchIfEmpty(Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder() - .setTargetNotFound(NotFound.getDefaultInstance()) - .build())); - } catch (InvalidInputException e) { - throw GrpcExceptions.fieldViolation("group_send_token", "malformed group send token"); - } catch (VerificationFailedException e) { + if (!groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), serviceIdentifier)) { yield Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder() .setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance()) .build()); } + yield lookUpAccount(serviceIdentifier) + .flatMap(targetAccount -> KeysGrpcHelper + .getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager)) + .map(preKeys -> GetPreKeysAnonymousResponse.newBuilder().setPreKeys(preKeys).build()) + .switchIfEmpty(Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder() + .setTargetNotFound(NotFound.getDefaultInstance()) + .build())); } case UNIDENTIFIED_ACCESS_KEY -> lookUpAccount(serviceIdentifier) .filter(targetAccount -> diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java index fc2b8b2eb..d72b36ba2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java @@ -6,14 +6,16 @@ package org.whispersystems.textsecuregcm.grpc; import com.google.protobuf.ByteString; -import io.grpc.Status; -import io.grpc.StatusException; 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.FailedUnidentifiedAuthorization; +import org.signal.chat.errors.NotFound; import org.signal.chat.messages.IndividualRecipientMessageBundle; import org.signal.chat.messages.MultiRecipientMismatchedDevices; +import org.signal.chat.messages.MultiRecipientSuccess; import org.signal.chat.messages.SendMessageResponse; import org.signal.chat.messages.SendMultiRecipientMessageRequest; import org.signal.chat.messages.SendMultiRecipientMessageResponse; @@ -52,7 +54,10 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me private final SpamChecker spamChecker; private final Clock clock; - private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse.newBuilder().build(); + private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse + .newBuilder() + .setSuccess(Empty.getDefaultInstance()) + .build(); public MessagesAnonymousGrpcService(final AccountsManager accountsManager, final RateLimiters rateLimiters, @@ -65,35 +70,51 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me this.accountsManager = accountsManager; this.rateLimiters = rateLimiters; this.messageSender = messageSender; + this.groupSendTokenUtil = groupSendTokenUtil; this.messageByteLimitEstimator = messageByteLimitEstimator; this.spamChecker = spamChecker; this.clock = clock; - this.groupSendTokenUtil = groupSendTokenUtil; } @Override public SendMessageResponse sendSingleRecipientMessage(final SendSealedSenderMessageRequest request) - throws StatusException, RateLimitExceededException { + throws RateLimitExceededException { final ServiceIdentifier destinationServiceIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getDestination()); - final Account destination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier) - .orElseThrow(Status.UNAUTHENTICATED::asException); + final Optional maybeDestination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier); - switch (request.getAuthorizationCase()) { + final boolean authorized = switch (request.getAuthorizationCase()) { case UNIDENTIFIED_ACCESS_KEY -> { - if (!UnidentifiedAccessUtil.checkUnidentifiedAccess(destination, request.getUnidentifiedAccessKey().toByteArray())) { - throw Status.UNAUTHENTICATED.asException(); + if (destinationServiceIdentifier.identityType() == IdentityType.PNI) { + throw GrpcExceptions.fieldViolation("authorization", + "message for PNI cannot be authenticated with an unidentified access token"); } + final byte[] uak = request.getUnidentifiedAccessKey().toByteArray(); + yield maybeDestination + .map(account -> UnidentifiedAccessUtil.checkUnidentifiedAccess(account, uak)) + // If the destination is not found, return an authorization error instead of not-found. Otherwise, + // this would provide an unauthenticated existence check. + .orElse(false); } case GROUP_SEND_TOKEN -> groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), destinationServiceIdentifier); + case AUTHORIZATION_NOT_SET -> + throw GrpcExceptions.fieldViolation("authorization", "expected authorization token not provided"); + }; - case AUTHORIZATION_NOT_SET -> throw Status.UNAUTHENTICATED.asException(); + if (!authorized) { + return SendMessageResponse.newBuilder() + .setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance()) + .build(); } - return sendIndividualMessage(destination, + if (maybeDestination.isEmpty()) { + return SendMessageResponse.newBuilder().setDestinationNotFound(NotFound.getDefaultInstance()).build(); + } + + return sendIndividualMessage(maybeDestination.get(), destinationServiceIdentifier, request.getMessages(), request.getEphemeral(), @@ -103,7 +124,7 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me @Override public SendMessageResponse sendStory(final SendStoryMessageRequest request) - throws StatusException, RateLimitExceededException { + throws RateLimitExceededException { final ServiceIdentifier destinationServiceIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getDestination()); @@ -132,7 +153,7 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me final IndividualRecipientMessageBundle messages, final boolean ephemeral, final boolean urgent, - final boolean story) throws StatusException, RateLimitExceededException { + final boolean story) throws RateLimitExceededException { final SpamCheckResult> spamCheckResult = spamChecker.checkForIndividualRecipientSpamGrpc( @@ -196,13 +217,16 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me } @Override - public SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SendMultiRecipientMessageRequest request) - throws StatusException { + public SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SendMultiRecipientMessageRequest request) { final SealedSenderMultiRecipientMessage multiRecipientMessage = parseAndValidateMultiRecipientMessage(request.getMessage().getPayload().toByteArray()); - groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), multiRecipientMessage.getRecipients().keySet()); + if (!groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), multiRecipientMessage.getRecipients().keySet())) { + return SendMultiRecipientMessageResponse.newBuilder() + .setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance()) + .build(); + } return sendMultiRecipientMessage(multiRecipientMessage, request.getMessage().getTimestamp(), @@ -212,21 +236,26 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me } @Override - public SendMultiRecipientMessageResponse sendMultiRecipientStory(final SendMultiRecipientStoryRequest request) - throws StatusException { + public SendMultiRecipientMessageResponse sendMultiRecipientStory(final SendMultiRecipientStoryRequest request) { final SealedSenderMultiRecipientMessage multiRecipientMessage = parseAndValidateMultiRecipientMessage(request.getMessage().getPayload().toByteArray()); - return sendMultiRecipientMessage(multiRecipientMessage, + final SendMultiRecipientMessageResponse sendMultiRecipientMessageResponse = sendMultiRecipientMessage( + multiRecipientMessage, request.getMessage().getTimestamp(), false, request.getUrgent(), - true) - .toBuilder() - // Don't identify unresolved recipients for stories - .clearUnresolvedRecipients() - .build(); + true); + if (sendMultiRecipientMessageResponse.hasSuccess()) { + // Clear the unresolved recipients for stories + return sendMultiRecipientMessageResponse.toBuilder() + .setSuccess(MultiRecipientSuccess.getDefaultInstance()) + .build(); + } else { + return sendMultiRecipientMessageResponse; + } + } private SendMultiRecipientMessageResponse sendMultiRecipientMessage( @@ -234,7 +263,7 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me final long timestamp, final boolean ephemeral, final boolean urgent, - final boolean story) throws StatusException { + final boolean story) { final SpamCheckResult> spamCheckResult = spamChecker.checkForMultiRecipientSpamGrpc(story @@ -257,20 +286,18 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me story, ephemeral, urgent, - RequestAttributesUtil.getUserAgent().orElse(null)); + RequestAttributesUtil.getUserAgent().orElse(null)) + .join(); - final SendMultiRecipientMessageResponse.Builder responseBuilder = SendMultiRecipientMessageResponse.newBuilder(); + final MultiRecipientSuccess.Builder responseBuilder = MultiRecipientSuccess.newBuilder(); MessageUtil.getUnresolvedRecipients(multiRecipientMessage, resolvedRecipients).stream() .map(ServiceIdentifierUtil::toGrpcServiceIdentifier) .forEach(responseBuilder::addUnresolvedRecipients); - return responseBuilder.build(); + return SendMultiRecipientMessageResponse.newBuilder().setSuccess(responseBuilder).build(); } catch (final MessageTooLargeException e) { - throw Status.INVALID_ARGUMENT - .withDescription("Message for an individual recipient was too large") - .withCause(e) - .asRuntimeException(); + throw GrpcExceptions.invalidArguments("message for an individual recipient was too large"); } catch (final MultiRecipientMismatchedDevicesException e) { final MultiRecipientMismatchedDevices.Builder mismatchedDevicesBuilder = MultiRecipientMismatchedDevices.newBuilder(); @@ -285,22 +312,29 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me } private SealedSenderMultiRecipientMessage parseAndValidateMultiRecipientMessage( - final byte[] serializedMultiRecipientMessage) throws StatusException { + final byte[] serializedMultiRecipientMessage) { final SealedSenderMultiRecipientMessage multiRecipientMessage; try { multiRecipientMessage = SealedSenderMultiRecipientMessage.parse(serializedMultiRecipientMessage); - } catch (final InvalidMessageException | InvalidVersionException e) { - throw Status.INVALID_ARGUMENT.withCause(e).asException(); + } catch (final InvalidMessageException _) { + throw GrpcExceptions.fieldViolation("payload", "invalid multi-recipient message"); + } catch (final InvalidVersionException e) { + throw GrpcExceptions.fieldViolation("payload", "unrecognized sealed sender major version"); + } + + if (multiRecipientMessage.getRecipients().isEmpty()) { + throw GrpcExceptions.fieldViolation("payload", "recipient list is empty"); } // Check that the request is well-formed and doesn't contain repeated entries for the same device for the same // recipient if (MessageUtil.hasDuplicateDevices(multiRecipientMessage)) { - throw Status.INVALID_ARGUMENT.withDescription("Multi-recipient message contains duplicate recipient").asException(); + throw GrpcExceptions.fieldViolation("payload", "multi-recipient message contains duplicate recipient"); } return multiRecipientMessage; } + } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcHelper.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcHelper.java index cabeca5be..5e753fce3 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcHelper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcHelper.java @@ -5,6 +5,7 @@ package org.whispersystems.textsecuregcm.grpc; +import com.google.protobuf.Empty; import io.grpc.Status; import io.grpc.StatusException; import java.util.Map; @@ -21,7 +22,10 @@ import org.whispersystems.textsecuregcm.storage.Account; public class MessagesGrpcHelper { - private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse.newBuilder().build(); + 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 @@ -37,9 +41,8 @@ public class MessagesGrpcHelper { * * @return a response object to send to callers * - * @throws StatusException if the message bundle could not be sent due to an out-of-date device set or an invalid - * message payload * @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, @@ -47,7 +50,7 @@ public class MessagesGrpcHelper { final Map messagesByDeviceId, final Map registrationIdsByDeviceId, @SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional syncMessageSenderDeviceId) - throws StatusException, RateLimitExceededException { + throws RateLimitExceededException { try { messageSender.sendMessages(destination, @@ -63,7 +66,7 @@ public class MessagesGrpcHelper { .setMismatchedDevices(buildMismatchedDevices(destinationServiceIdentifier, e.getMismatchedDevices())) .build(); } catch (final MessageTooLargeException e) { - throw Status.INVALID_ARGUMENT.withDescription("Message too large").withCause(e).asException(); + throw GrpcExceptions.invalidArguments("message too large"); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java index 932329a8f..482d3ab8e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java @@ -6,12 +6,11 @@ package org.whispersystems.textsecuregcm.grpc; import com.google.protobuf.ByteString; -import io.grpc.Status; -import io.grpc.StatusException; import java.time.Clock; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import org.signal.chat.errors.NotFound; import org.signal.chat.messages.AuthenticatedSenderMessageType; import org.signal.chat.messages.IndividualRecipientMessageBundle; import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest; @@ -60,24 +59,25 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { @Override public SendMessageResponse sendMessage(final SendAuthenticatedSenderMessageRequest request) - throws StatusException, RateLimitExceededException { + throws RateLimitExceededException { final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); final AciServiceIdentifier senderServiceIdentifier = new AciServiceIdentifier(authenticatedDevice.accountIdentifier()); - final Account sender = - accountsManager.getByServiceIdentifier(senderServiceIdentifier).orElseThrow(Status.UNAUTHENTICATED::asException); + final Account sender = accountsManager.getByServiceIdentifier(senderServiceIdentifier) + .orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials")); final ServiceIdentifier destinationServiceIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getDestination()); if (sender.isIdentifiedBy(destinationServiceIdentifier)) { - throw Status.INVALID_ARGUMENT - .withDescription("Use `sendSyncMessage` to send messages to own account") - .asException(); + throw GrpcExceptions.invalidArguments("use `sendSyncMessage` to send messages to own account"); } - final Account destination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier) - .orElseThrow(Status.NOT_FOUND::asException); + final Optional maybeDestination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier); + if (maybeDestination.isEmpty()) { + return SendMessageResponse.newBuilder().setDestinationNotFound(NotFound.getDefaultInstance()).build(); + } + final Account destination = maybeDestination.get(); rateLimiters.getMessagesLimiter().validate(authenticatedDevice.accountIdentifier(), destination.getUuid()); @@ -93,12 +93,12 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { @Override public SendMessageResponse sendSyncMessage(final SendSyncMessageRequest request) - throws StatusException, RateLimitExceededException { + throws RateLimitExceededException { final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); final AciServiceIdentifier senderServiceIdentifier = new AciServiceIdentifier(authenticatedDevice.accountIdentifier()); - final Account sender = - accountsManager.getByServiceIdentifier(senderServiceIdentifier).orElseThrow(Status.UNAUTHENTICATED::asException); + final Account sender = accountsManager.getByServiceIdentifier(senderServiceIdentifier) + .orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials")); return sendMessage(sender, senderServiceIdentifier, @@ -117,7 +117,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { final MessageType messageType, final IndividualRecipientMessageBundle messages, final boolean ephemeral, - final boolean urgent) throws StatusException, RateLimitExceededException { + final boolean urgent) throws RateLimitExceededException { try { final int totalPayloadLength = messages.getMessagesMap().values().stream() @@ -182,7 +182,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { case PREKEY_MESSAGE -> MessageProtos.Envelope.Type.PREKEY_BUNDLE; case PLAINTEXT_CONTENT -> MessageProtos.Envelope.Type.PLAINTEXT_CONTENT; case UNSPECIFIED, UNRECOGNIZED -> - throw Status.INVALID_ARGUMENT.withDescription("Unrecognized envelope type").asRuntimeException(); + throw GrpcExceptions.invalidArguments("unrecognized envelope type"); }; } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java index 92beb562d..260805e23 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java @@ -57,8 +57,9 @@ public class ProfileAnonymousGrpcService extends SimpleProfileAnonymousGrpc.Prof final Account account = switch (request.getAuthenticationCase()) { case GROUP_SEND_TOKEN -> { - groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), targetIdentifier); - + if (!groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), targetIdentifier)) { + throw Status.UNAUTHENTICATED.asException(); + } yield accountsManager.getByServiceIdentifier(targetIdentifier) .orElseThrow(Status.NOT_FOUND::asException); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcResponse.java index bc526f8fd..90f22f53f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcResponse.java @@ -7,6 +7,7 @@ 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; @@ -18,14 +19,18 @@ import java.util.Optional; */ public class GrpcResponse { - private final Status status; + @Nullable + private final StatusRuntimeException statusException; @Nullable private final R response; - private GrpcResponse(final Status status, @Nullable final R response) { - this.status = status; + 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"); + } } /** @@ -37,7 +42,7 @@ public class GrpcResponse { * * @param the type of response object */ - public static GrpcResponse withStatus(final Status status) { + public static GrpcResponse withStatusException(final StatusRuntimeException status) { return new GrpcResponse<>(status, null); } @@ -51,7 +56,7 @@ public class GrpcResponse { * @param the type of response object */ public static GrpcResponse withResponse(final R response) { - return new GrpcResponse<>(Status.OK, response); + return new GrpcResponse<>(null, response); } /** @@ -62,11 +67,10 @@ public class GrpcResponse { * * @throws StatusException if no message body is specified */ - public R getResponseOrThrowStatus() throws StatusException { - if (response != null) { - return response; + public R getResponseOrThrowStatus() throws StatusRuntimeException { + if (statusException != null) { + throw statusException; } - - throw status.asException(); + return response; } } diff --git a/service/src/main/proto/org/signal/chat/keys.proto b/service/src/main/proto/org/signal/chat/keys.proto index 4f72a21c9..eb3a81164 100644 --- a/service/src/main/proto/org/signal/chat/keys.proto +++ b/service/src/main/proto/org/signal/chat/keys.proto @@ -159,7 +159,7 @@ message GetPreKeysAnonymousResponse { // ID (if specified) was found on the target account. errors.NotFound target_not_found = 2; - // The provided anonymous authorization credential was invalid + // The provided unidentified authorization credential was invalid errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3; } } diff --git a/service/src/main/proto/org/signal/chat/messages.proto b/service/src/main/proto/org/signal/chat/messages.proto index 1161bb085..6df3de2fa 100644 --- a/service/src/main/proto/org/signal/chat/messages.proto +++ b/service/src/main/proto/org/signal/chat/messages.proto @@ -9,8 +9,11 @@ option java_multiple_files = true; package org.signal.chat.messages; +import "google/protobuf/empty.proto"; + import "org/signal/chat/common.proto"; import "org/signal/chat/require.proto"; +import "org/signal/chat/errors.proto"; // Provides methods for sending "unsealed sender" messages. service Messages { @@ -20,28 +23,12 @@ service Messages { // Sends an "unsealed sender" message to all devices linked to a single // destination account. // - // This RPC may fail with a `NOT_FOUND` status if the destination account was - // not found. It may also fail with an `INVALID_ARGUMENT` status if the - // destination account is the same as the authenticated caller (callers should - // use `SendSyncMessage` to send messages to themselves). It may also fail - // with a `RESOURCE_EXHAUSTED` status if a rate limit for sending messages has - // been exceeded, in which case a `retry-after` header containing an ISO 8601 - // duration string may be present in the response trailers. - // - // Note that message delivery may not succeed even if this RPC returns an `OK` - // status; callers must check the response object to verify that the message - // was actually accepted and sent. + // 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) {} // Sends a "sync" message to all other devices linked to the authenticated - // sender's account. This RPC may fail with a `RESOURCE_EXHAUSTED` status if a - // rate limit for sending messages has been exceeded, in which case a - // `retry-after` header containing an ISO 8601 duration string may be present - // in the response trailers. - // - // Note that message delivery may not succeed even if this RPC returns an `OK` - // status; callers must check the response object to verify that the message - // was actually accepted and sent. + // sender's account. rpc SendSyncMessage(SendSyncMessageRequest) returns (SendMessageResponse) {} } @@ -53,58 +40,23 @@ service MessagesAnonymous { // Sends a "sealed sender" message to all devices linked to a single // destination account. // - // This RPC may fail with an `UNAUTHENTICATED` status if the given credentials - // were not accepted for any reason or if the destination account was not - // found while using an unidentified access key (UAK) for authorization. It - // may also fail with a `NOT_FOUND` status if the destination account was not - // found while using a group send token for authorization. It may also fail - // with a `RESOURCE_EXHAUSTED` status if a rate limit for sending messages has - // been exceeded, in which case a `retry-after` header containing an ISO 8601 - // duration string may be present in the response trailers. - // - // Note that message delivery may not succeed even if this RPC returns an `OK` - // status; callers must check the response object to verify that the message - // was actually accepted and sent. + // If this RPC is authorized with an unidentified access key, it will fail + // with an authorization failure if the credential is invalid OR if the + // destination account was not found. If it is authorized using a group send + // token, it will fail with an authorization failure if the credential is + // invalid and with an destination not found error if the account does not + // exist rpc SendSingleRecipientMessage(SendSealedSenderMessageRequest) returns (SendMessageResponse) {} // Sends a "sealed sender" message with a common payload to all devices linked // to multiple destination accounts. - // - // This RPC may fail with a `NOT_FOUND` status if one or more destination - // accounts were not found. It may also fail with an `UNAUTHENTICATED` status - // if the given credentials were not accepted for any reason. It may also fail - // with a `RESOURCE_EXHAUSTED` status if a rate limit for sending messages has - // been exceeded, in which case a `retry-after` header containing an ISO 8601 - // duration string may be present in the response trailers. - // - // Note that message delivery may not succeed even if this RPC returns an `OK` - // status; callers must check the response object to verify that the message - // was actually accepted and sent. rpc SendMultiRecipientMessage(SendMultiRecipientMessageRequest) returns (SendMultiRecipientMessageResponse) {} // Sends a story message to devices linked to a single destination account. - // - // This RPC may fail with a `RESOURCE_EXHAUSTED` status if a rate limit for - // sending stories has been exceeded, in which case a `retry-after` header - // containing an ISO 8601 duration string may be present in the response - // trailers. - // - // Note that message delivery may not succeed even if this RPC returns an `OK` - // status; callers must check the response object to verify that the message - // was actually accepted and sent. rpc SendStory(SendStoryMessageRequest) returns (SendMessageResponse) {} // Sends a story message with a common payload to devices linked to devices // linked to multiple destination accounts. - // - // This RPC may fail with a `RESOURCE_EXHAUSTED` status if a rate limit for - // sending stories has been exceeded, in which case a `retry-after` header - // containing an ISO 8601 duration string may be present in the response - // trailers. - // - // Note that message delivery may not succeed even if this RPC returns an `OK` - // status; callers must check the response object to verify that the message - // was actually accepted and sent. rpc SendMultiRecipientStory(SendMultiRecipientStoryRequest) returns (SendMultiRecipientMessageResponse) {} } @@ -117,7 +69,7 @@ message IndividualRecipientMessageBundle { uint32 registration_id = 1 [(require.range).max = 0x3fff]; // The content of the message to deliver to the destination device. - bytes payload = 2 [(require.size).max = 262144]; // 256 KiB + bytes payload = 2 [(require.size) = {min: 1, max: 262144}]; // 256 KiB } // The time, in milliseconds since the epoch, at which this message was @@ -239,18 +191,27 @@ message SendStoryMessageRequest { message SendMessageResponse { - // An error preventing message delivery. If not set, then the message(s) in - // the original request were sent to all destination devices. - oneof error { + // 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 = 1; + MismatchedDevices mismatched_devices = 2; // A description of a challenge callers must complete before sending // additional messages. - ChallengeRequired challenge_required = 2; + ChallengeRequired challenge_required = 3; + + // The provided unidentified authorization credential was invalid + errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 4; + + // The destination account did not exist + errors.NotFound destination_not_found = 5; + } } @@ -283,7 +244,7 @@ message SendMultiRecipientMessageRequest { MultiRecipientMessage message = 3; // A group send endorsement token for the destination account. - bytes group_send_token = 4; + bytes group_send_token = 4 [(require.nonEmpty) = true]; } message SendMultiRecipientStoryRequest { @@ -298,19 +259,21 @@ message SendMultiRecipientStoryRequest { MultiRecipientMessage message = 2; } +message MultiRecipientSuccess { + // A list of destination service identifiers that could not be resolved to + // registered Signal accounts. The message in the original request was sent + // to all service identifiers/devices in the original request except for the + // destination devices associated with the service identifiers in this list. + repeated common.ServiceIdentifier unresolved_recipients = 1; +} + message SendMultiRecipientMessageResponse { - // A list of destination service identifiers that could not be resolved to - // registered Signal accounts. If `mismatched_devices` is empty, then the - // message in the original request was sent to all service identifiers/devices - // in the original request except for the destination devices associated with - // the service identifiers in this list. - repeated common.ServiceIdentifier unresolved_recipients = 1; - - // An error preventing message delivery. If not set, then the message was sent - // to some or all destination accounts/devices identified in the original - // request. - oneof error { + // The outcome of the message delivery + oneof response { + // The message was sent to at least some of the destination accounts/devices + // identified in the original request. + MultiRecipientSuccess success = 1; // A list of sets of discrepancies between the destination devices // identified in a request to send a message and the devices that are @@ -320,6 +283,9 @@ message SendMultiRecipientMessageResponse { // 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; } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java index 8cbaadf8c..c5b0e2fb4 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java @@ -12,7 +12,6 @@ import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -20,8 +19,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; import io.grpc.Status; -import io.grpc.StatusException; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -38,12 +37,14 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.mockito.Mock; +import org.signal.chat.errors.FailedUnidentifiedAuthorization; import org.signal.chat.messages.ChallengeRequired; import org.signal.chat.messages.IndividualRecipientMessageBundle; import org.signal.chat.messages.MessagesAnonymousGrpc; import org.signal.chat.messages.MismatchedDevices; import org.signal.chat.messages.MultiRecipientMessage; import org.signal.chat.messages.MultiRecipientMismatchedDevices; +import org.signal.chat.messages.MultiRecipientSuccess; import org.signal.chat.messages.SendMessageResponse; import org.signal.chat.messages.SendMultiRecipientMessageRequest; import org.signal.chat.messages.SendMultiRecipientMessageResponse; @@ -57,6 +58,7 @@ import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.limits.CardinalityEstimator; import org.whispersystems.textsecuregcm.limits.RateLimiter; @@ -119,7 +121,7 @@ class MessagesAnonymousGrpcServiceTest extends } @BeforeEach - void setUp() throws StatusException { + void setUp() { when(accountsManager.getByServiceIdentifier(any())).thenReturn(Optional.empty()); when(accountsManager.getByServiceIdentifierAsync(any())) .thenReturn(CompletableFuture.completedFuture(Optional.empty())); @@ -127,17 +129,15 @@ class MessagesAnonymousGrpcServiceTest extends when(rateLimiters.getInboundMessageBytes()).thenReturn(rateLimiter); when(rateLimiters.getStoriesLimiter()).thenReturn(rateLimiter); - doThrow(Status.UNAUTHENTICATED.asException()).when(groupSendTokenUtil) - .checkGroupSendToken(any(), any(ServiceIdentifier.class)); + when(groupSendTokenUtil.checkGroupSendToken(any(), any(ServiceIdentifier.class))).thenReturn(false); - doThrow(Status.UNAUTHENTICATED.asException()).when(groupSendTokenUtil) - .checkGroupSendToken(any(), anyCollection()); + when(groupSendTokenUtil.checkGroupSendToken(any(), anyCollection())).thenReturn(false); - doAnswer(invocation -> null).when(groupSendTokenUtil) - .checkGroupSendToken(eq(ByteString.copyFrom(GROUP_SEND_TOKEN)), any(ServiceIdentifier.class)); + when(groupSendTokenUtil.checkGroupSendToken(eq(ByteString.copyFrom(GROUP_SEND_TOKEN)), any(ServiceIdentifier.class))) + .thenReturn(true); - doAnswer(invocation -> null).when(groupSendTokenUtil) - .checkGroupSendToken(eq(ByteString.copyFrom(GROUP_SEND_TOKEN)), anyCollection()); + when(groupSendTokenUtil.checkGroupSendToken(eq(ByteString.copyFrom(GROUP_SEND_TOKEN)), anyCollection())) + .thenReturn(true); when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) .thenReturn(new SpamCheckResult<>(Optional.empty(), Optional.empty())); @@ -189,7 +189,7 @@ class MessagesAnonymousGrpcServiceTest extends useUak ? UNIDENTIFIED_ACCESS_KEY : null, useUak ? null : GROUP_SEND_TOKEN)); - assertEquals(SendMessageResponse.newBuilder().build(), response); + assertEquals(SendMessageResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response); final MessageProtos.Envelope.Builder expectedEnvelopeBuilder = MessageProtos.Envelope.newBuilder() .setType(MessageProtos.Envelope.Type.UNIDENTIFIED_SENDER) @@ -282,9 +282,11 @@ class MessagesAnonymousGrpcServiceTest extends final byte[] incorrectGroupSendToken = GROUP_SEND_TOKEN.clone(); incorrectGroupSendToken[0] += 1; - //noinspection ResultOfMethodCallIgnored - GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, - () -> unauthenticatedServiceStub().sendSingleRecipientMessage( + assertEquals( + SendMessageResponse.newBuilder() + .setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance()) + .build(), + unauthenticatedServiceStub().sendSingleRecipientMessage( generateRequest(serviceIdentifier, false, true, messages, useUak ? incorrectUnidentifiedAccessKey : null, useUak ? null : incorrectGroupSendToken))); @@ -302,14 +304,45 @@ class MessagesAnonymousGrpcServiceTest extends .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) .build()); - //noinspection ResultOfMethodCallIgnored - GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, - () -> unauthenticatedServiceStub().sendSingleRecipientMessage( - generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null))); + final SendMessageResponse response = unauthenticatedServiceStub().sendSingleRecipientMessage( + generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null)); + assertEquals( + SendMessageResponse.newBuilder() + .setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance()) + .build(), + response); verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any()); } + @Test + void pniIdentifierWithUak() throws MessageTooLargeException, MismatchedDevicesException { + final byte deviceId = Device.PRIMARY_ID; + final int registrationId = 7; + final Device destinationDevice = DevicesHelper.createDevice(deviceId, CLOCK.millis(), registrationId); + + final Account destinationAccount = mock(Account.class); + when(destinationAccount.getDevices()).thenReturn(List.of(destinationDevice)); + when(destinationAccount.getDevice(deviceId)).thenReturn(Optional.of(destinationDevice)); + when(destinationAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY)); + + final PniServiceIdentifier pniIdentifier = new PniServiceIdentifier(UUID.randomUUID()); + when(accountsManager.getByServiceIdentifier(pniIdentifier)).thenReturn(Optional.of(destinationAccount)); + + final Map messages = + Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() + .setRegistrationId(registrationId) + .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) + .build()); + + final SendSealedSenderMessageRequest request = + generateRequest(pniIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null); + + GrpcTestUtils.assertStatusException( + Status.INVALID_ARGUMENT, + () -> unauthenticatedServiceStub().sendSingleRecipientMessage(request)); + } + @Test void rateLimited() throws RateLimitExceededException, MessageTooLargeException, MismatchedDevicesException { final byte deviceId = Device.PRIMARY_ID; @@ -393,7 +426,9 @@ class MessagesAnonymousGrpcServiceTest extends .build()); when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) - .thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withStatus(Status.RESOURCE_EXHAUSTED)), Optional.empty())); + .thenReturn(new SpamCheckResult<>( + Optional.of(GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.empty())); //noinspection ResultOfMethodCallIgnored GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, @@ -510,6 +545,10 @@ class MessagesAnonymousGrpcServiceTest extends final byte[] payload = MultiRecipientMessageHelper.generateMultiRecipientMessage(List.of( resolvedRecipient, unresolvedRecipient)); + when(messageSender + .sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + final SendMultiRecipientMessageRequest request = SendMultiRecipientMessageRequest.newBuilder() .setGroupSendToken(ByteString.copyFrom(GROUP_SEND_TOKEN)) .setMessage(MultiRecipientMessage.newBuilder() @@ -524,7 +563,9 @@ class MessagesAnonymousGrpcServiceTest extends unauthenticatedServiceStub().sendMultiRecipientMessage(request); final SendMultiRecipientMessageResponse expectedResponse = SendMultiRecipientMessageResponse.newBuilder() - .addUnresolvedRecipients(ServiceIdentifierUtil.toGrpcServiceIdentifier(unresolvedServiceIdentifier)) + .setSuccess(MultiRecipientSuccess.newBuilder() + .addUnresolvedRecipients(ServiceIdentifierUtil.toGrpcServiceIdentifier(unresolvedServiceIdentifier)) + .build()) .build(); assertEquals(expectedResponse, response); @@ -608,8 +649,10 @@ class MessagesAnonymousGrpcServiceTest extends final byte[] incorrectGroupSendToken = GROUP_SEND_TOKEN.clone(); incorrectGroupSendToken[0] += 1; - //noinspection ResultOfMethodCallIgnored - GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, () -> + assertEquals( + SendMultiRecipientMessageResponse.newBuilder() + .setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance()) + .build(), unauthenticatedServiceStub().sendMultiRecipientMessage(SendMultiRecipientMessageRequest.newBuilder() .setGroupSendToken(ByteString.copyFrom(incorrectGroupSendToken)) .setMessage(MultiRecipientMessage.newBuilder() @@ -621,7 +664,7 @@ class MessagesAnonymousGrpcServiceTest extends .build())); //noinspection ResultOfMethodCallIgnored - GrpcTestUtils.assertStatusException(Status.UNAUTHENTICATED, () -> + GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientMessage(SendMultiRecipientMessageRequest.newBuilder() .setMessage(MultiRecipientMessage.newBuilder() .setTimestamp(CLOCK.millis()) @@ -749,7 +792,9 @@ class MessagesAnonymousGrpcServiceTest extends .build(); when(spamChecker.checkForMultiRecipientSpamGrpc(any())) - .thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withStatus(Status.RESOURCE_EXHAUSTED)), Optional.empty())); + .thenReturn(new SpamCheckResult<>( + Optional.of(GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.empty())); //noinspection ResultOfMethodCallIgnored GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, @@ -847,7 +892,7 @@ class MessagesAnonymousGrpcServiceTest extends final SendMessageResponse response = unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, urgent, messages)); - assertEquals(SendMessageResponse.newBuilder().build(), response); + assertEquals(SendMessageResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response); final MessageProtos.Envelope.Builder expectedEnvelopeBuilder = MessageProtos.Envelope.newBuilder() .setType(MessageProtos.Envelope.Type.UNIDENTIFIED_SENDER) @@ -924,7 +969,7 @@ class MessagesAnonymousGrpcServiceTest extends final SendMessageResponse response = unauthenticatedServiceStub().sendStory( generateRequest(new AciServiceIdentifier(UUID.randomUUID()), true, messages)); - assertEquals(SendMessageResponse.newBuilder().build(), response); + assertEquals(SendMessageResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response); verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any()); } @@ -1006,7 +1051,9 @@ class MessagesAnonymousGrpcServiceTest extends .build()); when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) - .thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withStatus(Status.RESOURCE_EXHAUSTED)), Optional.empty())); + .thenReturn(new SpamCheckResult<>( + Optional.of(GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.empty())); //noinspection ResultOfMethodCallIgnored GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, @@ -1106,6 +1153,10 @@ class MessagesAnonymousGrpcServiceTest extends final byte[] payload = MultiRecipientMessageHelper.generateMultiRecipientMessage(List.of( resolvedRecipient, unresolvedRecipient)); + when(messageSender + .sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + final SendMultiRecipientStoryRequest request = SendMultiRecipientStoryRequest.newBuilder() .setMessage(MultiRecipientMessage.newBuilder() .setTimestamp(CLOCK.millis()) @@ -1114,7 +1165,10 @@ class MessagesAnonymousGrpcServiceTest extends .setUrgent(urgent) .build(); - assertEquals(SendMultiRecipientMessageResponse.newBuilder().build(), + assertEquals( + SendMultiRecipientMessageResponse.newBuilder() + .setSuccess(MultiRecipientSuccess.getDefaultInstance()) + .build(), unauthenticatedServiceStub().sendMultiRecipientStory(request)); verify(messageSender).sendMultiRecipientMessage(any(), @@ -1280,7 +1334,9 @@ class MessagesAnonymousGrpcServiceTest extends .build(); when(spamChecker.checkForMultiRecipientSpamGrpc(any())) - .thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withStatus(Status.RESOURCE_EXHAUSTED)), Optional.empty())); + .thenReturn(new SpamCheckResult<>(Optional.of( + GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.empty())); //noinspection ResultOfMethodCallIgnored GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java index d86082d44..04dd659de 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java @@ -6,6 +6,7 @@ package org.whispersystems.textsecuregcm.grpc; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyByte; import static org.mockito.ArgumentMatchers.anyLong; @@ -17,6 +18,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; import io.grpc.Status; import java.time.Duration; import java.time.Instant; @@ -185,7 +187,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest MessageProtos.Envelope.Type.CIPHERTEXT; @@ -268,10 +270,9 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().sendMessage( - generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages))); + final SendMessageResponse response = authenticatedServiceStub().sendMessage( + generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages)); + assertTrue(response.hasDestinationNotFound()); verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any()); } @@ -357,7 +358,9 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest(Optional.of(GrpcResponse.withStatus(Status.RESOURCE_EXHAUSTED)), Optional.empty())); + .thenReturn(new SpamCheckResult<>( + Optional.of(GrpcResponse.withStatusException(Status.RESOURCE_EXHAUSTED.asRuntimeException())), + Optional.empty())); //noinspection ResultOfMethodCallIgnored GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, @@ -466,7 +469,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest MessageProtos.Envelope.Type.CIPHERTEXT;