diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java similarity index 94% rename from app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java rename to app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index b257b6773..57e4e0b16 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Collections; @@ -46,22 +47,29 @@ import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; -public class GroupMessageProcessor { +public final class GroupV1MessageProcessor { - private static final String TAG = GroupMessageProcessor.class.getSimpleName(); + private static final String TAG = Log.tag(GroupV1MessageProcessor.class); public static @Nullable Long process(@NonNull Context context, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, boolean outgoing) { - if (!message.getGroupInfo().isPresent() || message.getGroupInfo().get().getGroupId() == null) { + SignalServiceGroupContext signalServiceGroupContext = message.getGroupContext().get(); + Optional groupV1 = signalServiceGroupContext.getGroupV1(); + + if (signalServiceGroupContext.getGroupV2().isPresent()) { + throw new AssertionError("Cannot process GV2"); + } + + if (!groupV1.isPresent() || groupV1.get().getGroupId() == null) { Log.w(TAG, "Received group message with no id! Ignoring..."); return null; } GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - SignalServiceGroup group = message.getGroupInfo().get(); + SignalServiceGroup group = groupV1.get(); GroupId id = GroupId.v1(group.getGroupId()); Optional record = database.getGroup(id); @@ -278,7 +286,7 @@ public class GroupMessageProcessor { .map(a -> a.getNumber().get()) .toList()); builder.addAllMembers(Stream.of(group.getMembers().get()) - .map(GroupMessageProcessor::createMember) + .map(GroupV1MessageProcessor::createMember) .toList()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java index f1b5bcb6c..c04b19416 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java @@ -120,7 +120,7 @@ final class V1GroupManager { for (RecipientId member : members) { Recipient recipient = Recipient.resolved(member); - uuidMembers.add(GroupMessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient))); + uuidMembers.add(GroupV1MessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient))); } GroupContext.Builder groupContextBuilder = GroupContext.newBuilder() diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index 670a87c70..72d0ae9fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -29,13 +29,13 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.util.guava.Optional; @@ -233,7 +233,7 @@ public final class PushDecryptMessageJob extends BaseJob { return new PushProcessMessageJob.ExceptionMetadata(sender, e.getSenderDevice(), - e.getGroup().transform(g -> GroupId.v1(g.getGroupId())).orNull()); + e.getGroup().transform(GroupUtil::idFromGroupContext).orNull()); } private static PushProcessMessageJob.ExceptionMetadata toExceptionMetadata(@NonNull ProtocolException e) throws NoSenderException { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 26be5a3bb..b767915c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.groups.GroupMessageProcessor; +import org.thoughtcrime.securesms.groups.GroupV1MessageProcessor; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; @@ -75,6 +75,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.Base64; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; @@ -86,6 +87,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; @@ -263,17 +265,24 @@ public final class PushProcessMessageJob extends BaseJob { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); + Optional groupId = GroupUtil.idFromGroupContext(message.getGroupContext()); + boolean isGv2Message = groupId.isPresent() && groupId.get().isV2(); - if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), toEncodedId(message.getGroupInfo()), content.getTimestamp(), smsMessageId); + if (isGv2Message) { + Log.w(TAG, "Ignoring GV2 message."); + return; + } + + if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId); else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); - else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); + else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId); else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId); else if (message.getReaction().isPresent()) handleReaction(content, message); else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId); - else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId); + else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId); - if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupId.v1(message.getGroupInfo().get().getGroupId()))) { - handleUnknownGroupMessage(content, message.getGroupInfo().get()); + if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { + handleUnknownGroupMessage(content, message.getGroupContext().get()); } if (message.getProfileKey().isPresent()) { @@ -327,10 +336,6 @@ public final class PushProcessMessageJob extends BaseJob { } } - private static @NonNull Optional toEncodedId(@NonNull Optional groupInfo) { - return groupInfo.transform(g -> GroupId.v1(g.getGroupId())); - } - private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional smsMessageId) { switch (messageState) { @@ -415,7 +420,7 @@ public final class PushProcessMessageJob extends BaseJob { { Log.i(TAG, "handleCallIceUpdateMessage... " + messages.size()); - ArrayList iceCandidates = new ArrayList(messages.size()); + ArrayList iceCandidates = new ArrayList<>(messages.size()); long callId = -1; for (IceUpdateMessage iceMessage : messages) { iceCandidates.add(new IceCandidateParcel(iceMessage)); @@ -526,12 +531,12 @@ public final class PushProcessMessageJob extends BaseJob { return threadId; } - private void handleGroupMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId) + private void handleGroupV1Message(@NonNull SignalServiceContent content, + @NonNull SignalServiceDataMessage message, + @NonNull Optional smsMessageId) throws StorageFailedException { - GroupMessageProcessor.process(context, content, message, false); + GroupV1MessageProcessor.process(context, content, message, false); if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getMessageDestination(content, message).getExpireMessages()) { handleExpirationUpdate(content, message, Optional.absent()); @@ -543,12 +548,17 @@ public final class PushProcessMessageJob extends BaseJob { } private void handleUnknownGroupMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceGroup group) + @NonNull SignalServiceGroupContext group) { - if (group.getType() != SignalServiceGroup.Type.REQUEST_INFO) { - ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(group.getGroupId()))); + if (group.getGroupV1().isPresent()) { + SignalServiceGroup groupV1 = group.getGroupV1().get(); + if (groupV1.getType() != SignalServiceGroup.Type.REQUEST_INFO) { + ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(groupV1.getGroupId()))); + } else { + Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring."); + } } else { - Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring."); + Log.w(TAG, "Received a message for a group we don't know about without a GV1 context. Ignoring."); } } @@ -567,7 +577,7 @@ public final class PushProcessMessageJob extends BaseJob { false, content.isNeedsReceipt(), Optional.absent(), - message.getGroupInfo(), + message.getGroupContext(), Optional.absent(), Optional.absent(), Optional.absent(), @@ -729,8 +739,8 @@ public final class PushProcessMessageJob extends BaseJob { handleGroupRecipientUpdate(message); } else if (message.getMessage().isEndSession()) { threadId = handleSynchronizeSentEndSessionMessage(message); - } else if (message.getMessage().isGroupUpdate()) { - threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true); + } else if (message.getMessage().isGroupV1Update()) { + threadId = GroupV1MessageProcessor.process(context, content, message.getMessage(), true); } else if (message.getMessage().isExpirationUpdate()) { threadId = handleSynchronizeSentExpirationUpdate(message); } else if (message.getMessage().getReaction().isPresent()) { @@ -743,8 +753,8 @@ public final class PushProcessMessageJob extends BaseJob { threadId = handleSynchronizeSentTextMessage(message); } - if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupId.v1(message.getMessage().getGroupInfo().get().getGroupId()))) { - handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); + if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) { + handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get()); } if (message.getMessage().getProfileKey().isPresent()) { @@ -854,7 +864,7 @@ public final class PushProcessMessageJob extends BaseJob { message.isViewOnce(), content.isNeedsReceipt(), message.getBody(), - message.getGroupInfo(), + message.getGroupContext(), message.getAttachments(), quote, sharedContacts, @@ -1040,7 +1050,8 @@ public final class PushProcessMessageJob extends BaseJob { private void handleTextMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId) + @NonNull Optional smsMessageId, + @NonNull Optional groupId) throws StorageFailedException { SmsDatabase database = DatabaseFactory.getSmsDatabase(context); @@ -1053,7 +1064,7 @@ public final class PushProcessMessageJob extends BaseJob { Long threadId; - if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { + if (smsMessageId.isPresent() && !message.getGroupContext().isPresent()) { threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second; } else { notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice()); @@ -1061,7 +1072,7 @@ public final class PushProcessMessageJob extends BaseJob { IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), message.getTimestamp(), body, - toEncodedId(message.getGroupInfo()), + groupId, message.getExpiresInSeconds() * 1000L, content.isNeedsReceipt()); @@ -1488,20 +1499,20 @@ public final class PushProcessMessageJob extends BaseJob { return database.insertMessageInbox(textMessage); } - private Recipient getSyncMessageDestination(SentTranscriptMessage message) { - if (message.getMessage().getGroupInfo().isPresent()) { - return Recipient.externalGroup(context, GroupId.v1(message.getMessage().getGroupInfo().get().getGroupId())); - } else { - return Recipient.externalPush(context, message.getDestination().get()); - } + private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message) { + return getGroupRecipient(message.getMessage().getGroupContext()) + .or(() -> Recipient.externalPush(context, message.getDestination().get())); } - private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) { - if (message.getGroupInfo().isPresent()) { - return Recipient.externalGroup(context, GroupId.v1(message.getGroupInfo().get().getGroupId())); - } else { - return Recipient.externalPush(context, content.getSender()); - } + private Recipient getMessageDestination(@NonNull SignalServiceContent content, + @NonNull SignalServiceDataMessage message) + { + return getGroupRecipient(message.getGroupContext()) + .or(() -> Recipient.externalPush(context, content.getSender())); + } + + private Optional getGroupRecipient(Optional message) { + return message.transform(groupContext -> Recipient.externalGroup(context, GroupUtil.idFromGroupContext(groupContext))); } private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull SignalServiceAddress sender, int device) { @@ -1530,8 +1541,7 @@ public final class PushProcessMessageJob extends BaseJob { return true; } else if (conversation.isGroup()) { GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - Optional groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupId.v1(message.getGroupInfo().get().getGroupId())) - : Optional.absent(); + Optional groupId = message.getGroupContext().transform(GroupUtil::idFromGroupContext); if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { return false; @@ -1540,9 +1550,9 @@ public final class PushProcessMessageJob extends BaseJob { boolean isTextMessage = message.getBody().isPresent(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent(); boolean isExpireMessage = message.isExpirationUpdate(); - boolean isContentMessage = !message.isGroupUpdate() && !isExpireMessage && (isTextMessage || isMediaMessage); + boolean isContentMessage = !message.isGroupV1Update() && !isExpireMessage && (isTextMessage || isMediaMessage); boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get()); - boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT; + boolean isLeaveMessage = message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.QUIT; return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index c7905f062..de4575065 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -8,9 +8,10 @@ import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import java.util.Collections; import java.util.LinkedList; @@ -68,7 +69,7 @@ public class IncomingMediaMessage { boolean viewOnce, boolean unidentified, Optional body, - Optional group, + Optional group, Optional> attachments, Optional quote, Optional> sharedContacts, @@ -86,7 +87,7 @@ public class IncomingMediaMessage { this.quote = quote.orNull(); this.unidentified = unidentified; - if (group.isPresent()) this.groupId = GroupId.v1(group.get().getGroupId()); + if (group.isPresent()) this.groupId = GroupUtil.idFromGroupContext(group.get()); else this.groupId = null; this.attachments.addAll(PointerAttachment.forPointers(attachments)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index f8cfc0dc6..0ba0f5571 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -1690,7 +1690,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, RemotePeer remotePeer = (RemotePeer)remote; Intent intent = new Intent(this, WebRtcCallService.class); - ArrayList iceCandidateParcels = new ArrayList(iceCandidates.size()); + ArrayList iceCandidateParcels = new ArrayList<>(iceCandidates.size()); for (IceCandidate iceCandidate : iceCandidates) { iceCandidateParcels.add(new IceCandidateParcel(iceCandidate)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java index 710408690..8650e46d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -34,6 +35,26 @@ public final class GroupUtil { private static final String TAG = Log.tag(GroupUtil.class); + /** + * Result may be a v1 or v2 GroupId. + */ + public static GroupId idFromGroupContext(@NonNull SignalServiceGroupContext groupContext) { + if (groupContext.getGroupV1().isPresent()) { + return GroupId.v1(groupContext.getGroupV1().get().getGroupId()); + } else if (groupContext.getGroupV2().isPresent()) { + return GroupId.v2(groupContext.getGroupV2().get().getMasterKey()); + } else { + throw new AssertionError(); + } + } + + /** + * Result may be a v1 or v2 GroupId. + */ + public static @NonNull Optional idFromGroupContext(@NonNull Optional groupContext) { + return groupContext.transform(GroupUtil::idFromGroupContext); + } + @WorkerThread public static Optional createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) { GroupId encodedGroupId = groupRecipient.requireGroupId(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 9c28a3a95..80e8ce059 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -27,6 +27,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; @@ -64,6 +66,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMe import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.NullMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage; @@ -451,8 +454,15 @@ public class SignalServiceMessageSender { builder.setBody(message.getBody().get()); } - if (message.getGroupInfo().isPresent()) { - builder.setGroup(createGroupContent(message.getGroupInfo().get())); + if (message.getGroupContext().isPresent()) { + SignalServiceGroupContext groupContext = message.getGroupContext().get(); + if (groupContext.getGroupV1().isPresent()) { + builder.setGroup(createGroupContent(groupContext.getGroupV1().get())); + } + + if (groupContext.getGroupV2().isPresent()) { + builder.setGroupV2(createGroupContent(groupContext.getGroupV2().get())); + } } if (message.isEndSession()) { @@ -966,6 +976,19 @@ public class SignalServiceMessageSender { return builder.build(); } + private static GroupContextV2 createGroupContent(SignalServiceGroupV2 group) { + GroupContextV2.Builder builder = GroupContextV2.newBuilder() + .setMasterKey(ByteString.copyFrom(group.getMasterKey().serialize())) + .setRevision(group.getRevision()); + + byte[] signedGroupChange = group.getSignedGroupChange(); + if (signedGroupChange != null) { + builder.setGroupChange(ByteString.copyFrom(signedGroupChange)); + } + + return builder.build(); + } + private List createSharedContactContent(List contacts) throws IOException { List results = new LinkedList<>(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index e9d009124..2ac40641e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -11,6 +11,8 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.signal.libsignal.metadata.ProtocolInvalidKeyException; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidMessageException; @@ -253,7 +255,17 @@ public final class SignalServiceContent { private static SignalServiceDataMessage createSignalServiceMessage(SignalServiceMetadata metadata, SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException, UnsupportedDataMessageException { - SignalServiceGroup groupInfo = createGroupInfo(content); + SignalServiceGroup groupInfoV1 = createGroupV1Info(content); + SignalServiceGroupV2 groupInfoV2 = createGroupV2Info(content); + Optional groupContext; + + try { + groupContext = SignalServiceGroupContext.createOptional(groupInfoV1, groupInfoV2); + } catch (InvalidMessageException e) { + throw new ProtocolInvalidMessageException(e, null, 0); + } + + List attachments = new LinkedList<>(); boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE ) != 0); boolean expirationUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0); @@ -269,7 +281,7 @@ public final class SignalServiceContent { content.getRequiredProtocolVersion(), metadata.getSender().getIdentifier(), metadata.getSenderDevice(), - Optional.fromNullable(groupInfo)); + groupContext); } for (SignalServiceProtos.AttachmentPointer pointer : content.getAttachmentsList()) { @@ -283,7 +295,7 @@ public final class SignalServiceContent { } return new SignalServiceDataMessage(metadata.getTimestamp(), - groupInfo, + groupInfoV1, groupInfoV2, attachments, content.getBody(), endSession, @@ -310,7 +322,7 @@ public final class SignalServiceContent { ? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164())) : Optional.absent(); - if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) { + if (!address.isPresent() && !dataMessage.getGroupContext().isPresent()) { throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0); } @@ -739,7 +751,7 @@ public final class SignalServiceContent { } - private static SignalServiceGroup createGroupInfo(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { + private static SignalServiceGroup createGroupV1Info(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { if (!content.hasGroup()) return null; SignalServiceGroup.Type type; @@ -799,4 +811,30 @@ public final class SignalServiceContent { return new SignalServiceGroup(content.getGroup().getId().toByteArray()); } + + private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { + if (!content.hasGroupV2()) return null; + + SignalServiceProtos.GroupContextV2 groupV2 = content.getGroupV2(); + if (!groupV2.hasMasterKey()) { + throw new ProtocolInvalidMessageException(new InvalidMessageException("No GV2 master key on message"), null, 0); + } + if (!groupV2.hasRevision()) { + throw new ProtocolInvalidMessageException(new InvalidMessageException("No GV2 revision on message"), null, 0); + } + + SignalServiceGroupV2.Builder builder; + try { + builder = SignalServiceGroupV2.newBuilder(new GroupMasterKey(groupV2.getMasterKey().toByteArray())) + .withRevision(groupV2.getRevision()); + } catch (InvalidInputException e) { + throw new ProtocolInvalidMessageException(new InvalidMessageException(e), null, 0); + } + + if (groupV2.hasGroupChange()) { + builder.withSignedGroupChange(groupV2.getGroupChange().toByteArray()); + } + + return builder.build(); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java index 28b8a5a50..a3d4067f4 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java @@ -6,6 +6,7 @@ package org.whispersystems.signalservice.api.messages; +import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -21,7 +22,7 @@ public class SignalServiceDataMessage { private final long timestamp; private final Optional> attachments; private final Optional body; - private final Optional group; + private final Optional group; private final Optional profileKey; private final boolean endSession; private final boolean expirationUpdate; @@ -34,101 +35,33 @@ public class SignalServiceDataMessage { private final boolean viewOnce; private final Optional reaction; - /** - * Construct a SignalServiceDataMessage with a body and no attachments. - * - * @param timestamp The sent timestamp. - * @param body The message contents. - */ - public SignalServiceDataMessage(long timestamp, String body) { - this(timestamp, body, 0); - } - - /** - * Construct an expiring SignalServiceDataMessage with a body and no attachments. - * - * @param timestamp The sent timestamp. - * @param body The message contents. - * @param expiresInSeconds The number of seconds in which the message should expire after having been seen. - */ - public SignalServiceDataMessage(long timestamp, String body, int expiresInSeconds) { - this(timestamp, (List)null, body, expiresInSeconds); - } - - - public SignalServiceDataMessage(final long timestamp, final SignalServiceAttachment attachment, final String body) { - this(timestamp, new LinkedList() {{add(attachment);}}, body); - } - - /** - * Construct a SignalServiceDataMessage with a body and list of attachments. - * - * @param timestamp The sent timestamp. - * @param attachments The attachments. - * @param body The message contents. - */ - public SignalServiceDataMessage(long timestamp, List attachments, String body) { - this(timestamp, attachments, body, 0); - } - - /** - * Construct an expiring SignalServiceDataMessage with a body and list of attachments. - * - * @param timestamp The sent timestamp. - * @param attachments The attachments. - * @param body The message contents. - * @param expiresInSeconds The number of seconds in which the message should expire after having been seen. - */ - public SignalServiceDataMessage(long timestamp, List attachments, String body, int expiresInSeconds) { - this(timestamp, null, attachments, body, expiresInSeconds); - } - - /** - * Construct a SignalServiceDataMessage group message with attachments and body. - * - * @param timestamp The sent timestamp. - * @param group The group information. - * @param attachments The attachments. - * @param body The message contents. - */ - public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List attachments, String body) { - this(timestamp, group, attachments, body, 0); - } - - - /** - * Construct an expiring SignalServiceDataMessage group message with attachments and body. - * - * @param timestamp The sent timestamp. - * @param group The group information. - * @param attachments The attachments. - * @param body The message contents. - * @param expiresInSeconds The number of seconds in which a message should disappear after having been seen. - */ - public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List attachments, String body, int expiresInSeconds) { - this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null, null, false, null); - } - /** * Construct a SignalServiceDataMessage. * * @param timestamp The sent timestamp. * @param group The group information (or null if none). + * @param groupV2 The group information (or null if none). * @param attachments The attachments (or null if none). * @param body The message contents. * @param endSession Flag indicating whether this message should close a session. * @param expiresInSeconds Number of seconds in which the message should disappear after being seen. */ - public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, - List attachments, - String body, boolean endSession, int expiresInSeconds, - boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate, - Quote quote, List sharedContacts, List previews, - Sticker sticker, boolean viewOnce, Reaction reaction) + SignalServiceDataMessage(long timestamp, + SignalServiceGroup group, SignalServiceGroupV2 groupV2, + List attachments, + String body, boolean endSession, int expiresInSeconds, + boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate, + Quote quote, List sharedContacts, List previews, + Sticker sticker, boolean viewOnce, Reaction reaction) { + try { + this.group = SignalServiceGroupContext.createOptional(group, groupV2); + } catch (InvalidMessageException e) { + throw new AssertionError(e); + } + this.timestamp = timestamp; this.body = Optional.fromNullable(body); - this.group = Optional.fromNullable(group); this.endSession = endSession; this.expiresInSeconds = expiresInSeconds; this.expirationUpdate = expirationUpdate; @@ -184,9 +117,9 @@ public class SignalServiceDataMessage { } /** - * @return The message group info (if any). + * @return The message group context (if any). */ - public Optional getGroupInfo() { + public Optional getGroupContext() { return group; } @@ -202,8 +135,10 @@ public class SignalServiceDataMessage { return profileKeyUpdate; } - public boolean isGroupUpdate() { - return group.isPresent() && group.get().getType() != SignalServiceGroup.Type.DELIVER; + public boolean isGroupV1Update() { + return group.isPresent() && + group.get().getGroupV1().isPresent() && + group.get().getGroupV1().get().getType() != SignalServiceGroup.Type.DELIVER; } public int getExpiresInSeconds() { @@ -244,18 +179,19 @@ public class SignalServiceDataMessage { private List sharedContacts = new LinkedList<>(); private List previews = new LinkedList<>(); - private long timestamp; - private SignalServiceGroup group; - private String body; - private boolean endSession; - private int expiresInSeconds; - private boolean expirationUpdate; - private byte[] profileKey; - private boolean profileKeyUpdate; - private Quote quote; - private Sticker sticker; - private boolean viewOnce; - private Reaction reaction; + private long timestamp; + private SignalServiceGroup group; + private SignalServiceGroupV2 groupV2; + private String body; + private boolean endSession; + private int expiresInSeconds; + private boolean expirationUpdate; + private byte[] profileKey; + private boolean profileKeyUpdate; + private Quote quote; + private Sticker sticker; + private boolean viewOnce; + private Reaction reaction; private Builder() {} @@ -265,10 +201,21 @@ public class SignalServiceDataMessage { } public Builder asGroupMessage(SignalServiceGroup group) { + if (this.groupV2 != null) { + throw new AssertionError("Can not contain both V1 and V2 group contexts."); + } this.group = group; return this; } + public Builder asGroupMessage(SignalServiceGroupV2 group) { + if (this.group != null) { + throw new AssertionError("Can not contain both V1 and V2 group contexts."); + } + this.groupV2 = group; + return this; + } + public Builder withAttachment(SignalServiceAttachment attachment) { this.attachments.add(attachment); return this; @@ -354,7 +301,7 @@ public class SignalServiceDataMessage { public SignalServiceDataMessage build() { if (timestamp == 0) timestamp = System.currentTimeMillis(); - return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession, + return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, viewOnce, reaction); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java index 218f111b8..321557aaf 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2014-2016 Open Whisper Systems * * Licensed according to the LICENSE file in this repository. diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java new file mode 100644 index 000000000..6f1ca258e --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java @@ -0,0 +1,59 @@ +package org.whispersystems.signalservice.api.messages; + +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.util.guava.Optional; + +public final class SignalServiceGroupContext { + + private final Optional groupV1; + private final Optional groupV2; + + private SignalServiceGroupContext(SignalServiceGroup groupV1) { + this.groupV1 = Optional.of(groupV1); + this.groupV2 = Optional.absent(); + } + + private SignalServiceGroupContext(SignalServiceGroupV2 groupV2) { + this.groupV1 = Optional.absent(); + this.groupV2 = Optional.of(groupV2); + } + + public Optional getGroupV1() { + return groupV1; + } + + public Optional getGroupV2() { + return groupV2; + } + + static Optional createOptional(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2) + throws InvalidMessageException + { + return Optional.fromNullable(create(groupV1, groupV2)); + } + + public static SignalServiceGroupContext create(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2) + throws InvalidMessageException + { + if (groupV1 == null && groupV2 == null) { + return null; + } + + if (groupV1 != null && groupV2 != null) { + throw new InvalidMessageException("Message cannot have both V1 and V2 group contexts."); + } + + if (groupV1 != null) { + return new SignalServiceGroupContext(groupV1); + } else { + return new SignalServiceGroupContext(groupV2); + } + } + + public SignalServiceGroup.Type getGroupV1Type() { + if (groupV1.isPresent()) { + return groupV1.get().getType(); + } + return null; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java new file mode 100644 index 000000000..1362ad4e1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java @@ -0,0 +1,66 @@ +package org.whispersystems.signalservice.api.messages; + +import org.signal.zkgroup.groups.GroupMasterKey; + +/** + * Group information to include in SignalServiceMessages destined to v2 groups. + *

+ * This class represents a "context" that is included with Signal Service messages + * to make them group messages. + */ +public final class SignalServiceGroupV2 { + + private final GroupMasterKey masterKey; + private final int revision; + private final byte[] signedGroupChange; + + private SignalServiceGroupV2(Builder builder) { + this.masterKey = builder.masterKey; + this.revision = builder.revision; + this.signedGroupChange = builder.signedGroupChange != null ? builder.signedGroupChange.clone() : null; + } + + public GroupMasterKey getMasterKey() { + return masterKey; + } + + public int getRevision() { + return revision; + } + + public byte[] getSignedGroupChange() { + return signedGroupChange; + } + + public static Builder newBuilder(GroupMasterKey masterKey) { + return new Builder(masterKey); + } + + public static class Builder { + + private final GroupMasterKey masterKey; + private int revision; + private byte[] signedGroupChange; + + private Builder(GroupMasterKey masterKey) { + if (masterKey == null) { + throw new IllegalArgumentException(); + } + this.masterKey = masterKey; + } + + Builder withRevision(int revision) { + this.revision = revision; + return this; + } + + Builder withSignedGroupChange(byte[] signedGroupChange) { + this.signedGroupChange = signedGroupChange; + return this; + } + + public SignalServiceGroupV2 build() { + return new SignalServiceGroupV2(this); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java index 3f89c8e6c..1891aa762 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java @@ -1,8 +1,7 @@ package org.whispersystems.signalservice.internal.push; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; /** * Exception that indicates that the data message has a higher required protocol version than the @@ -10,16 +9,16 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup; */ public class UnsupportedDataMessageException extends Exception { - private final int requiredVersion; - private final String sender; - private final int senderDevice; - private final Optional group; + private final int requiredVersion; + private final String sender; + private final int senderDevice; + private final Optional group; public UnsupportedDataMessageException(int currentVersion, int requiredVersion, String sender, int senderDevice, - Optional group) + Optional group) { super("Required version: " + requiredVersion + ", Our version: " + currentVersion); this.requiredVersion = requiredVersion; @@ -40,7 +39,7 @@ public class UnsupportedDataMessageException extends Exception { return senderDevice; } - public Optional getGroup() { + public Optional getGroup() { return group; } } diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index e583f5e97..37121b2ba 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -198,6 +198,7 @@ message DataMessage { optional string body = 1; repeated AttachmentPointer attachments = 2; optional GroupContext group = 3; + optional GroupContextV2 groupV2 = 15; optional uint32 flags = 4; optional uint32 expireTimer = 5; optional bytes profileKey = 6; @@ -412,6 +413,12 @@ message GroupContext { optional AttachmentPointer avatar = 5; } +message GroupContextV2 { + optional bytes masterKey = 1; + optional uint32 revision = 2; + optional bytes groupChange = 3; +} + message ContactDetails { message Avatar { optional string contentType = 1;