/* * Copyright (C) 2014-2016 Open Whisper Systems * * Licensed according to the LICENSE file in this repository. */ package org.whispersystems.signalservice.api.crypto; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; 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.SealedSessionCipher; 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; import org.whispersystems.libsignal.InvalidVersionException; import org.whispersystems.libsignal.LegacyMessageException; 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.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.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.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. * * @author Moxie Marlinspike */ public class SignalServiceCipher { @SuppressWarnings("unused") private static final String TAG = SignalServiceCipher.class.getSimpleName(); private final SignalProtocolStore signalProtocolStore; private final SignalServiceAddress localAddress; private final CertificateValidator certificateValidator; public SignalServiceCipher(SignalServiceAddress localAddress, SignalProtocolStore signalProtocolStore, CertificateValidator certificateValidator) { this.signalProtocolStore = signalProtocolStore; this.localAddress = localAddress; this.certificateValidator = certificateValidator; } public OutgoingPushMessage encrypt(SignalProtocolAddress destination, Optional unidentifiedAccess, byte[] unpaddedMessage) throws UntrustedIdentityException, InvalidKeyException { if (unidentifiedAccess.isPresent()) { SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1); PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination)); byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage)); String body = Base64.encodeBytes(ciphertext); int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination); return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body); } else { SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, destination); PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion()); CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage)); int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(); String body = Base64.encodeBytes(message.serialize()); int type; switch (message.getType()) { case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break; case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break; default: throw new AssertionError("Bad type: " + message.getType()); } return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body); } } /** * Decrypt a received {@link SignalServiceEnvelope} * * @param envelope The received SignalServiceEnvelope * * @return a decrypted SignalServiceContent */ public SignalServiceContent decrypt(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, InvalidMetadataVersionException, ProtocolInvalidKeyIdException, ProtocolLegacyMessageException, ProtocolUntrustedIdentityException, ProtocolNoSessionException, 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()); 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); } } return null; } catch (InvalidProtocolBufferException e) { throw new InvalidMetadataMessageException(e); } } private Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException, InvalidMetadataVersionException, ProtocolDuplicateMessageException, ProtocolUntrustedIdentityException, ProtocolLegacyMessageException, ProtocolInvalidKeyException, ProtocolInvalidVersionException, ProtocolInvalidMessageException, ProtocolInvalidKeyIdException, ProtocolNoSessionException, SelfSendException { try { byte[] paddedMessage; Metadata metadata; int sessionVersion; if (!envelope.hasSource() && !envelope.isUnidentifiedSender()) { throw new ProtocolInvalidMessageException(new InvalidMessageException("Non-UD envelope is missing a source!"), null, 0); } if (envelope.isPreKeySignalMessage()) { SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice()); SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress); paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext)); metadata = new Metadata(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); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isUnidentifiedSender()) { SealedSessionCipher sealedSessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1); DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp()); SignalServiceAddress resultAddress = new SignalServiceAddress(UuidUtil.parse(result.getSenderUuid().orNull()), result.getSenderE164()); SignalProtocolAddress protocolAddress = getPreferredProtocolAddress(signalProtocolStore, resultAddress, result.getDeviceId()); paddedMessage = result.getPaddedMessage(); metadata = new Metadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), true); sessionVersion = sealedSessionCipher.getSessionVersion(protocolAddress); } else { throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); } PushTransportDetails transportDetails = new PushTransportDetails(sessionVersion); byte[] data = transportDetails.getStrippedPaddingMessageBody(paddedMessage); return new Plaintext(metadata, data); } catch (DuplicateMessageException e) { throw new ProtocolDuplicateMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice()); } catch (LegacyMessageException e) { throw new ProtocolLegacyMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice()); } catch (InvalidMessageException e) { throw new ProtocolInvalidMessageException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice()); } catch (InvalidKeyIdException e) { throw new ProtocolInvalidKeyIdException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice()); } catch (InvalidKeyException e) { throw new ProtocolInvalidKeyException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice()); } catch (UntrustedIdentityException e) { throw new ProtocolUntrustedIdentityException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice()); } catch (InvalidVersionException e) { throw new ProtocolInvalidVersionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice()); } catch (NoSessionException e) { throw new ProtocolNoSessionException(e, envelope.getSourceIdentifier(), envelope.getSourceDevice()); } } private static SignalProtocolAddress getPreferredProtocolAddress(SignalProtocolStore store, SignalServiceAddress address, int sourceDevice) { SignalProtocolAddress uuidAddress = address.getUuid().isPresent() ? new SignalProtocolAddress(address.getUuid().get().toString(), sourceDevice) : null; SignalProtocolAddress e164Address = address.getNumber().isPresent() ? new SignalProtocolAddress(address.getNumber().get(), sourceDevice) : null; if (uuidAddress != null && store.containsSession(uuidAddress)) { return uuidAddress; } else if (e164Address != null && store.containsSession(e164Address)) { return e164Address; } else { return new SignalProtocolAddress(address.getIdentifier(), sourceDevice); } } 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); 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()); } 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 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 byte[] data; private Plaintext(Metadata metadata, byte[] data) { this.metadata = metadata; this.data = data; } public Metadata getMetadata() { return metadata; } public byte[] getData() { return data; } } }