Separate message decryption from message processing.

master
Alan Evans 2019-12-20 16:50:50 -05:00
parent 3b5d9a2cae
commit b598431237
22 changed files with 1526 additions and 883 deletions

View File

@ -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;
}

View File

@ -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<SignalServiceAttachment> 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<SharedContact> sharedContacts = createSharedContacts(content);
List<Preview> 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<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid(), sentContent.getDestinationE164())
? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164()))
: Optional.<SignalServiceAddress>absent();
Map<SignalServiceAddress, Boolean> 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<ReadMessage> 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<StickerPackOperationMessage> 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<String> numbers = content.getBlocked().getNumbersList();
List<String> uuids = content.getBlocked().getUuidsList();
List<SignalServiceAddress> addresses = new ArrayList<>(numbers.size() + uuids.size());
List<byte[]> groupIds = new ArrayList<>(content.getBlocked().getGroupIdsList().size());
for (String e164 : numbers) {
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(null, e164);
if (address.isPresent()) {
addresses.add(address.get());
}
}
for (String uuid : uuids) {
Optional<SignalServiceAddress> 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<IceUpdateMessage> 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.<byte[]>absent());
}
private SignalServiceDataMessage.Quote createQuote(DataMessage content) {
if (!content.hasQuote()) return null;
List<SignalServiceDataMessage.Quote.QuotedAttachment> 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<Preview> createPreviews(DataMessage content) {
if (content.getPreviewCount() <= 0) return null;
List<Preview> 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<SharedContact> createSharedContacts(DataMessage content) {
if (content.getContactCount() <= 0) return null;
List<SharedContact> 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.<Integer>absent(),
pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.<byte[]>absent(),
pointer.getWidth(), pointer.getHeight(),
pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.<byte[]>absent(),
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.<String>absent(),
(pointer.getFlags() & AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) != 0,
pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.<String>absent(),
pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.<String>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<SignalServiceAddress> 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.<byte[]>absent(), 0, 0,
Optional.fromNullable(pointer.hasDigest() ? pointer.getDigest().toByteArray() : null),
Optional.<String>absent(),
false,
Optional.<String>absent(),
Optional.<String>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 {
}
}

View File

@ -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<SignalServiceDataMessage> message;
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
@ -24,11 +64,12 @@ public class SignalServiceContent {
private final Optional<SignalServiceReceiptMessage> readMessage;
private final Optional<SignalServiceTypingMessage> 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<SignalServiceAttachment> 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<SharedContact> sharedContacts = createSharedContacts(content);
List<SignalServiceDataMessage.Preview> 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<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent();
SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage());
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid(), sentContent.getDestinationE164())
? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164()))
: Optional.<SignalServiceAddress>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<ReadMessage> 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<StickerPackOperationMessage> 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<String> numbers = content.getBlocked().getNumbersList();
List<String> uuids = content.getBlocked().getUuidsList();
List<SignalServiceAddress> addresses = new ArrayList<>(numbers.size() + uuids.size());
List<byte[]> groupIds = new ArrayList<>(content.getBlocked().getGroupIdsList().size());
for (String e164 : numbers) {
Optional<SignalServiceAddress> address = SignalServiceAddress.fromRaw(null, e164);
if (address.isPresent()) {
addresses.add(address.get());
}
}
for (String uuid : uuids) {
Optional<SignalServiceAddress> 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<IceUpdateMessage> 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.<byte[]>absent());
}
private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content) {
if (!content.hasQuote()) return null;
List<SignalServiceDataMessage.Quote.QuotedAttachment> 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<SignalServiceDataMessage.Preview> createPreviews(SignalServiceProtos.DataMessage content) {
if (content.getPreviewCount() <= 0) return null;
List<SignalServiceDataMessage.Preview> 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<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) {
if (content.getContactCount() <= 0) return null;
List<SharedContact> 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.<Integer>absent(),
pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.<byte[]>absent(),
pointer.getWidth(), pointer.getHeight(),
pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.<byte[]>absent(),
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.<String>absent(),
(pointer.getFlags() & SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) != 0,
pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.<String>absent(),
pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.<String>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<SignalServiceAddress> 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.<byte[]>absent(), 0, 0,
Optional.fromNullable(pointer.hasDigest() ? pointer.getDigest().toByteArray() : null),
Optional.<String>absent(),
false,
Optional.<String>absent(),
Optional.<String>absent());
}
return new SignalServiceGroup(type, content.getGroup().getId().toByteArray(), name, members, avatar);
}
return new SignalServiceGroup(content.getGroup().getId().toByteArray());
}
}

View File

@ -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;
}
}

View File

@ -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> uuid = addressProto.hasUuid() ? Optional.of(UuidUtil.parseOrThrow(addressProto.getUuid().toByteArray())) : Optional.<UUID>absent();
Optional<String> number = addressProto.hasE164() ? Optional.of(addressProto.getE164() ) : Optional.<String>absent();
Optional<String> relay = addressProto.hasRelay() ? Optional.of(addressProto.getRelay() ) : Optional.<String>absent();
return new SignalServiceAddress(uuid, number, relay);
}
}

View File

@ -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());
}
}

View File

@ -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 {

View File

@ -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());
}
}

View File

@ -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.<String>absent(), Optional.<String>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.<UUID>absent(), Optional.of("+15552345678"), Optional.<String>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.<String>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);
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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<String> 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) {

View File

@ -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> insertResult = smsDatabase.insertMessageInbox(groupMessage);

View File

@ -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;
}

View File

@ -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);

View File

@ -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());

View File

@ -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.
* <p>
* 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<MarkerJob> {
@Override
public @NonNull MarkerJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new MarkerJob(parameters);
}
}
}

View File

@ -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<Job> 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<Job> 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<Job> 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<PushDecryptMessageJob> {
@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 {}
}

View File

@ -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<Long> 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<Long> 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<Long> smsMessageId) {
private void handleMessage(@NonNull byte[] plaintextDataBuffer, @NonNull Optional<Long> 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<String> toEncodedId(@NonNull Optional<SignalServiceGroup> groupInfo) {
return groupInfo.transform(g -> GroupUtil.getEncodedId(g.getGroupId(), false));
}
private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional<Long> 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<SignalServiceGroup> group,
@NonNull Optional<String> groupId,
long timestamp,
@NonNull Optional<Long> smsMessageId)
{
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
if (!smsMessageId.isPresent()) {
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp, group);
Optional<InsertResult> 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<SignalServiceGroup> group,
@NonNull Optional<String> groupId,
long timestamp,
@NonNull Optional<Long> smsMessageId)
{
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
if (!smsMessageId.isPresent()) {
Optional<InsertResult> insertResult = insertPlaceholder(sender.getIdentifier(), senderDevice, timestamp, group);
Optional<InsertResult> 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<SignalServiceAttachment> 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<List<Contact>> getContacts(Optional<List<SharedContact>> sharedContacts) {
private static Optional<List<Contact>> getContacts(Optional<List<SharedContact>> sharedContacts) {
if (!sharedContacts.isPresent()) return Optional.absent();
List<Contact> contacts = new ArrayList<>(sharedContacts.get().size());
@ -1338,7 +1365,7 @@ public class PushDecryptJob extends BaseJob {
return Optional.of(contacts);
}
private Optional<List<LinkPreview>> getLinkPreviews(Optional<List<Preview>> previews, @NonNull String message) {
private static Optional<List<LinkPreview>> getLinkPreviews(Optional<List<Preview>> previews, @NonNull String message) {
if (!previews.isPresent()) return Optional.absent();
List<LinkPreview> linkPreviews = new ArrayList<>(previews.get().size());
@ -1366,11 +1393,11 @@ public class PushDecryptJob extends BaseJob {
return insertPlaceholder(sender, senderDevice, timestamp, Optional.absent());
}
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional<SignalServiceGroup> group) {
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional<String> 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<PushDecryptJob> {
public static final class Factory implements Job.Factory<PushProcessMessageJob> {
@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);
}
}
}

View File

@ -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();
}
}

View File

@ -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<SignalServiceGroup> group,
String encodedBody, Optional<String> 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) {

View File

@ -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);