Initial pre-alpha support for sender key.

This commit is contained in:
Greyson Parrelli
2021-05-14 14:03:35 -04:00
parent c54f016213
commit 57c0b8fd0f
124 changed files with 3668 additions and 444 deletions
+1 -1
View File
@@ -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'
@@ -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");
@@ -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;
}
@@ -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 {
}
@@ -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);
}
@@ -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;
}
}
}
@@ -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 shouldnt 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);
}
}
@@ -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;
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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());
}
@@ -42,6 +42,10 @@ public class SendMessageResult {
return success;
}
public boolean isSuccess() {
return success != null;
}
public boolean isNetworkFailure() {
return networkFailure || proofRequiredFailure != null;
}
@@ -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;
@@ -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<>();
@@ -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())
@@ -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;
}
}
@@ -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() {
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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
{
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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'],
]
}