mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 09:18:39 +01:00
Initial pre-alpha support for sender key.
This commit is contained in:
@@ -21,7 +21,7 @@ dependencies {
|
||||
api 'com.googlecode.libphonenumber:libphonenumber:8.12.17'
|
||||
api 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2'
|
||||
|
||||
api 'org.whispersystems:signal-client-java:0.5.1'
|
||||
implementation 'org.whispersystems:signal-client-java:0.8.1'
|
||||
api 'com.squareup.okhttp3:okhttp:3.12.10'
|
||||
implementation 'org.threeten:threetenbp:1.3.6'
|
||||
|
||||
|
||||
+29
@@ -31,6 +31,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
|
||||
import org.whispersystems.signalservice.internal.push.ProofRequiredResponse;
|
||||
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
|
||||
@@ -46,6 +47,7 @@ import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
@@ -175,6 +177,33 @@ public class SignalServiceMessagePipe {
|
||||
}
|
||||
}
|
||||
|
||||
public Future<SendGroupMessageResponse> sendToGroup(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online) throws IOException {
|
||||
List<String> headers = new LinkedList<String>() {{
|
||||
add("content-type:application/vnd.signal-messenger.mrm");
|
||||
add("Unidentified-Access-Key:" + Base64.encodeBytes(joinedUnidentifiedAccess));
|
||||
}};
|
||||
|
||||
String path = String.format(Locale.US, "/v1/messages/multi_recipient?ts=%s&online=%s", timestamp, online);
|
||||
|
||||
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||
.setId(new SecureRandom().nextLong())
|
||||
.setVerb("PUT")
|
||||
.setPath(path)
|
||||
.addAllHeaders(headers)
|
||||
.setBody(ByteString.copyFrom(body))
|
||||
.build();
|
||||
|
||||
ListenableFuture<WebsocketResponse> response = websocket.sendRequest(requestMessage);
|
||||
|
||||
return FutureTransformers.map(response, value -> {
|
||||
if (value.getStatus() == 200) {
|
||||
return JsonUtil.fromJson(value.getBody(), SendGroupMessageResponse.class);
|
||||
} else {
|
||||
throw new NonSuccessfulResponseCodeException(value.getStatus());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Future<SendMessageResponse> send(OutgoingPushMessageList list, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
|
||||
List<String> headers = new LinkedList<String>() {{
|
||||
add("content-type:application/json");
|
||||
|
||||
+450
-131
@@ -8,15 +8,26 @@ package org.whispersystems.signalservice.api;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
|
||||
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.NoSessionException;
|
||||
import org.whispersystems.libsignal.SessionBuilder;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.groups.GroupSessionBuilder;
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||
import org.whispersystems.libsignal.protocol.PlaintextContent;
|
||||
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
|
||||
import org.whispersystems.libsignal.state.PreKeyBundle;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeContent;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
@@ -28,6 +39,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
@@ -51,6 +63,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException;
|
||||
@@ -62,14 +75,17 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.Uint64RangeException;
|
||||
import org.whispersystems.signalservice.api.util.Uint64Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.GroupMismatchedDevices;
|
||||
import org.whispersystems.signalservice.internal.push.GroupStaleDevices;
|
||||
import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse;
|
||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
|
||||
import org.whispersystems.signalservice.internal.push.ProofRequiredResponse;
|
||||
import org.whispersystems.signalservice.internal.push.ProvisioningProtos;
|
||||
import org.whispersystems.signalservice.internal.push.PushAttachmentData;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
@@ -86,6 +102,8 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMe
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Verified;
|
||||
import org.whispersystems.signalservice.internal.push.StaleDevices;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupStaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory;
|
||||
@@ -94,17 +112,20 @@ import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.util.Base64;
|
||||
import org.whispersystems.util.ByteArrayUtil;
|
||||
import org.whispersystems.util.FlagUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -114,6 +135,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The main interface for sending Signal Service messages.
|
||||
@@ -205,9 +227,26 @@ public class SignalServiceMessageSender {
|
||||
SignalServiceReceiptMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] content = createReceiptContent(message);
|
||||
Content content = createReceiptContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, null);
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a retry receipt for a bad-encrypted envelope.
|
||||
*/
|
||||
public void sendRetryReceipt(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
Optional<byte[]> groupId,
|
||||
DecryptionErrorMessage errorMessage)
|
||||
throws IOException, UntrustedIdentityException
|
||||
|
||||
{
|
||||
PlaintextContent content = new PlaintextContent(errorMessage);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.plaintext(content, groupId);
|
||||
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,9 +262,10 @@ public class SignalServiceMessageSender {
|
||||
SignalServiceTypingMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] content = createTypingContent(message);
|
||||
Content content = createTypingContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, null);
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null);
|
||||
}
|
||||
|
||||
public void sendTyping(List<SignalServiceAddress> recipients,
|
||||
@@ -234,8 +274,23 @@ public class SignalServiceMessageSender {
|
||||
CancelationSignal cancelationSignal)
|
||||
throws IOException
|
||||
{
|
||||
byte[] content = createTypingContent(message);
|
||||
sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, cancelationSignal);
|
||||
Content content = createTypingContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, cancelationSignal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a typing indicator a group. Doesn't bother with return results, since these are best-effort.
|
||||
*/
|
||||
public void sendGroupTyping(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceTypingMessage message)
|
||||
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException
|
||||
{
|
||||
Content content = createTypingContent(message);
|
||||
sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId().orNull(), false);
|
||||
}
|
||||
|
||||
|
||||
@@ -251,8 +306,10 @@ public class SignalServiceMessageSender {
|
||||
SignalServiceCallMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] content = createCallContent(message);
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, null);
|
||||
Content content = createCallContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.absent());
|
||||
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,18 +334,22 @@ public class SignalServiceMessageSender {
|
||||
* @throws UntrustedIdentityException
|
||||
* @throws IOException
|
||||
*/
|
||||
public SendMessageResult sendMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
SignalServiceDataMessage message)
|
||||
public SendMessageResult sendDataMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message)
|
||||
throws UntrustedIdentityException, IOException
|
||||
{
|
||||
byte[] content = createMessageContent(message);
|
||||
long timestamp = message.getTimestamp();
|
||||
SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, null);
|
||||
Content content = createMessageContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
|
||||
long timestamp = message.getTimestamp();
|
||||
SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null);
|
||||
|
||||
if (result.getSuccess() != null && result.getSuccess().isNeedsSync()) {
|
||||
byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false);
|
||||
sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, null);
|
||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false);
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
sendMessage(localAddress, Optional.absent(), timestamp, syncMessageContent, false, null);
|
||||
}
|
||||
|
||||
// TODO [greyson][session] Delete this when we delete the button
|
||||
@@ -309,21 +370,81 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a group.
|
||||
* Gives you a {@link SenderKeyDistributionMessage} that can then be sent out to recipients to tell them about your sender key.
|
||||
* Will create a sender key session for the provided DistributionId if one doesn't exist.
|
||||
*/
|
||||
public SenderKeyDistributionMessage getOrCreateNewGroupSession(DistributionId distributionId) {
|
||||
SignalProtocolAddress self = new SignalProtocolAddress(localAddress.getIdentifier(), SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
return new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).create(self, distributionId.asUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the provided {@link SenderKeyDistributionMessage} to the specified recipients.
|
||||
*/
|
||||
public List<SendMessageResult> sendSenderKeyDistributionMessage(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
SenderKeyDistributionMessage message,
|
||||
byte[] groupId)
|
||||
throws IOException
|
||||
{
|
||||
ByteString distributionBytes = ByteString.copyFrom(message.serialize());
|
||||
Content content = Content.newBuilder().setSenderKeyDistributionMessage(distributionBytes).build();
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.of(groupId));
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an inbound {@link SenderKeyDistributionMessage}.
|
||||
*/
|
||||
public void processSenderKeyDistributionMessage(SignalProtocolAddress sender, SenderKeyDistributionMessage senderKeyDistributionMessage) {
|
||||
new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).process(sender, senderKeyDistributionMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a {@link SignalServiceDataMessage} to a group using sender keys.
|
||||
*/
|
||||
public List<SendMessageResult> sendGroupDataMessage(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message)
|
||||
throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException
|
||||
{
|
||||
Content content = createMessageContent(message);
|
||||
Optional<byte[]> groupId = message.getGroupId();
|
||||
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId.orNull(), false);
|
||||
|
||||
if (isMultiDevice.get()) {
|
||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.absent(), message.getTimestamp(), results, isRecipientUpdate);
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
sendMessage(localAddress, Optional.absent(), message.getTimestamp(), syncMessageContent, false, null);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to a group using client-side fanout.
|
||||
*
|
||||
* @param recipients The group members.
|
||||
* @param message The group message.
|
||||
* @throws IOException
|
||||
*/
|
||||
public List<SendMessageResult> sendMessage(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
boolean isRecipientUpdate,
|
||||
SignalServiceDataMessage message)
|
||||
public List<SendMessageResult> sendDataMessage(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] content = createMessageContent(message);
|
||||
Content content = createMessageContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
|
||||
long timestamp = message.getTimestamp();
|
||||
List<SendMessageResult> results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, null);
|
||||
List<SendMessageResult> results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null);
|
||||
boolean needsSyncInResults = false;
|
||||
|
||||
for (SendMessageResult result : results) {
|
||||
@@ -339,17 +460,19 @@ public class SignalServiceMessageSender {
|
||||
recipient = Optional.of(recipients.get(0));
|
||||
}
|
||||
|
||||
byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate);
|
||||
sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, null);
|
||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate);
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
sendMessage(localAddress, Optional.absent(), timestamp, syncMessageContent, false, null);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public void sendMessage(SignalServiceSyncMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
public void sendSyncMessage(SignalServiceSyncMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] content;
|
||||
Content content;
|
||||
|
||||
if (message.getContacts().isPresent()) {
|
||||
content = createMultiDeviceContactsContent(message.getContacts().get().getContactsStream().asStream(),
|
||||
@@ -379,7 +502,7 @@ public class SignalServiceMessageSender {
|
||||
} else if (message.getKeys().isPresent()) {
|
||||
content = createMultiDeviceSyncKeysContent(message.getKeys().get());
|
||||
} else if (message.getVerified().isPresent()) {
|
||||
sendMessage(message.getVerified().get(), unidentifiedAccess);
|
||||
sendVerifiedMessage(message.getVerified().get(), unidentifiedAccess);
|
||||
return;
|
||||
} else {
|
||||
throw new IOException("Unsupported sync message!");
|
||||
@@ -388,7 +511,9 @@ public class SignalServiceMessageSender {
|
||||
long timestamp = message.getSent().isPresent() ? message.getSent().get().getTimestamp()
|
||||
: System.currentTimeMillis();
|
||||
|
||||
sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), timestamp, content, false, null);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
sendMessage(localAddress, Optional.absent(), timestamp, envelopeContent, false, null);
|
||||
}
|
||||
|
||||
public void setSoTimeoutMillis(long soTimeoutMillis) {
|
||||
@@ -506,7 +631,7 @@ public class SignalServiceMessageSender {
|
||||
attachment.getUploadTimestamp());
|
||||
}
|
||||
|
||||
private void sendMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
private void sendVerifiedMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] nullMessageBody = DataMessage.newBuilder()
|
||||
@@ -518,16 +643,19 @@ public class SignalServiceMessageSender {
|
||||
.setPadding(ByteString.copyFrom(nullMessageBody))
|
||||
.build();
|
||||
|
||||
byte[] content = Content.newBuilder()
|
||||
Content content = Content.newBuilder()
|
||||
.setNullMessage(nullMessage)
|
||||
.build()
|
||||
.toByteArray();
|
||||
.build();
|
||||
|
||||
SendMessageResult result = sendMessage(message.getDestination(), getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, false, null);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
SendMessageResult result = sendMessage(message.getDestination(), getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, false, null);
|
||||
|
||||
if (result.getSuccess().isNeedsSync()) {
|
||||
byte[] syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.toByteArray());
|
||||
sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), message.getTimestamp(), syncMessage, false, null);
|
||||
Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.toByteArray());
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
sendMessage(localAddress, Optional.absent(), message.getTimestamp(), syncMessageContent, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,15 +671,16 @@ public class SignalServiceMessageSender {
|
||||
.setPadding(ByteString.copyFrom(nullMessageBody))
|
||||
.build();
|
||||
|
||||
byte[] content = Content.newBuilder()
|
||||
.setNullMessage(nullMessage)
|
||||
.build()
|
||||
.toByteArray();
|
||||
Content content = Content.newBuilder()
|
||||
.setNullMessage(nullMessage)
|
||||
.build();
|
||||
|
||||
return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, null);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null);
|
||||
}
|
||||
|
||||
private byte[] createTypingContent(SignalServiceTypingMessage message) {
|
||||
private Content createTypingContent(SignalServiceTypingMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
TypingMessage.Builder builder = TypingMessage.newBuilder();
|
||||
|
||||
@@ -565,10 +694,10 @@ public class SignalServiceMessageSender {
|
||||
builder.setGroupId(ByteString.copyFrom(message.getGroupId().get()));
|
||||
}
|
||||
|
||||
return container.setTypingMessage(builder).build().toByteArray();
|
||||
return container.setTypingMessage(builder).build();
|
||||
}
|
||||
|
||||
private byte[] createReceiptContent(SignalServiceReceiptMessage message) {
|
||||
private Content createReceiptContent(SignalServiceReceiptMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
ReceiptMessage.Builder builder = ReceiptMessage.newBuilder();
|
||||
|
||||
@@ -580,10 +709,10 @@ public class SignalServiceMessageSender {
|
||||
else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ);
|
||||
else if (message.isViewedReceipt()) builder.setType(ReceiptMessage.Type.VIEWED);
|
||||
|
||||
return container.setReceiptMessage(builder).build().toByteArray();
|
||||
return container.setReceiptMessage(builder).build();
|
||||
}
|
||||
|
||||
private byte[] createMessageContent(SignalServiceDataMessage message) throws IOException {
|
||||
private Content createMessageContent(SignalServiceDataMessage message) throws IOException {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
DataMessage.Builder builder = DataMessage.newBuilder();
|
||||
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
|
||||
@@ -779,10 +908,10 @@ public class SignalServiceMessageSender {
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
return enforceMaxContentSize(container.setDataMessage(builder).build().toByteArray());
|
||||
return enforceMaxContentSize(container.setDataMessage(builder).build());
|
||||
}
|
||||
|
||||
private byte[] createCallContent(SignalServiceCallMessage callMessage) {
|
||||
private Content createCallContent(SignalServiceCallMessage callMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
CallMessage.Builder builder = CallMessage.newBuilder();
|
||||
|
||||
@@ -862,30 +991,30 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
container.setCallMessage(builder);
|
||||
return container.build().toByteArray();
|
||||
return container.build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) throws IOException {
|
||||
private Content createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) throws IOException {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder builder = createSyncMessageBuilder();
|
||||
builder.setContacts(SyncMessage.Contacts.newBuilder()
|
||||
.setBlob(createAttachmentPointer(contacts))
|
||||
.setComplete(complete));
|
||||
|
||||
return container.setSyncMessage(builder).build().toByteArray();
|
||||
return container.setSyncMessage(builder).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceGroupsContent(SignalServiceAttachmentStream groups) throws IOException {
|
||||
private Content createMultiDeviceGroupsContent(SignalServiceAttachmentStream groups) throws IOException {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder builder = createSyncMessageBuilder();
|
||||
|
||||
builder.setGroups(SyncMessage.Groups.newBuilder()
|
||||
.setBlob(createAttachmentPointer(groups)));
|
||||
|
||||
return container.setSyncMessage(builder).build().toByteArray();
|
||||
return container.setSyncMessage(builder).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, Optional<UnidentifiedAccessPair> unidentifiedAccess) throws IOException {
|
||||
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, Optional<UnidentifiedAccessPair> unidentifiedAccess) throws IOException {
|
||||
SignalServiceAddress address = transcript.getDestination().get();
|
||||
SendMessageResult result = SendMessageResult.success(address, unidentifiedAccess.isPresent(), true, -1);
|
||||
|
||||
@@ -896,60 +1025,56 @@ public class SignalServiceMessageSender {
|
||||
false);
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceSentTranscriptContent(byte[] content, Optional<SignalServiceAddress> recipient,
|
||||
long timestamp, List<SendMessageResult> sendMessageResults,
|
||||
boolean isRecipientUpdate)
|
||||
private Content createMultiDeviceSentTranscriptContent(Content content, Optional<SignalServiceAddress> recipient,
|
||||
long timestamp, List<SendMessageResult> sendMessageResults,
|
||||
boolean isRecipientUpdate)
|
||||
{
|
||||
try {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
|
||||
DataMessage dataMessage = Content.parseFrom(content).getDataMessage();
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
|
||||
DataMessage dataMessage = content.getDataMessage();
|
||||
|
||||
sentMessage.setTimestamp(timestamp);
|
||||
sentMessage.setMessage(dataMessage);
|
||||
sentMessage.setTimestamp(timestamp);
|
||||
sentMessage.setMessage(dataMessage);
|
||||
|
||||
for (SendMessageResult result : sendMessageResults) {
|
||||
if (result.getSuccess() != null) {
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder builder = SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder();
|
||||
for (SendMessageResult result : sendMessageResults) {
|
||||
if (result.getSuccess() != null) {
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder builder = SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder();
|
||||
|
||||
if (result.getAddress().getUuid().isPresent()) {
|
||||
builder = builder.setDestinationUuid(result.getAddress().getUuid().get().toString());
|
||||
}
|
||||
|
||||
if (result.getAddress().getNumber().isPresent()) {
|
||||
builder = builder.setDestinationE164(result.getAddress().getNumber().get());
|
||||
}
|
||||
|
||||
builder.setUnidentified(result.getSuccess().isUnidentified());
|
||||
|
||||
sentMessage.addUnidentifiedStatus(builder.build());
|
||||
if (result.getAddress().getUuid().isPresent()) {
|
||||
builder = builder.setDestinationUuid(result.getAddress().getUuid().get().toString());
|
||||
}
|
||||
|
||||
if (result.getAddress().getNumber().isPresent()) {
|
||||
builder = builder.setDestinationE164(result.getAddress().getNumber().get());
|
||||
}
|
||||
|
||||
builder.setUnidentified(result.getSuccess().isUnidentified());
|
||||
|
||||
sentMessage.addUnidentifiedStatus(builder.build());
|
||||
}
|
||||
|
||||
if (recipient.isPresent()) {
|
||||
if (recipient.get().getUuid().isPresent()) sentMessage.setDestinationUuid(recipient.get().getUuid().get().toString());
|
||||
if (recipient.get().getNumber().isPresent()) sentMessage.setDestinationE164(recipient.get().getNumber().get());
|
||||
}
|
||||
|
||||
if (dataMessage.getExpireTimer() > 0) {
|
||||
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
if (dataMessage.getIsViewOnce()) {
|
||||
dataMessage = dataMessage.toBuilder().clearAttachments().build();
|
||||
sentMessage.setMessage(dataMessage);
|
||||
}
|
||||
|
||||
sentMessage.setIsRecipientUpdate(isRecipientUpdate);
|
||||
|
||||
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build().toByteArray();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
if (recipient.isPresent()) {
|
||||
if (recipient.get().getUuid().isPresent()) sentMessage.setDestinationUuid(recipient.get().getUuid().get().toString());
|
||||
if (recipient.get().getNumber().isPresent()) sentMessage.setDestinationE164(recipient.get().getNumber().get());
|
||||
}
|
||||
|
||||
if (dataMessage.getExpireTimer() > 0) {
|
||||
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
if (dataMessage.getIsViewOnce()) {
|
||||
dataMessage = dataMessage.toBuilder().clearAttachments().build();
|
||||
sentMessage.setMessage(dataMessage);
|
||||
}
|
||||
|
||||
sentMessage.setIsRecipientUpdate(isRecipientUpdate);
|
||||
|
||||
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceReadContent(List<ReadMessage> readMessages) {
|
||||
private Content createMultiDeviceReadContent(List<ReadMessage> readMessages) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder builder = createSyncMessageBuilder();
|
||||
|
||||
@@ -967,10 +1092,10 @@ public class SignalServiceMessageSender {
|
||||
builder.addRead(readBuilder.build());
|
||||
}
|
||||
|
||||
return container.setSyncMessage(builder).build().toByteArray();
|
||||
return container.setSyncMessage(builder).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceViewedContent(List<ViewedMessage> readMessages) {
|
||||
private Content createMultiDeviceViewedContent(List<ViewedMessage> readMessages) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder builder = createSyncMessageBuilder();
|
||||
|
||||
@@ -988,10 +1113,10 @@ public class SignalServiceMessageSender {
|
||||
builder.addViewed(viewedBuilder.build());
|
||||
}
|
||||
|
||||
return container.setSyncMessage(builder).build().toByteArray();
|
||||
return container.setSyncMessage(builder).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceViewOnceOpenContent(ViewOnceOpenMessage readMessage) {
|
||||
private Content createMultiDeviceViewOnceOpenContent(ViewOnceOpenMessage readMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder builder = createSyncMessageBuilder();
|
||||
SyncMessage.ViewOnceOpen.Builder viewOnceBuilder = SyncMessage.ViewOnceOpen.newBuilder().setTimestamp(readMessage.getTimestamp());
|
||||
@@ -1006,10 +1131,10 @@ public class SignalServiceMessageSender {
|
||||
|
||||
builder.setViewOnceOpen(viewOnceBuilder.build());
|
||||
|
||||
return container.setSyncMessage(builder).build().toByteArray();
|
||||
return container.setSyncMessage(builder).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceBlockedContent(BlockedListMessage blocked) {
|
||||
private Content createMultiDeviceBlockedContent(BlockedListMessage blocked) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.Blocked.Builder blockedMessage = SyncMessage.Blocked.newBuilder();
|
||||
@@ -1027,10 +1152,10 @@ public class SignalServiceMessageSender {
|
||||
blockedMessage.addGroupIds(ByteString.copyFrom(groupId));
|
||||
}
|
||||
|
||||
return container.setSyncMessage(syncMessage.setBlocked(blockedMessage)).build().toByteArray();
|
||||
return container.setSyncMessage(syncMessage.setBlocked(blockedMessage)).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceConfigurationContent(ConfigurationMessage configuration) {
|
||||
private Content createMultiDeviceConfigurationContent(ConfigurationMessage configuration) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.Configuration.Builder configurationMessage = SyncMessage.Configuration.newBuilder();
|
||||
@@ -1053,10 +1178,10 @@ public class SignalServiceMessageSender {
|
||||
|
||||
configurationMessage.setProvisioningVersion(ProvisioningProtos.ProvisioningVersion.CURRENT_VALUE);
|
||||
|
||||
return container.setSyncMessage(syncMessage.setConfiguration(configurationMessage)).build().toByteArray();
|
||||
return container.setSyncMessage(syncMessage.setConfiguration(configurationMessage)).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceStickerPackOperationContent(List<StickerPackOperationMessage> stickerPackOperations) {
|
||||
private Content createMultiDeviceStickerPackOperationContent(List<StickerPackOperationMessage> stickerPackOperations) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
|
||||
@@ -1081,10 +1206,10 @@ public class SignalServiceMessageSender {
|
||||
syncMessage.addStickerPackOperation(builder);
|
||||
}
|
||||
|
||||
return container.setSyncMessage(syncMessage).build().toByteArray();
|
||||
return container.setSyncMessage(syncMessage).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceFetchTypeContent(SignalServiceSyncMessage.FetchType fetchType) {
|
||||
private Content createMultiDeviceFetchTypeContent(SignalServiceSyncMessage.FetchType fetchType) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.FetchLatest.Builder fetchMessage = SyncMessage.FetchLatest.newBuilder();
|
||||
@@ -1101,10 +1226,10 @@ public class SignalServiceMessageSender {
|
||||
break;
|
||||
}
|
||||
|
||||
return container.setSyncMessage(syncMessage.setFetchLatest(fetchMessage)).build().toByteArray();
|
||||
return container.setSyncMessage(syncMessage.setFetchLatest(fetchMessage)).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) {
|
||||
private Content createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.MessageRequestResponse.Builder responseMessage = SyncMessage.MessageRequestResponse.newBuilder();
|
||||
@@ -1143,10 +1268,10 @@ public class SignalServiceMessageSender {
|
||||
|
||||
syncMessage.setMessageRequestResponse(responseMessage);
|
||||
|
||||
return container.setSyncMessage(syncMessage).build().toByteArray();
|
||||
return container.setSyncMessage(syncMessage).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceOutgoingPaymentContent(OutgoingPaymentMessage message) {
|
||||
private Content createMultiDeviceOutgoingPaymentContent(OutgoingPaymentMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.OutgoingPayment.Builder paymentMessage = SyncMessage.OutgoingPayment.newBuilder();
|
||||
@@ -1180,10 +1305,10 @@ public class SignalServiceMessageSender {
|
||||
|
||||
syncMessage.setOutgoingPayment(paymentMessage);
|
||||
|
||||
return container.setSyncMessage(syncMessage).build().toByteArray();
|
||||
return container.setSyncMessage(syncMessage).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceSyncKeysContent(KeysMessage keysMessage) {
|
||||
private Content createMultiDeviceSyncKeysContent(KeysMessage keysMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.Keys.Builder builder = SyncMessage.Keys.newBuilder();
|
||||
@@ -1194,10 +1319,10 @@ public class SignalServiceMessageSender {
|
||||
Log.w(TAG, "Invalid keys message!");
|
||||
}
|
||||
|
||||
return container.setSyncMessage(syncMessage.setKeys(builder)).build().toByteArray();
|
||||
return container.setSyncMessage(syncMessage.setKeys(builder)).build();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) {
|
||||
private Content createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
Verified.Builder verifiedMessageBuilder = Verified.newBuilder();
|
||||
@@ -1221,7 +1346,7 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
syncMessage.setVerified(verifiedMessageBuilder);
|
||||
return container.setSyncMessage(syncMessage).build().toByteArray();
|
||||
return container.setSyncMessage(syncMessage).build();
|
||||
}
|
||||
|
||||
private SyncMessage.Builder createSyncMessageBuilder() {
|
||||
@@ -1389,7 +1514,7 @@ public class SignalServiceMessageSender {
|
||||
private List<SendMessageResult> sendMessage(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
EnvelopeContent content,
|
||||
boolean online,
|
||||
CancelationSignal cancelationSignal)
|
||||
throws IOException
|
||||
@@ -1461,7 +1586,7 @@ public class SignalServiceMessageSender {
|
||||
private SendMessageResult sendMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
EnvelopeContent content,
|
||||
boolean online,
|
||||
CancelationSignal cancelationSignal)
|
||||
throws UntrustedIdentityException, IOException
|
||||
@@ -1491,7 +1616,7 @@ public class SignalServiceMessageSender {
|
||||
return SendMessageResult.success(recipient, false, response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime);
|
||||
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
|
||||
Log.w(TAG, e);
|
||||
Log.w(TAG, "[sendMessage] Pipe failed, falling back...");
|
||||
Log.w(TAG, "[sendMessage] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||
}
|
||||
} else if (unidentifiedPipe.isPresent() && unidentifiedAccess.isPresent()) {
|
||||
try {
|
||||
@@ -1530,7 +1655,190 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Failed to resolve conflicts after 3 attempts!");
|
||||
throw new IOException("Failed to resolve conflicts after " + RETRY_COUNT + " attempts!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Will send a message using sender keys to all of the specified recipients. It is assumed that
|
||||
* all of the recipients have UUIDs.
|
||||
*
|
||||
* This method will handle sending out SenderKeyDistributionMessages as necessary.
|
||||
*/
|
||||
private List<SendMessageResult> sendGroupMessage(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
Content content,
|
||||
ContentHint contentHint,
|
||||
byte[] groupId,
|
||||
boolean online)
|
||||
throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException
|
||||
{
|
||||
if (recipients.isEmpty()) {
|
||||
Log.w(TAG, "[sendGroupMessage] Empty recipient list!");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Preconditions.checkArgument(recipients.size() == unidentifiedAccess.size(), "Unidentified access mismatch!");
|
||||
|
||||
if (recipients.stream().anyMatch(r -> !r.getUuid().isPresent())) {
|
||||
throw new IllegalArgumentException("All recipients must have a UUID!");
|
||||
}
|
||||
|
||||
Map<UUID, UnidentifiedAccess> accessByUuid = new HashMap<>();
|
||||
Iterator<SignalServiceAddress> addressIterator = recipients.iterator();
|
||||
Iterator<UnidentifiedAccess> accessIterator = unidentifiedAccess.iterator();
|
||||
|
||||
while (addressIterator.hasNext()) {
|
||||
accessByUuid.put(addressIterator.next().getUuid().get(), accessIterator.next());
|
||||
}
|
||||
|
||||
for (int i = 0; i < RETRY_COUNT; i++) {
|
||||
List<SignalProtocolAddress> destinations = new LinkedList<>();
|
||||
|
||||
for (SignalServiceAddress recipient : recipients) {
|
||||
destinations.add(new SignalProtocolAddress(recipient.getUuid().get().toString(), SignalServiceAddress.DEFAULT_DEVICE_ID));
|
||||
|
||||
for (int deviceId : store.getSubDeviceSessions(recipient.getIdentifier())) {
|
||||
if (store.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) {
|
||||
destinations.add(new SignalProtocolAddress(recipient.getUuid().get().toString(), deviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<SignalProtocolAddress> sharedWith = store.getSenderKeySharedWith(distributionId);
|
||||
List<SignalServiceAddress> needsSenderKey = destinations.stream()
|
||||
.filter(a -> !sharedWith.contains(a))
|
||||
.map(a -> UuidUtil.parseOrThrow(a.getName()))
|
||||
.distinct()
|
||||
.map(uuid -> new SignalServiceAddress(uuid, null))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (needsSenderKey.size() > 0) {
|
||||
Log.i(TAG, "[sendGroupMessage] Need to send the distribution message to " + needsSenderKey.size() + " addresses.");
|
||||
SenderKeyDistributionMessage message = getOrCreateNewGroupSession(distributionId);
|
||||
List<Optional<UnidentifiedAccessPair>> access = needsSenderKey.stream()
|
||||
.map(r -> {
|
||||
UnidentifiedAccess targetAccess = accessByUuid.get(r.getUuid().get());
|
||||
return Optional.of(new UnidentifiedAccessPair(targetAccess, targetAccess));
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<SendMessageResult> results = sendSenderKeyDistributionMessage(needsSenderKey, access, message, groupId);
|
||||
|
||||
List<SignalServiceAddress> successes = results.stream()
|
||||
.filter(SendMessageResult::isSuccess)
|
||||
.map(SendMessageResult::getAddress)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Set<String> successUuids = successes.stream().map(a -> a.getUuid().get().toString()).collect(Collectors.toSet());
|
||||
Set<SignalProtocolAddress> successAddresses = destinations.stream().filter(a -> successUuids.contains(a.getName())).collect(Collectors.toSet());;
|
||||
|
||||
store.markSenderKeySharedWith(distributionId, successAddresses);
|
||||
|
||||
Log.i(TAG, "[sendGroupMessage] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKey.size() + " recipients.");
|
||||
|
||||
int failureCount = results.size() - successes.size();
|
||||
if (failureCount > 0) {
|
||||
Log.w(TAG, "[sendGroupMessage] Failed to send sender keys to " + failureCount + " recipients. Sending back failed results now.");
|
||||
|
||||
List<SendMessageResult> trueFailures = results.stream()
|
||||
.filter(r -> !r.isSuccess())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Set<SignalServiceAddress> failedAddresses = trueFailures.stream()
|
||||
.map(SendMessageResult::getAddress)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
List<SendMessageResult> fakeNetworkFailures = recipients.stream()
|
||||
.filter(r -> !failedAddresses.contains(r))
|
||||
.map(SendMessageResult::networkFailure)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<SendMessageResult> modifiedResults = new LinkedList<>();
|
||||
modifiedResults.addAll(trueFailures);
|
||||
modifiedResults.addAll(fakeNetworkFailures);
|
||||
|
||||
return modifiedResults;
|
||||
}
|
||||
}
|
||||
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sessionLock, null);
|
||||
SenderCertificate senderCertificate = unidentifiedAccess.get(0).getUnidentifiedCertificate();
|
||||
|
||||
byte[] ciphertext;
|
||||
try {
|
||||
ciphertext = cipher.encryptForGroup(distributionId, destinations, senderCertificate, content.toByteArray(), contentHint, groupId);
|
||||
} catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
|
||||
throw new UntrustedIdentityException("Untrusted during group encrypt", e.getName(), e.getUntrustedIdentity());
|
||||
}
|
||||
|
||||
byte[] joinedUnidentifiedAccess = new byte[16];
|
||||
for (UnidentifiedAccess access : unidentifiedAccess) {
|
||||
joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.getUnidentifiedAccessKey());
|
||||
}
|
||||
|
||||
Optional<SignalServiceMessagePipe> pipe = this.unidentifiedPipe.get();
|
||||
|
||||
if (pipe.isPresent()) {
|
||||
try {
|
||||
SendGroupMessageResponse response = pipe.get().sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online).get(10, TimeUnit.SECONDS);
|
||||
return transformGroupResponseToMessageResults(recipients, response);
|
||||
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
|
||||
Log.w(TAG, "[sendGroupMessage] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "[sendGroupMessage] No pipe available.");
|
||||
}
|
||||
|
||||
try {
|
||||
SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online);
|
||||
return transformGroupResponseToMessageResults(recipients, response);
|
||||
} catch (GroupMismatchedDevicesException e) {
|
||||
Log.w(TAG, "[sendGroupMessage] Handling mismatched devices.", e);
|
||||
for (GroupMismatchedDevices mismatched : e.getMismatchedDevices()) {
|
||||
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parse(mismatched.getUuid()), Optional.absent());
|
||||
handleMismatchedDevices(socket, address, mismatched.getDevices());
|
||||
List<SignalProtocolAddress> clearAddresses = mismatched.getDevices().getExtraDevices().stream()
|
||||
.map(device -> new SignalProtocolAddress(address.getIdentifier(), device))
|
||||
.collect(Collectors.toList());
|
||||
store.clearSenderKeySharedWith(distributionId, clearAddresses);
|
||||
}
|
||||
} catch (GroupStaleDevicesException e) {
|
||||
Log.w(TAG, "[sendGroupMessage] Handling stale devices.", e);
|
||||
for (GroupStaleDevices stale : e.getStaleDevices()) {
|
||||
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parse(stale.getUuid()), Optional.absent());
|
||||
handleStaleDevices(address, stale.getDevices());
|
||||
List<SignalProtocolAddress> clearAddresses = stale.getDevices().getStaleDevices().stream()
|
||||
.map(device -> new SignalProtocolAddress(address.getIdentifier(), device))
|
||||
.collect(Collectors.toList());
|
||||
store.clearSenderKeySharedWith(distributionId, clearAddresses);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "[sendGroupMessage] Attempt failed (i = " + i + ")");
|
||||
}
|
||||
|
||||
throw new IOException("Failed to resolve conflicts after " + RETRY_COUNT + " attempts!");
|
||||
}
|
||||
|
||||
private List<SendMessageResult> transformGroupResponseToMessageResults(List<SignalServiceAddress> recipients, SendGroupMessageResponse response) {
|
||||
Set<UUID> unregistered = response.getUnsentTargets();
|
||||
|
||||
List<SendMessageResult> failures = unregistered.stream()
|
||||
.map(uuid -> new SignalServiceAddress(uuid, null))
|
||||
.map(SendMessageResult::unregisteredFailure)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<SendMessageResult> success = recipients.stream()
|
||||
.filter(r -> !unregistered.contains(r.getUuid().get()))
|
||||
.map(a -> SendMessageResult.success(a, true, isMultiDevice.get(), -1))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<SendMessageResult> results = new LinkedList<>(success);
|
||||
results.addAll(failures);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<AttachmentPointer> createAttachmentPointers(Optional<List<SignalServiceAttachment>> attachments) throws IOException {
|
||||
@@ -1625,7 +1933,7 @@ public class SignalServiceMessageSender {
|
||||
SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] plaintext,
|
||||
EnvelopeContent plaintext,
|
||||
boolean online)
|
||||
throws IOException, InvalidKeyException, UntrustedIdentityException
|
||||
{
|
||||
@@ -1648,7 +1956,7 @@ public class SignalServiceMessageSender {
|
||||
SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
int deviceId,
|
||||
byte[] plaintext)
|
||||
EnvelopeContent plaintext)
|
||||
throws IOException, InvalidKeyException, UntrustedIdentityException
|
||||
{
|
||||
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId);
|
||||
@@ -1742,9 +2050,20 @@ public class SignalServiceMessageSender {
|
||||
return results;
|
||||
}
|
||||
|
||||
private byte[] enforceMaxContentSize(byte[] content) {
|
||||
if (maxEnvelopeSize > 0 && content.length > maxEnvelopeSize) {
|
||||
throw new ContentTooLargeException(content.length);
|
||||
private EnvelopeContent enforceMaxContentSize(EnvelopeContent content) {
|
||||
int size = content.size();
|
||||
|
||||
if (maxEnvelopeSize > 0 && size > maxEnvelopeSize) {
|
||||
throw new ContentTooLargeException(size);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private Content enforceMaxContentSize(Content content) {
|
||||
int size = content.toByteArray().length;
|
||||
|
||||
if (maxEnvelopeSize > 0 && size > maxEnvelopeSize) {
|
||||
throw new ContentTooLargeException(size);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,5 +6,5 @@ import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
* And extension of the normal protocol store interface that has additional methods that are needed
|
||||
* in the service layer, but not the protocol layer.
|
||||
*/
|
||||
public interface SignalServiceProtocolStore extends SignalProtocolStore, SignalServiceSessionStore {
|
||||
public interface SignalServiceProtocolStore extends SignalProtocolStore, SignalServiceSessionStore, SignalServiceSenderKeyStore {
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.groups.state.SenderKeyStore;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* And extension of the normal protocol sender key store interface that has additional methods that are
|
||||
* needed in the service layer, but not the protocol layer.
|
||||
*/
|
||||
public interface SignalServiceSenderKeyStore extends SenderKeyStore {
|
||||
/**
|
||||
* @return A set of protocol addresses that have previously been sent the sender key data for the provided distributionId.
|
||||
*/
|
||||
Set<SignalProtocolAddress> getSenderKeySharedWith(DistributionId distributionId);
|
||||
|
||||
/**
|
||||
* Marks the provided addresses as having been sent the sender key data for the provided distributionId.
|
||||
*/
|
||||
void markSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses);
|
||||
|
||||
/**
|
||||
* Marks the provided addresses as not knowing about the provided distributionId.
|
||||
*/
|
||||
void clearSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses);
|
||||
}
|
||||
+9
-1
@@ -128,14 +128,18 @@ public class AccountAttributes {
|
||||
@JsonProperty("gv1-migration")
|
||||
private boolean gv1Migration;
|
||||
|
||||
@JsonProperty
|
||||
private boolean senderKey;
|
||||
|
||||
@JsonCreator
|
||||
public Capabilities() {}
|
||||
|
||||
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration) {
|
||||
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey) {
|
||||
this.uuid = uuid;
|
||||
this.gv2 = gv2;
|
||||
this.storage = storage;
|
||||
this.gv1Migration = gv1Migration;
|
||||
this.senderKey = senderKey;
|
||||
}
|
||||
|
||||
public boolean isUuid() {
|
||||
@@ -153,5 +157,9 @@ public class AccountAttributes {
|
||||
public boolean isGv1Migration() {
|
||||
return gv1Migration;
|
||||
}
|
||||
|
||||
public boolean isSenderKey() {
|
||||
return senderKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum ContentHint {
|
||||
/** This message has content, but you shouldn’t expect it to be re-sent to you. */
|
||||
DEFAULT(UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT),
|
||||
|
||||
/** You should expect to be able to have this content be re-sent to you. */
|
||||
RESENDABLE(UnidentifiedSenderMessageContent.CONTENT_HINT_RESENDABLE),
|
||||
|
||||
/** This message has no real content and likely cannot be re-sent to you. */
|
||||
IMPLICIT(UnidentifiedSenderMessageContent.CONTENT_HINT_IMPLICIT);
|
||||
|
||||
private static final Map<Integer, ContentHint> TYPE_MAP = new HashMap<>();
|
||||
static {
|
||||
for (ContentHint value : values()) {
|
||||
TYPE_MAP.put(value.getType(), value);
|
||||
}
|
||||
}
|
||||
|
||||
private final int type;
|
||||
|
||||
ContentHint(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public static ContentHint fromType(int type) {
|
||||
return TYPE_MAP.getOrDefault(type, DEFAULT);
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.UntrustedIdentityException;
|
||||
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||
import org.whispersystems.libsignal.protocol.PlaintextContent;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||
import org.whispersystems.signalservice.internal.push.PushTransportDetails;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
/**
|
||||
* An abstraction over the different types of message contents we can have.
|
||||
*/
|
||||
public interface EnvelopeContent {
|
||||
|
||||
/**
|
||||
* Processes the content using sealed sender.
|
||||
*/
|
||||
OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher,
|
||||
SignalSealedSessionCipher sealedSessionCipher,
|
||||
SignalProtocolAddress destination,
|
||||
SenderCertificate senderCertificate)
|
||||
throws UntrustedIdentityException, InvalidKeyException;
|
||||
|
||||
/**
|
||||
* Processes the content using unsealed sender.
|
||||
*/
|
||||
OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) throws UntrustedIdentityException;
|
||||
|
||||
/**
|
||||
* An estimated size, in bytes.
|
||||
*/
|
||||
int size();
|
||||
|
||||
/**
|
||||
* Wrap {@link Content} you plan on sending as an encrypted message.
|
||||
* This is the default. Consider anything else exceptional.
|
||||
*/
|
||||
static EnvelopeContent encrypted(Content content, ContentHint contentHint, Optional<byte[]> groupId) {
|
||||
return new Encrypted(content.toByteArray(), contentHint, groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a {@link PlaintextContent}. This is exceptional, currently limited only to {@link DecryptionErrorMessage}.
|
||||
*/
|
||||
static EnvelopeContent plaintext(PlaintextContent content, Optional<byte[]> groupId) {
|
||||
return new Plaintext(content, groupId);
|
||||
}
|
||||
|
||||
class Encrypted implements EnvelopeContent {
|
||||
|
||||
private final byte[] unpaddedMessage;
|
||||
private final ContentHint contentHint;
|
||||
private final Optional<byte[]> groupId;
|
||||
|
||||
public Encrypted(byte[] unpaddedMessage, ContentHint contentHint, Optional<byte[]> groupId) {
|
||||
this.unpaddedMessage = unpaddedMessage;
|
||||
this.contentHint = contentHint;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher,
|
||||
SignalSealedSessionCipher sealedSessionCipher,
|
||||
SignalProtocolAddress destination,
|
||||
SenderCertificate senderCertificate)
|
||||
throws UntrustedIdentityException, InvalidKeyException
|
||||
{
|
||||
PushTransportDetails transportDetails = new PushTransportDetails();
|
||||
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(message,
|
||||
senderCertificate,
|
||||
contentHint.getType(),
|
||||
groupId);
|
||||
|
||||
byte[] ciphertext = sealedSessionCipher.encrypt(destination, messageContent);
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int remoteRegistrationId = sealedSessionCipher.getRemoteRegistrationId(destination);
|
||||
|
||||
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) throws UntrustedIdentityException {
|
||||
PushTransportDetails transportDetails = new PushTransportDetails();
|
||||
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
|
||||
String body = Base64.encodeBytes(message.serialize());
|
||||
|
||||
int type;
|
||||
|
||||
switch (message.getType()) {
|
||||
case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
|
||||
case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
|
||||
default: throw new AssertionError("Bad type: " + message.getType());
|
||||
}
|
||||
|
||||
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return unpaddedMessage.length;
|
||||
}
|
||||
}
|
||||
|
||||
class Plaintext implements EnvelopeContent {
|
||||
|
||||
private final PlaintextContent plaintextContent;
|
||||
private final Optional<byte[]> groupId;
|
||||
|
||||
public Plaintext(PlaintextContent plaintextContent, Optional<byte[]> groupId) {
|
||||
this.plaintextContent = plaintextContent;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher,
|
||||
SignalSealedSessionCipher sealedSessionCipher,
|
||||
SignalProtocolAddress destination,
|
||||
SenderCertificate senderCertificate)
|
||||
throws UntrustedIdentityException, InvalidKeyException
|
||||
{
|
||||
UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(plaintextContent,
|
||||
senderCertificate,
|
||||
ContentHint.IMPLICIT.getType(),
|
||||
groupId);
|
||||
|
||||
byte[] ciphertext = sealedSessionCipher.encrypt(destination, messageContent);
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int remoteRegistrationId = sealedSessionCipher.getRemoteRegistrationId(destination);
|
||||
|
||||
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) {
|
||||
String body = Base64.encodeBytes(plaintextContent.getBody());
|
||||
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
|
||||
|
||||
return new OutgoingPushMessage(Type.PLAINTEXT_CONTENT_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return plaintextContent.getBody().length;
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
import org.whispersystems.libsignal.DuplicateMessageException;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.LegacyMessageException;
|
||||
import org.whispersystems.libsignal.NoSessionException;
|
||||
import org.whispersystems.libsignal.groups.GroupCipher;
|
||||
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A thread-safe wrapper around {@link GroupCipher}.
|
||||
*/
|
||||
public class SignalGroupCipher {
|
||||
|
||||
private final SignalSessionLock lock;
|
||||
private final GroupCipher cipher;
|
||||
|
||||
public SignalGroupCipher(SignalSessionLock lock, GroupCipher cipher) {
|
||||
this.lock = lock;
|
||||
this.cipher = cipher;
|
||||
}
|
||||
|
||||
public CiphertextMessage encrypt(UUID distributionId, byte[] paddedPlaintext) throws NoSessionException {
|
||||
try (SignalSessionLock.Lock unused = lock.acquire()) {
|
||||
return cipher.encrypt(distributionId, paddedPlaintext);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] senderKeyMessageBytes)
|
||||
throws LegacyMessageException, DuplicateMessageException, InvalidMessageException, NoSessionException
|
||||
{
|
||||
try (SignalSessionLock.Lock unused = lock.acquire()) {
|
||||
return cipher.decrypt(senderKeyMessageBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
import org.whispersystems.libsignal.SessionBuilder;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.groups.GroupSessionBuilder;
|
||||
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A thread-safe wrapper around {@link SessionBuilder}.
|
||||
*/
|
||||
public class SignalGroupSessionBuilder {
|
||||
|
||||
private final SignalSessionLock lock;
|
||||
private final GroupSessionBuilder builder;
|
||||
|
||||
public SignalGroupSessionBuilder(SignalSessionLock lock, GroupSessionBuilder builder) {
|
||||
this.lock = lock;
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
public void process(SignalProtocolAddress sender, SenderKeyDistributionMessage senderKeyDistributionMessage) {
|
||||
try (SignalSessionLock.Lock unused = lock.acquire()) {
|
||||
builder.process(sender, senderKeyDistributionMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public SenderKeyDistributionMessage create(SignalProtocolAddress sender, UUID distributionId) {
|
||||
try (SignalSessionLock.Lock unused = lock.acquire()) {
|
||||
return builder.create(sender, distributionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-2
@@ -14,10 +14,15 @@ import org.signal.libsignal.metadata.SealedSessionCipher;
|
||||
import org.signal.libsignal.metadata.SelfSendException;
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.NoSessionException;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A thread-safe wrapper around {@link SealedSessionCipher}.
|
||||
*/
|
||||
@@ -31,9 +36,19 @@ public class SignalSealedSessionCipher {
|
||||
this.cipher = cipher;
|
||||
}
|
||||
|
||||
public byte[] encrypt(SignalProtocolAddress destinationAddress, SenderCertificate senderCertificate, byte[] paddedPlaintext) throws InvalidKeyException, org.whispersystems.libsignal.UntrustedIdentityException {
|
||||
public byte[] encrypt(SignalProtocolAddress destinationAddress, UnidentifiedSenderMessageContent content)
|
||||
throws InvalidKeyException, UntrustedIdentityException
|
||||
{
|
||||
try (SignalSessionLock.Lock unused = lock.acquire()) {
|
||||
return cipher.encrypt(destinationAddress, senderCertificate, paddedPlaintext);
|
||||
return cipher.encrypt(destinationAddress, content);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] multiRecipientEncrypt(List<SignalProtocolAddress> recipients, UnidentifiedSenderMessageContent content)
|
||||
throws InvalidKeyException, UntrustedIdentityException, NoSessionException
|
||||
{
|
||||
try (SignalSessionLock.Lock unused = lock.acquire()) {
|
||||
return cipher.multiRecipientEncrypt(recipients, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+40
-28
@@ -22,6 +22,8 @@ import org.signal.libsignal.metadata.SealedSessionCipher;
|
||||
import org.signal.libsignal.metadata.SealedSessionCipher.DecryptionResult;
|
||||
import org.signal.libsignal.metadata.SelfSendException;
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
|
||||
import org.whispersystems.libsignal.DuplicateMessageException;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
@@ -32,7 +34,9 @@ import org.whispersystems.libsignal.NoSessionException;
|
||||
import org.whispersystems.libsignal.SessionCipher;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.UntrustedIdentityException;
|
||||
import org.whispersystems.libsignal.groups.GroupCipher;
|
||||
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||
import org.whispersystems.libsignal.protocol.PlaintextContent;
|
||||
import org.whispersystems.libsignal.protocol.PreKeySignalMessage;
|
||||
import org.whispersystems.libsignal.protocol.SignalMessage;
|
||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
@@ -41,6 +45,7 @@ import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||
@@ -53,10 +58,10 @@ import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadata
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is used to decrypt received {@link SignalServiceEnvelope}s.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
* This is used to encrypt + decrypt received {@link SignalServiceEnvelope}s.
|
||||
*/
|
||||
public class SignalServiceCipher {
|
||||
|
||||
@@ -79,35 +84,41 @@ public class SignalServiceCipher {
|
||||
this.certificateValidator = certificateValidator;
|
||||
}
|
||||
|
||||
public byte[] encryptForGroup(DistributionId distributionId,
|
||||
List<SignalProtocolAddress> destinations,
|
||||
SenderCertificate senderCertificate,
|
||||
byte[] unpaddedMessage,
|
||||
ContentHint contentHint,
|
||||
byte[] groupId)
|
||||
throws NoSessionException, UntrustedIdentityException, InvalidKeyException
|
||||
{
|
||||
PushTransportDetails transport = new PushTransportDetails();
|
||||
SignalProtocolAddress localProtocolAddress = new SignalProtocolAddress(localAddress.getIdentifier(), SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
SignalGroupCipher groupCipher = new SignalGroupCipher(sessionLock, new GroupCipher(signalProtocolStore, localProtocolAddress));
|
||||
SignalSealedSessionCipher sessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1));
|
||||
CiphertextMessage message = groupCipher.encrypt(distributionId.asUuid(), transport.getPaddedMessageBody(unpaddedMessage));
|
||||
UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(message,
|
||||
senderCertificate,
|
||||
contentHint.getType(),
|
||||
Optional.of(groupId));
|
||||
|
||||
return sessionCipher.multiRecipientEncrypt(destinations, messageContent);
|
||||
}
|
||||
|
||||
public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
byte[] unpaddedMessage)
|
||||
EnvelopeContent content)
|
||||
throws UntrustedIdentityException, InvalidKeyException
|
||||
{
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
SignalSealedSessionCipher sessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1));
|
||||
PushTransportDetails transportDetails = new PushTransportDetails();
|
||||
byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
|
||||
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination));
|
||||
SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1));
|
||||
|
||||
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, unidentifiedAccess.get().getUnidentifiedCertificate());
|
||||
} else {
|
||||
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination));
|
||||
PushTransportDetails transportDetails = new PushTransportDetails();
|
||||
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
|
||||
String body = Base64.encodeBytes(message.serialize());
|
||||
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination));
|
||||
|
||||
int type;
|
||||
|
||||
switch (message.getType()) {
|
||||
case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
|
||||
case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
|
||||
default: throw new AssertionError("Bad type: " + message.getType());
|
||||
}
|
||||
|
||||
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
return content.processUnsealedSender(sessionCipher, destination);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,20 +190,21 @@ public class SignalServiceCipher {
|
||||
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress));
|
||||
|
||||
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext));
|
||||
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid());
|
||||
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.absent());
|
||||
} else if (envelope.isSignalMessage()) {
|
||||
SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice());
|
||||
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress));
|
||||
|
||||
paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext));
|
||||
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid());
|
||||
metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.absent());
|
||||
} else if (envelope.isUnidentifiedSender()) {
|
||||
SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1));
|
||||
SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), SignalServiceAddress.DEFAULT_DEVICE_ID));
|
||||
DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerReceivedTimestamp());
|
||||
SignalServiceAddress resultAddress = new SignalServiceAddress(UuidUtil.parse(result.getSenderUuid()), result.getSenderE164());
|
||||
Optional<byte[]> groupId = result.getGroupId();
|
||||
|
||||
paddedMessage = result.getPaddedMessage();
|
||||
metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), true, envelope.getServerGuid());
|
||||
metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), true, envelope.getServerGuid(), groupId);
|
||||
} else {
|
||||
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
|
||||
}
|
||||
|
||||
+4
@@ -42,6 +42,10 @@ public class SendMessageResult {
|
||||
return success;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success != null;
|
||||
}
|
||||
|
||||
public boolean isNetworkFailure() {
|
||||
return networkFailure || proofRequiredFailure != null;
|
||||
}
|
||||
|
||||
+187
-31
@@ -16,7 +16,10 @@ import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.LegacyMessageException;
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||
@@ -70,14 +73,18 @@ public final class SignalServiceContent {
|
||||
private final boolean needsReceipt;
|
||||
private final SignalServiceContentProto serializedState;
|
||||
private final String serverUuid;
|
||||
private final Optional<byte[]> groupId;
|
||||
|
||||
private final Optional<SignalServiceDataMessage> message;
|
||||
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
|
||||
private final Optional<SignalServiceCallMessage> callMessage;
|
||||
private final Optional<SignalServiceReceiptMessage> readMessage;
|
||||
private final Optional<SignalServiceTypingMessage> typingMessage;
|
||||
private final Optional<SignalServiceDataMessage> message;
|
||||
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
|
||||
private final Optional<SignalServiceCallMessage> callMessage;
|
||||
private final Optional<SignalServiceReceiptMessage> readMessage;
|
||||
private final Optional<SignalServiceTypingMessage> typingMessage;
|
||||
private final Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage;
|
||||
private final Optional<DecryptionErrorMessage> decryptionErrorMessage;
|
||||
|
||||
private SignalServiceContent(SignalServiceDataMessage message,
|
||||
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
|
||||
SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
long timestamp,
|
||||
@@ -85,6 +92,7 @@ public final class SignalServiceContent {
|
||||
long serverDeliveredTimestamp,
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -94,16 +102,20 @@ public final class SignalServiceContent {
|
||||
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.fromNullable(message);
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.message = Optional.fromNullable(message);
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
|
||||
this.decryptionErrorMessage = Optional.absent();
|
||||
}
|
||||
|
||||
private SignalServiceContent(SignalServiceSyncMessage synchronizeMessage,
|
||||
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
|
||||
SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
long timestamp,
|
||||
@@ -111,6 +123,7 @@ public final class SignalServiceContent {
|
||||
long serverDeliveredTimestamp,
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -120,16 +133,20 @@ public final class SignalServiceContent {
|
||||
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.fromNullable(synchronizeMessage);
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.fromNullable(synchronizeMessage);
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
|
||||
this.decryptionErrorMessage = Optional.absent();
|
||||
}
|
||||
|
||||
private SignalServiceContent(SignalServiceCallMessage callMessage,
|
||||
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
|
||||
SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
long timestamp,
|
||||
@@ -137,6 +154,7 @@ public final class SignalServiceContent {
|
||||
long serverDeliveredTimestamp,
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -146,16 +164,20 @@ public final class SignalServiceContent {
|
||||
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.of(callMessage);
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.of(callMessage);
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
|
||||
this.decryptionErrorMessage = Optional.absent();
|
||||
}
|
||||
|
||||
private SignalServiceContent(SignalServiceReceiptMessage receiptMessage,
|
||||
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
|
||||
SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
long timestamp,
|
||||
@@ -163,6 +185,7 @@ public final class SignalServiceContent {
|
||||
long serverDeliveredTimestamp,
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -172,16 +195,51 @@ public final class SignalServiceContent {
|
||||
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.of(receiptMessage);
|
||||
this.typingMessage = Optional.absent();
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.of(receiptMessage);
|
||||
this.typingMessage = Optional.absent();
|
||||
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
|
||||
this.decryptionErrorMessage = Optional.absent();
|
||||
}
|
||||
|
||||
private SignalServiceContent(DecryptionErrorMessage errorMessage,
|
||||
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
|
||||
SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
long timestamp,
|
||||
long serverReceivedTimestamp,
|
||||
long serverDeliveredTimestamp,
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
this.senderDevice = senderDevice;
|
||||
this.timestamp = timestamp;
|
||||
this.serverReceivedTimestamp = serverReceivedTimestamp;
|
||||
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
|
||||
this.decryptionErrorMessage = Optional.of(errorMessage);
|
||||
}
|
||||
|
||||
private SignalServiceContent(SignalServiceTypingMessage typingMessage,
|
||||
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
|
||||
SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
long timestamp,
|
||||
@@ -189,6 +247,7 @@ public final class SignalServiceContent {
|
||||
long serverDeliveredTimestamp,
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -198,13 +257,46 @@ public final class SignalServiceContent {
|
||||
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.of(typingMessage);
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.of(typingMessage);
|
||||
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
|
||||
this.decryptionErrorMessage = Optional.absent();
|
||||
}
|
||||
|
||||
private SignalServiceContent(SenderKeyDistributionMessage senderKeyDistributionMessage,
|
||||
SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
long timestamp,
|
||||
long serverReceivedTimestamp,
|
||||
long serverDeliveredTimestamp,
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
this.senderDevice = senderDevice;
|
||||
this.timestamp = timestamp;
|
||||
this.serverReceivedTimestamp = serverReceivedTimestamp;
|
||||
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.absent();
|
||||
this.synchronizeMessage = Optional.absent();
|
||||
this.callMessage = Optional.absent();
|
||||
this.readMessage = Optional.absent();
|
||||
this.typingMessage = Optional.absent();
|
||||
this.senderKeyDistributionMessage = Optional.of(senderKeyDistributionMessage);
|
||||
this.decryptionErrorMessage = Optional.absent();
|
||||
}
|
||||
|
||||
public Optional<SignalServiceDataMessage> getDataMessage() {
|
||||
@@ -227,6 +319,14 @@ public final class SignalServiceContent {
|
||||
return typingMessage;
|
||||
}
|
||||
|
||||
public Optional<SenderKeyDistributionMessage> getSenderKeyDistributionMessage() {
|
||||
return senderKeyDistributionMessage;
|
||||
}
|
||||
|
||||
public Optional<DecryptionErrorMessage> getDecryptionErrorMessage() {
|
||||
return decryptionErrorMessage;
|
||||
}
|
||||
|
||||
public SignalServiceAddress getSender() {
|
||||
return sender;
|
||||
}
|
||||
@@ -255,6 +355,10 @@ public final class SignalServiceContent {
|
||||
return serverUuid;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return serializedState.toByteArray();
|
||||
}
|
||||
@@ -285,6 +389,7 @@ public final class SignalServiceContent {
|
||||
SignalServiceProtos.DataMessage message = serviceContentProto.getLegacyDataMessage();
|
||||
|
||||
return new SignalServiceContent(createSignalServiceMessage(metadata, message),
|
||||
Optional.absent(),
|
||||
metadata.getSender(),
|
||||
metadata.getSenderDevice(),
|
||||
metadata.getTimestamp(),
|
||||
@@ -292,12 +397,23 @@ public final class SignalServiceContent {
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
serviceContentProto);
|
||||
} else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) {
|
||||
SignalServiceProtos.Content message = serviceContentProto.getContent();
|
||||
SignalServiceProtos.Content message = serviceContentProto.getContent();
|
||||
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage = Optional.absent();
|
||||
|
||||
if (message.hasSenderKeyDistributionMessage()) {
|
||||
try {
|
||||
senderKeyDistributionMessage = Optional.of(new SenderKeyDistributionMessage(message.getSenderKeyDistributionMessage().toByteArray()));
|
||||
} catch (LegacyMessageException | InvalidMessageException e) {
|
||||
Log.w(TAG, "Failed to parse SenderKeyDistributionMessage!", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.hasDataMessage()) {
|
||||
return new SignalServiceContent(createSignalServiceMessage(metadata, message.getDataMessage()),
|
||||
senderKeyDistributionMessage,
|
||||
metadata.getSender(),
|
||||
metadata.getSenderDevice(),
|
||||
metadata.getTimestamp(),
|
||||
@@ -305,9 +421,11 @@ public final class SignalServiceContent {
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) {
|
||||
return new SignalServiceContent(createSynchronizeMessage(metadata, message.getSyncMessage()),
|
||||
senderKeyDistributionMessage,
|
||||
metadata.getSender(),
|
||||
metadata.getSenderDevice(),
|
||||
metadata.getTimestamp(),
|
||||
@@ -315,9 +433,11 @@ public final class SignalServiceContent {
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasCallMessage()) {
|
||||
return new SignalServiceContent(createCallMessage(message.getCallMessage()),
|
||||
senderKeyDistributionMessage,
|
||||
metadata.getSender(),
|
||||
metadata.getSenderDevice(),
|
||||
metadata.getTimestamp(),
|
||||
@@ -325,9 +445,11 @@ public final class SignalServiceContent {
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasReceiptMessage()) {
|
||||
return new SignalServiceContent(createReceiptMessage(metadata, message.getReceiptMessage()),
|
||||
senderKeyDistributionMessage,
|
||||
metadata.getSender(),
|
||||
metadata.getSenderDevice(),
|
||||
metadata.getTimestamp(),
|
||||
@@ -335,9 +457,11 @@ public final class SignalServiceContent {
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasTypingMessage()) {
|
||||
return new SignalServiceContent(createTypingMessage(metadata, message.getTypingMessage()),
|
||||
senderKeyDistributionMessage,
|
||||
metadata.getSender(),
|
||||
metadata.getSenderDevice(),
|
||||
metadata.getTimestamp(),
|
||||
@@ -345,6 +469,30 @@ public final class SignalServiceContent {
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
false,
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasDecryptionErrorMessage()) {
|
||||
return new SignalServiceContent(createDecryptionErrorMessage(metadata, message.getDecryptionErrorMessage()),
|
||||
senderKeyDistributionMessage,
|
||||
metadata.getSender(),
|
||||
metadata.getSenderDevice(),
|
||||
metadata.getTimestamp(),
|
||||
metadata.getServerReceivedTimestamp(),
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
serviceContentProto);
|
||||
} else if (senderKeyDistributionMessage.isPresent()) {
|
||||
return new SignalServiceContent(senderKeyDistributionMessage.get(),
|
||||
metadata.getSender(),
|
||||
metadata.getSenderDevice(),
|
||||
metadata.getTimestamp(),
|
||||
metadata.getServerReceivedTimestamp(),
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
false,
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
serviceContentProto);
|
||||
}
|
||||
}
|
||||
@@ -720,6 +868,14 @@ public final class SignalServiceContent {
|
||||
return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp());
|
||||
}
|
||||
|
||||
private static DecryptionErrorMessage createDecryptionErrorMessage(SignalServiceMetadata metadata, ByteString content) throws ProtocolInvalidMessageException {
|
||||
try {
|
||||
return new DecryptionErrorMessage(content.toByteArray());
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new ProtocolInvalidMessageException(e, metadata.getSender().getIdentifier(), metadata.getSenderDevice());
|
||||
}
|
||||
}
|
||||
|
||||
private static SignalServiceTypingMessage createTypingMessage(SignalServiceMetadata metadata, SignalServiceProtos.TypingMessage content) throws ProtocolInvalidMessageException {
|
||||
SignalServiceTypingMessage.Action action;
|
||||
|
||||
|
||||
+15
@@ -6,6 +6,7 @@
|
||||
|
||||
package org.whispersystems.signalservice.api.messages;
|
||||
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
@@ -234,6 +235,20 @@ public class SignalServiceDataMessage {
|
||||
return payment;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getGroupId() {
|
||||
byte[] groupId = null;
|
||||
|
||||
if (getGroupContext().isPresent() && getGroupContext().get().getGroupV2().isPresent()) {
|
||||
SignalServiceGroupV2 gv2 = getGroupContext().get().getGroupV2().get();
|
||||
groupId = GroupSecretParams.deriveFromMasterKey(gv2.getMasterKey())
|
||||
.getPublicParams()
|
||||
.getGroupIdentifier()
|
||||
.serialize();
|
||||
}
|
||||
|
||||
return Optional.fromNullable(groupId);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
|
||||
+4
@@ -246,6 +246,10 @@ public class SignalServiceEnvelope {
|
||||
return envelope.getType().getNumber() == Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
|
||||
}
|
||||
|
||||
public boolean isPlaintextContent() {
|
||||
return envelope.getType().getNumber() == Envelope.Type.PLAINTEXT_CONTENT_VALUE;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
SignalServiceEnvelopeProto.Builder builder = SignalServiceEnvelopeProto.newBuilder()
|
||||
.setType(getType())
|
||||
|
||||
+9
-1
@@ -1,5 +1,6 @@
|
||||
package org.whispersystems.signalservice.api.messages;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public final class SignalServiceMetadata {
|
||||
@@ -10,6 +11,7 @@ public final class SignalServiceMetadata {
|
||||
private final long serverDeliveredTimestamp;
|
||||
private final boolean needsReceipt;
|
||||
private final String serverGuid;
|
||||
private final Optional<byte[]> groupId;
|
||||
|
||||
public SignalServiceMetadata(SignalServiceAddress sender,
|
||||
int senderDevice,
|
||||
@@ -17,7 +19,8 @@ public final class SignalServiceMetadata {
|
||||
long serverReceivedTimestamp,
|
||||
long serverDeliveredTimestamp,
|
||||
boolean needsReceipt,
|
||||
String serverGuid)
|
||||
String serverGuid,
|
||||
Optional<byte[]> groupId)
|
||||
{
|
||||
this.sender = sender;
|
||||
this.senderDevice = senderDevice;
|
||||
@@ -26,6 +29,7 @@ public final class SignalServiceMetadata {
|
||||
this.serverDeliveredTimestamp = serverDeliveredTimestamp;
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverGuid = serverGuid;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public SignalServiceAddress getSender() {
|
||||
@@ -55,4 +59,8 @@ public final class SignalServiceMetadata {
|
||||
public String getServerGuid() {
|
||||
return serverGuid;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -121,6 +121,8 @@ public class SignalServiceProfile {
|
||||
@JsonProperty("gv1-migration")
|
||||
private boolean gv1Migration;
|
||||
|
||||
private boolean senderKey;
|
||||
|
||||
@JsonCreator
|
||||
public Capabilities() {}
|
||||
|
||||
@@ -135,6 +137,10 @@ public class SignalServiceProfile {
|
||||
public boolean isGv1Migration() {
|
||||
return gv1Migration;
|
||||
}
|
||||
|
||||
public boolean isSenderKey() {
|
||||
return senderKey;
|
||||
}
|
||||
}
|
||||
|
||||
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() {
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package org.whispersystems.signalservice.api.push;
|
||||
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents the distributionId that is used to identify this group's sender key session.
|
||||
*
|
||||
* This is just a UUID, but we wrap it in order to provide some type safety and limit confusion
|
||||
* around the multiple UUIDs we throw around.
|
||||
*/
|
||||
public final class DistributionId {
|
||||
|
||||
private final UUID uuid;
|
||||
|
||||
public static DistributionId from(String id) {
|
||||
return new DistributionId(UuidUtil.parseOrThrow(id));
|
||||
}
|
||||
|
||||
public static DistributionId from(UUID uuid) {
|
||||
return new DistributionId(uuid);
|
||||
}
|
||||
|
||||
public static DistributionId create() {
|
||||
return new DistributionId(UUID.randomUUID());
|
||||
}
|
||||
|
||||
private DistributionId(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public UUID asUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return uuid.toString();
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -16,11 +16,12 @@ public class NonSuccessfulResponseCodeException extends IOException {
|
||||
private final int code;
|
||||
|
||||
public NonSuccessfulResponseCodeException(int code) {
|
||||
super("StatusCode: " + code);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public NonSuccessfulResponseCodeException(int code, String s) {
|
||||
super(s);
|
||||
super("[" + code + "] " + s);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents the body of a 409 response from the service during a sender key send.
|
||||
*/
|
||||
public class GroupMismatchedDevices {
|
||||
@JsonProperty
|
||||
private String uuid;
|
||||
|
||||
@JsonProperty
|
||||
private MismatchedDevices devices;
|
||||
|
||||
public GroupMismatchedDevices() {}
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public MismatchedDevices getDevices() {
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents the body of a 410 response from the service during a sender key send.
|
||||
*/
|
||||
public class GroupStaleDevices {
|
||||
|
||||
@JsonProperty
|
||||
private String uuid;
|
||||
|
||||
@JsonProperty
|
||||
private StaleDevices devices;
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public StaleDevices getDevices() {
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
+62
@@ -86,8 +86,11 @@ import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResp
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupExistsException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupStaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.PaymentsRegionException;
|
||||
@@ -189,6 +192,7 @@ public class PushServiceSocket {
|
||||
private static final String DIRECTORY_AUTH_PATH = "/v1/directory/auth";
|
||||
private static final String DIRECTORY_FEEDBACK_PATH = "/v1/directory/feedback-v3/%s";
|
||||
private static final String MESSAGE_PATH = "/v1/messages/%s";
|
||||
private static final String GROUP_MESSAGE_PATH = "/v1/messages/multi_recipient?ts=%s&online=%s";
|
||||
private static final String SENDER_ACK_MESSAGE_PATH = "/v1/messages/%s/%d";
|
||||
private static final String UUID_ACK_MESSAGE_PATH = "/v1/messages/uuid/%s";
|
||||
private static final String ATTACHMENT_V2_PATH = "/v2/attachments/form/upload";
|
||||
@@ -407,6 +411,64 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
|
||||
}
|
||||
|
||||
public SendGroupMessageResponse sendGroupMessage(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online)
|
||||
throws IOException
|
||||
{
|
||||
ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random);
|
||||
|
||||
String path = String.format(Locale.US, GROUP_MESSAGE_PATH, timestamp, online);
|
||||
|
||||
Request.Builder requestBuilder = new Request.Builder();
|
||||
requestBuilder.url(String.format("%s%s", connectionHolder.getUrl(), path));
|
||||
requestBuilder.put(RequestBody.create(MediaType.get("application/vnd.signal-messenger.mrm"), body));
|
||||
requestBuilder.addHeader("Unidentified-Access-Key", Base64.encodeBytes(joinedUnidentifiedAccess));
|
||||
|
||||
if (signalAgent != null) {
|
||||
requestBuilder.addHeader("X-Signal-Agent", signalAgent);
|
||||
}
|
||||
|
||||
if (connectionHolder.getHostHeader().isPresent()) {
|
||||
requestBuilder.addHeader("Host", connectionHolder.getHostHeader().get());
|
||||
}
|
||||
|
||||
Call call = connectionHolder.getUnidentifiedClient().newCall(requestBuilder.build());
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
Response response;
|
||||
|
||||
try {
|
||||
response = call.execute();
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
} finally {
|
||||
synchronized (connections) {
|
||||
connections.remove(call);
|
||||
}
|
||||
}
|
||||
|
||||
switch (response.code()) {
|
||||
case 200:
|
||||
return readBodyJson(response.body(), SendGroupMessageResponse.class);
|
||||
case 401:
|
||||
throw new InvalidUnidentifiedAccessHeaderException();
|
||||
case 404:
|
||||
throw new NotFoundException("At least one unregistered user in message send.");
|
||||
case 409:
|
||||
GroupMismatchedDevices[] mismatchedDevices = readBodyJson(response.body(), GroupMismatchedDevices[].class);
|
||||
throw new GroupMismatchedDevicesException(mismatchedDevices);
|
||||
case 410:
|
||||
GroupStaleDevices[] staleDevices = readBodyJson(response.body(), GroupStaleDevices[].class);
|
||||
throw new GroupStaleDevicesException(staleDevices);
|
||||
case 508:
|
||||
throw new ServerRejectedException();
|
||||
default:
|
||||
throw new NonSuccessfulResponseCodeException(response.code());
|
||||
}
|
||||
}
|
||||
|
||||
public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||
throws IOException
|
||||
{
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SendGroupMessageResponse {
|
||||
|
||||
@JsonProperty
|
||||
private String[] uuids404;
|
||||
|
||||
public SendGroupMessageResponse() {}
|
||||
|
||||
public Set<UUID> getUnsentTargets() {
|
||||
Set<UUID> uuids = new HashSet<>(uuids404.length);
|
||||
|
||||
for (String raw : uuids404) {
|
||||
Optional<UUID> parsed = UuidUtil.parse(raw);
|
||||
if (parsed.isPresent()) {
|
||||
uuids.add(parsed.get());
|
||||
}
|
||||
}
|
||||
|
||||
return uuids;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.signalservice.internal.push.exceptions;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.internal.push.GroupMismatchedDevices;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a 409 response from the service during a sender key send.
|
||||
*/
|
||||
public class GroupMismatchedDevicesException extends NonSuccessfulResponseCodeException {
|
||||
|
||||
private final List<GroupMismatchedDevices> mismatchedDevices;
|
||||
|
||||
public GroupMismatchedDevicesException(GroupMismatchedDevices[] mismatchedDevices) {
|
||||
super(409);
|
||||
this.mismatchedDevices = Arrays.asList(mismatchedDevices);
|
||||
}
|
||||
|
||||
public List<GroupMismatchedDevices> getMismatchedDevices() {
|
||||
return mismatchedDevices;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.signalservice.internal.push.exceptions;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.internal.push.GroupStaleDevices;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a 410 response from the service during a sender key send.
|
||||
*/
|
||||
public class GroupStaleDevicesException extends NonSuccessfulResponseCodeException {
|
||||
|
||||
private final List<GroupStaleDevices> staleDevices;
|
||||
|
||||
public GroupStaleDevicesException(GroupStaleDevices[] staleDevices) {
|
||||
super(410);
|
||||
this.staleDevices = Arrays.asList(staleDevices);
|
||||
}
|
||||
|
||||
public List<GroupStaleDevices> getStaleDevices() {
|
||||
return staleDevices;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package org.whispersystems.signalservice.internal.push.exceptions;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
/**
|
||||
* Indicates that the unidentified authorization header provided to the multi_recipient endpoint
|
||||
* was incorrect (i.e. one or more of your unauthorized access keys is invalid);
|
||||
*/
|
||||
public class InvalidUnidentifiedAccessHeaderException extends NonSuccessfulResponseCodeException {
|
||||
|
||||
public InvalidUnidentifiedAccessHeaderException() {
|
||||
super(401);
|
||||
}
|
||||
}
|
||||
+19
-10
@@ -1,5 +1,8 @@
|
||||
package org.whispersystems.signalservice.internal.serialize;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata;
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto;
|
||||
|
||||
@@ -9,15 +12,20 @@ public final class SignalServiceMetadataProtobufSerializer {
|
||||
}
|
||||
|
||||
public static MetadataProto toProtobuf(SignalServiceMetadata metadata) {
|
||||
return MetadataProto.newBuilder()
|
||||
.setAddress(SignalServiceAddressProtobufSerializer.toProtobuf(metadata.getSender()))
|
||||
.setSenderDevice(metadata.getSenderDevice())
|
||||
.setNeedsReceipt(metadata.isNeedsReceipt())
|
||||
.setTimestamp(metadata.getTimestamp())
|
||||
.setServerReceivedTimestamp(metadata.getServerReceivedTimestamp())
|
||||
.setServerDeliveredTimestamp(metadata.getServerDeliveredTimestamp())
|
||||
.setServerGuid(metadata.getServerGuid())
|
||||
.build();
|
||||
MetadataProto.Builder builder = MetadataProto.newBuilder()
|
||||
.setAddress(SignalServiceAddressProtobufSerializer.toProtobuf(metadata.getSender()))
|
||||
.setSenderDevice(metadata.getSenderDevice())
|
||||
.setNeedsReceipt(metadata.isNeedsReceipt())
|
||||
.setTimestamp(metadata.getTimestamp())
|
||||
.setServerReceivedTimestamp(metadata.getServerReceivedTimestamp())
|
||||
.setServerDeliveredTimestamp(metadata.getServerDeliveredTimestamp())
|
||||
.setServerGuid(metadata.getServerGuid());
|
||||
|
||||
if (metadata.getGroupId().isPresent()) {
|
||||
builder.setGroupId(ByteString.copyFrom(metadata.getGroupId().get()));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static SignalServiceMetadata fromProtobuf(MetadataProto metadata) {
|
||||
@@ -27,6 +35,7 @@ public final class SignalServiceMetadataProtobufSerializer {
|
||||
metadata.getServerReceivedTimestamp(),
|
||||
metadata.getServerDeliveredTimestamp(),
|
||||
metadata.getNeedsReceipt(),
|
||||
metadata.getServerGuid());
|
||||
metadata.getServerGuid(),
|
||||
Optional.fromNullable(metadata.getGroupId()).transform(ByteString::toByteArray));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ message MetadataProto {
|
||||
optional int64 serverDeliveredTimestamp = 6;
|
||||
optional bool needsReceipt = 4;
|
||||
optional string serverGuid = 7;
|
||||
optional bytes groupId = 8;
|
||||
}
|
||||
|
||||
message AddressProto {
|
||||
|
||||
@@ -18,6 +18,8 @@ message Envelope {
|
||||
PREKEY_BUNDLE = 3;
|
||||
RECEIPT = 5;
|
||||
UNIDENTIFIED_SENDER = 6;
|
||||
reserved 7; // SENDERKEY_MESSAGE
|
||||
PLAINTEXT_CONTENT = 8;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
@@ -33,12 +35,14 @@ message Envelope {
|
||||
}
|
||||
|
||||
message Content {
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional SyncMessage syncMessage = 2;
|
||||
optional CallMessage callMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional ReceiptMessage receiptMessage = 5;
|
||||
optional TypingMessage typingMessage = 6;
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional SyncMessage syncMessage = 2;
|
||||
optional CallMessage callMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional ReceiptMessage receiptMessage = 5;
|
||||
optional TypingMessage typingMessage = 6;
|
||||
optional bytes senderKeyDistributionMessage = 7;
|
||||
optional bytes decryptionErrorMessage = 8;
|
||||
}
|
||||
|
||||
message CallMessage {
|
||||
@@ -612,3 +616,9 @@ message PaymentAddress {
|
||||
optional bytes signature = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message DecryptionErrorMessage {
|
||||
optional bytes ratchetKey = 1;
|
||||
optional uint64 timestamp = 2;
|
||||
optional uint32 deviceId = 3;
|
||||
}
|
||||
|
||||
+6
-6
@@ -16,7 +16,7 @@ public final class AccountAttributesTest {
|
||||
"reglock1234",
|
||||
new byte[10],
|
||||
false,
|
||||
new AccountAttributes.Capabilities(true, true, true, true),
|
||||
new AccountAttributes.Capabilities(true, true, true, true, true),
|
||||
false));
|
||||
assertEquals("{\"signalingKey\":\"skey\"," +
|
||||
"\"registrationId\":123," +
|
||||
@@ -28,19 +28,19 @@ public final class AccountAttributesTest {
|
||||
"\"unidentifiedAccessKey\":\"AAAAAAAAAAAAAA==\"," +
|
||||
"\"unrestrictedUnidentifiedAccess\":false," +
|
||||
"\"discoverableByPhoneNumber\":false," +
|
||||
"\"capabilities\":{\"uuid\":true,\"storage\":true,\"gv2-3\":true,\"gv1-migration\":true}}", json);
|
||||
"\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"gv2-3\":true,\"gv1-migration\":true}}", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gv2_true() {
|
||||
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false));
|
||||
assertEquals("{\"uuid\":false,\"storage\":false,\"gv2-3\":true,\"gv1-migration\":false}", json);
|
||||
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false));
|
||||
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"gv2-3\":true,\"gv1-migration\":false}", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gv2_false() {
|
||||
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false));
|
||||
assertEquals("{\"uuid\":false,\"storage\":false,\"gv2-3\":false,\"gv1-migration\":false}", json);
|
||||
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false));
|
||||
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"gv2-3\":false,\"gv1-migration\":false}", json);
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class GroupMismatchedDevicesTest {
|
||||
|
||||
@Test
|
||||
public void testSimpleParse() throws IOException {
|
||||
GroupMismatchedDevices[] parsed = JsonUtil.fromJson("[\n" +
|
||||
" {\n" +
|
||||
" \"uuid\": \"12345678-1234-1234-1234-123456789012\",\n" +
|
||||
" \"devices\": {\n" +
|
||||
" \"missingDevices\": [1, 2],\n" +
|
||||
" \"extraDevices\": [3]\n" +
|
||||
" }\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"uuid\": \"22345678-1234-1234-1234-123456789012\",\n" +
|
||||
" \"devices\": {\n" +
|
||||
" \"missingDevices\": [],\n" +
|
||||
" \"extraDevices\": [2]\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"]", GroupMismatchedDevices[].class);
|
||||
|
||||
assertEquals(2, parsed.length);
|
||||
assertEquals("12345678-1234-1234-1234-123456789012", parsed[0].getUuid());
|
||||
assertEquals(1, (int) parsed[0].getDevices().getMissingDevices().get(0));
|
||||
assertEquals(2, (int) parsed[0].getDevices().getMissingDevices().get(1));
|
||||
assertEquals(3, (int) parsed[0].getDevices().getExtraDevices().get(0));
|
||||
|
||||
assertEquals("22345678-1234-1234-1234-123456789012", parsed[1].getUuid());
|
||||
assertEquals(2, (int) parsed[1].getDevices().getExtraDevices().get(0));
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class GroupStaleDevicesTest {
|
||||
|
||||
@Test
|
||||
public void testSimpleParse() throws IOException {
|
||||
GroupStaleDevices[] parsed = JsonUtil.fromJson("[\n" +
|
||||
" {\n" +
|
||||
" \"uuid\": \"12345678-1234-1234-1234-123456789012\",\n" +
|
||||
" \"devices\": {\n" +
|
||||
" \"staleDevices\": [3]\n" +
|
||||
" }\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"uuid\": \"22345678-1234-1234-1234-123456789012\",\n" +
|
||||
" \"devices\": {\n" +
|
||||
" \"staleDevices\": [2]\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"]", GroupStaleDevices[].class);
|
||||
|
||||
assertEquals(2, parsed.length);
|
||||
assertEquals("12345678-1234-1234-1234-123456789012", parsed[0].getUuid());
|
||||
assertEquals(3, (int) parsed[0].getDevices().getStaleDevices().get(0));
|
||||
|
||||
assertEquals("22345678-1234-1234-1234-123456789012", parsed[1].getUuid());
|
||||
assertEquals(2, (int) parsed[1].getDevices().getStaleDevices().get(0));
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ dependencyVerification {
|
||||
['org.threeten:threetenbp:1.3.6',
|
||||
'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'],
|
||||
|
||||
['org.whispersystems:signal-client-java:0.5.1',
|
||||
'682a8094d38a91c8759071b77177ed8196a7137314fdfbb17e819c9ca57a0397'],
|
||||
['org.whispersystems:signal-client-java:0.8.1',
|
||||
'6bcf9ab3a77be20b43086fd802d9ade3940f36ed7b99bac2a79b9bcaf0a7808b'],
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user