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 1a281e939..f5888d280 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java @@ -14,6 +14,7 @@ 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.SendMessageType; import org.signal.chat.messages.MultiRecipientMismatchedDevices; import org.signal.chat.messages.MultiRecipientSuccess; import org.signal.chat.messages.SendMessageResponse; @@ -182,6 +183,9 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me .collect(Collectors.toMap( entry -> DeviceIdUtil.validate(entry.getKey()), entry -> { + if (entry.getValue().getType() != SendMessageType.UNIDENTIFIED_SENDER) { + throw GrpcExceptions.invalidArguments("sealed sender messages must have a type of UNIDENTIFIED_SENDER"); + } final MessageProtos.Envelope.Builder envelopeBuilder = MessageProtos.Envelope.newBuilder() .setType(MessageProtos.Envelope.Type.UNIDENTIFIED_SENDER) .setClientTimestamp(messages.getTimestamp()) 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 3b6351b4a..a8ad18228 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java @@ -12,7 +12,7 @@ import java.util.Optional; import java.util.stream.Collectors; import com.google.protobuf.Empty; import org.signal.chat.errors.NotFound; -import org.signal.chat.messages.AuthenticatedSenderMessageType; +import org.signal.chat.messages.SendMessageType; import org.signal.chat.messages.IndividualRecipientMessageBundle; import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest; import org.signal.chat.messages.SendMessageAuthenticatedSenderResponse; @@ -94,7 +94,6 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { return sendMessage(destination, destinationServiceIdentifier, authenticatedDevice, - request.getType(), MessageType.INDIVIDUAL_IDENTIFIED_SENDER, request.getMessages(), request.getEphemeral(), @@ -113,7 +112,6 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { return sendMessage(sender, senderServiceIdentifier, authenticatedDevice, - request.getType(), MessageType.SYNC, request.getMessages(), false, @@ -123,7 +121,6 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { private SendMessageAuthenticatedSenderResponse sendMessage(final Account destination, final ServiceIdentifier destinationServiceIdentifier, final AuthenticatedDevice sender, - final AuthenticatedSenderMessageType envelopeType, final MessageType messageType, final IndividualRecipientMessageBundle messages, final boolean ephemeral, @@ -158,7 +155,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { entry -> DeviceIdUtil.validate(entry.getKey()), entry -> { final MessageProtos.Envelope.Builder envelopeBuilder = MessageProtos.Envelope.newBuilder() - .setType(getEnvelopeType(envelopeType)) + .setType(getEnvelopeType(entry.getValue().getType())) .setClientTimestamp(messages.getTimestamp()) .setServerTimestamp(clock.millis()) .setDestinationServiceId(destinationServiceIdentifier.toServiceIdentifierString()) @@ -198,11 +195,13 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { } } - private static MessageProtos.Envelope.Type getEnvelopeType(final AuthenticatedSenderMessageType type) { + private static MessageProtos.Envelope.Type getEnvelopeType(final SendMessageType type) { return switch (type) { case DOUBLE_RATCHET -> MessageProtos.Envelope.Type.CIPHERTEXT; case PREKEY_MESSAGE -> MessageProtos.Envelope.Type.PREKEY_BUNDLE; case PLAINTEXT_CONTENT -> MessageProtos.Envelope.Type.PLAINTEXT_CONTENT; + case UNIDENTIFIED_SENDER -> + throw GrpcExceptions.invalidArguments("illegal envelope type for identified sends"); case UNSPECIFIED, UNRECOGNIZED -> throw GrpcExceptions.invalidArguments("unrecognized envelope type"); }; diff --git a/service/src/main/proto/org/signal/chat/messages.proto b/service/src/main/proto/org/signal/chat/messages.proto index 6b6790c44..7a51e9d80 100644 --- a/service/src/main/proto/org/signal/chat/messages.proto +++ b/service/src/main/proto/org/signal/chat/messages.proto @@ -70,6 +70,10 @@ message IndividualRecipientMessageBundle { // The content of the message to deliver to the destination device. bytes payload = 2 [(require.size) = {min: 1, max: 262144}]; // 256 KiB + + // The message type of the message. If this message is part of an + // unidentified send, this must be UNIDENTIFIED_SENDER + SendMessageType type = 3; } // The time, in milliseconds since the epoch, at which this message was @@ -87,7 +91,7 @@ message IndividualRecipientMessageBundle { map messages = 2 [(require.nonEmpty) = true]; } -enum AuthenticatedSenderMessageType { +enum SendMessageType { UNSPECIFIED = 0; // A double-ratchet message represents a "normal," "unsealed-sender" message @@ -102,7 +106,7 @@ enum AuthenticatedSenderMessageType { // A plaintext message is used solely to convey encryption error receipts // and never contains encrypted message content. Encryption error receipts - // must be delivered in plaintext because, encryption/decryption of a prior + // must be delivered in plaintext because encryption/decryption of a prior // message failed and there is no reason to believe that // encryption/decryption of subsequent messages with the same key material // would succeed. @@ -110,6 +114,14 @@ enum AuthenticatedSenderMessageType { // Critically, plaintext messages never have "real" message content // generated by users. Plaintext messages include sender information. PLAINTEXT_CONTENT = 3; + + // An unidentified sender message is an encrypted message. No other + // information about the type of the encrypted message is known to the server. + // + // Unidenitfied sender messages require an unidentified access token or a + // group send endorsement token to prove the unidentified sender is authorized + // to send messages to the destination. + UNIDENTIFIED_SENDER = 4; } message SendAuthenticatedSenderMessageRequest { @@ -117,20 +129,17 @@ message SendAuthenticatedSenderMessageRequest { // The service identifier of the account to which to deliver the message. common.ServiceIdentifier destination = 1; - // The type identifier for this message. - AuthenticatedSenderMessageType type = 2 [(require.specified) = true]; - // If true, this message will only be delivered to destination devices that // have an active message delivery channel with a Signal server. - bool ephemeral = 3; + bool ephemeral = 2; // Indicates whether this message is urgent and should trigger a high-priority // notification if the destination device does not have an active message // delivery channel with a Signal server - bool urgent = 4; + bool urgent = 3; // The messages to send to the destination account. - IndividualRecipientMessageBundle messages = 5; + IndividualRecipientMessageBundle messages = 4; } message SendMessageAuthenticatedSenderResponse { @@ -159,16 +168,13 @@ message SendMessageAuthenticatedSenderResponse { message SendSyncMessageRequest { - // The type identifier for this message. - AuthenticatedSenderMessageType type = 1 [(require.specified) = true]; - // Indicates whether this message is urgent and should trigger a high-priority // notification if the destination device does not have an active message // delivery channel with a Signal server - bool urgent = 2; + bool urgent = 1; // The messages to send to the destination account. - IndividualRecipientMessageBundle messages = 3; + IndividualRecipientMessageBundle messages = 2; } message SendSealedSenderMessageRequest { 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 3e6210889..7a61893d5 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; @@ -40,6 +41,7 @@ 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.SendMessageType; import org.signal.chat.messages.MessagesAnonymousGrpc; import org.signal.chat.messages.MismatchedDevices; import org.signal.chat.messages.MultiRecipientMessage; @@ -175,6 +177,7 @@ class MessagesAnonymousGrpcServiceTest extends Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(registrationId) .setPayload(ByteString.copyFrom(payload)) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); final byte[] reportSpamToken = TestRandomUtil.nextBytes(64); @@ -217,6 +220,37 @@ class MessagesAnonymousGrpcServiceTest extends null); } + + @Test + void wrongMessageType() { + 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 AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); + when(accountsManager.getByServiceIdentifier(serviceIdentifier)).thenReturn(Optional.of(destinationAccount)); + + final byte[] payload = TestRandomUtil.nextBytes(128); + + final Map messages = + Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() + .setRegistrationId(registrationId) + .setType(SendMessageType.DOUBLE_RATCHET) + .setPayload(ByteString.copyFrom(payload)) + .build()); + final byte[] reportSpamToken = TestRandomUtil.nextBytes(64); + GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, + () -> unauthenticatedServiceStub().sendSingleRecipientMessage( + generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null))); + verifyNoInteractions(messageSender); + } + @Test void mismatchedDevices() throws MessageTooLargeException, MismatchedDevicesException { final byte missingDeviceId = Device.PRIMARY_ID; @@ -233,6 +267,7 @@ class MessagesAnonymousGrpcServiceTest extends staleDeviceId, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(Device.PRIMARY_ID) .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices( @@ -879,6 +914,7 @@ class MessagesAnonymousGrpcServiceTest extends Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(registrationId) .setPayload(ByteString.copyFrom(payload)) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); final byte[] reportSpamToken = TestRandomUtil.nextBytes(64); @@ -936,6 +972,7 @@ class MessagesAnonymousGrpcServiceTest extends staleDeviceId, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(Device.PRIMARY_ID) .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); doThrow(new MismatchedDevicesException(new org.whispersystems.textsecuregcm.controllers.MismatchedDevices( @@ -963,6 +1000,7 @@ class MessagesAnonymousGrpcServiceTest extends Map.of(Device.PRIMARY_ID, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(7) .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); final SendMessageResponse response = unauthenticatedServiceStub().sendStory( @@ -995,6 +1033,7 @@ class MessagesAnonymousGrpcServiceTest extends Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(registrationId) .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); //noinspection ResultOfMethodCallIgnored @@ -1020,6 +1059,7 @@ class MessagesAnonymousGrpcServiceTest extends staleDeviceId, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(Device.PRIMARY_ID) .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); doThrow(new MessageTooLargeException()).when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); @@ -1047,6 +1087,7 @@ class MessagesAnonymousGrpcServiceTest extends Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(registrationId) .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) @@ -1084,6 +1125,7 @@ class MessagesAnonymousGrpcServiceTest extends Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(registrationId) .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) + .setType(SendMessageType.UNIDENTIFIED_SENDER) .build()); final ChallengeRequired challengeResponse = 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 54d701c6e..59857a06f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java @@ -15,6 +15,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; @@ -33,9 +34,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.mockito.Mock; -import org.signal.chat.messages.AuthenticatedSenderMessageType; import org.signal.chat.messages.ChallengeRequired; import org.signal.chat.messages.IndividualRecipientMessageBundle; +import org.signal.chat.messages.SendMessageType; import org.signal.chat.messages.MessagesGrpc; import org.signal.chat.messages.MismatchedDevices; import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest; @@ -151,7 +152,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest MessageProtos.Envelope.Type.CIPHERTEXT; case PREKEY_MESSAGE -> MessageProtos.Envelope.Type.PREKEY_BUNDLE; case PLAINTEXT_CONTENT -> MessageProtos.Envelope.Type.PLAINTEXT_CONTENT; - case UNSPECIFIED, UNRECOGNIZED -> throw new IllegalArgumentException("Unexpected message type: " + messageType); + case UNIDENTIFIED_SENDER, UNSPECIFIED, UNRECOGNIZED -> throw new IllegalArgumentException("Unexpected message type: " + messageType); }; final MessageProtos.Envelope.Builder expectedEnvelopeBuilder = MessageProtos.Envelope.newBuilder() @@ -224,6 +226,36 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest messages = + Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() + .setRegistrationId(registrationId) + .setPayload(ByteString.copyFrom(payload)) + .setType(SendMessageType.UNIDENTIFIED_SENDER) + .build()); + + GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub() + .sendMessage(generateRequest(serviceIdentifier, false, true, messages))); + + verifyNoInteractions(messageSender); + } + @Test void mismatchedDevices() throws MessageTooLargeException, MismatchedDevicesException { final byte missingDeviceId = Device.PRIMARY_ID; @@ -239,6 +271,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().sendMessage( - generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages))); + generateRequest(serviceIdentifier, false, true, messages))); verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any()); verify(messageByteLimitEstimator).add(serviceIdentifier.uuid().toString()); @@ -326,6 +361,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().sendMessage( - generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages))); + generateRequest(serviceIdentifier, false, true, messages))); } @Test @@ -355,6 +391,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().sendMessage( - generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages))); + GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> authenticatedServiceStub() + .sendMessage(generateRequest(serviceIdentifier, false, true, messages))); verify(spamChecker).checkForIndividualRecipientSpamGrpc(MessageType.INDIVIDUAL_IDENTIFIED_SENDER, Optional.of(new AuthenticatedDevice(AUTHENTICATED_ACI, AUTHENTICATED_DEVICE_ID)), @@ -393,6 +429,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest(Optional.of(GrpcChallengeResponse.withResponse(challengeRequired)), Optional.empty())); assertEquals(expectedResponse, authenticatedServiceStub().sendMessage( - generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages))); + generateRequest(serviceIdentifier, false, true, messages))); verify(spamChecker).checkForIndividualRecipientSpamGrpc(MessageType.INDIVIDUAL_IDENTIFIED_SENDER, Optional.of(new AuthenticatedDevice(AUTHENTICATED_ACI, AUTHENTICATED_DEVICE_ID)), @@ -417,7 +454,6 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest messages) { @@ -429,7 +465,6 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest MessageProtos.Envelope.Type.CIPHERTEXT; case PREKEY_MESSAGE -> MessageProtos.Envelope.Type.PREKEY_BUNDLE; case PLAINTEXT_CONTENT -> MessageProtos.Envelope.Type.PLAINTEXT_CONTENT; - case UNSPECIFIED, UNRECOGNIZED -> throw new IllegalArgumentException("Unexpected message type: " + messageType); + case UNIDENTIFIED_SENDER, UNSPECIFIED, UNRECOGNIZED -> throw new IllegalArgumentException("Unexpected message type: " + messageType); }; final Map expectedEnvelopes = new HashMap<>(Map.of( @@ -535,6 +572,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().sendSyncMessage( - generateRequest(AuthenticatedSenderMessageType.DOUBLE_RATCHET, true, messages))); + GrpcTestUtils.assertRateLimitExceeded(retryDuration, () -> + authenticatedServiceStub().sendSyncMessage(generateRequest(true, messages))); verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any()); verify(messageByteLimitEstimator).add(AUTHENTICATED_ACI.toString()); @@ -592,18 +630,18 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().sendSyncMessage( - generateRequest(AuthenticatedSenderMessageType.DOUBLE_RATCHET, true, messages))); + GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub() + .sendSyncMessage( generateRequest( true, messages))); } - private static SendSyncMessageRequest generateRequest(final AuthenticatedSenderMessageType messageType, + private static SendSyncMessageRequest generateRequest( final boolean urgent, final Map messages) { @@ -613,7 +651,6 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest