mirror of
https://github.com/signalapp/Signal-Server
synced 2026-02-15 07:36:31 +00:00
Update error model in messages.proto
This commit is contained in:
@@ -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<ServiceId> serviceIds) throws StatusException {
|
||||
|
||||
public boolean checkGroupSendToken(final ByteString groupSendToken, final Collection<ServiceId> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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<Account> 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<GrpcResponse<SendMessageResponse>> 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<GrpcResponse<SendMultiRecipientMessageResponse>> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Byte, MessageProtos.Envelope> messagesByDeviceId,
|
||||
final Map<Byte, Integer> registrationIdsByDeviceId,
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<Byte> 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Account> 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");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<R> {
|
||||
|
||||
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<R> {
|
||||
*
|
||||
* @param <R> the type of response object
|
||||
*/
|
||||
public static <R> GrpcResponse<R> withStatus(final Status status) {
|
||||
public static <R> GrpcResponse<R> withStatusException(final StatusRuntimeException status) {
|
||||
return new GrpcResponse<>(status, null);
|
||||
}
|
||||
|
||||
@@ -51,7 +56,7 @@ public class GrpcResponse<R> {
|
||||
* @param <R> the type of response object
|
||||
*/
|
||||
public static <R> GrpcResponse<R> withResponse(final R response) {
|
||||
return new GrpcResponse<>(Status.OK, response);
|
||||
return new GrpcResponse<>(null, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,11 +67,10 @@ public class GrpcResponse<R> {
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Byte, IndividualRecipientMessageBundle.Message> 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,
|
||||
|
||||
@@ -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<MessagesGrpcService, Me
|
||||
final SendMessageResponse response = authenticatedServiceStub().sendMessage(
|
||||
generateRequest(serviceIdentifier, messageType, ephemeral, urgent, messages));
|
||||
|
||||
assertEquals(SendMessageResponse.newBuilder().build(), response);
|
||||
assertEquals(SendMessageResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response);
|
||||
|
||||
final MessageProtos.Envelope.Type expectedEnvelopeType = switch (messageType) {
|
||||
case DOUBLE_RATCHET -> MessageProtos.Envelope.Type.CIPHERTEXT;
|
||||
@@ -268,10 +270,9 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||
.setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128)))
|
||||
.build());
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
GrpcTestUtils.assertStatusException(Status.NOT_FOUND,
|
||||
() -> 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<MessagesGrpcService, Me
|
||||
.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,
|
||||
@@ -466,7 +469,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest<MessagesGrpcService, Me
|
||||
final SendMessageResponse response =
|
||||
authenticatedServiceStub().sendSyncMessage(generateRequest(messageType, urgent, messages));
|
||||
|
||||
assertEquals(SendMessageResponse.newBuilder().build(), response);
|
||||
assertEquals(SendMessageResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response);
|
||||
|
||||
final MessageProtos.Envelope.Type expectedEnvelopeType = switch (messageType) {
|
||||
case DOUBLE_RATCHET -> MessageProtos.Envelope.Type.CIPHERTEXT;
|
||||
|
||||
Reference in New Issue
Block a user