diff --git a/libsignal/service/protobuf/InternalSerialization.proto b/libsignal/service/protobuf/InternalSerialization.proto new file mode 100644 index 0000000000..b85daa9e06 --- /dev/null +++ b/libsignal/service/protobuf/InternalSerialization.proto @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2019 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ +syntax = "proto2"; + +package textsecure; + +import "SignalService.proto"; + +option java_package = "org.whispersystems.signalservice.internal.serialize.protos"; +option java_multiple_files = true; + +message SignalServiceContentProto { + optional AddressProto localAddress = 1; + optional MetadataProto metadata = 2; + oneof data { + signalservice.DataMessage legacyDataMessage = 3; + signalservice.Content content = 4; + } +} + +message MetadataProto { + optional AddressProto address = 1; + optional int32 senderDevice = 2; + optional int64 timestamp = 3; + optional bool needsReceipt = 4; +} + +message AddressProto { + optional bytes uuid = 1; + optional string e164 = 2; + optional string relay = 3; +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java index 3f53c40b7b..01c40abd24 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java @@ -6,7 +6,6 @@ package org.whispersystems.signalservice.api.crypto; -import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import org.signal.libsignal.metadata.InvalidMetadataMessageException; @@ -24,7 +23,6 @@ import org.signal.libsignal.metadata.SealedSessionCipher.DecryptionResult; import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.whispersystems.libsignal.DuplicateMessageException; -import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.InvalidMessageException; @@ -34,65 +32,26 @@ import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.SessionCipher; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.UntrustedIdentityException; -import org.whispersystems.libsignal.logging.Log; import org.whispersystems.libsignal.protocol.CiphertextMessage; import org.whispersystems.libsignal.protocol.PreKeySignalMessage; import org.whispersystems.libsignal.protocol.SignalMessage; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; 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.SignalServiceDataMessage.Reaction; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Sticker; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; -import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; -import org.whispersystems.signalservice.api.messages.calls.BusyMessage; -import org.whispersystems.signalservice.api.messages.calls.HangupMessage; -import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; -import org.whispersystems.signalservice.api.messages.calls.OfferMessage; -import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; -import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; -import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; -import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; -import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage.VerifiedState; -import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage; -import org.whispersystems.signalservice.api.messages.shared.SharedContact; +import org.whispersystems.signalservice.api.messages.SignalServiceMetadata; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; import org.whispersystems.signalservice.internal.push.PushTransportDetails; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Verified; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; +import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer; +import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer; +import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto; import org.whispersystems.util.Base64; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage; -import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext.Type.DELIVER; - /** * This is used to decrypt received {@link SignalServiceEnvelope}s. * @@ -162,52 +121,30 @@ public class SignalServiceCipher { ProtocolInvalidVersionException, ProtocolInvalidMessageException, ProtocolInvalidKeyException, ProtocolDuplicateMessageException, SelfSendException, UnsupportedDataMessageException - { try { if (envelope.hasLegacyMessage()) { - Plaintext plaintext = decrypt(envelope, envelope.getLegacyMessage()); - DataMessage message = DataMessage.parseFrom(plaintext.getData()); - return new SignalServiceContent(createSignalServiceMessage(plaintext.getMetadata(), message), - plaintext.getMetadata().getSender(), - plaintext.getMetadata().getSenderDevice(), - plaintext.getMetadata().getTimestamp(), - plaintext.getMetadata().isNeedsReceipt()); - } else if (envelope.hasContent()) { - Plaintext plaintext = decrypt(envelope, envelope.getContent()); - Content message = Content.parseFrom(plaintext.getData()); + Plaintext plaintext = decrypt(envelope, envelope.getLegacyMessage()); + SignalServiceProtos.DataMessage dataMessage = SignalServiceProtos.DataMessage.parseFrom(plaintext.getData()); - if (message.hasDataMessage()) { - return new SignalServiceContent(createSignalServiceMessage(plaintext.getMetadata(), message.getDataMessage()), - plaintext.getMetadata().getSender(), - plaintext.getMetadata().getSenderDevice(), - plaintext.getMetadata().getTimestamp(), - plaintext.getMetadata().isNeedsReceipt()); - } else if (message.hasSyncMessage() && localAddress.matches(plaintext.getMetadata().getSender())) { - return new SignalServiceContent(createSynchronizeMessage(plaintext.getMetadata(), message.getSyncMessage()), - plaintext.getMetadata().getSender(), - plaintext.getMetadata().getSenderDevice(), - plaintext.getMetadata().getTimestamp(), - plaintext.getMetadata().isNeedsReceipt()); - } else if (message.hasCallMessage()) { - return new SignalServiceContent(createCallMessage(message.getCallMessage()), - plaintext.getMetadata().getSender(), - plaintext.getMetadata().getSenderDevice(), - plaintext.getMetadata().getTimestamp(), - plaintext.getMetadata().isNeedsReceipt()); - } else if (message.hasReceiptMessage()) { - return new SignalServiceContent(createReceiptMessage(plaintext.getMetadata(), message.getReceiptMessage()), - plaintext.getMetadata().getSender(), - plaintext.getMetadata().getSenderDevice(), - plaintext.getMetadata().getTimestamp(), - plaintext.getMetadata().isNeedsReceipt()); - } else if (message.hasTypingMessage()) { - return new SignalServiceContent(createTypingMessage(plaintext.getMetadata(), message.getTypingMessage()), - plaintext.getMetadata().getSender(), - plaintext.getMetadata().getSenderDevice(), - plaintext.getMetadata().getTimestamp(), - false); - } + SignalServiceContentProto contentProto = SignalServiceContentProto.newBuilder() + .setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress)) + .setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(plaintext.metadata)) + .setLegacyDataMessage(dataMessage) + .build(); + + return SignalServiceContent.createFromProto(contentProto); + } else if (envelope.hasContent()) { + Plaintext plaintext = decrypt(envelope, envelope.getContent()); + SignalServiceProtos.Content content = SignalServiceProtos.Content.parseFrom(plaintext.getData()); + + SignalServiceContentProto contentProto = SignalServiceContentProto.newBuilder() + .setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress)) + .setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(plaintext.metadata)) + .setContent(content) + .build(); + + return SignalServiceContent.createFromProto(contentProto); } return null; @@ -226,9 +163,9 @@ public class SignalServiceCipher { { try { - byte[] paddedMessage; - Metadata metadata; - int sessionVersion; + byte[] paddedMessage; + SignalServiceMetadata metadata; + int sessionVersion; if (!envelope.hasSource() && !envelope.isUnidentifiedSender()) { throw new ProtocolInvalidMessageException(new InvalidMessageException("Non-UD envelope is missing a source!"), null, 0); @@ -239,14 +176,14 @@ public class SignalServiceCipher { SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress); paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext)); - metadata = new Metadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false); + metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isSignalMessage()) { SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice()); SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress); paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext)); - metadata = new Metadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false); + metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), false); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isUnidentifiedSender()) { SealedSessionCipher sealedSessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1); @@ -255,7 +192,7 @@ public class SignalServiceCipher { SignalProtocolAddress protocolAddress = getPreferredProtocolAddress(signalProtocolStore, resultAddress, result.getDeviceId()); paddedMessage = result.getPaddedMessage(); - metadata = new Metadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), true); + metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), true); sessionVersion = sealedSessionCipher.getSessionVersion(protocolAddress); } else { throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); @@ -297,558 +234,16 @@ public class SignalServiceCipher { } } - private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content) - throws ProtocolInvalidMessageException, UnsupportedDataMessageException - { - SignalServiceGroup groupInfo = createGroupInfo(content); - List attachments = new LinkedList<>(); - boolean endSession = ((content.getFlags() & DataMessage.Flags.END_SESSION_VALUE ) != 0); - boolean expirationUpdate = ((content.getFlags() & DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0); - boolean profileKeyUpdate = ((content.getFlags() & DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE ) != 0); - SignalServiceDataMessage.Quote quote = createQuote(content); - List sharedContacts = createSharedContacts(content); - List previews = createPreviews(content); - Sticker sticker = createSticker(content); - Reaction reaction = createReaction(content); - - if (content.getRequiredProtocolVersion() > DataMessage.ProtocolVersion.CURRENT.getNumber()) { - throw new UnsupportedDataMessageException(DataMessage.ProtocolVersion.CURRENT.getNumber(), - content.getRequiredProtocolVersion(), - metadata.getSender().getIdentifier(), - metadata.getSenderDevice(), - Optional.fromNullable(groupInfo)); - } - - for (AttachmentPointer pointer : content.getAttachmentsList()) { - attachments.add(createAttachmentPointer(pointer)); - } - - if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) { - throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()), - metadata.getSender().getIdentifier(), - metadata.getSenderDevice()); - } - - return new SignalServiceDataMessage(metadata.getTimestamp(), - groupInfo, - attachments, - content.getBody(), - endSession, - content.getExpireTimer(), - expirationUpdate, - content.hasProfileKey() ? content.getProfileKey().toByteArray() : null, - profileKeyUpdate, - quote, - sharedContacts, - previews, - sticker, - content.getIsViewOnce(), - reaction); - } - - private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content) - throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException - { - if (content.hasSent()) { - SyncMessage.Sent sentContent = content.getSent(); - SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage()); - Optional address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid(), sentContent.getDestinationE164()) - ? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164())) - : Optional.absent(); - Map unidentifiedStatuses = new HashMap<>(); - - if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) { - throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0); - } - - for (SyncMessage.Sent.UnidentifiedDeliveryStatus status : sentContent.getUnidentifiedStatusList()) { - if (SignalServiceAddress.isValidAddress(status.getDestinationUuid(), status.getDestinationE164())) { - SignalServiceAddress recipient = new SignalServiceAddress(UuidUtil.parseOrNull(status.getDestinationUuid()), status.getDestinationE164()); - unidentifiedStatuses.put(recipient, status.getUnidentified()); - } else { - Log.w(TAG, "Encountered an invalid UnidentifiedDeliveryStatus in a SentTranscript! Ignoring."); - } - } - - return SignalServiceSyncMessage.forSentTranscript(new SentTranscriptMessage(address, - sentContent.getTimestamp(), - dataMessage, - sentContent.getExpirationStartTimestamp(), - unidentifiedStatuses, - sentContent.getIsRecipientUpdate())); - } - - if (content.hasRequest()) { - return SignalServiceSyncMessage.forRequest(new RequestMessage(content.getRequest())); - } - - if (content.getReadList().size() > 0) { - List readMessages = new LinkedList<>(); - - for (SyncMessage.Read read : content.getReadList()) { - if (SignalServiceAddress.isValidAddress(read.getSenderUuid(), read.getSenderE164())) { - SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(read.getSenderUuid()), read.getSenderE164()); - readMessages.add(new ReadMessage(address, read.getTimestamp())); - } else { - Log.w(TAG, "Encountered an invalid ReadMessage! Ignoring."); - } - } - - return SignalServiceSyncMessage.forRead(readMessages); - } - - if (content.hasViewOnceOpen()) { - if (SignalServiceAddress.isValidAddress(content.getViewOnceOpen().getSenderUuid(), content.getViewOnceOpen().getSenderE164())) { - SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getViewOnceOpen().getSenderUuid()), content.getViewOnceOpen().getSenderE164()); - ViewOnceOpenMessage timerRead = new ViewOnceOpenMessage(address, content.getViewOnceOpen().getTimestamp()); - return SignalServiceSyncMessage.forViewOnceOpen(timerRead); - } else { - throw new ProtocolInvalidMessageException(new InvalidMessageException("ViewOnceOpen message has no sender!"), null, 0); - } - } - - if (content.hasVerified()) { - if (SignalServiceAddress.isValidAddress(content.getVerified().getDestinationUuid(), content.getVerified().getDestinationE164())) { - try { - Verified verified = content.getVerified(); - SignalServiceAddress destination = new SignalServiceAddress(UuidUtil.parseOrNull(verified.getDestinationUuid()), verified.getDestinationE164()); - IdentityKey identityKey = new IdentityKey(verified.getIdentityKey().toByteArray(), 0); - - VerifiedState verifiedState; - - if (verified.getState() == Verified.State.DEFAULT) { - verifiedState = VerifiedState.DEFAULT; - } else if (verified.getState() == Verified.State.VERIFIED) { - verifiedState = VerifiedState.VERIFIED; - } else if (verified.getState() == Verified.State.UNVERIFIED) { - verifiedState = VerifiedState.UNVERIFIED; - } else { - throw new ProtocolInvalidMessageException(new InvalidMessageException("Unknown state: " + verified.getState().getNumber()), - metadata.getSender().getIdentifier(), metadata.getSenderDevice()); - } - - return SignalServiceSyncMessage.forVerified(new VerifiedMessage(destination, identityKey, verifiedState, System.currentTimeMillis())); - } catch (InvalidKeyException e) { - throw new ProtocolInvalidKeyException(e, metadata.getSender().getIdentifier(), metadata.getSenderDevice()); - } - } else { - throw new ProtocolInvalidMessageException(new InvalidMessageException("Verified message has no sender!"), null, 0); - } - } - - if (content.getStickerPackOperationList().size() > 0) { - List operations = new LinkedList<>(); - - for (SyncMessage.StickerPackOperation operation : content.getStickerPackOperationList()) { - byte[] packId = operation.hasPackId() ? operation.getPackId().toByteArray() : null; - byte[] packKey = operation.hasPackKey() ? operation.getPackKey().toByteArray() : null; - StickerPackOperationMessage.Type type = null; - - if (operation.hasType()) { - switch (operation.getType()) { - case INSTALL: type = StickerPackOperationMessage.Type.INSTALL; break; - case REMOVE: type = StickerPackOperationMessage.Type.REMOVE; break; - } - } - operations.add(new StickerPackOperationMessage(packId, packKey, type)); - } - - return SignalServiceSyncMessage.forStickerPackOperations(operations); - } - - if (content.hasBlocked()) { - List numbers = content.getBlocked().getNumbersList(); - List uuids = content.getBlocked().getUuidsList(); - List addresses = new ArrayList<>(numbers.size() + uuids.size()); - List groupIds = new ArrayList<>(content.getBlocked().getGroupIdsList().size()); - - for (String e164 : numbers) { - Optional address = SignalServiceAddress.fromRaw(null, e164); - if (address.isPresent()) { - addresses.add(address.get()); - } - } - - for (String uuid : uuids) { - Optional address = SignalServiceAddress.fromRaw(uuid, null); - if (address.isPresent()) { - addresses.add(address.get()); - } - } - - for (ByteString groupId : content.getBlocked().getGroupIdsList()) { - groupIds.add(groupId.toByteArray()); - } - - return SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)); - } - - if (content.hasConfiguration()) { - Boolean readReceipts = content.getConfiguration().hasReadReceipts() ? content.getConfiguration().getReadReceipts() : null; - Boolean unidentifiedDeliveryIndicators = content.getConfiguration().hasUnidentifiedDeliveryIndicators() ? content.getConfiguration().getUnidentifiedDeliveryIndicators() : null; - Boolean typingIndicators = content.getConfiguration().hasTypingIndicators() ? content.getConfiguration().getTypingIndicators() : null; - Boolean linkPreviews = content.getConfiguration().hasLinkPreviews() ? content.getConfiguration().getLinkPreviews() : null; - - return SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.fromNullable(readReceipts), - Optional.fromNullable(unidentifiedDeliveryIndicators), - Optional.fromNullable(typingIndicators), - Optional.fromNullable(linkPreviews))); - } - - if (content.hasFetchLatest() && content.getFetchLatest().hasType()) { - switch (content.getFetchLatest().getType()) { - case LOCAL_PROFILE: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE); - case STORAGE_MANIFEST: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST); - } - } - - return SignalServiceSyncMessage.empty(); - } - - private SignalServiceCallMessage createCallMessage(CallMessage content) { - if (content.hasOffer()) { - CallMessage.Offer offerContent = content.getOffer(); - return SignalServiceCallMessage.forOffer(new OfferMessage(offerContent.getId(), offerContent.getDescription())); - } else if (content.hasAnswer()) { - CallMessage.Answer answerContent = content.getAnswer(); - return SignalServiceCallMessage.forAnswer(new AnswerMessage(answerContent.getId(), answerContent.getDescription())); - } else if (content.getIceUpdateCount() > 0) { - List iceUpdates = new LinkedList<>(); - - for (CallMessage.IceUpdate iceUpdate : content.getIceUpdateList()) { - iceUpdates.add(new IceUpdateMessage(iceUpdate.getId(), iceUpdate.getSdpMid(), iceUpdate.getSdpMLineIndex(), iceUpdate.getSdp())); - } - - return SignalServiceCallMessage.forIceUpdates(iceUpdates); - } else if (content.hasHangup()) { - CallMessage.Hangup hangup = content.getHangup(); - return SignalServiceCallMessage.forHangup(new HangupMessage(hangup.getId())); - } else if (content.hasBusy()) { - CallMessage.Busy busy = content.getBusy(); - return SignalServiceCallMessage.forBusy(new BusyMessage(busy.getId())); - } - - return SignalServiceCallMessage.empty(); - } - - private SignalServiceReceiptMessage createReceiptMessage(Metadata metadata, ReceiptMessage content) { - SignalServiceReceiptMessage.Type type; - - if (content.getType() == ReceiptMessage.Type.DELIVERY) type = SignalServiceReceiptMessage.Type.DELIVERY; - else if (content.getType() == ReceiptMessage.Type.READ) type = SignalServiceReceiptMessage.Type.READ; - else type = SignalServiceReceiptMessage.Type.UNKNOWN; - - return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp()); - } - - private SignalServiceTypingMessage createTypingMessage(Metadata metadata, TypingMessage content) throws ProtocolInvalidMessageException { - SignalServiceTypingMessage.Action action; - - if (content.getAction() == TypingMessage.Action.STARTED) action = SignalServiceTypingMessage.Action.STARTED; - else if (content.getAction() == TypingMessage.Action.STOPPED) action = SignalServiceTypingMessage.Action.STOPPED; - else action = SignalServiceTypingMessage.Action.UNKNOWN; - - if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) { - throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()), - metadata.getSender().getIdentifier(), - metadata.getSenderDevice()); - } - - return new SignalServiceTypingMessage(action, content.getTimestamp(), - content.hasGroupId() ? Optional.of(content.getGroupId().toByteArray()) : - Optional.absent()); - } - - private SignalServiceDataMessage.Quote createQuote(DataMessage content) { - if (!content.hasQuote()) return null; - - List attachments = new LinkedList<>(); - - for (DataMessage.Quote.QuotedAttachment attachment : content.getQuote().getAttachmentsList()) { - attachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(), - attachment.getFileName(), - attachment.hasThumbnail() ? createAttachmentPointer(attachment.getThumbnail()) : null)); - } - - if (SignalServiceAddress.isValidAddress(content.getQuote().getAuthorUuid(), content.getQuote().getAuthorE164())) { - SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getQuote().getAuthorUuid()), content.getQuote().getAuthorE164()); - - return new SignalServiceDataMessage.Quote(content.getQuote().getId(), - address, - content.getQuote().getText(), - attachments); - } else { - Log.w(TAG, "Quote was missing an author! Returning null."); - return null; - } - } - - private List createPreviews(DataMessage content) { - if (content.getPreviewCount() <= 0) return null; - - List results = new LinkedList<>(); - - for (DataMessage.Preview preview : content.getPreviewList()) { - SignalServiceAttachment attachment = null; - - if (preview.hasImage()) { - attachment = createAttachmentPointer(preview.getImage()); - } - - results.add(new Preview(preview.getUrl(), - preview.getTitle(), - Optional.fromNullable(attachment))); - } - - return results; - } - - private Sticker createSticker(DataMessage content) { - if (!content.hasSticker() || - !content.getSticker().hasPackId() || - !content.getSticker().hasPackKey() || - !content.getSticker().hasStickerId() || - !content.getSticker().hasData()) - { - return null; - } - - DataMessage.Sticker sticker = content.getSticker(); - - return new Sticker(sticker.getPackId().toByteArray(), - sticker.getPackKey().toByteArray(), - sticker.getStickerId(), - createAttachmentPointer(sticker.getData())); - } - - private Reaction createReaction(DataMessage content) { - if (!content.hasReaction() || - !content.getReaction().hasEmoji() || - !(content.getReaction().hasTargetAuthorE164() || content.getReaction().hasTargetAuthorUuid()) || - !content.getReaction().hasTargetSentTimestamp()) - { - return null; - } - - DataMessage.Reaction reaction = content.getReaction(); - - return new Reaction(reaction.getEmoji(), - reaction.getRemove(), - new SignalServiceAddress(UuidUtil.parseOrNull(reaction.getTargetAuthorUuid()), reaction.getTargetAuthorE164()), - reaction.getTargetSentTimestamp()); - } - - private List createSharedContacts(DataMessage content) { - if (content.getContactCount() <= 0) return null; - - List results = new LinkedList<>(); - - for (DataMessage.Contact contact : content.getContactList()) { - SharedContact.Builder builder = SharedContact.newBuilder() - .setName(SharedContact.Name.newBuilder() - .setDisplay(contact.getName().getDisplayName()) - .setFamily(contact.getName().getFamilyName()) - .setGiven(contact.getName().getGivenName()) - .setMiddle(contact.getName().getMiddleName()) - .setPrefix(contact.getName().getPrefix()) - .setSuffix(contact.getName().getSuffix()) - .build()); - - if (contact.getAddressCount() > 0) { - for (DataMessage.Contact.PostalAddress address : contact.getAddressList()) { - SharedContact.PostalAddress.Type type = SharedContact.PostalAddress.Type.HOME; - - switch (address.getType()) { - case WORK: type = SharedContact.PostalAddress.Type.WORK; break; - case HOME: type = SharedContact.PostalAddress.Type.HOME; break; - case CUSTOM: type = SharedContact.PostalAddress.Type.CUSTOM; break; - } - - builder.withAddress(SharedContact.PostalAddress.newBuilder() - .setCity(address.getCity()) - .setCountry(address.getCountry()) - .setLabel(address.getLabel()) - .setNeighborhood(address.getNeighborhood()) - .setPobox(address.getPobox()) - .setPostcode(address.getPostcode()) - .setRegion(address.getRegion()) - .setStreet(address.getStreet()) - .setType(type) - .build()); - } - } - - if (contact.getNumberCount() > 0) { - for (DataMessage.Contact.Phone phone : contact.getNumberList()) { - SharedContact.Phone.Type type = SharedContact.Phone.Type.HOME; - - switch (phone.getType()) { - case HOME: type = SharedContact.Phone.Type.HOME; break; - case WORK: type = SharedContact.Phone.Type.WORK; break; - case MOBILE: type = SharedContact.Phone.Type.MOBILE; break; - case CUSTOM: type = SharedContact.Phone.Type.CUSTOM; break; - } - - builder.withPhone(SharedContact.Phone.newBuilder() - .setLabel(phone.getLabel()) - .setType(type) - .setValue(phone.getValue()) - .build()); - } - } - - if (contact.getEmailCount() > 0) { - for (DataMessage.Contact.Email email : contact.getEmailList()) { - SharedContact.Email.Type type = SharedContact.Email.Type.HOME; - - switch (email.getType()) { - case HOME: type = SharedContact.Email.Type.HOME; break; - case WORK: type = SharedContact.Email.Type.WORK; break; - case MOBILE: type = SharedContact.Email.Type.MOBILE; break; - case CUSTOM: type = SharedContact.Email.Type.CUSTOM; break; - } - - builder.withEmail(SharedContact.Email.newBuilder() - .setLabel(email.getLabel()) - .setType(type) - .setValue(email.getValue()) - .build()); - } - } - - if (contact.hasAvatar()) { - builder.setAvatar(SharedContact.Avatar.newBuilder() - .withAttachment(createAttachmentPointer(contact.getAvatar().getAvatar())) - .withProfileFlag(contact.getAvatar().getIsProfile()) - .build()); - } - - if (contact.hasOrganization()) { - builder.withOrganization(contact.getOrganization()); - } - - results.add(builder.build()); - } - - return results; - } - - private SignalServiceAttachmentPointer createAttachmentPointer(AttachmentPointer pointer) { - return new SignalServiceAttachmentPointer(pointer.getId(), - pointer.getContentType(), - pointer.getKey().toByteArray(), - pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.absent(), - pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.absent(), - pointer.getWidth(), pointer.getHeight(), - pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.absent(), - pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.absent(), - (pointer.getFlags() & AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) != 0, - pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.absent(), - pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.absent()); - - } - - private SignalServiceGroup createGroupInfo(DataMessage content) throws ProtocolInvalidMessageException { - if (!content.hasGroup()) return null; - - SignalServiceGroup.Type type; - - switch (content.getGroup().getType()) { - case DELIVER: type = SignalServiceGroup.Type.DELIVER; break; - case UPDATE: type = SignalServiceGroup.Type.UPDATE; break; - case QUIT: type = SignalServiceGroup.Type.QUIT; break; - case REQUEST_INFO: type = SignalServiceGroup.Type.REQUEST_INFO; break; - default: type = SignalServiceGroup.Type.UNKNOWN; break; - } - - if (content.getGroup().getType() != DELIVER) { - String name = null; - List members = null; - SignalServiceAttachmentPointer avatar = null; - - if (content.getGroup().hasName()) { - name = content.getGroup().getName(); - } - - if (content.getGroup().getMembersCount() > 0) { - members = new ArrayList<>(content.getGroup().getMembersCount()); - - for (SignalServiceProtos.GroupContext.Member member : content.getGroup().getMembersList()) { - if (SignalServiceAddress.isValidAddress(member.getUuid(), member.getE164())) { - members.add(new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164())); - } else { - throw new ProtocolInvalidMessageException(new InvalidMessageException("GroupContext.Member had no address!"), null, 0); - } - } - } else if (content.getGroup().getMembersE164Count() > 0) { - members = new ArrayList<>(content.getGroup().getMembersE164Count()); - - for (String member : content.getGroup().getMembersE164List()) { - members.add(new SignalServiceAddress(null, member)); - } - } - - if (content.getGroup().hasAvatar()) { - AttachmentPointer pointer = content.getGroup().getAvatar(); - - avatar = new SignalServiceAttachmentPointer(pointer.getId(), - pointer.getContentType(), - pointer.getKey().toByteArray(), - Optional.of(pointer.getSize()), - Optional.absent(), 0, 0, - Optional.fromNullable(pointer.hasDigest() ? pointer.getDigest().toByteArray() : null), - Optional.absent(), - false, - Optional.absent(), - Optional.absent()); - } - - return new SignalServiceGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar); - } - - return new SignalServiceGroup(content.getGroup().getId().toByteArray()); - } - - private static class Metadata { - private final SignalServiceAddress sender; - private final int senderDevice; - private final long timestamp; - private final boolean needsReceipt; - - private Metadata(SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.needsReceipt = needsReceipt; - } - - public SignalServiceAddress getSender() { - return sender; - } - - public int getSenderDevice() { - return senderDevice; - } - - public long getTimestamp() { - return timestamp; - } - - public boolean isNeedsReceipt() { - return needsReceipt; - } - } - private static class Plaintext { - private final Metadata metadata; + private final SignalServiceMetadata metadata; private final byte[] data; - private Plaintext(Metadata metadata, byte[] data) { + private Plaintext(SignalServiceMetadata metadata, byte[] data) { this.metadata = metadata; this.data = data; } - public Metadata getMetadata() { + public SignalServiceMetadata getMetadata() { return metadata; } @@ -858,4 +253,3 @@ public class SignalServiceCipher { } } - 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 6be5d26ef1..2fd9950287 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 @@ -6,17 +6,57 @@ package org.whispersystems.signalservice.api.messages; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; + +import org.signal.libsignal.metadata.ProtocolInvalidKeyException; +import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.logging.Log; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; +import org.whispersystems.signalservice.api.messages.calls.BusyMessage; +import org.whispersystems.signalservice.api.messages.calls.HangupMessage; +import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; +import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; +import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; +import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; +import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage; +import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; +import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; +import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer; +import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer; +import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto; -public class SignalServiceContent { +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; - private final SignalServiceAddress sender; - private final int senderDevice; - private final long timestamp; - private final boolean needsReceipt; +import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext.Type.DELIVER; + +public final class SignalServiceContent { + + private static final String TAG = SignalServiceContent.class.getSimpleName(); + + private final SignalServiceAddress sender; + private final int senderDevice; + private final long timestamp; + private final boolean needsReceipt; + private final SignalServiceContentProto serializedState; private final Optional message; private final Optional synchronizeMessage; @@ -24,11 +64,12 @@ public class SignalServiceContent { private final Optional readMessage; private final Optional typingMessage; - public SignalServiceContent(SignalServiceDataMessage message, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.needsReceipt = needsReceipt; + private SignalServiceContent(SignalServiceDataMessage message, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.fromNullable(message); this.synchronizeMessage = Optional.absent(); @@ -37,11 +78,12 @@ public class SignalServiceContent { this.typingMessage = Optional.absent(); } - public SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.needsReceipt = needsReceipt; + private SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.absent(); this.synchronizeMessage = Optional.fromNullable(synchronizeMessage); @@ -50,11 +92,12 @@ public class SignalServiceContent { this.typingMessage = Optional.absent(); } - public SignalServiceContent(SignalServiceCallMessage callMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.needsReceipt = needsReceipt; + private SignalServiceContent(SignalServiceCallMessage callMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.absent(); this.synchronizeMessage = Optional.absent(); @@ -63,11 +106,12 @@ public class SignalServiceContent { this.typingMessage = Optional.absent(); } - public SignalServiceContent(SignalServiceReceiptMessage receiptMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.needsReceipt = needsReceipt; + private SignalServiceContent(SignalServiceReceiptMessage receiptMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.absent(); this.synchronizeMessage = Optional.absent(); @@ -76,11 +120,12 @@ public class SignalServiceContent { this.typingMessage = Optional.absent(); } - public SignalServiceContent(SignalServiceTypingMessage typingMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.needsReceipt = needsReceipt; + private SignalServiceContent(SignalServiceTypingMessage typingMessage, SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.absent(); this.synchronizeMessage = Optional.absent(); @@ -124,4 +169,595 @@ public class SignalServiceContent { public boolean isNeedsReceipt() { return needsReceipt; } + + public byte[] serialize() { + return serializedState.toByteArray(); + } + + public static SignalServiceContent deserialize(byte[] data) { + try { + if (data == null) return null; + + SignalServiceContentProto signalServiceContentProto = SignalServiceContentProto.parseFrom(data); + + return createFromProto(signalServiceContentProto); + } catch (InvalidProtocolBufferException | ProtocolInvalidMessageException | ProtocolInvalidKeyException | UnsupportedDataMessageException e) { + // We do not expect any of these exceptions if this byte[] has come from serialize. + throw new AssertionError(e); + } + } + + /** + * Takes internal protobuf serialization format and processes it into a {@link SignalServiceContent}. + */ + public static SignalServiceContent createFromProto(SignalServiceContentProto serviceContentProto) + throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException + { + SignalServiceMetadata metadata = SignalServiceMetadataProtobufSerializer.fromProtobuf(serviceContentProto.getMetadata()); + SignalServiceAddress localAddress = SignalServiceAddressProtobufSerializer.fromProtobuf(serviceContentProto.getLocalAddress()); + + if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.LEGACYDATAMESSAGE) { + SignalServiceProtos.DataMessage message = serviceContentProto.getLegacyDataMessage(); + + return new SignalServiceContent(createSignalServiceMessage(metadata, message), + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.isNeedsReceipt(), + serviceContentProto); + } else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) { + SignalServiceProtos.Content message = serviceContentProto.getContent(); + + if (message.hasDataMessage()) { + return new SignalServiceContent(createSignalServiceMessage(metadata, message.getDataMessage()), + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.isNeedsReceipt(), + serviceContentProto); + } else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) { + return new SignalServiceContent(createSynchronizeMessage(metadata, message.getSyncMessage()), + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.isNeedsReceipt(), + serviceContentProto); + } else if (message.hasCallMessage()) { + return new SignalServiceContent(createCallMessage(message.getCallMessage()), + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.isNeedsReceipt(), + serviceContentProto); + } else if (message.hasReceiptMessage()) { + return new SignalServiceContent(createReceiptMessage(metadata, message.getReceiptMessage()), + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.isNeedsReceipt(), + serviceContentProto); + } else if (message.hasTypingMessage()) { + return new SignalServiceContent(createTypingMessage(metadata, message.getTypingMessage()), + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + false, + serviceContentProto); + } + } + + return null; + } + + private static SignalServiceDataMessage createSignalServiceMessage(SignalServiceMetadata metadata, SignalServiceProtos.DataMessage content) + throws ProtocolInvalidMessageException, UnsupportedDataMessageException + { + SignalServiceGroup groupInfo = createGroupInfo(content); + 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); + boolean profileKeyUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE ) != 0); + SignalServiceDataMessage.Quote quote = createQuote(content); + List sharedContacts = createSharedContacts(content); + List previews = createPreviews(content); + SignalServiceDataMessage.Sticker sticker = createSticker(content); + SignalServiceDataMessage.Reaction reaction = createReaction(content); + + if (content.getRequiredProtocolVersion() > SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT.getNumber()) { + throw new UnsupportedDataMessageException(SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT.getNumber(), + content.getRequiredProtocolVersion(), + metadata.getSender().getIdentifier(), + metadata.getSenderDevice(), + Optional.fromNullable(groupInfo)); + } + + for (SignalServiceProtos.AttachmentPointer pointer : content.getAttachmentsList()) { + attachments.add(createAttachmentPointer(pointer)); + } + + if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) { + throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()), + metadata.getSender().getIdentifier(), + metadata.getSenderDevice()); + } + + return new SignalServiceDataMessage(metadata.getTimestamp(), + groupInfo, + attachments, + content.getBody(), + endSession, + content.getExpireTimer(), + expirationUpdate, + content.hasProfileKey() ? content.getProfileKey().toByteArray() : null, + profileKeyUpdate, + quote, + sharedContacts, + previews, + sticker, + content.getIsViewOnce(), + reaction); + } + + private static SignalServiceSyncMessage createSynchronizeMessage(SignalServiceMetadata metadata, SignalServiceProtos.SyncMessage content) + throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException + { + if (content.hasSent()) { + Map unidentifiedStatuses = new HashMap<>(); + SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent(); + SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage()); + Optional address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid(), sentContent.getDestinationE164()) + ? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164())) + : Optional.absent(); + + if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) { + throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0); + } + + for (SignalServiceProtos.SyncMessage.Sent.UnidentifiedDeliveryStatus status : sentContent.getUnidentifiedStatusList()) { + if (SignalServiceAddress.isValidAddress(status.getDestinationUuid(), status.getDestinationE164())) { + SignalServiceAddress recipient = new SignalServiceAddress(UuidUtil.parseOrNull(status.getDestinationUuid()), status.getDestinationE164()); + unidentifiedStatuses.put(recipient, status.getUnidentified()); + } else { + Log.w(TAG, "Encountered an invalid UnidentifiedDeliveryStatus in a SentTranscript! Ignoring."); + } + } + + return SignalServiceSyncMessage.forSentTranscript(new SentTranscriptMessage(address, + sentContent.getTimestamp(), + dataMessage, + sentContent.getExpirationStartTimestamp(), + unidentifiedStatuses, + sentContent.getIsRecipientUpdate())); + } + + if (content.hasRequest()) { + return SignalServiceSyncMessage.forRequest(new RequestMessage(content.getRequest())); + } + + if (content.getReadList().size() > 0) { + List readMessages = new LinkedList<>(); + + for (SignalServiceProtos.SyncMessage.Read read : content.getReadList()) { + if (SignalServiceAddress.isValidAddress(read.getSenderUuid(), read.getSenderE164())) { + SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(read.getSenderUuid()), read.getSenderE164()); + readMessages.add(new ReadMessage(address, read.getTimestamp())); + } else { + Log.w(TAG, "Encountered an invalid ReadMessage! Ignoring."); + } + } + + return SignalServiceSyncMessage.forRead(readMessages); + } + + if (content.hasViewOnceOpen()) { + if (SignalServiceAddress.isValidAddress(content.getViewOnceOpen().getSenderUuid(), content.getViewOnceOpen().getSenderE164())) { + SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getViewOnceOpen().getSenderUuid()), content.getViewOnceOpen().getSenderE164()); + ViewOnceOpenMessage timerRead = new ViewOnceOpenMessage(address, content.getViewOnceOpen().getTimestamp()); + return SignalServiceSyncMessage.forViewOnceOpen(timerRead); + } else { + throw new ProtocolInvalidMessageException(new InvalidMessageException("ViewOnceOpen message has no sender!"), null, 0); + } + } + + if (content.hasVerified()) { + if (SignalServiceAddress.isValidAddress(content.getVerified().getDestinationUuid(), content.getVerified().getDestinationE164())) { + try { + SignalServiceProtos.Verified verified = content.getVerified(); + SignalServiceAddress destination = new SignalServiceAddress(UuidUtil.parseOrNull(verified.getDestinationUuid()), verified.getDestinationE164()); + IdentityKey identityKey = new IdentityKey(verified.getIdentityKey().toByteArray(), 0); + + VerifiedMessage.VerifiedState verifiedState; + + if (verified.getState() == SignalServiceProtos.Verified.State.DEFAULT) { + verifiedState = VerifiedMessage.VerifiedState.DEFAULT; + } else if (verified.getState() == SignalServiceProtos.Verified.State.VERIFIED) { + verifiedState = VerifiedMessage.VerifiedState.VERIFIED; + } else if (verified.getState() == SignalServiceProtos.Verified.State.UNVERIFIED) { + verifiedState = VerifiedMessage.VerifiedState.UNVERIFIED; + } else { + throw new ProtocolInvalidMessageException(new InvalidMessageException("Unknown state: " + verified.getState().getNumber()), + metadata.getSender().getIdentifier(), metadata.getSenderDevice()); + } + + return SignalServiceSyncMessage.forVerified(new VerifiedMessage(destination, identityKey, verifiedState, System.currentTimeMillis())); + } catch (InvalidKeyException e) { + throw new ProtocolInvalidKeyException(e, metadata.getSender().getIdentifier(), metadata.getSenderDevice()); + } + } else { + throw new ProtocolInvalidMessageException(new InvalidMessageException("Verified message has no sender!"), null, 0); + } + } + + if (content.getStickerPackOperationList().size() > 0) { + List operations = new LinkedList<>(); + + for (SignalServiceProtos.SyncMessage.StickerPackOperation operation : content.getStickerPackOperationList()) { + byte[] packId = operation.hasPackId() ? operation.getPackId().toByteArray() : null; + byte[] packKey = operation.hasPackKey() ? operation.getPackKey().toByteArray() : null; + StickerPackOperationMessage.Type type = null; + + if (operation.hasType()) { + switch (operation.getType()) { + case INSTALL: type = StickerPackOperationMessage.Type.INSTALL; break; + case REMOVE: type = StickerPackOperationMessage.Type.REMOVE; break; + } + } + operations.add(new StickerPackOperationMessage(packId, packKey, type)); + } + + return SignalServiceSyncMessage.forStickerPackOperations(operations); + } + + if (content.hasBlocked()) { + List numbers = content.getBlocked().getNumbersList(); + List uuids = content.getBlocked().getUuidsList(); + List addresses = new ArrayList<>(numbers.size() + uuids.size()); + List groupIds = new ArrayList<>(content.getBlocked().getGroupIdsList().size()); + + for (String e164 : numbers) { + Optional address = SignalServiceAddress.fromRaw(null, e164); + if (address.isPresent()) { + addresses.add(address.get()); + } + } + + for (String uuid : uuids) { + Optional address = SignalServiceAddress.fromRaw(uuid, null); + if (address.isPresent()) { + addresses.add(address.get()); + } + } + + for (ByteString groupId : content.getBlocked().getGroupIdsList()) { + groupIds.add(groupId.toByteArray()); + } + + return SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)); + } + + if (content.hasConfiguration()) { + Boolean readReceipts = content.getConfiguration().hasReadReceipts() ? content.getConfiguration().getReadReceipts() : null; + Boolean unidentifiedDeliveryIndicators = content.getConfiguration().hasUnidentifiedDeliveryIndicators() ? content.getConfiguration().getUnidentifiedDeliveryIndicators() : null; + Boolean typingIndicators = content.getConfiguration().hasTypingIndicators() ? content.getConfiguration().getTypingIndicators() : null; + Boolean linkPreviews = content.getConfiguration().hasLinkPreviews() ? content.getConfiguration().getLinkPreviews() : null; + + return SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.fromNullable(readReceipts), + Optional.fromNullable(unidentifiedDeliveryIndicators), + Optional.fromNullable(typingIndicators), + Optional.fromNullable(linkPreviews))); + } + + if (content.hasFetchLatest() && content.getFetchLatest().hasType()) { + switch (content.getFetchLatest().getType()) { + case LOCAL_PROFILE: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE); + case STORAGE_MANIFEST: return SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST); + } + } + + return SignalServiceSyncMessage.empty(); + } + + private static SignalServiceCallMessage createCallMessage(SignalServiceProtos.CallMessage content) { + if (content.hasOffer()) { + SignalServiceProtos.CallMessage.Offer offerContent = content.getOffer(); + return SignalServiceCallMessage.forOffer(new OfferMessage(offerContent.getId(), offerContent.getDescription())); + } else if (content.hasAnswer()) { + SignalServiceProtos.CallMessage.Answer answerContent = content.getAnswer(); + return SignalServiceCallMessage.forAnswer(new AnswerMessage(answerContent.getId(), answerContent.getDescription())); + } else if (content.getIceUpdateCount() > 0) { + List iceUpdates = new LinkedList<>(); + + for (SignalServiceProtos.CallMessage.IceUpdate iceUpdate : content.getIceUpdateList()) { + iceUpdates.add(new IceUpdateMessage(iceUpdate.getId(), iceUpdate.getSdpMid(), iceUpdate.getSdpMLineIndex(), iceUpdate.getSdp())); + } + + return SignalServiceCallMessage.forIceUpdates(iceUpdates); + } else if (content.hasHangup()) { + SignalServiceProtos.CallMessage.Hangup hangup = content.getHangup(); + return SignalServiceCallMessage.forHangup(new HangupMessage(hangup.getId())); + } else if (content.hasBusy()) { + SignalServiceProtos.CallMessage.Busy busy = content.getBusy(); + return SignalServiceCallMessage.forBusy(new BusyMessage(busy.getId())); + } + + return SignalServiceCallMessage.empty(); + } + + private static SignalServiceReceiptMessage createReceiptMessage(SignalServiceMetadata metadata, SignalServiceProtos.ReceiptMessage content) { + SignalServiceReceiptMessage.Type type; + + if (content.getType() == SignalServiceProtos.ReceiptMessage.Type.DELIVERY) type = SignalServiceReceiptMessage.Type.DELIVERY; + else if (content.getType() == SignalServiceProtos.ReceiptMessage.Type.READ) type = SignalServiceReceiptMessage.Type.READ; + else type = SignalServiceReceiptMessage.Type.UNKNOWN; + + return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp()); + } + + private static SignalServiceTypingMessage createTypingMessage(SignalServiceMetadata metadata, SignalServiceProtos.TypingMessage content) throws ProtocolInvalidMessageException { + SignalServiceTypingMessage.Action action; + + if (content.getAction() == SignalServiceProtos.TypingMessage.Action.STARTED) action = SignalServiceTypingMessage.Action.STARTED; + else if (content.getAction() == SignalServiceProtos.TypingMessage.Action.STOPPED) action = SignalServiceTypingMessage.Action.STOPPED; + else action = SignalServiceTypingMessage.Action.UNKNOWN; + + if (content.hasTimestamp() && content.getTimestamp() != metadata.getTimestamp()) { + throw new ProtocolInvalidMessageException(new InvalidMessageException("Timestamps don't match: " + content.getTimestamp() + " vs " + metadata.getTimestamp()), + metadata.getSender().getIdentifier(), + metadata.getSenderDevice()); + } + + return new SignalServiceTypingMessage(action, content.getTimestamp(), + content.hasGroupId() ? Optional.of(content.getGroupId().toByteArray()) : + Optional.absent()); + } + + private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content) { + if (!content.hasQuote()) return null; + + List attachments = new LinkedList<>(); + + for (SignalServiceProtos.DataMessage.Quote.QuotedAttachment attachment : content.getQuote().getAttachmentsList()) { + attachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(), + attachment.getFileName(), + attachment.hasThumbnail() ? createAttachmentPointer(attachment.getThumbnail()) : null)); + } + + if (SignalServiceAddress.isValidAddress(content.getQuote().getAuthorUuid(), content.getQuote().getAuthorE164())) { + SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getQuote().getAuthorUuid()), content.getQuote().getAuthorE164()); + + return new SignalServiceDataMessage.Quote(content.getQuote().getId(), + address, + content.getQuote().getText(), + attachments); + } else { + Log.w(TAG, "Quote was missing an author! Returning null."); + return null; + } + } + + private static List createPreviews(SignalServiceProtos.DataMessage content) { + if (content.getPreviewCount() <= 0) return null; + + List results = new LinkedList<>(); + + for (SignalServiceProtos.DataMessage.Preview preview : content.getPreviewList()) { + SignalServiceAttachment attachment = null; + + if (preview.hasImage()) { + attachment = createAttachmentPointer(preview.getImage()); + } + + results.add(new SignalServiceDataMessage.Preview(preview.getUrl(), + preview.getTitle(), + Optional.fromNullable(attachment))); + } + + return results; + } + + private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) { + if (!content.hasSticker() || + !content.getSticker().hasPackId() || + !content.getSticker().hasPackKey() || + !content.getSticker().hasStickerId() || + !content.getSticker().hasData()) + { + return null; + } + + SignalServiceProtos.DataMessage.Sticker sticker = content.getSticker(); + + return new SignalServiceDataMessage.Sticker(sticker.getPackId().toByteArray(), + sticker.getPackKey().toByteArray(), + sticker.getStickerId(), + createAttachmentPointer(sticker.getData())); + } + + private static SignalServiceDataMessage.Reaction createReaction(SignalServiceProtos.DataMessage content) { + if (!content.hasReaction() || + !content.getReaction().hasEmoji() || + !(content.getReaction().hasTargetAuthorE164() || content.getReaction().hasTargetAuthorUuid()) || + !content.getReaction().hasTargetSentTimestamp()) + { + return null; + } + + SignalServiceProtos.DataMessage.Reaction reaction = content.getReaction(); + + return new SignalServiceDataMessage.Reaction(reaction.getEmoji(), + reaction.getRemove(), + new SignalServiceAddress(UuidUtil.parseOrNull(reaction.getTargetAuthorUuid()), reaction.getTargetAuthorE164()), + reaction.getTargetSentTimestamp()); + } + + private static List createSharedContacts(SignalServiceProtos.DataMessage content) { + if (content.getContactCount() <= 0) return null; + + List results = new LinkedList<>(); + + for (SignalServiceProtos.DataMessage.Contact contact : content.getContactList()) { + SharedContact.Builder builder = SharedContact.newBuilder() + .setName(SharedContact.Name.newBuilder() + .setDisplay(contact.getName().getDisplayName()) + .setFamily(contact.getName().getFamilyName()) + .setGiven(contact.getName().getGivenName()) + .setMiddle(contact.getName().getMiddleName()) + .setPrefix(contact.getName().getPrefix()) + .setSuffix(contact.getName().getSuffix()) + .build()); + + if (contact.getAddressCount() > 0) { + for (SignalServiceProtos.DataMessage.Contact.PostalAddress address : contact.getAddressList()) { + SharedContact.PostalAddress.Type type = SharedContact.PostalAddress.Type.HOME; + + switch (address.getType()) { + case WORK: type = SharedContact.PostalAddress.Type.WORK; break; + case HOME: type = SharedContact.PostalAddress.Type.HOME; break; + case CUSTOM: type = SharedContact.PostalAddress.Type.CUSTOM; break; + } + + builder.withAddress(SharedContact.PostalAddress.newBuilder() + .setCity(address.getCity()) + .setCountry(address.getCountry()) + .setLabel(address.getLabel()) + .setNeighborhood(address.getNeighborhood()) + .setPobox(address.getPobox()) + .setPostcode(address.getPostcode()) + .setRegion(address.getRegion()) + .setStreet(address.getStreet()) + .setType(type) + .build()); + } + } + + if (contact.getNumberCount() > 0) { + for (SignalServiceProtos.DataMessage.Contact.Phone phone : contact.getNumberList()) { + SharedContact.Phone.Type type = SharedContact.Phone.Type.HOME; + + switch (phone.getType()) { + case HOME: type = SharedContact.Phone.Type.HOME; break; + case WORK: type = SharedContact.Phone.Type.WORK; break; + case MOBILE: type = SharedContact.Phone.Type.MOBILE; break; + case CUSTOM: type = SharedContact.Phone.Type.CUSTOM; break; + } + + builder.withPhone(SharedContact.Phone.newBuilder() + .setLabel(phone.getLabel()) + .setType(type) + .setValue(phone.getValue()) + .build()); + } + } + + if (contact.getEmailCount() > 0) { + for (SignalServiceProtos.DataMessage.Contact.Email email : contact.getEmailList()) { + SharedContact.Email.Type type = SharedContact.Email.Type.HOME; + + switch (email.getType()) { + case HOME: type = SharedContact.Email.Type.HOME; break; + case WORK: type = SharedContact.Email.Type.WORK; break; + case MOBILE: type = SharedContact.Email.Type.MOBILE; break; + case CUSTOM: type = SharedContact.Email.Type.CUSTOM; break; + } + + builder.withEmail(SharedContact.Email.newBuilder() + .setLabel(email.getLabel()) + .setType(type) + .setValue(email.getValue()) + .build()); + } + } + + if (contact.hasAvatar()) { + builder.setAvatar(SharedContact.Avatar.newBuilder() + .withAttachment(createAttachmentPointer(contact.getAvatar().getAvatar())) + .withProfileFlag(contact.getAvatar().getIsProfile()) + .build()); + } + + if (contact.hasOrganization()) { + builder.withOrganization(contact.getOrganization()); + } + + results.add(builder.build()); + } + + return results; + } + + private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) { + return new SignalServiceAttachmentPointer(pointer.getId(), + pointer.getContentType(), + pointer.getKey().toByteArray(), + pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.absent(), + pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.absent(), + pointer.getWidth(), pointer.getHeight(), + pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.absent(), + pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.absent(), + (pointer.getFlags() & SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) != 0, + pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.absent(), + pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.absent()); + + } + + private static SignalServiceGroup createGroupInfo(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { + if (!content.hasGroup()) return null; + + SignalServiceGroup.Type type; + + switch (content.getGroup().getType()) { + case DELIVER: type = SignalServiceGroup.Type.DELIVER; break; + case UPDATE: type = SignalServiceGroup.Type.UPDATE; break; + case QUIT: type = SignalServiceGroup.Type.QUIT; break; + case REQUEST_INFO: type = SignalServiceGroup.Type.REQUEST_INFO; break; + default: type = SignalServiceGroup.Type.UNKNOWN; break; + } + + if (content.getGroup().getType() != DELIVER) { + String name = null; + List members = null; + SignalServiceAttachmentPointer avatar = null; + + if (content.getGroup().hasName()) { + name = content.getGroup().getName(); + } + + if (content.getGroup().getMembersCount() > 0) { + members = new ArrayList<>(content.getGroup().getMembersCount()); + + for (SignalServiceProtos.GroupContext.Member member : content.getGroup().getMembersList()) { + if (SignalServiceAddress.isValidAddress(member.getUuid(), member.getE164())) { + members.add(new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164())); + } else { + throw new ProtocolInvalidMessageException(new InvalidMessageException("GroupContext.Member had no address!"), null, 0); + } + } + } else if (content.getGroup().getMembersE164Count() > 0) { + members = new ArrayList<>(content.getGroup().getMembersE164Count()); + + for (String member : content.getGroup().getMembersE164List()) { + members.add(new SignalServiceAddress(null, member)); + } + } + + if (content.getGroup().hasAvatar()) { + SignalServiceProtos.AttachmentPointer pointer = content.getGroup().getAvatar(); + + avatar = new SignalServiceAttachmentPointer(pointer.getId(), + pointer.getContentType(), + pointer.getKey().toByteArray(), + Optional.of(pointer.getSize()), + Optional.absent(), 0, 0, + Optional.fromNullable(pointer.hasDigest() ? pointer.getDigest().toByteArray() : null), + Optional.absent(), + false, + Optional.absent(), + Optional.absent()); + } + + return new SignalServiceGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar); + } + + return new SignalServiceGroup(content.getGroup().getId().toByteArray()); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java new file mode 100644 index 0000000000..15175ced9a --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java @@ -0,0 +1,33 @@ +package org.whispersystems.signalservice.api.messages; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public final class SignalServiceMetadata { + private final SignalServiceAddress sender; + private final int senderDevice; + private final long timestamp; + private final boolean needsReceipt; + + public SignalServiceMetadata(SignalServiceAddress sender, int senderDevice, long timestamp, boolean needsReceipt) { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.needsReceipt = needsReceipt; + } + + public SignalServiceAddress getSender() { + return sender; + } + + public int getSenderDevice() { + return senderDevice; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isNeedsReceipt() { + return needsReceipt; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceAddressProtobufSerializer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceAddressProtobufSerializer.java new file mode 100644 index 0000000000..89492b743c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceAddressProtobufSerializer.java @@ -0,0 +1,37 @@ +package org.whispersystems.signalservice.internal.serialize; + +import com.google.protobuf.ByteString; + +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.serialize.protos.AddressProto; + +import java.util.UUID; + +public final class SignalServiceAddressProtobufSerializer { + + private SignalServiceAddressProtobufSerializer() { + } + + public static AddressProto toProtobuf(SignalServiceAddress signalServiceAddress) { + AddressProto.Builder builder = AddressProto.newBuilder(); + if(signalServiceAddress.getNumber().isPresent()){ + builder.setE164(signalServiceAddress.getNumber().get()); + } + if(signalServiceAddress.getUuid().isPresent()){ + builder.setUuid(ByteString.copyFrom(UuidUtil.toByteArray(signalServiceAddress.getUuid().get()))); + } + if(signalServiceAddress.getRelay().isPresent()){ + builder.setRelay(signalServiceAddress.getRelay().get()); + } + return builder.build(); + } + + public static SignalServiceAddress fromProtobuf(AddressProto addressProto) { + Optional uuid = addressProto.hasUuid() ? Optional.of(UuidUtil.parseOrThrow(addressProto.getUuid().toByteArray())) : Optional.absent(); + Optional number = addressProto.hasE164() ? Optional.of(addressProto.getE164() ) : Optional.absent(); + Optional relay = addressProto.hasRelay() ? Optional.of(addressProto.getRelay() ) : Optional.absent(); + return new SignalServiceAddress(uuid, number, relay); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java new file mode 100644 index 0000000000..3cd7eefb49 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java @@ -0,0 +1,26 @@ +package org.whispersystems.signalservice.internal.serialize; + +import org.whispersystems.signalservice.api.messages.SignalServiceMetadata; +import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto; + +public final class SignalServiceMetadataProtobufSerializer { + + private SignalServiceMetadataProtobufSerializer() { + } + + public static MetadataProto toProtobuf(SignalServiceMetadata metadata) { + return MetadataProto.newBuilder() + .setAddress(SignalServiceAddressProtobufSerializer.toProtobuf(metadata.getSender())) + .setSenderDevice(metadata.getSenderDevice()) + .setNeedsReceipt(metadata.isNeedsReceipt()) + .setTimestamp(metadata.getTimestamp()) + .build(); + } + + public static SignalServiceMetadata fromProtobuf(MetadataProto metadata) { + return new SignalServiceMetadata(SignalServiceAddressProtobufSerializer.fromProtobuf(metadata.getAddress()), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.getNeedsReceipt()); + } +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/util/PhoneNumberFormatterTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatterTest.java similarity index 96% rename from libsignal/service/src/test/java/org/whispersystems/signalservice/util/PhoneNumberFormatterTest.java rename to libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatterTest.java index f7feacbc5e..308d7c6703 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/util/PhoneNumberFormatterTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/PhoneNumberFormatterTest.java @@ -1,12 +1,9 @@ -package org.whispersystems.signalservice.util; +package org.whispersystems.signalservice.api.util; import junit.framework.AssertionFailedError; import junit.framework.TestCase; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; - import static org.assertj.core.api.Assertions.assertThat; public class PhoneNumberFormatterTest extends TestCase { diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/UuidUtilTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/UuidUtilTest.java new file mode 100644 index 0000000000..53962bfdfd --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/UuidUtilTest.java @@ -0,0 +1,49 @@ +package org.whispersystems.signalservice.api.util; + +import org.junit.Test; +import org.whispersystems.libsignal.util.Hex; + +import java.io.IOException; +import java.util.UUID; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public final class UuidUtilTest { + + @Test + public void toByteArray() throws IOException { + UUID uuid = UUID.fromString("67dfd496-ea02-4720-b13d-83a462168b1d"); + + byte[] serialized = UuidUtil.toByteArray(uuid); + + assertArrayEquals(Hex.fromStringCondensed("67dfd496ea024720b13d83a462168b1d"), serialized); + } + + @Test + public void toByteArray_alternativeValues() throws IOException { + UUID uuid = UUID.fromString("b70df6ac-3b21-4b39-a514-613561f51e2a"); + + byte[] serialized = UuidUtil.toByteArray(uuid); + + assertArrayEquals(Hex.fromStringCondensed("b70df6ac3b214b39a514613561f51e2a"), serialized); + } + + @Test + public void parseOrThrow_from_byteArray() throws IOException { + byte[] bytes = Hex.fromStringCondensed("3dc48790568b49c19bd6ab6604a5bc32"); + + UUID uuid = UuidUtil.parseOrThrow(bytes); + + assertEquals("3dc48790-568b-49c1-9bd6-ab6604a5bc32", uuid.toString()); + } + + @Test + public void parseOrThrow_from_byteArray_alternativeValues() throws IOException { + byte[] bytes = Hex.fromStringCondensed("b83dfb0b67f141aa992e030c167cd011"); + + UUID uuid = UuidUtil.parseOrThrow(bytes); + + assertEquals("b83dfb0b-67f1-41aa-992e-030c167cd011", uuid.toString()); + } +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/serialize/SignalServiceAddressProtobufSerializerTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/serialize/SignalServiceAddressProtobufSerializerTest.java new file mode 100644 index 0000000000..7b482633fb --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/serialize/SignalServiceAddressProtobufSerializerTest.java @@ -0,0 +1,49 @@ +package org.whispersystems.signalservice.internal.serialize; + +import org.junit.Test; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.internal.serialize.protos.AddressProto; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +public final class SignalServiceAddressProtobufSerializerTest { + + @Test + public void serialize_and_deserialize_uuid_address() { + SignalServiceAddress address = new SignalServiceAddress(Optional.fromNullable(UUID.randomUUID()), Optional.absent(), Optional.absent()); + AddressProto addressProto = org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer.toProtobuf(address); + SignalServiceAddress deserialized = org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer.fromProtobuf(addressProto); + + assertEquals(address, deserialized); + } + + @Test + public void serialize_and_deserialize_e164_address() { + SignalServiceAddress address = new SignalServiceAddress(Optional.absent(), Optional.of("+15552345678"), Optional.absent()); + AddressProto addressProto = org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer.toProtobuf(address); + SignalServiceAddress deserialized = org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer.fromProtobuf(addressProto); + + assertEquals(address, deserialized); + } + + @Test + public void serialize_and_deserialize_both_address() { + SignalServiceAddress address = new SignalServiceAddress(Optional.fromNullable(UUID.randomUUID()), Optional.of("+15552345678"), Optional.absent()); + AddressProto addressProto = org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer.toProtobuf(address); + SignalServiceAddress deserialized = org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer.fromProtobuf(addressProto); + + assertEquals(address, deserialized); + } + + @Test + public void serialize_and_deserialize_both_address_with_relay() { + SignalServiceAddress address = new SignalServiceAddress(Optional.fromNullable(UUID.randomUUID()), Optional.of("+15552345678"), Optional.of("relay")); + AddressProto addressProto = org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer.toProtobuf(address); + SignalServiceAddress deserialized = SignalServiceAddressProtobufSerializer.fromProtobuf(addressProto); + + assertEquals(address, deserialized); + } +} diff --git a/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java b/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java index 0477f20319..607c67b570 100644 --- a/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java +++ b/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java @@ -5,12 +5,13 @@ import android.content.Context; import android.content.DialogInterface; import android.database.Cursor; import android.os.AsyncTask; -import androidx.appcompat.app.AlertDialog; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; + import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; @@ -20,13 +21,12 @@ import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobs.PushDecryptJob; +import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.VerifySpan; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.util.guava.Optional; @@ -179,7 +179,7 @@ public class ConfirmIdentityDialog extends AlertDialog { long pushId = pushDatabase.insert(envelope); - ApplicationDependencies.getJobManager().add(new PushDecryptJob(getContext(), pushId, messageRecord.getId())); + ApplicationDependencies.getJobManager().add(new PushDecryptMessageJob(getContext(), pushId, messageRecord.getId())); } catch (IOException e) { throw new AssertionError(e); } diff --git a/src/org/thoughtcrime/securesms/IncomingMessageProcessor.java b/src/org/thoughtcrime/securesms/IncomingMessageProcessor.java index a42b619bac..4668aee0f3 100644 --- a/src/org/thoughtcrime/securesms/IncomingMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/IncomingMessageProcessor.java @@ -5,22 +5,18 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.PushDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; -import org.thoughtcrime.securesms.jobs.PushDecryptJob; +import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import java.io.Closeable; -import java.io.IOException; import java.util.Locale; import java.util.concurrent.locks.ReentrantLock; @@ -63,21 +59,19 @@ public class IncomingMessageProcessor { public class Processor implements Closeable { private final Context context; - private final RecipientDatabase recipientDatabase; private final PushDatabase pushDatabase; private final MmsSmsDatabase mmsSmsDatabase; private final JobManager jobManager; private Processor(@NonNull Context context) { this.context = context; - this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context); this.pushDatabase = DatabaseFactory.getPushDatabase(context); this.mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); this.jobManager = ApplicationDependencies.getJobManager(); } /** - * @return The id of the {@link PushDecryptJob} that was scheduled to process the message, if + * @return The id of the {@link PushDecryptMessageJob} that was scheduled to process the message, if * one was created. Otherwise null. */ public @Nullable String processEnvelope(@NonNull SignalServiceEnvelope envelope) { @@ -99,8 +93,8 @@ public class IncomingMessageProcessor { private @NonNull String processMessage(@NonNull SignalServiceEnvelope envelope) { Log.i(TAG, "Received message. Inserting in PushDatabase."); - long id = pushDatabase.insert(envelope); - PushDecryptJob job = new PushDecryptJob(context, id); + long id = pushDatabase.insert(envelope); + PushDecryptMessageJob job = new PushDecryptMessageJob(context, id); jobManager.add(job); diff --git a/src/org/thoughtcrime/securesms/gcm/RestStrategy.java b/src/org/thoughtcrime/securesms/gcm/RestStrategy.java index 33aacf6632..1df1ff899f 100644 --- a/src/org/thoughtcrime/securesms/gcm/RestStrategy.java +++ b/src/org/thoughtcrime/securesms/gcm/RestStrategy.java @@ -2,21 +2,21 @@ package org.thoughtcrime.securesms.gcm; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; -import androidx.lifecycle.Observer; import org.thoughtcrime.securesms.IncomingMessageProcessor; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobTracker; +import org.thoughtcrime.securesms.jobs.MarkerJob; +import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob; +import org.thoughtcrime.securesms.jobs.PushProcessMessageJob; import org.thoughtcrime.securesms.logging.Log; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; /** * Retrieves messages over the REST endpoint. @@ -33,9 +33,8 @@ public class RestStrategy implements MessageRetriever.Strategy { long startTime = System.currentTimeMillis(); try (IncomingMessageProcessor.Processor processor = ApplicationDependencies.getIncomingMessageProcessor().acquire()) { - SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); - AtomicReference lastJobId = new AtomicReference<>(null); - AtomicInteger jobCount = new AtomicInteger(0); + SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); + AtomicInteger jobCount = new AtomicInteger(0); receiver.setSoTimeoutMillis(SOCKET_TIMEOUT); @@ -44,16 +43,17 @@ public class RestStrategy implements MessageRetriever.Strategy { String jobId = processor.processEnvelope(envelope); if (jobId != null) { - lastJobId.set(jobId); jobCount.incrementAndGet(); } Log.i(TAG, "Successfully processed an envelope." + timeSuffix(startTime)); }); - Log.d(TAG, jobCount.get() + " PushDecryptJob(s) were enqueued."); + Log.d(TAG, jobCount.get() + " PushDecryptMessageJob(s) were enqueued."); - if (lastJobId.get() != null) { - blockUntilJobIsFinished(lastJobId.get()); + long timeRemainingMs = blockUntilQueueDrained(PushDecryptMessageJob.QUEUE, TimeUnit.SECONDS.toMillis(10)); + + if (timeRemainingMs > 0) { + blockUntilQueueDrained(PushProcessMessageJob.QUEUE, timeRemainingMs); } return true; @@ -63,29 +63,40 @@ public class RestStrategy implements MessageRetriever.Strategy { return false; } } - private static void blockUntilJobIsFinished(@NonNull String jobId) { + + private static long blockUntilQueueDrained(@NonNull String queue, long timeoutMs) { + final JobManager jobManager = ApplicationDependencies.getJobManager(); + final MarkerJob markerJob = new MarkerJob(queue); + + jobManager.add(markerJob); + long startTime = System.currentTimeMillis(); CountDownLatch latch = new CountDownLatch(1); - ApplicationDependencies.getJobManager().addListener(jobId, new JobTracker.JobListener() { + jobManager.addListener(markerJob.getId(), new JobTracker.JobListener() { @Override public void onStateChanged(@NonNull JobTracker.JobState jobState) { if (jobState.isComplete()) { - ApplicationDependencies.getJobManager().removeListener(this); + jobManager.removeListener(this); latch.countDown(); } } }); try { - if (!latch.await(10, TimeUnit.SECONDS)) { - Log.w(TAG, "Timed out waiting for PushDecryptJob(s) to finish!"); + if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + Log.w(TAG, "Timed out waiting for " + queue + " job(s) to finish!"); + return 0; } } catch (InterruptedException e) { throw new AssertionError(e); } - Log.d(TAG, "Waited " + (System.currentTimeMillis() - startTime) + " ms for the PushDecryptJob(s) to finish."); + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + Log.d(TAG, "Waited " + duration + " ms for the " + queue + " job(s) to finish."); + return timeoutMs - duration; } private static String timeSuffix(long startTime) { diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 1bf9cb3f2a..fb341f8774 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -2,13 +2,13 @@ package org.thoughtcrime.securesms.groups; import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.annimon.stream.Stream; import com.google.protobuf.ByteString; -import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; @@ -232,7 +232,7 @@ public class GroupMessageProcessor { } else { SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); String body = Base64.encodeBytes(storage.toByteArray()); - IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), body, Optional.of(group), 0, content.isNeedsReceipt()); + IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), body, Optional.of(GroupUtil.getEncodedId(group.getGroupId(), false)), 0, content.isNeedsReceipt()); IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, storage, body); Optional insertResult = smsDatabase.insertMessageInbox(groupMessage); diff --git a/src/org/thoughtcrime/securesms/jobmanager/Job.java b/src/org/thoughtcrime/securesms/jobmanager/Job.java index f2835484f8..8a5509cbe8 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/Job.java +++ b/src/org/thoughtcrime/securesms/jobmanager/Job.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.jobmanager; import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; @@ -250,7 +251,7 @@ public abstract class Job { return maxInstances; } - @Nullable String getQueue() { + public @Nullable String getQueue() { return queue; } diff --git a/src/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java b/src/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java index 37497602d8..3ab1290823 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java +++ b/src/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java @@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob; -import org.thoughtcrime.securesms.jobs.PushDecryptJob; +import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob; import org.thoughtcrime.securesms.jobs.PushGroupSendJob; import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.thoughtcrime.securesms.jobs.PushMediaSendJob; @@ -71,7 +71,7 @@ public class WorkManagerFactoryMappings { put(MultiDeviceReadUpdateJob.class.getName(), MultiDeviceReadUpdateJob.KEY); put(MultiDeviceVerifiedUpdateJob.class.getName(), MultiDeviceVerifiedUpdateJob.KEY); put("PushContentReceiveJob", FailingJob.KEY); - put(PushDecryptJob.class.getName(), PushDecryptJob.KEY); + put("PushDecryptJob", PushDecryptMessageJob.KEY); put(PushGroupSendJob.class.getName(), PushGroupSendJob.KEY); put(PushGroupUpdateJob.class.getName(), PushGroupUpdateJob.KEY); put(PushMediaSendJob.class.getName(), PushMediaSendJob.KEY); diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 364ed84edd..4f7fd59a22 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -63,7 +63,8 @@ public final class JobManagerFactories { put(MultiDeviceStorageSyncRequestJob.KEY, new MultiDeviceStorageSyncRequestJob.Factory()); put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory()); put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory()); - put(PushDecryptJob.KEY, new PushDecryptJob.Factory()); + put(PushDecryptMessageJob.KEY, new PushDecryptMessageJob.Factory()); + put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory()); put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory()); put(PushGroupUpdateJob.KEY, new PushGroupUpdateJob.Factory()); put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory()); @@ -92,6 +93,7 @@ public final class JobManagerFactories { put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); put(TypingSendJob.KEY, new TypingSendJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); + put(MarkerJob.KEY, new MarkerJob.Factory()); // Migrations put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory()); diff --git a/src/org/thoughtcrime/securesms/jobs/MarkerJob.java b/src/org/thoughtcrime/securesms/jobs/MarkerJob.java new file mode 100644 index 0000000000..af443cff6c --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/MarkerJob.java @@ -0,0 +1,61 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.logging.Log; + +/** + * Useful for putting in a queue as a marker to know that previously enqueued jobs have been processed. + *

+ * Does no work. + */ +public final class MarkerJob extends BaseJob { + + private static final String TAG = Log.tag(MarkerJob.class); + + public static final String KEY = "MarkerJob"; + + public MarkerJob(@Nullable String queue) { + this(new Parameters.Builder() + .setQueue(queue) + .build()); + } + + private MarkerJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + protected void onRun() { + Log.i(TAG, String.format("Marker reached in %s queue", getParameters().getQueue())); + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return false; + } + + @Override + public @NonNull Data serialize() { + return Data.EMPTY; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onCanceled() { + } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull MarkerJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new MarkerJob(parameters); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java new file mode 100644 index 0000000000..a0f34de197 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -0,0 +1,253 @@ +package org.thoughtcrime.securesms.jobs; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import org.signal.libsignal.metadata.InvalidMetadataMessageException; +import org.signal.libsignal.metadata.InvalidMetadataVersionException; +import org.signal.libsignal.metadata.ProtocolDuplicateMessageException; +import org.signal.libsignal.metadata.ProtocolException; +import org.signal.libsignal.metadata.ProtocolInvalidKeyException; +import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException; +import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.signal.libsignal.metadata.ProtocolInvalidVersionException; +import org.signal.libsignal.metadata.ProtocolLegacyMessageException; +import org.signal.libsignal.metadata.ProtocolNoSessionException; +import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; +import org.signal.libsignal.metadata.SelfSendException; +import org.thoughtcrime.securesms.MainActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; +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.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; +import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PushDecryptMessageJob extends BaseJob { + + public static final String KEY = "PushDecryptJob"; + public static final String QUEUE = "__PUSH_DECRYPT_JOB__"; + + public static final String TAG = Log.tag(PushDecryptMessageJob.class); + + private static final String KEY_MESSAGE_ID = "message_id"; + private static final String KEY_SMS_MESSAGE_ID = "sms_message_id"; + + private final long messageId; + private final long smsMessageId; + + public PushDecryptMessageJob(Context context, long pushMessageId) { + this(context, pushMessageId, -1); + } + + public PushDecryptMessageJob(Context context, long pushMessageId, long smsMessageId) { + this(new Parameters.Builder() + .setQueue(QUEUE) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + pushMessageId, + smsMessageId); + setContext(context); + } + + private PushDecryptMessageJob(@NonNull Parameters parameters, long pushMessageId, long smsMessageId) { + super(parameters); + + this.messageId = pushMessageId; + this.smsMessageId = smsMessageId; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) + .putLong(KEY_SMS_MESSAGE_ID, smsMessageId) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onRun() throws NoSuchMessageException, RetryLaterException { + if (needsMigration()) { + Log.w(TAG, "Migration is still needed."); + postMigrationNotification(); + throw new RetryLaterException(); + } + + PushDatabase database = DatabaseFactory.getPushDatabase(context); + SignalServiceEnvelope envelope = database.get(messageId); + JobManager jobManager = ApplicationDependencies.getJobManager(); + + try { + List jobs = handleMessage(envelope); + + for (Job job: jobs) { + jobManager.add(job); + } + } catch (NoSenderException e) { + Log.w(TAG, "Invalid message, but no sender info!"); + } + + database.delete(messageId); + } + + @Override + public boolean onShouldRetry(@NonNull Exception exception) { + return exception instanceof RetryLaterException; + } + + @Override + public void onCanceled() { + } + + private boolean needsMigration() { + return !IdentityKeyUtil.hasIdentityKey(context) || TextSecurePreferences.getNeedsSqlCipherMigration(context); + } + + private void postMigrationNotification() { + // TODO [greyson] Navigation + NotificationManagerCompat.from(context).notify(494949, + new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context)) + .setSmallIcon(R.drawable.icon_notification) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message)) + .setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages)) + .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0)) + .setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) + .build()); + + } + + private @NonNull List handleMessage(@NonNull SignalServiceEnvelope envelope) throws NoSenderException { + try { + SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); + SignalServiceAddress localAddress = new SignalServiceAddress(Optional.of(TextSecurePreferences.getLocalUuid(context)), Optional.of(TextSecurePreferences.getLocalNumber(context))); + SignalServiceCipher cipher = new SignalServiceCipher(localAddress, axolotlStore, UnidentifiedAccessUtil.getCertificateValidator()); + + SignalServiceContent content = cipher.decrypt(envelope); + + List jobs = new ArrayList<>(2); + + jobs.add(new PushProcessMessageJob(content.serialize(), messageId, smsMessageId, envelope.getTimestamp())); + + if (envelope.isPreKeySignalMessage()) { + jobs.add(new RefreshPreKeysJob()); + } + + return jobs; + + } catch (ProtocolInvalidVersionException e) { + Log.w(TAG, e); + return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.INVALID_VERSION, + toExceptionMetadata(e), + messageId, + smsMessageId, + envelope.getTimestamp())); + + } catch (ProtocolInvalidMessageException | ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException e) { + Log.w(TAG, e); + return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.CORRUPT_MESSAGE, + toExceptionMetadata(e), + messageId, + smsMessageId, + envelope.getTimestamp())); + + } catch (ProtocolNoSessionException e) { + Log.w(TAG, e); + return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.NO_SESSION, + toExceptionMetadata(e), + messageId, + smsMessageId, + envelope.getTimestamp())); + + } catch (ProtocolLegacyMessageException e) { + Log.w(TAG, e); + return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.LEGACY_MESSAGE, + toExceptionMetadata(e), + messageId, + smsMessageId, + envelope.getTimestamp())); + + } catch (ProtocolDuplicateMessageException e) { + Log.w(TAG, e); + return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.DUPLICATE_MESSAGE, + toExceptionMetadata(e), + messageId, + smsMessageId, + envelope.getTimestamp())); + + } catch (InvalidMetadataVersionException | InvalidMetadataMessageException e) { + Log.w(TAG, e); + return Collections.emptyList(); + + } catch (SelfSendException e) { + Log.i(TAG, "Dropping UD message from self."); + return Collections.emptyList(); + + } catch (UnsupportedDataMessageException e) { + Log.w(TAG, e); + return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.UNSUPPORTED_DATA_MESSAGE, + toExceptionMetadata(e), + messageId, + smsMessageId, + envelope.getTimestamp())); + } + } + + private static PushProcessMessageJob.ExceptionMetadata toExceptionMetadata(@NonNull UnsupportedDataMessageException e) throws NoSenderException { + String sender = e.getSender(); + + if (sender == null) throw new NoSenderException(); + + return new PushProcessMessageJob.ExceptionMetadata(sender, + e.getSenderDevice(), + e.getGroup().transform(g -> GroupUtil.getEncodedId(g.getGroupId(), false)).orNull()); + } + + private static PushProcessMessageJob.ExceptionMetadata toExceptionMetadata(@NonNull ProtocolException e) throws NoSenderException { + String sender = e.getSender(); + + if (sender == null) throw new NoSenderException(); + + return new PushProcessMessageJob.ExceptionMetadata(sender, e.getSenderDevice()); + } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull PushDecryptMessageJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new PushDecryptMessageJob(parameters, data.getLong(KEY_MESSAGE_ID), data.getLong(KEY_SMS_MESSAGE_ID)); + } + } + + private static class NoSenderException extends Exception {} +} diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java similarity index 87% rename from src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java rename to src/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 12631e3e1c..ef303a1852 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -1,34 +1,18 @@ package org.thoughtcrime.securesms.jobs; import android.annotation.SuppressLint; -import android.app.PendingIntent; -import android.content.Context; import android.content.Intent; import android.os.Build; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; import android.text.TextUtils; import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.annimon.stream.Collectors; import com.annimon.stream.Stream; -import org.signal.libsignal.metadata.InvalidMetadataMessageException; -import org.signal.libsignal.metadata.InvalidMetadataVersionException; -import org.signal.libsignal.metadata.ProtocolDuplicateMessageException; -import org.signal.libsignal.metadata.ProtocolInvalidKeyException; -import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException; -import org.signal.libsignal.metadata.ProtocolInvalidMessageException; -import org.signal.libsignal.metadata.ProtocolInvalidVersionException; -import org.signal.libsignal.metadata.ProtocolLegacyMessageException; -import org.signal.libsignal.metadata.ProtocolNoSessionException; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; -import org.signal.libsignal.metadata.SelfSendException; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.MainActivity; -import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.PointerAttachment; @@ -36,10 +20,7 @@ import org.thoughtcrime.securesms.attachments.TombstoneAttachment; import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.ContactModelMapper; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.SecurityEvent; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -51,8 +32,6 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.StickerDatabase; @@ -79,7 +58,6 @@ import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.StickerSlide; import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.service.WebRtcCallService; @@ -90,21 +68,18 @@ import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; -import org.thoughtcrime.securesms.transport.RetryLaterException; +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; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.state.SessionStore; -import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; 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.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; @@ -125,8 +100,8 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; +import java.io.IOException; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.ArrayList; @@ -135,44 +110,112 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -public class PushDecryptJob extends BaseJob { +public final class PushProcessMessageJob extends BaseJob { - public static final String KEY = "PushDecryptJob"; + public static final String KEY = "PushProcessJob"; + public static final String QUEUE = "__PUSH_PROCESS_JOB__"; - public static final String TAG = PushDecryptJob.class.getSimpleName(); + public static final String TAG = Log.tag(PushProcessMessageJob.class); - private static final String KEY_MESSAGE_ID = "message_id"; - private static final String KEY_SMS_MESSAGE_ID = "sms_message_id"; + private static final String KEY_MESSAGE_STATE = "message_state"; + private static final String KEY_MESSAGE_PLAINTEXT = "message_content"; + private static final String KEY_MESSAGE_ID = "message_id"; + private static final String KEY_SMS_MESSAGE_ID = "sms_message_id"; + private static final String KEY_TIMESTAMP = "timestamp"; + private static final String KEY_EXCEPTION_SENDER = "exception_sender"; + private static final String KEY_EXCEPTION_DEVICE = "exception_device"; + private static final String KEY_EXCEPTION_GROUP_ID = "exception_groupId"; - private long messageId; - private long smsMessageId; + @NonNull private final MessageState messageState; + @Nullable private final byte[] serializedPlaintextContent; + @Nullable private final ExceptionMetadata exceptionMetadata; + private final long messageId; + private final long smsMessageId; + private final long timestamp; - public PushDecryptJob(Context context, long pushMessageId) { - this(context, pushMessageId, -1); - } - - public PushDecryptJob(Context context, long pushMessageId, long smsMessageId) { - this(new Job.Parameters.Builder() - .setQueue("__PUSH_DECRYPT_JOB__") - .setMaxAttempts(Parameters.UNLIMITED) - .build(), + PushProcessMessageJob(@NonNull byte[] serializedPlaintextContent, + long pushMessageId, + long smsMessageId, + long timestamp) + { + this(MessageState.DECRYPTED_OK, + serializedPlaintextContent, + null, pushMessageId, - smsMessageId); - setContext(context); + smsMessageId, + timestamp); } - private PushDecryptJob(@NonNull Job.Parameters parameters, long pushMessageId, long smsMessageId) { + PushProcessMessageJob(@NonNull MessageState messageState, + @NonNull ExceptionMetadata exceptionMetadata, + long pushMessageId, + long smsMessageId, + long timestamp) + { + this(messageState, + null, + exceptionMetadata, + pushMessageId, + smsMessageId, + timestamp); + } + + private PushProcessMessageJob(@NonNull MessageState messageState, + @Nullable byte[] serializedPlaintextContent, + @Nullable ExceptionMetadata exceptionMetadata, + long pushMessageId, + long smsMessageId, + long timestamp) + { + this(new Parameters.Builder() + .setQueue(QUEUE) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + messageState, + serializedPlaintextContent, + exceptionMetadata, + pushMessageId, + smsMessageId, + timestamp); + } + + private PushProcessMessageJob(@NonNull Parameters parameters, + @NonNull MessageState messageState, + @Nullable byte[] serializedPlaintextContent, + @Nullable ExceptionMetadata exceptionMetadata, + long pushMessageId, + long smsMessageId, + long timestamp) + { super(parameters); - this.messageId = pushMessageId; - this.smsMessageId = smsMessageId; + this.messageState = messageState; + this.exceptionMetadata = exceptionMetadata; + this.serializedPlaintextContent = serializedPlaintextContent; + this.messageId = pushMessageId; + this.smsMessageId = smsMessageId; + this.timestamp = timestamp; } @Override public @NonNull Data serialize() { - return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) - .putLong(KEY_SMS_MESSAGE_ID, smsMessageId) - .build(); + Data.Builder dataBuilder = new Data.Builder() + .putInt(KEY_MESSAGE_STATE, messageState.ordinal()) + .putLong(KEY_MESSAGE_ID, messageId) + .putLong(KEY_SMS_MESSAGE_ID, smsMessageId) + .putLong(KEY_TIMESTAMP, timestamp); + + if (messageState == MessageState.DECRYPTED_OK) { + //noinspection ConstantConditions + dataBuilder.putString(KEY_MESSAGE_PLAINTEXT, Base64.encodeBytes(serializedPlaintextContent)); + } else { + //noinspection ConstantConditions + dataBuilder.putString(KEY_EXCEPTION_SENDER, exceptionMetadata.sender) + .putInt(KEY_EXCEPTION_DEVICE, exceptionMetadata.senderDevice) + .putString(KEY_EXCEPTION_GROUP_ID, exceptionMetadata.groupId); + } + + return dataBuilder.build(); } @Override @@ -181,59 +224,33 @@ public class PushDecryptJob extends BaseJob { } @Override - public void onRun() throws NoSuchMessageException, RetryLaterException { - if (needsMigration()) { - Log.w(TAG, "Migration is still needed."); - postMigrationNotification(); - throw new RetryLaterException(); + public void onRun() { + Optional optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent(); + + if (messageState == MessageState.DECRYPTED_OK) { + //noinspection ConstantConditions + handleMessage(serializedPlaintextContent, optionalSmsMessageId); + } else { + //noinspection ConstantConditions + handleExceptionMessage(exceptionMetadata, optionalSmsMessageId); } - - PushDatabase database = DatabaseFactory.getPushDatabase(context); - SignalServiceEnvelope envelope = database.get(messageId); - Optional optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent(); - - handleMessage(envelope, optionalSmsMessageId); - database.delete(messageId); } @Override public boolean onShouldRetry(@NonNull Exception exception) { - return exception instanceof RetryLaterException; + return false; } @Override public void onCanceled() { } - private boolean needsMigration() { - return !IdentityKeyUtil.hasIdentityKey(context) || TextSecurePreferences.getNeedsSqlCipherMigration(context); - } - - private void postMigrationNotification() { - // TODO [greyson] Navigation - NotificationManagerCompat.from(context).notify(494949, - new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context)) - .setSmallIcon(R.drawable.icon_notification) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message)) - .setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages)) - .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0)) - .setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) - .build()); - - } - - private void handleMessage(@NonNull SignalServiceEnvelope envelope, @NonNull Optional smsMessageId) { + private void handleMessage(@NonNull byte[] plaintextDataBuffer, @NonNull Optional smsMessageId) { try { GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); - SignalServiceAddress localAddress = new SignalServiceAddress(Optional.of(TextSecurePreferences.getLocalUuid(context)), Optional.of(TextSecurePreferences.getLocalNumber(context))); - SignalServiceCipher cipher = new SignalServiceCipher(localAddress, axolotlStore, UnidentifiedAccessUtil.getCertificateValidator()); + SignalServiceContent content = SignalServiceContent.deserialize(plaintextDataBuffer); - SignalServiceContent content = cipher.decrypt(envelope); - - if (shouldIgnore(content)) { + if (content == null || shouldIgnore(content)) { Log.i(TAG, "Ignoring message."); return; } @@ -242,7 +259,7 @@ public class PushDecryptJob extends BaseJob { SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); - if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), message.getGroupInfo(), content.getTimestamp(), smsMessageId); + if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), toEncodedId(message.getGroupInfo()), content.getTimestamp(), smsMessageId); else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId); @@ -298,41 +315,51 @@ public class PushDecryptJob extends BaseJob { resetRecipientToPush(Recipient.externalPush(context, content.getSender())); - if (envelope.isPreKeySignalMessage()) { - ApplicationDependencies.getJobManager().add(new RefreshPreKeysJob()); - } - } catch (ProtocolInvalidVersionException e) { - Log.w(TAG, e); - handleInvalidVersionMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId); - } catch (ProtocolInvalidMessageException e) { - if (!TextUtils.isEmpty(e.getSender())) { - Log.w(TAG, e); - handleCorruptMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId); - } else { - Log.w(TAG, "Invalid message, but no sender info!", e); - } - } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException e) { - Log.w(TAG, e); - handleCorruptMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId); } catch (StorageFailedException e) { Log.w(TAG, e); - handleCorruptMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId); - } catch (ProtocolNoSessionException e) { - Log.w(TAG, e); - handleNoSessionMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId); - } catch (ProtocolLegacyMessageException e) { - Log.w(TAG, e); - handleLegacyMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId); - } catch (ProtocolDuplicateMessageException e) { - Log.w(TAG, e); - handleDuplicateMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId); - } catch (InvalidMetadataVersionException | InvalidMetadataMessageException e) { - Log.w(TAG, e); - } catch (SelfSendException e) { - Log.i(TAG, "Dropping UD message from self."); - } catch (UnsupportedDataMessageException e) { - Log.w(TAG, e); - handleUnsupportedDataMessage(e.getSender(), e.getSenderDevice(), e.getGroup(), envelope.getTimestamp(), smsMessageId); + handleCorruptMessage(e.getSender(), e.getSenderDevice(), timestamp, smsMessageId); + } + } + + private static @NonNull Optional toEncodedId(@NonNull Optional groupInfo) { + return groupInfo.transform(g -> GroupUtil.getEncodedId(g.getGroupId(), false)); + } + + private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional smsMessageId) { + switch (messageState) { + + case INVALID_VERSION: + Log.w(TAG, "Handling invalid version"); + handleInvalidVersionMessage(e.sender, e.senderDevice, timestamp, smsMessageId); + break; + + case CORRUPT_MESSAGE: + Log.w(TAG, "Handling corrupt message"); + handleCorruptMessage(e.sender, e.senderDevice, timestamp, smsMessageId); + break; + + case NO_SESSION: + Log.w(TAG, "Handling no session"); + handleNoSessionMessage(e.sender, e.senderDevice, timestamp, smsMessageId); + break; + + case LEGACY_MESSAGE: + Log.w(TAG, "Handling legacy message"); + handleLegacyMessage(e.sender, e.senderDevice, timestamp, smsMessageId); + break; + + case DUPLICATE_MESSAGE: + Log.w(TAG, "Handling duplicate message"); + handleDuplicateMessage(e.sender, e.senderDevice, timestamp, smsMessageId); + break; + + case UNSUPPORTED_DATA_MESSAGE: + Log.w(TAG, "Handling unsupported data message"); + handleUnsupportedDataMessage(e.sender, e.senderDevice, Optional.fromNullable(e.groupId), timestamp, smsMessageId); + break; + + default: + throw new AssertionError("Not handled " + messageState); } } @@ -599,7 +626,7 @@ public class PushDecryptJob extends BaseJob { DatabaseFactory.getRecipientDatabase(context).applyBlockedUpdate(blockMessage.getAddresses(), blockMessage.getGroupIds()); } - private void handleSynchronizeFetchMessage(@NonNull SignalServiceSyncMessage.FetchType fetchType) { + private static void handleSynchronizeFetchMessage(@NonNull SignalServiceSyncMessage.FetchType fetchType) { if (fetchType == SignalServiceSyncMessage.FetchType.LOCAL_PROFILE) { ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob()); } else { @@ -952,7 +979,7 @@ public class PushDecryptJob extends BaseJob { IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), message.getTimestamp(), body, - message.getGroupInfo(), + toEncodedId(message.getGroupInfo()), message.getExpiresInSeconds() * 1000L, content.isNeedsReceipt()); @@ -1074,14 +1101,14 @@ public class PushDecryptJob extends BaseJob { private void handleUnsupportedDataMessage(@NonNull String sender, int senderDevice, - @NonNull Optional group, + @NonNull Optional groupId, long timestamp, @NonNull Optional smsMessageId) { SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); if (!smsMessageId.isPresent()) { - Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp, group); + Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp, groupId); if (insertResult.isPresent()) { smsDatabase.markAsUnsupportedProtocolVersion(insertResult.get().getMessageId()); @@ -1094,14 +1121,14 @@ public class PushDecryptJob extends BaseJob { private void handleInvalidMessage(@NonNull SignalServiceAddress sender, int senderDevice, - @NonNull Optional group, + @NonNull Optional groupId, long timestamp, @NonNull Optional smsMessageId) { SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); if (!smsMessageId.isPresent()) { - Optional insertResult = insertPlaceholder(sender.getIdentifier(), senderDevice, timestamp, group); + Optional insertResult = insertPlaceholder(sender.getIdentifier(), senderDevice, timestamp, groupId); if (insertResult.isPresent()) { smsDatabase.markAsInvalidMessage(insertResult.get().getMessageId()); @@ -1223,7 +1250,7 @@ public class PushDecryptJob extends BaseJob { } } - private boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) { + private static boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) { if (message.isViewOnce()) { List attachments = message.getAttachments().or(Collections.emptyList()); @@ -1234,7 +1261,7 @@ public class PushDecryptJob extends BaseJob { return false; } - private boolean isViewOnceSupportedContentType(@NonNull String contentType) { + private static boolean isViewOnceSupportedContentType(@NonNull String contentType) { return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType); } @@ -1326,7 +1353,7 @@ public class PushDecryptJob extends BaseJob { } } - private Optional> getContacts(Optional> sharedContacts) { + private static Optional> getContacts(Optional> sharedContacts) { if (!sharedContacts.isPresent()) return Optional.absent(); List contacts = new ArrayList<>(sharedContacts.get().size()); @@ -1338,7 +1365,7 @@ public class PushDecryptJob extends BaseJob { return Optional.of(contacts); } - private Optional> getLinkPreviews(Optional> previews, @NonNull String message) { + private static Optional> getLinkPreviews(Optional> previews, @NonNull String message) { if (!previews.isPresent()) return Optional.absent(); List linkPreviews = new ArrayList<>(previews.get().size()); @@ -1366,11 +1393,11 @@ public class PushDecryptJob extends BaseJob { return insertPlaceholder(sender, senderDevice, timestamp, Optional.absent()); } - private Optional insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional group) { + private Optional insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional groupId) { SmsDatabase database = DatabaseFactory.getSmsDatabase(context); IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.external(context, sender).getId(), senderDevice, timestamp, "", - group, 0, false); + groupId, 0, false); textMessage = new IncomingEncryptedMessage(textMessage, ""); return database.insertMessageInbox(textMessage); @@ -1470,7 +1497,7 @@ public class PushDecryptJob extends BaseJob { @SuppressWarnings("WeakerAccess") private static class StorageFailedException extends Exception { private final String sender; - private final int senderDevice; + private final int senderDevice; private StorageFailedException(Exception e, String sender, int senderDevice) { super(e); @@ -1487,10 +1514,62 @@ public class PushDecryptJob extends BaseJob { } } - public static final class Factory implements Job.Factory { + public static final class Factory implements Job.Factory { @Override - public @NonNull PushDecryptJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new PushDecryptJob(parameters, data.getLong(KEY_MESSAGE_ID), data.getLong(KEY_SMS_MESSAGE_ID)); + public @NonNull PushProcessMessageJob create(@NonNull Parameters parameters, @NonNull Data data) { + try { + MessageState state = MessageState.values()[data.getInt(KEY_MESSAGE_STATE)]; + + if (state == MessageState.DECRYPTED_OK) { + return new PushProcessMessageJob(parameters, + state, + Base64.decode(data.getString(KEY_MESSAGE_PLAINTEXT)), + null, + data.getLong(KEY_MESSAGE_ID), + data.getLong(KEY_SMS_MESSAGE_ID), + data.getLong(KEY_TIMESTAMP)); + } else { + ExceptionMetadata exceptionMetadata = new ExceptionMetadata(data.getString(KEY_EXCEPTION_SENDER), + data.getInt(KEY_EXCEPTION_DEVICE), + data.getStringOrDefault(KEY_EXCEPTION_GROUP_ID, null)); + + return new PushProcessMessageJob(parameters, + state, + null, + exceptionMetadata, + data.getLong(KEY_MESSAGE_ID), + data.getLong(KEY_SMS_MESSAGE_ID), + data.getLong(KEY_TIMESTAMP)); + } + } catch (IOException e) { + throw new AssertionError(e); + } + } + } + + public enum MessageState { + DECRYPTED_OK, + INVALID_VERSION, + CORRUPT_MESSAGE, + NO_SESSION, + LEGACY_MESSAGE, + DUPLICATE_MESSAGE, + UNSUPPORTED_DATA_MESSAGE + } + + static class ExceptionMetadata { + @NonNull private final String sender; + private final int senderDevice; + @Nullable private final String groupId; + + ExceptionMetadata(@NonNull String sender, int senderDevice, @Nullable String groupId) { + this.sender = sender; + this.senderDevice = senderDevice; + this.groupId = groupId; + } + + ExceptionMetadata(@NonNull String sender, int senderDevice) { + this(sender, senderDevice, null); } } } diff --git a/src/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java b/src/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java index 3b868179d7..75925446d2 100644 --- a/src/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java +++ b/src/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java @@ -6,7 +6,6 @@ import android.database.Cursor; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; -import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy; @@ -21,10 +20,11 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; -import org.thoughtcrime.securesms.jobs.PushDecryptJob; +import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideApp; @@ -294,20 +294,15 @@ public class LegacyMigrationJob extends MigrationJob { } } - private void scheduleMessagesInPushDatabase(Context context) { + private static void scheduleMessagesInPushDatabase(@NonNull Context context) { PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(context); - Cursor pushReader = null; - - try { - pushReader = pushDatabase.getPending(); + JobManager jobManager = ApplicationDependencies.getJobManager(); + try (Cursor pushReader = pushDatabase.getPending()) { while (pushReader != null && pushReader.moveToNext()) { - ApplicationDependencies.getJobManager().add(new PushDecryptJob(context, - pushReader.getLong(pushReader.getColumnIndexOrThrow(PushDatabase.ID)))); + jobManager.add(new PushDecryptMessageJob(context, + pushReader.getLong(pushReader.getColumnIndexOrThrow(PushDatabase.ID)))); } - } finally { - if (pushReader != null) - pushReader.close(); } } diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index 5860741380..89e7f3db8c 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -2,15 +2,13 @@ package org.thoughtcrime.securesms.sms; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.telephony.SmsMessage; -import org.thoughtcrime.securesms.database.model.ReactionRecord; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + 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.SignalServiceGroup; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.List; @@ -61,7 +59,7 @@ public class IncomingTextMessage implements Parcelable { } public IncomingTextMessage(@NonNull RecipientId sender, int senderDeviceId, long sentTimestampMillis, - String encodedBody, Optional group, + String encodedBody, Optional groupId, long expiresInMillis, boolean unidentified) { this.message = encodedBody; @@ -76,12 +74,7 @@ public class IncomingTextMessage implements Parcelable { this.subscriptionId = -1; this.expiresInMillis = expiresInMillis; this.unidentified = unidentified; - - if (group.isPresent()) { - this.groupId = GroupUtil.getEncodedId(group.get().getGroupId(), false); - } else { - this.groupId = null; - } + this.groupId = groupId.orNull(); } public IncomingTextMessage(Parcel in) { diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java index 5146493a22..0313bbdc1f 100644 --- a/src/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import android.os.AsyncTask; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -34,7 +35,6 @@ import org.whispersystems.libsignal.state.IdentityKeyStore; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import java.util.List; @@ -75,17 +75,16 @@ public class IdentityUtil { while ((groupRecord = reader.getNext()) != null) { if (groupRecord.getMembers().contains(recipient.getId()) && groupRecord.isActive() && !groupRecord.isMms()) { - SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId()); if (remote) { - IncomingTextMessage incoming = new IncomingTextMessage(recipient.getId(), 1, time, null, Optional.of(group), 0, false); + IncomingTextMessage incoming = new IncomingTextMessage(recipient.getId(), 1, time, null, Optional.of(groupRecord.getEncodedId()), 0, false); if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming); else incoming = new IncomingIdentityDefaultMessage(incoming); smsDatabase.insertMessageInbox(incoming); } else { - RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupUtil.getEncodedId(group.getGroupId(), false)); + RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupRecord.getEncodedId()); Recipient groupRecipient = Recipient.resolved(recipientId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); OutgoingTextMessage outgoing ; @@ -128,8 +127,7 @@ public class IdentityUtil { while ((groupRecord = reader.getNext()) != null) { if (groupRecord.getMembers().contains(recipient.getId()) && groupRecord.isActive()) { - SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId()); - IncomingTextMessage incoming = new IncomingTextMessage(recipient.getId(), 1, time, null, Optional.of(group), 0, false); + IncomingTextMessage incoming = new IncomingTextMessage(recipient.getId(), 1, time, null, Optional.of(groupRecord.getEncodedId()), 0, false); IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming); smsDatabase.insertMessageInbox(groupUpdate);