mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-01 22:25:46 +01:00
Add Group Send Endorsements support.
This commit is contained in:
@@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api;
|
||||
|
||||
import com.squareup.wire.FieldEncoding;
|
||||
|
||||
import org.signal.core.util.Base64;
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
@@ -26,6 +27,7 @@ import org.whispersystems.signalservice.api.account.PreKeyUpload;
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveApi;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
@@ -88,7 +90,6 @@ import org.whispersystems.signalservice.internal.storage.protos.WriteOperation;
|
||||
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper;
|
||||
import org.signal.core.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
@@ -757,7 +758,7 @@ public class SignalServiceAccountManager {
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
try {
|
||||
ProfileAndCredential credential = this.pushServiceSocket.retrieveVersionedProfileAndCredential(serviceId, profileKey, Optional.empty(), locale).get(10, TimeUnit.SECONDS);
|
||||
ProfileAndCredential credential = this.pushServiceSocket.retrieveVersionedProfileAndCredential(serviceId, profileKey, SealedSenderAccess.NONE, locale).get(10, TimeUnit.SECONDS);
|
||||
return credential.getExpiringProfileKeyCredential();
|
||||
} catch (InterruptedException | TimeoutException e) {
|
||||
throw new PushNetworkException(e);
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.whispersystems.signalservice.api.backup.BackupKey;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
@@ -98,7 +98,7 @@ public class SignalServiceMessageReceiver {
|
||||
|
||||
public ListenableFuture<ProfileAndCredential> retrieveProfile(SignalServiceAddress address,
|
||||
Optional<ProfileKey> profileKey,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
SignalServiceProfile.RequestType requestType,
|
||||
Locale locale)
|
||||
{
|
||||
@@ -115,16 +115,16 @@ public class SignalServiceMessageReceiver {
|
||||
}
|
||||
|
||||
if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) {
|
||||
return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), unidentifiedAccess, locale);
|
||||
return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), sealedSenderAccess, locale);
|
||||
} else {
|
||||
return FutureTransformers.map(socket.retrieveVersionedProfile(aci, profileKey.get(), unidentifiedAccess, locale), profile -> {
|
||||
return FutureTransformers.map(socket.retrieveVersionedProfile(aci, profileKey.get(), sealedSenderAccess, locale), profile -> {
|
||||
return new ProfileAndCredential(profile,
|
||||
SignalServiceProfile.RequestType.PROFILE,
|
||||
Optional.empty());
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return FutureTransformers.map(socket.retrieveProfile(address, unidentifiedAccess, locale), profile -> {
|
||||
return FutureTransformers.map(socket.retrieveProfile(address, sealedSenderAccess, locale), profile -> {
|
||||
return new ProfileAndCredential(profile,
|
||||
SignalServiceProfile.RequestType.PROFILE,
|
||||
Optional.empty());
|
||||
@@ -146,8 +146,8 @@ public class SignalServiceMessageReceiver {
|
||||
return new FileInputStream(destination);
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull Optional<UnidentifiedAccess> unidentifiedAccess, @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
|
||||
return socket.performIdentityCheck(request, unidentifiedAccess, responseMapper);
|
||||
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
|
||||
return socket.performIdentityCheck(request, responseMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,16 +22,18 @@ import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage;
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle;
|
||||
import org.signal.libsignal.protocol.state.SessionRecord;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeContent;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
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;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
@@ -92,8 +94,8 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
|
||||
import org.whispersystems.signalservice.internal.crypto.AttachmentDigest;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentPointer;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange;
|
||||
import org.whispersystems.signalservice.internal.push.CallMessage;
|
||||
import org.whispersystems.signalservice.internal.push.Content;
|
||||
@@ -131,7 +133,6 @@ import org.whispersystems.signalservice.internal.push.http.PartialSendBatchCompl
|
||||
import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener;
|
||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.util.ByteArrayUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -145,10 +146,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
@@ -223,7 +222,7 @@ public class SignalServiceMessageSender {
|
||||
* @param message The read receipt to deliver.
|
||||
*/
|
||||
public SendMessageResult sendReceipt(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
SignalServiceReceiptMessage message,
|
||||
boolean includePniSignature)
|
||||
throws IOException, UntrustedIdentityException
|
||||
@@ -240,14 +239,14 @@ public class SignalServiceMessageSender {
|
||||
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
return sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null, null, false, false);
|
||||
return sendMessage(recipient, sealedSenderAccess, message.getWhen(), envelopeContent, false, null, null, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a retry receipt for a bad-encrypted envelope.
|
||||
*/
|
||||
public void sendRetryReceipt(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
Optional<byte[]> groupId,
|
||||
DecryptionErrorMessage errorMessage)
|
||||
throws IOException, UntrustedIdentityException
|
||||
@@ -258,16 +257,16 @@ public class SignalServiceMessageSender {
|
||||
PlaintextContent content = new PlaintextContent(errorMessage);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.plaintext(content, groupId);
|
||||
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, null, false, false);
|
||||
sendMessage(recipient, sealedSenderAccess, System.currentTimeMillis(), envelopeContent, false, null, null, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a typing indicator using client-side fanout. Doesn't bother with return results, since these are best-effort.
|
||||
*/
|
||||
public void sendTyping(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
SignalServiceTypingMessage message,
|
||||
CancelationSignal cancelationSignal)
|
||||
public void sendTyping(List<SignalServiceAddress> recipients,
|
||||
List<SealedSenderAccess> sealedSenderAccesses,
|
||||
SignalServiceTypingMessage message,
|
||||
CancelationSignal cancelationSignal)
|
||||
throws IOException
|
||||
{
|
||||
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a typing message to " + recipients.size() + " recipient(s) using 1:1 messages.");
|
||||
@@ -275,22 +274,23 @@ public class SignalServiceMessageSender {
|
||||
Content content = createTypingContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null, cancelationSignal, null, false, false);
|
||||
sendMessage(recipients, sealedSenderAccesses, message.getTimestamp(), envelopeContent, true, null, cancelationSignal, null, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a typing indicator to a group using sender key. Doesn't bother with return results, since these are best-effort.
|
||||
*/
|
||||
public void sendGroupTyping(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceTypingMessage message)
|
||||
public void sendGroupTyping(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable GroupSendEndorsements groupSendEndorsements,
|
||||
SignalServiceTypingMessage message)
|
||||
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
|
||||
{
|
||||
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a typing message to " + recipients.size() + " recipient(s) using sender key.");
|
||||
|
||||
Content content = createTypingContent(message);
|
||||
sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY, false, false);
|
||||
sendGroupMessage(distributionId, recipients, unidentifiedAccess, groupSendEndorsements, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,28 +311,29 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, isRecipientUpdate, manifest);
|
||||
sendSyncMessage(syncMessage, Optional.empty());
|
||||
sendSyncMessage(syncMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a story using sender key. Note: This is not just for group stories -- it's for any story. Just following the naming convention of making sender key
|
||||
* method named "sendGroup*"
|
||||
*/
|
||||
public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
|
||||
Optional<byte[]> groupId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
boolean isRecipientUpdate,
|
||||
SignalServiceStoryMessage message,
|
||||
long timestamp,
|
||||
public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
|
||||
Optional<byte[]> groupId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable GroupSendEndorsements groupSendEndorsements,
|
||||
boolean isRecipientUpdate,
|
||||
SignalServiceStoryMessage message,
|
||||
long timestamp,
|
||||
Set<SignalServiceStoryMessageRecipient> manifest,
|
||||
PartialSendBatchCompleteListener partialListener)
|
||||
PartialSendBatchCompleteListener partialListener)
|
||||
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
|
||||
{
|
||||
Log.d(TAG, "[" + timestamp + "] Sending a story.");
|
||||
|
||||
Content content = createStoryContent(message);
|
||||
List<SendMessageResult> sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.IMPLICIT, groupId, false, SenderKeyGroupEvents.EMPTY, false, true);
|
||||
List<SendMessageResult> sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, groupSendEndorsements, timestamp, content, ContentHint.IMPLICIT, groupId, false, SenderKeyGroupEvents.EMPTY, false, true);
|
||||
|
||||
if (partialListener != null) {
|
||||
partialListener.onPartialSendComplete(sendMessageResults);
|
||||
@@ -354,7 +355,7 @@ public class SignalServiceMessageSender {
|
||||
* @throws IOException
|
||||
*/
|
||||
public void sendCallMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
SignalServiceCallMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
@@ -364,11 +365,11 @@ public class SignalServiceMessageSender {
|
||||
Content content = createCallContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.empty());
|
||||
|
||||
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, message.isUrgent(), false);
|
||||
sendMessage(recipient, sealedSenderAccess, timestamp, envelopeContent, false, null, null, message.isUrgent(), false);
|
||||
}
|
||||
|
||||
public List<SendMessageResult> sendCallMessage(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
List<SealedSenderAccess> sealedSenderAccesses,
|
||||
SignalServiceCallMessage message)
|
||||
throws IOException
|
||||
{
|
||||
@@ -378,12 +379,13 @@ public class SignalServiceMessageSender {
|
||||
Content content = createCallContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.empty());
|
||||
|
||||
return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, null, message.isUrgent(), false);
|
||||
return sendMessage(recipients, sealedSenderAccesses, timestamp, envelopeContent, false, null, null, null, message.isUrgent(), false);
|
||||
}
|
||||
|
||||
public List<SendMessageResult> sendCallMessage(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable GroupSendEndorsements groupSendEndorsements,
|
||||
SignalServiceCallMessage message,
|
||||
PartialSendBatchCompleteListener partialListener)
|
||||
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
|
||||
@@ -392,7 +394,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
Content content = createCallContent(message);
|
||||
|
||||
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId(), false, SenderKeyGroupEvents.EMPTY, message.isUrgent(), false);
|
||||
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, groupSendEndorsements, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId(), false, SenderKeyGroupEvents.EMPTY, message.isUrgent(), false);
|
||||
|
||||
if (partialListener != null) {
|
||||
partialListener.onPartialSendComplete(results);
|
||||
@@ -423,27 +425,27 @@ public class SignalServiceMessageSender {
|
||||
* @throws UntrustedIdentityException
|
||||
* @throws IOException
|
||||
*/
|
||||
public SendMessageResult sendDataMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
IndividualSendEvents sendEvents,
|
||||
boolean urgent,
|
||||
boolean includePniSignature)
|
||||
public SendMessageResult sendDataMessage(SignalServiceAddress recipient,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
IndividualSendEvents sendEvents,
|
||||
boolean urgent,
|
||||
boolean includePniSignature)
|
||||
throws UntrustedIdentityException, IOException
|
||||
{
|
||||
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message.");
|
||||
|
||||
Content content = createMessageContent(message);
|
||||
|
||||
return sendContent(recipient, unidentifiedAccess, contentHint, message, sendEvents, urgent, includePniSignature, content);
|
||||
return sendContent(recipient, sealedSenderAccess, contentHint, message, sendEvents, urgent, includePniSignature, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an edit message to a single recipient.
|
||||
*/
|
||||
public SendMessageResult sendEditMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
IndividualSendEvents sendEvents,
|
||||
@@ -455,14 +457,14 @@ public class SignalServiceMessageSender {
|
||||
|
||||
Content content = createEditMessageContent(new SignalServiceEditMessage(targetSentTimestamp, message));
|
||||
|
||||
return sendContent(recipient, unidentifiedAccess, contentHint, message, sendEvents, urgent, false, content);
|
||||
return sendContent(recipient, sealedSenderAccess, contentHint, message, sendEvents, urgent, false, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends content to a single recipient.
|
||||
*/
|
||||
private SendMessageResult sendContent(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
IndividualSendEvents sendEvents,
|
||||
@@ -481,7 +483,7 @@ public class SignalServiceMessageSender {
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
|
||||
|
||||
long timestamp = message.getTimestamp();
|
||||
SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, sendEvents, urgent, false);
|
||||
SendMessageResult result = sendMessage(recipient, sealedSenderAccess, timestamp, envelopeContent, false, null, sendEvents, urgent, false);
|
||||
|
||||
sendEvents.onMessageSent();
|
||||
|
||||
@@ -489,7 +491,7 @@ public class SignalServiceMessageSender {
|
||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false, Collections.emptySet());
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, null, false, false);
|
||||
sendMessage(localAddress, SealedSenderAccess.NONE, timestamp, syncMessageContent, false, null, null, false, false);
|
||||
}
|
||||
|
||||
sendEvents.onSyncMessageSent();
|
||||
@@ -509,13 +511,13 @@ public class SignalServiceMessageSender {
|
||||
/**
|
||||
* Sends the provided {@link SenderKeyDistributionMessage} to the specified recipients.
|
||||
*/
|
||||
public List<SendMessageResult> sendSenderKeyDistributionMessage(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
SenderKeyDistributionMessage message,
|
||||
Optional<byte[]> groupId,
|
||||
boolean urgent,
|
||||
boolean story)
|
||||
public List<SendMessageResult> sendSenderKeyDistributionMessage(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<SealedSenderAccess> sealedSenderAccesses,
|
||||
SenderKeyDistributionMessage message,
|
||||
Optional<byte[]> groupId,
|
||||
boolean urgent,
|
||||
boolean story)
|
||||
throws IOException
|
||||
{
|
||||
ByteString distributionBytes = ByteString.of(message.serialize());
|
||||
@@ -525,14 +527,14 @@ public class SignalServiceMessageSender {
|
||||
|
||||
Log.d(TAG, "[" + timestamp + "] Sending SKDM to " + recipients.size() + " recipients for DistributionId " + distributionId);
|
||||
|
||||
return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, null, urgent, story);
|
||||
return sendMessage(recipients, sealedSenderAccesses, timestamp, envelopeContent, false, null, null, null, urgent, story);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend a previously-sent message.
|
||||
*/
|
||||
public SendMessageResult resendContent(SignalServiceAddress address,
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
long timestamp,
|
||||
Content content,
|
||||
ContentHint contentHint,
|
||||
@@ -543,13 +545,12 @@ public class SignalServiceMessageSender {
|
||||
Log.d(TAG, "[" + timestamp + "] Resending content.");
|
||||
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, groupId);
|
||||
Optional<UnidentifiedAccess> access = unidentifiedAccess.isPresent() ? unidentifiedAccess.get().getTargetUnidentifiedAccess() : Optional.empty();
|
||||
|
||||
if (address.getServiceId().equals(localAddress.getServiceId())) {
|
||||
access = Optional.empty();
|
||||
sealedSenderAccess = SealedSenderAccess.NONE;
|
||||
}
|
||||
|
||||
return sendMessage(address, access, timestamp, envelopeContent, false, null, null, urgent, false);
|
||||
return sendMessage(address, sealedSenderAccess, timestamp, envelopeContent, false, null, null, urgent, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -558,6 +559,7 @@ public class SignalServiceMessageSender {
|
||||
public List<SendMessageResult> sendGroupDataMessage(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable GroupSendEndorsements groupSendEndorsements,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
@@ -579,7 +581,7 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
Optional<byte[]> groupId = message.getGroupId();
|
||||
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId, false, sendEvents, urgent, isForStory);
|
||||
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, groupSendEndorsements, message.getTimestamp(), content, contentHint, groupId, false, sendEvents, urgent, isForStory);
|
||||
|
||||
if (partialListener != null) {
|
||||
partialListener.onPartialSendComplete(results);
|
||||
@@ -591,7 +593,7 @@ public class SignalServiceMessageSender {
|
||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate, Collections.emptySet());
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null, null, false, false);
|
||||
sendMessage(localAddress, SealedSenderAccess.NONE, message.getTimestamp(), syncMessageContent, false, null, null, false, false);
|
||||
}
|
||||
|
||||
sendEvents.onSyncMessageSent();
|
||||
@@ -605,15 +607,15 @@ public class SignalServiceMessageSender {
|
||||
* @param partialListener A listener that will be called when an individual send is completed. Will be invoked on an arbitrary background thread, *not*
|
||||
* the calling thread.
|
||||
*/
|
||||
public List<SendMessageResult> sendDataMessage(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
LegacyGroupEvents sendEvents,
|
||||
PartialSendCompleteListener partialListener,
|
||||
CancelationSignal cancelationSignal,
|
||||
boolean urgent)
|
||||
public List<SendMessageResult> sendDataMessage(List<SignalServiceAddress> recipients,
|
||||
List<SealedSenderAccess> sealedSenderAccesses,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
LegacyGroupEvents sendEvents,
|
||||
PartialSendCompleteListener partialListener,
|
||||
CancelationSignal cancelationSignal,
|
||||
boolean urgent)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message to " + recipients.size() + " recipients.");
|
||||
@@ -621,7 +623,7 @@ public class SignalServiceMessageSender {
|
||||
Content content = createMessageContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
|
||||
long timestamp = message.getTimestamp();
|
||||
List<SendMessageResult> results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, partialListener, cancelationSignal, sendEvents, urgent, false);
|
||||
List<SendMessageResult> results = sendMessage(recipients, sealedSenderAccesses, timestamp, envelopeContent, false, partialListener, cancelationSignal, sendEvents, urgent, false);
|
||||
boolean needsSyncInResults = false;
|
||||
|
||||
sendEvents.onMessageSent();
|
||||
@@ -642,7 +644,7 @@ public class SignalServiceMessageSender {
|
||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet());
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, null, false, false);
|
||||
sendMessage(localAddress, SealedSenderAccess.NONE, timestamp, syncMessageContent, false, null, null, false, false);
|
||||
}
|
||||
|
||||
sendEvents.onSyncMessageSent();
|
||||
@@ -656,16 +658,16 @@ public class SignalServiceMessageSender {
|
||||
* @param partialListener A listener that will be called when an individual send is completed. Will be invoked on an arbitrary background thread, *not*
|
||||
* the calling thread.
|
||||
*/
|
||||
public List<SendMessageResult> sendEditMessage(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
LegacyGroupEvents sendEvents,
|
||||
PartialSendCompleteListener partialListener,
|
||||
CancelationSignal cancelationSignal,
|
||||
boolean urgent,
|
||||
long targetSentTimestamp)
|
||||
public List<SendMessageResult> sendEditMessage(List<SignalServiceAddress> recipients,
|
||||
List<SealedSenderAccess> sealedSenderAccesses,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
SignalServiceDataMessage message,
|
||||
LegacyGroupEvents sendEvents,
|
||||
PartialSendCompleteListener partialListener,
|
||||
CancelationSignal cancelationSignal,
|
||||
boolean urgent,
|
||||
long targetSentTimestamp)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a edit message to " + recipients.size() + " recipients.");
|
||||
@@ -673,7 +675,7 @@ public class SignalServiceMessageSender {
|
||||
Content content = createEditMessageContent(new SignalServiceEditMessage(targetSentTimestamp, message));
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
|
||||
long timestamp = message.getTimestamp();
|
||||
List<SendMessageResult> results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, partialListener, cancelationSignal, null, urgent, false);
|
||||
List<SendMessageResult> results = sendMessage(recipients, sealedSenderAccesses, timestamp, envelopeContent, false, partialListener, cancelationSignal, null, urgent, false);
|
||||
boolean needsSyncInResults = false;
|
||||
|
||||
sendEvents.onMessageSent();
|
||||
@@ -694,7 +696,7 @@ public class SignalServiceMessageSender {
|
||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet());
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, null, false, false);
|
||||
sendMessage(localAddress, SealedSenderAccess.NONE, timestamp, syncMessageContent, false, null, null, false, false);
|
||||
}
|
||||
|
||||
sendEvents.onSyncMessageSent();
|
||||
@@ -706,17 +708,17 @@ public class SignalServiceMessageSender {
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
Log.d(TAG, "[" + dataMessage.getTimestamp() + "] Sending self-sync message.");
|
||||
return sendSyncMessage(createSelfSendSyncMessage(dataMessage), Optional.empty());
|
||||
return sendSyncMessage(createSelfSendSyncMessage(dataMessage));
|
||||
}
|
||||
|
||||
public SendMessageResult sendSelfSyncEditMessage(SignalServiceEditMessage editMessage)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
Log.d(TAG, "[" + editMessage.getDataMessage().getTimestamp() + "] Sending self-sync edit message for " + editMessage.getTargetSentTimestamp() + ".");
|
||||
return sendSyncMessage(createSelfSendSyncEditMessage(editMessage), Optional.empty());
|
||||
return sendSyncMessage(createSelfSendSyncEditMessage(editMessage));
|
||||
}
|
||||
|
||||
public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
Content content;
|
||||
@@ -736,7 +738,7 @@ public class SignalServiceMessageSender {
|
||||
} else if (message.getConfiguration().isPresent()) {
|
||||
content = createMultiDeviceConfigurationContent(message.getConfiguration().get());
|
||||
} else if (message.getSent().isPresent()) {
|
||||
content = createMultiDeviceSentTranscriptContent(message.getSent().get(), unidentifiedAccess.isPresent());
|
||||
content = createMultiDeviceSentTranscriptContent(message.getSent().get());
|
||||
} else if (message.getStickerPackOperations().isPresent()) {
|
||||
content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get());
|
||||
} else if (message.getFetchType().isPresent()) {
|
||||
@@ -774,7 +776,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
return sendMessage(localAddress, Optional.empty(), timestamp, envelopeContent, false, null, null, urgent, false);
|
||||
return sendMessage(localAddress, SealedSenderAccess.NONE, timestamp, envelopeContent, false, null, null, urgent, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -792,7 +794,7 @@ public class SignalServiceMessageSender {
|
||||
Content.Builder content = new Content.Builder().syncMessage(syncMessage.build());
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content.build(), ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
return getEncryptedMessage(localAddress, Optional.empty(), deviceId, envelopeContent, false);
|
||||
return getEncryptedMessage(localAddress, SealedSenderAccess.NONE, deviceId, envelopeContent, false);
|
||||
}
|
||||
|
||||
public void cancelInFlightRequests() {
|
||||
@@ -927,19 +929,19 @@ public class SignalServiceMessageSender {
|
||||
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
SendMessageResult result = sendMessage(message.getDestination(), Optional.empty(), message.getTimestamp(), envelopeContent, false, null, null, false, false);
|
||||
SendMessageResult result = sendMessage(message.getDestination(), null, message.getTimestamp(), envelopeContent, false, null, null, false, false);
|
||||
|
||||
if (result.getSuccess().isNeedsSync()) {
|
||||
Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.encode());
|
||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null, null, false, false);
|
||||
sendMessage(localAddress, SealedSenderAccess.NONE, message.getTimestamp(), syncMessageContent, false, null, null, false, false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public SendMessageResult sendNullMessage(SignalServiceAddress address, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
public SendMessageResult sendNullMessage(SignalServiceAddress address, @Nullable SealedSenderAccess sealedSenderAccess)
|
||||
throws UntrustedIdentityException, IOException
|
||||
{
|
||||
byte[] nullMessageBody = new DataMessage.Builder()
|
||||
@@ -957,7 +959,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty());
|
||||
|
||||
return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, null, false, false);
|
||||
return sendMessage(address, sealedSenderAccess, System.currentTimeMillis(), envelopeContent, false, null, null, false, false);
|
||||
}
|
||||
|
||||
private PniSignatureMessage createPniSignatureMessage() {
|
||||
@@ -1380,10 +1382,10 @@ public class SignalServiceMessageSender {
|
||||
return container.syncMessage(builder.build()).build();
|
||||
}
|
||||
|
||||
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, boolean unidentifiedAccess) throws IOException {
|
||||
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript) throws IOException {
|
||||
SignalServiceAddress address = transcript.getDestination().get();
|
||||
Content content = createMessageContent(transcript);
|
||||
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.ofNullable(content));
|
||||
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), false, true, -1, Optional.ofNullable(content));
|
||||
|
||||
|
||||
return createMultiDeviceSentTranscriptContent(content,
|
||||
@@ -1424,7 +1426,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
unidentifiedDeliveryStatuses.add(new SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder()
|
||||
.destinationServiceId(result.getAddress().getServiceId().toString())
|
||||
.unidentified(result.getSuccess().isUnidentified())
|
||||
.unidentified(false)
|
||||
.destinationIdentityKey(identity)
|
||||
.build());
|
||||
}
|
||||
@@ -1933,15 +1935,15 @@ public class SignalServiceMessageSender {
|
||||
return SignalServiceSyncMessage.forSentTranscript(transcript);
|
||||
}
|
||||
|
||||
private SendMessageResult sendMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
EnvelopeContent content,
|
||||
boolean online,
|
||||
CancelationSignal cancelationSignal,
|
||||
SendEvents sendEvents,
|
||||
boolean urgent,
|
||||
boolean story)
|
||||
private SendMessageResult sendMessage(SignalServiceAddress recipient,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
long timestamp,
|
||||
EnvelopeContent content,
|
||||
boolean online,
|
||||
CancelationSignal cancelationSignal,
|
||||
SendEvents sendEvents,
|
||||
boolean urgent,
|
||||
boolean story)
|
||||
throws UntrustedIdentityException, IOException
|
||||
{
|
||||
enforceMaxContentSize(content);
|
||||
@@ -1954,7 +1956,13 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
try {
|
||||
OutgoingPushMessageList messages = getEncryptedMessages(recipient, unidentifiedAccess, timestamp, content, online, urgent, story);
|
||||
OutgoingPushMessageList messages = getEncryptedMessages(recipient,
|
||||
sealedSenderAccess,
|
||||
timestamp,
|
||||
content,
|
||||
online,
|
||||
urgent,
|
||||
story);
|
||||
if (i == 0 && sendEvents != null) {
|
||||
sendEvents.onMessageEncrypted();
|
||||
}
|
||||
@@ -1969,52 +1977,41 @@ public class SignalServiceMessageSender {
|
||||
throw new CancelationException();
|
||||
}
|
||||
|
||||
if (!unidentifiedAccess.isPresent()) {
|
||||
try {
|
||||
SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, Optional.empty(), story).blockingGet()).getResultOrThrow();
|
||||
return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
||||
} catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) {
|
||||
// Non-technical failures shouldn't be retried with socket
|
||||
throw e;
|
||||
} catch (WebSocketUnavailableException e) {
|
||||
Log.i(TAG, "[sendMessage][" + timestamp + "] Pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
Log.w(TAG, "[sendMessage][" + timestamp + "] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||
}
|
||||
} else if (unidentifiedAccess.isPresent()) {
|
||||
try {
|
||||
SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, unidentifiedAccess, story).blockingGet()).getResultOrThrow();
|
||||
return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
||||
} catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) {
|
||||
// Non-technical failures shouldn't be retried with socket
|
||||
throw e;
|
||||
} catch (WebSocketUnavailableException e) {
|
||||
Log.i(TAG, "[sendMessage][" + timestamp + "] Unidentified pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||
} catch (IOException e) {
|
||||
Throwable cause = e;
|
||||
if (e.getCause() != null) {
|
||||
cause = e.getCause();
|
||||
}
|
||||
Log.w(TAG, "[sendMessage][" + timestamp + "] Unidentified pipe failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")");
|
||||
try {
|
||||
SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, sealedSenderAccess, story).blockingGet()).getResultOrThrow();
|
||||
return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
||||
} catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) {
|
||||
// Non-technical failures shouldn't be retried with socket
|
||||
throw e;
|
||||
} catch (WebSocketUnavailableException e) {
|
||||
String pipe = sealedSenderAccess == null ? "Pipe" : "Unidentified pipe";
|
||||
Log.i(TAG, "[sendMessage][" + timestamp + "] " + pipe + " unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||
} catch (IOException e) {
|
||||
String pipe = sealedSenderAccess == null ? "Pipe" : "Unidentified pipe";
|
||||
Throwable cause = e;
|
||||
if (e.getCause() != null) {
|
||||
cause = e.getCause();
|
||||
}
|
||||
Log.w(TAG, "[sendMessage][" + timestamp + "] " + pipe + " failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")");
|
||||
}
|
||||
|
||||
if (cancelationSignal != null && cancelationSignal.isCanceled()) {
|
||||
throw new CancelationException();
|
||||
}
|
||||
|
||||
SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess, story);
|
||||
SendMessageResponse response = socket.sendMessage(messages, sealedSenderAccess, story);
|
||||
|
||||
return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
|
||||
|
||||
} catch (InvalidKeyException ike) {
|
||||
Log.w(TAG, ike);
|
||||
unidentifiedAccess = Optional.empty();
|
||||
if (sealedSenderAccess != null) {
|
||||
sealedSenderAccess = sealedSenderAccess.switchToFallback();
|
||||
}
|
||||
} catch (AuthorizationFailedException afe) {
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
if (sealedSenderAccess != null) {
|
||||
Log.w(TAG, "Got an AuthorizationFailedException when trying to send using sealed sender. Falling back.");
|
||||
unidentifiedAccess = Optional.empty();
|
||||
sealedSenderAccess = sealedSenderAccess.switchToFallback();
|
||||
} else {
|
||||
Log.w(TAG, "Got an AuthorizationFailedException without using sealed sender!", afe);
|
||||
throw afe;
|
||||
@@ -2038,7 +2035,7 @@ public class SignalServiceMessageSender {
|
||||
* @throws IOException - Unknown failure or a failure not representable by an unsuccessful {@code SendMessageResult}.
|
||||
*/
|
||||
private List<SendMessageResult> sendMessage(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
|
||||
List<SealedSenderAccess> sealedSenderAccesses,
|
||||
long timestamp,
|
||||
EnvelopeContent content,
|
||||
boolean online,
|
||||
@@ -2052,15 +2049,16 @@ public class SignalServiceMessageSender {
|
||||
Log.d(TAG, "[" + timestamp + "] Sending to " + recipients.size() + " recipients.");
|
||||
enforceMaxContentSize(content);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
List<Observable<SendMessageResult>> singleResults = new LinkedList<>();
|
||||
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
|
||||
Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator();
|
||||
long startTime = System.currentTimeMillis();
|
||||
List<Observable<SendMessageResult>> singleResults = new LinkedList<>();
|
||||
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
|
||||
Iterator<SealedSenderAccess> sealedSenderAccessIterator = sealedSenderAccesses.iterator();
|
||||
|
||||
while (recipientIterator.hasNext()) {
|
||||
SignalServiceAddress recipient = recipientIterator.next();
|
||||
Optional<UnidentifiedAccess> access = unidentifiedAccessIterator.next();
|
||||
singleResults.add(sendMessageRx(recipient, access, timestamp, content, online, cancelationSignal, sendEvents, urgent, story, 0).toObservable());
|
||||
SignalServiceAddress recipient = recipientIterator.next();
|
||||
SealedSenderAccess sealedSenderAccess = sealedSenderAccessIterator.next();
|
||||
|
||||
singleResults.add(sendMessageRx(recipient, sealedSenderAccess, timestamp, content, online, cancelationSignal, sendEvents, urgent, story, 0).toObservable());
|
||||
}
|
||||
|
||||
List<SendMessageResult> results;
|
||||
@@ -2126,7 +2124,7 @@ public class SignalServiceMessageSender {
|
||||
* errors via {@code onError}
|
||||
*/
|
||||
private Single<SendMessageResult> sendMessageRx(SignalServiceAddress recipient,
|
||||
final Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
long timestamp,
|
||||
EnvelopeContent content,
|
||||
boolean online,
|
||||
@@ -2140,7 +2138,7 @@ public class SignalServiceMessageSender {
|
||||
enforceMaxContentSize(content);
|
||||
|
||||
Single<OutgoingPushMessageList> messagesSingle = Single.fromCallable(() -> {
|
||||
OutgoingPushMessageList messages = getEncryptedMessages(recipient, unidentifiedAccess, timestamp, content, online, urgent, story);
|
||||
OutgoingPushMessageList messages = getEncryptedMessages(recipient, sealedSenderAccess, timestamp, content, online, urgent, story);
|
||||
|
||||
if (retryCount == 0 && sendEvents != null) {
|
||||
sendEvents.onMessageEncrypted();
|
||||
@@ -2161,7 +2159,7 @@ public class SignalServiceMessageSender {
|
||||
return Single.error(new CancelationException());
|
||||
}
|
||||
|
||||
return messagingService.send(messages, unidentifiedAccess, story)
|
||||
return messagingService.send(messages, sealedSenderAccess, story)
|
||||
.map(r -> new kotlin.Pair<>(messages, r));
|
||||
})
|
||||
.observeOn(scheduler)
|
||||
@@ -2196,14 +2194,14 @@ public class SignalServiceMessageSender {
|
||||
// Non-technical failures shouldn't be retried with socket
|
||||
return Single.error(throwable);
|
||||
} else if (throwable instanceof WebSocketUnavailableException) {
|
||||
Log.i(TAG, "[sendMessage][" + timestamp + "] " + (unidentifiedAccess.isPresent() ? "Unidentified " : "") + "pipe unavailable, falling back... (" + throwable.getClass().getSimpleName() + ": " + throwable.getMessage() + ")");
|
||||
Log.i(TAG, "[sendMessage][" + timestamp + "] " + (sealedSenderAccess != null ? "Unidentified " : "") + "pipe unavailable, falling back... (" + throwable.getClass().getSimpleName() + ": " + throwable.getMessage() + ")");
|
||||
} else if (throwable instanceof IOException) {
|
||||
Throwable cause = throwable.getCause() != null ? throwable.getCause() : throwable;
|
||||
Log.w(TAG, "[sendMessage][" + timestamp + "] " + (unidentifiedAccess.isPresent() ? "Unidentified " : "") + "pipe failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")");
|
||||
Log.w(TAG, "[sendMessage][" + timestamp + "] " + (sealedSenderAccess != null ? "Unidentified " : "") + "pipe failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")");
|
||||
}
|
||||
|
||||
return Single.fromCallable(() -> {
|
||||
SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess, story);
|
||||
SendMessageResponse response = socket.sendMessage(messages, sealedSenderAccess, story);
|
||||
return SendMessageResult.success(
|
||||
recipient,
|
||||
messages.getDevices(),
|
||||
@@ -2229,7 +2227,7 @@ public class SignalServiceMessageSender {
|
||||
Log.w(TAG, t);
|
||||
return sendMessageRx(
|
||||
recipient,
|
||||
Optional.empty(),
|
||||
SealedSenderAccess.NONE,
|
||||
timestamp,
|
||||
content,
|
||||
online,
|
||||
@@ -2240,11 +2238,11 @@ public class SignalServiceMessageSender {
|
||||
retryCount + 1
|
||||
);
|
||||
} else if (t instanceof AuthorizationFailedException) {
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
if (sealedSenderAccess != null) {
|
||||
Log.w(TAG, "Got an AuthorizationFailedException when trying to send using sealed sender. Falling back.");
|
||||
return sendMessageRx(
|
||||
recipient,
|
||||
Optional.empty(),
|
||||
sealedSenderAccess.switchToFallback(),
|
||||
timestamp,
|
||||
content,
|
||||
online,
|
||||
@@ -2268,7 +2266,7 @@ public class SignalServiceMessageSender {
|
||||
})
|
||||
.flatMap(unused -> sendMessageRx(
|
||||
recipient,
|
||||
unidentifiedAccess,
|
||||
sealedSenderAccess,
|
||||
timestamp,
|
||||
content,
|
||||
online,
|
||||
@@ -2288,7 +2286,7 @@ public class SignalServiceMessageSender {
|
||||
})
|
||||
.flatMap(unused -> sendMessageRx(
|
||||
recipient,
|
||||
unidentifiedAccess,
|
||||
sealedSenderAccess,
|
||||
timestamp,
|
||||
content,
|
||||
online,
|
||||
@@ -2336,17 +2334,18 @@ public class SignalServiceMessageSender {
|
||||
*
|
||||
* This method will handle sending out SenderKeyDistributionMessages as necessary.
|
||||
*/
|
||||
private List<SendMessageResult> sendGroupMessage(DistributionId distributionId,
|
||||
private List<SendMessageResult> sendGroupMessage(DistributionId distributionId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
Content content,
|
||||
ContentHint contentHint,
|
||||
Optional<byte[]> groupId,
|
||||
boolean online,
|
||||
SenderKeyGroupEvents sendEvents,
|
||||
boolean urgent,
|
||||
boolean story)
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable GroupSendEndorsements groupSendEndorsements,
|
||||
long timestamp,
|
||||
Content content,
|
||||
ContentHint contentHint,
|
||||
Optional<byte[]> groupId,
|
||||
boolean online,
|
||||
SenderKeyGroupEvents sendEvents,
|
||||
boolean urgent,
|
||||
boolean story)
|
||||
throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException
|
||||
{
|
||||
if (recipients.isEmpty()) {
|
||||
@@ -2364,34 +2363,36 @@ public class SignalServiceMessageSender {
|
||||
accessBySid.put(addressIterator.next().getServiceId(), accessIterator.next());
|
||||
}
|
||||
|
||||
SealedSenderAccess sealedSenderAccess = SealedSenderAccess.forGroupSend(groupSendEndorsements, unidentifiedAccess, story);
|
||||
|
||||
for (int i = 0; i < RETRY_COUNT; i++) {
|
||||
GroupTargetInfo targetInfo = buildGroupTargetInfo(recipients);
|
||||
final GroupTargetInfo targetInfoSnapshot = targetInfo;
|
||||
|
||||
Set<SignalProtocolAddress> sharedWith = aciStore.getSenderKeySharedWith(distributionId);
|
||||
List<SignalServiceAddress> needsSenderKey = targetInfo.destinations.stream()
|
||||
.filter(a -> !sharedWith.contains(a) || targetInfoSnapshot.sessions.get(a) == null)
|
||||
.map(a -> ServiceId.parseOrThrow(a.getName()))
|
||||
.distinct()
|
||||
.map(SignalServiceAddress::new)
|
||||
.collect(Collectors.toList());
|
||||
if (needsSenderKey.size() > 0) {
|
||||
Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Need to send the distribution message to " + needsSenderKey.size() + " addresses.");
|
||||
SenderKeyDistributionMessage message = getOrCreateNewGroupSession(distributionId);
|
||||
List<Optional<UnidentifiedAccessPair>> access = needsSenderKey.stream()
|
||||
.map(r -> {
|
||||
UnidentifiedAccess targetAccess = accessBySid.get(r.getServiceId());
|
||||
return Optional.of(new UnidentifiedAccessPair(targetAccess, targetAccess));
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
Set<SignalProtocolAddress> sharedWith = aciStore.getSenderKeySharedWith(distributionId);
|
||||
List<SignalServiceAddress> needsSenderKeyTargets = targetInfo.destinations.stream()
|
||||
.filter(a -> !sharedWith.contains(a) || targetInfoSnapshot.sessions.get(a) == null)
|
||||
.map(a -> ServiceId.parseOrThrow(a.getName()))
|
||||
.distinct()
|
||||
.map(SignalServiceAddress::new)
|
||||
.collect(Collectors.toList());
|
||||
if (needsSenderKeyTargets.size() > 0) {
|
||||
Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Need to send the distribution message to " + needsSenderKeyTargets.size() + " addresses.");
|
||||
SenderKeyDistributionMessage senderKeyDistributionMessage = getOrCreateNewGroupSession(distributionId);
|
||||
List<UnidentifiedAccess> needsSenderKeyAccesses = needsSenderKeyTargets.stream()
|
||||
.map(r -> accessBySid.get(r.getServiceId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<GroupSendFullToken> needsSenderKeyGroupSendTokens = groupSendEndorsements != null ? groupSendEndorsements.forIndividuals(needsSenderKeyTargets) : null;
|
||||
List<SealedSenderAccess> needsSenderKeySealedSenderAccesses = SealedSenderAccess.forFanOutGroupSend(needsSenderKeyGroupSendTokens, sealedSenderAccess.getSenderCertificate(), needsSenderKeyAccesses);
|
||||
|
||||
List<SendMessageResult> results = sendSenderKeyDistributionMessage(distributionId,
|
||||
needsSenderKey,
|
||||
access,
|
||||
message,
|
||||
needsSenderKeyTargets,
|
||||
needsSenderKeySealedSenderAccesses,
|
||||
senderKeyDistributionMessage,
|
||||
groupId,
|
||||
urgent,
|
||||
story && !groupId.isPresent()); // We don't want to flag SKDM's as stories for group stories, since we reuse distributionIds for normal group messages
|
||||
story && groupId.isEmpty()); // We don't want to flag SKDM's as stories for group stories, since we reuse distributionIds for normal group messages
|
||||
|
||||
List<SignalServiceAddress> successes = results.stream()
|
||||
.filter(SendMessageResult::isSuccess)
|
||||
@@ -2403,7 +2404,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
aciStore.markSenderKeySharedWith(distributionId, successAddresses);
|
||||
|
||||
Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKey.size() + " recipients.");
|
||||
Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKeyTargets.size() + " recipients.");
|
||||
|
||||
int failureCount = results.size() - successes.size();
|
||||
if (failureCount > 0) {
|
||||
@@ -2434,26 +2435,20 @@ public class SignalServiceMessageSender {
|
||||
|
||||
sendEvents.onSenderKeyShared();
|
||||
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, aciStore, sessionLock, null);
|
||||
SenderCertificate senderCertificate = unidentifiedAccess.get(0).getUnidentifiedCertificate();
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, aciStore, sessionLock, null);
|
||||
|
||||
byte[] ciphertext;
|
||||
try {
|
||||
ciphertext = cipher.encryptForGroup(distributionId, targetInfo.destinations, targetInfo.sessions, senderCertificate, content.encode(), contentHint, groupId);
|
||||
ciphertext = cipher.encryptForGroup(distributionId, targetInfo.destinations, targetInfo.sessions, sealedSenderAccess.getSenderCertificate(), content.encode(), contentHint, groupId);
|
||||
} catch (org.signal.libsignal.protocol.UntrustedIdentityException e) {
|
||||
throw new UntrustedIdentityException("Untrusted during group encrypt", e.getName(), e.getUntrustedIdentity());
|
||||
}
|
||||
|
||||
sendEvents.onMessageEncrypted();
|
||||
|
||||
byte[] joinedUnidentifiedAccess = new byte[16];
|
||||
for (UnidentifiedAccess access : unidentifiedAccess) {
|
||||
joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.getUnidentifiedAccessKey());
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
SendGroupMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent, story).blockingGet()).getResultOrThrow();
|
||||
SendGroupMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.sendToGroup(ciphertext, sealedSenderAccess, timestamp, online, urgent, story).blockingGet()).getResultOrThrow();
|
||||
return transformGroupResponseToMessageResults(targetInfo.devices, response, content);
|
||||
} catch (InvalidUnidentifiedAccessHeaderException | NotFoundException | GroupMismatchedDevicesException | GroupStaleDevicesException e) {
|
||||
// Non-technical failures shouldn't be retried with socket
|
||||
@@ -2464,7 +2459,7 @@ public class SignalServiceMessageSender {
|
||||
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
|
||||
}
|
||||
|
||||
SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent, story);
|
||||
SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, sealedSenderAccess, timestamp, online, urgent, story);
|
||||
return transformGroupResponseToMessageResults(targetInfo.devices, response, content);
|
||||
} catch (GroupMismatchedDevicesException e) {
|
||||
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling mismatched devices. (" + e.getMessage() + ")");
|
||||
@@ -2478,6 +2473,13 @@ public class SignalServiceMessageSender {
|
||||
SignalServiceAddress address = new SignalServiceAddress(ServiceId.parseOrThrow(stale.getUuid()), Optional.empty());
|
||||
handleStaleDevices(address, stale.getDevices());
|
||||
}
|
||||
} catch (InvalidUnidentifiedAccessHeaderException e) {
|
||||
sealedSenderAccess = sealedSenderAccess.switchToFallback();
|
||||
if (sealedSenderAccess != null) {
|
||||
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling invalid group send endorsements. (" + e.getMessage() + ")");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Attempt failed (i = " + i + ")");
|
||||
@@ -2637,7 +2639,7 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
private OutgoingPushMessageList getEncryptedMessages(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
long timestamp,
|
||||
EnvelopeContent plaintext,
|
||||
boolean online,
|
||||
@@ -2659,7 +2661,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
for (int deviceId : deviceIds) {
|
||||
if (deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID || aciStore.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) {
|
||||
messages.add(getEncryptedMessage(recipient, unidentifiedAccess, deviceId, plaintext, story));
|
||||
messages.add(getEncryptedMessage(recipient, sealedSenderAccess, deviceId, plaintext, story));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2668,7 +2670,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
// Visible for testing only
|
||||
public OutgoingPushMessage getEncryptedMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
int deviceId,
|
||||
EnvelopeContent plaintext,
|
||||
boolean story)
|
||||
@@ -2679,7 +2681,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
if (!aciStore.containsSession(signalProtocolAddress)) {
|
||||
try {
|
||||
List<PreKeyBundle> preKeys = getPreKeys(recipient, unidentifiedAccess, deviceId, story);
|
||||
List<PreKeyBundle> preKeys = getPreKeys(recipient, sealedSenderAccess, deviceId, story);
|
||||
|
||||
for (PreKeyBundle preKey : preKeys) {
|
||||
Log.d(TAG, "Initializing prekey session for " + signalProtocolAddress);
|
||||
@@ -2702,24 +2704,24 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
try {
|
||||
return cipher.encrypt(signalProtocolAddress, unidentifiedAccess, plaintext);
|
||||
return cipher.encrypt(signalProtocolAddress, sealedSenderAccess, plaintext);
|
||||
} catch (org.signal.libsignal.protocol.UntrustedIdentityException e) {
|
||||
throw new UntrustedIdentityException("Untrusted on send", recipient.getIdentifier(), e.getUntrustedIdentity());
|
||||
}
|
||||
}
|
||||
|
||||
private List<PreKeyBundle> getPreKeys(SignalServiceAddress recipient, Optional<UnidentifiedAccess> unidentifiedAccess, int deviceId, boolean story) throws IOException {
|
||||
private List<PreKeyBundle> getPreKeys(SignalServiceAddress recipient, @Nullable SealedSenderAccess sealedSenderAccess, int deviceId, boolean story) throws IOException {
|
||||
try {
|
||||
// If it's only unrestricted because it's a story send, then we know it'll fail
|
||||
if (story && unidentifiedAccess.isPresent() && unidentifiedAccess.get().isUnrestrictedForStory()) {
|
||||
unidentifiedAccess = Optional.empty();
|
||||
if (story && SealedSenderAccess.isUnrestrictedForStory(sealedSenderAccess)) {
|
||||
sealedSenderAccess = null;
|
||||
}
|
||||
|
||||
return socket.getPreKeys(recipient, unidentifiedAccess, deviceId);
|
||||
return socket.getPreKeys(recipient, sealedSenderAccess, deviceId);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.getCode() == 401 && story) {
|
||||
Log.d(TAG, "Got 401 when fetching prekey for story. Trying without UD.");
|
||||
return socket.getPreKeys(recipient, Optional.empty(), deviceId);
|
||||
return socket.getPreKeys(recipient, null, deviceId);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
@@ -2782,25 +2784,6 @@ public class SignalServiceMessageSender {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
private Optional<UnidentifiedAccess> getTargetUnidentifiedAccess(Optional<UnidentifiedAccessPair> unidentifiedAccess) {
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private List<Optional<UnidentifiedAccess>> getTargetUnidentifiedAccess(List<Optional<UnidentifiedAccessPair>> unidentifiedAccess) {
|
||||
List<Optional<UnidentifiedAccess>> results = new LinkedList<>();
|
||||
|
||||
for (Optional<UnidentifiedAccessPair> item : unidentifiedAccess) {
|
||||
if (item.isPresent()) results.add(item.get().getTargetUnidentifiedAccess());
|
||||
else results.add(Optional.empty());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private EnvelopeContent enforceMaxContentSize(EnvelopeContent content) {
|
||||
int size = content.size();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
import org.whispersystems.signalservice.api.messages.EnvelopeResponse;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
|
||||
@@ -11,7 +11,6 @@ import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketResponseMessage;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse;
|
||||
import org.signal.core.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -19,6 +18,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
@@ -198,10 +199,11 @@ public final class SignalWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public Single<WebsocketResponse> request(WebSocketRequestMessage requestMessage, Optional<UnidentifiedAccess> unidentifiedAccess) {
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
public Single<WebsocketResponse> request(WebSocketRequestMessage requestMessage, @Nullable SealedSenderAccess sealedSenderAccess) {
|
||||
if (sealedSenderAccess != null) {
|
||||
List<String> headers = new ArrayList<>(requestMessage.headers);
|
||||
headers.add("Unidentified-Access-Key:" + Base64.encodeWithPadding(unidentifiedAccess.get().getUnidentifiedAccessKey()));
|
||||
headers.add(sealedSenderAccess.getHeader());
|
||||
|
||||
WebSocketRequestMessage message = requestMessage.newBuilder()
|
||||
.headers(headers)
|
||||
.build();
|
||||
@@ -209,7 +211,7 @@ public final class SignalWebSocket {
|
||||
return getUnidentifiedWebSocket().sendRequest(message)
|
||||
.flatMap(r -> {
|
||||
if (r.getStatus() == 401) {
|
||||
return request(requestMessage);
|
||||
return request(requestMessage, sealedSenderAccess.switchToFallback());
|
||||
}
|
||||
return Single.just(r);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.crypto
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements
|
||||
import org.whispersystems.util.ByteArrayUtil
|
||||
|
||||
/**
|
||||
* Provides single interface for the various ways to send via sealed sender.
|
||||
*/
|
||||
sealed class SealedSenderAccess {
|
||||
|
||||
abstract val senderCertificate: SenderCertificate
|
||||
abstract val headerName: String
|
||||
abstract val headerValue: String
|
||||
|
||||
val header: String
|
||||
get() = "$headerName:$headerValue"
|
||||
|
||||
abstract fun switchToFallback(): SealedSenderAccess?
|
||||
|
||||
/**
|
||||
* For sending to an single recipient using group send endorsement/token first and then fallback to
|
||||
* access key if available.
|
||||
*/
|
||||
class IndividualGroupSendTokenFirst(
|
||||
private val groupSendToken: GroupSendFullToken,
|
||||
override val senderCertificate: SenderCertificate,
|
||||
val unidentifiedAccess: UnidentifiedAccess? = null
|
||||
) : SealedSenderAccess() {
|
||||
|
||||
override val headerName: String = "Group-Send-Token"
|
||||
override val headerValue: String by lazy { Base64.encodeWithPadding(groupSendToken.serialize()) }
|
||||
|
||||
override fun switchToFallback(): SealedSenderAccess? {
|
||||
fallbackListener?.onTokenToAccessFallback(unidentifiedAccess != null)
|
||||
return if (unidentifiedAccess != null) {
|
||||
IndividualUnidentifiedAccessFirst(unidentifiedAccess)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For sending to an single recipient using access key first and then fallback to group send
|
||||
* token if available. The token is created lazily via the provided [createGroupSendToken] function.
|
||||
*/
|
||||
class IndividualUnidentifiedAccessFirst(
|
||||
val unidentifiedAccess: UnidentifiedAccess,
|
||||
private val createGroupSendToken: CreateGroupSendToken? = null
|
||||
) : SealedSenderAccess() {
|
||||
|
||||
override val senderCertificate: SenderCertificate
|
||||
get() = unidentifiedAccess.unidentifiedCertificate
|
||||
|
||||
override val headerName: String = "Unidentified-Access-Key"
|
||||
override val headerValue: String by lazy { Base64.encodeWithPadding(unidentifiedAccess.unidentifiedAccessKey) }
|
||||
|
||||
override fun switchToFallback(): SealedSenderAccess? {
|
||||
val groupSendToken = createGroupSendToken?.create()
|
||||
return if (groupSendToken != null) {
|
||||
fallbackListener?.onAccessToTokenFallback()
|
||||
IndividualGroupSendTokenFirst(groupSendToken, senderCertificate)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For sending to a "group" of recipients using group send endorsements/tokens.
|
||||
*/
|
||||
class GroupGroupSendToken(
|
||||
private val groupSendEndorsements: GroupSendEndorsements
|
||||
) : SealedSenderAccess() {
|
||||
|
||||
override val headerName: String = "Group-Send-Token"
|
||||
override val headerValue: String by lazy { Base64.encodeWithPadding(groupSendEndorsements.serialize()) }
|
||||
|
||||
override val senderCertificate: SenderCertificate
|
||||
get() = groupSendEndorsements.sealedSenderCertificate
|
||||
|
||||
override fun switchToFallback(): SealedSenderAccess? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For sending to a "group" of recipients using access keys.
|
||||
*/
|
||||
class GroupUnidentifiedAccess(
|
||||
private val unidentifiedAccess: List<UnidentifiedAccess>,
|
||||
override val senderCertificate: SenderCertificate = unidentifiedAccess.first().unidentifiedCertificate
|
||||
) : SealedSenderAccess() {
|
||||
|
||||
override val headerName: String = "Unidentified-Access-Key"
|
||||
override val headerValue: String by lazy {
|
||||
var joinedUnidentifiedAccess = ByteArray(16)
|
||||
for (access in unidentifiedAccess) {
|
||||
joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.unidentifiedAccessKey)
|
||||
}
|
||||
|
||||
Base64.encodeWithPadding(joinedUnidentifiedAccess)
|
||||
}
|
||||
|
||||
override fun switchToFallback(): SealedSenderAccess? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a lazy way to create a group send token.
|
||||
*/
|
||||
fun interface CreateGroupSendToken {
|
||||
fun create(): GroupSendFullToken?
|
||||
}
|
||||
|
||||
interface FallbackListener {
|
||||
fun onAccessToTokenFallback()
|
||||
fun onTokenToAccessFallback(hasAccessKeyFallback: Boolean)
|
||||
}
|
||||
|
||||
companion object {
|
||||
var fallbackListener: FallbackListener? = null
|
||||
|
||||
@JvmField
|
||||
val NONE: SealedSenderAccess? = null
|
||||
|
||||
@JvmStatic
|
||||
fun forIndividualWithGroupFallback(
|
||||
unidentifiedAccess: UnidentifiedAccess?,
|
||||
senderCertificate: SenderCertificate?,
|
||||
createGroupSendToken: CreateGroupSendToken?
|
||||
): SealedSenderAccess? {
|
||||
if (unidentifiedAccess != null) {
|
||||
return IndividualUnidentifiedAccessFirst(unidentifiedAccess, createGroupSendToken)
|
||||
}
|
||||
|
||||
val groupSendToken = createGroupSendToken?.create()
|
||||
if (groupSendToken != null && senderCertificate != null) {
|
||||
return IndividualGroupSendTokenFirst(groupSendToken, senderCertificate)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forIndividual(unidentifiedAccess: UnidentifiedAccess?): SealedSenderAccess? {
|
||||
return unidentifiedAccess?.let { IndividualUnidentifiedAccessFirst(it) }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forFanOutGroupSend(groupSendTokens: List<GroupSendFullToken?>?, senderCertificate: SenderCertificate?, unidentifiedAccesses: List<UnidentifiedAccess?>): List<SealedSenderAccess?> {
|
||||
if (groupSendTokens == null) {
|
||||
return unidentifiedAccesses.map { a -> forIndividual(a) }
|
||||
}
|
||||
|
||||
require(groupSendTokens.size == unidentifiedAccesses.size)
|
||||
|
||||
return groupSendTokens
|
||||
.zip(unidentifiedAccesses)
|
||||
.map { (token, unidentifiedAccess) ->
|
||||
if (unidentifiedAccess != null) {
|
||||
IndividualUnidentifiedAccessFirst(unidentifiedAccess) { token }
|
||||
} else if (token != null && senderCertificate != null) {
|
||||
IndividualGroupSendTokenFirst(token, senderCertificate)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forGroupSend(groupSendEndorsements: GroupSendEndorsements?, unidentifiedAccess: List<UnidentifiedAccess>, forStory: Boolean): SealedSenderAccess {
|
||||
return if (groupSendEndorsements != null && !forStory) {
|
||||
GroupGroupSendToken(groupSendEndorsements)
|
||||
} else {
|
||||
GroupUnidentifiedAccess(unidentifiedAccess)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUnrestrictedForStory(sealedSenderAccess: SealedSenderAccess?): Boolean {
|
||||
return when (sealedSenderAccess) {
|
||||
is IndividualGroupSendTokenFirst -> sealedSenderAccess.unidentifiedAccess?.isUnrestrictedForStory ?: false
|
||||
is IndividualUnidentifiedAccessFirst -> sealedSenderAccess.unidentifiedAccess.isUnrestrictedForStory
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This is used to encrypt + decrypt received envelopes.
|
||||
*/
|
||||
@@ -110,17 +112,17 @@ public class SignalServiceCipher {
|
||||
}
|
||||
|
||||
public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
EnvelopeContent content)
|
||||
throws UntrustedIdentityException, InvalidKeyException
|
||||
{
|
||||
try {
|
||||
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination));
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
if (sealedSenderAccess != null) {
|
||||
SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getServiceId().getRawUuid(), localAddress.getNumber()
|
||||
.orElse(null), localDeviceId));
|
||||
|
||||
return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, unidentifiedAccess.get().getUnidentifiedCertificate());
|
||||
return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, sealedSenderAccess.getSenderCertificate());
|
||||
} else {
|
||||
return content.processUnsealedSender(sessionCipher, destination);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ public class UnidentifiedAccess {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static byte[] createEmptyByteArray(int length) {
|
||||
return new byte[length];
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class UnidentifiedAccessPair {
|
||||
|
||||
private final Optional<UnidentifiedAccess> targetUnidentifiedAccess;
|
||||
private final Optional<UnidentifiedAccess> selfUnidentifiedAccess;
|
||||
|
||||
public UnidentifiedAccessPair(UnidentifiedAccess targetUnidentifiedAccess, UnidentifiedAccess selfUnidentifiedAccess) {
|
||||
this.targetUnidentifiedAccess = Optional.of(targetUnidentifiedAccess);
|
||||
this.selfUnidentifiedAccess = Optional.of(selfUnidentifiedAccess);
|
||||
}
|
||||
|
||||
public Optional<UnidentifiedAccess> getTargetUnidentifiedAccess() {
|
||||
return targetUnidentifiedAccess;
|
||||
}
|
||||
|
||||
public Optional<UnidentifiedAccess> getSelfUnidentifiedAccess() {
|
||||
return selfUnidentifiedAccess;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.groupsv2
|
||||
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
|
||||
/**
|
||||
* Decrypted response from server operations that includes our global group state and
|
||||
* our specific-to-us group send endorsements.
|
||||
*/
|
||||
class DecryptedGroupResponse(
|
||||
val group: DecryptedGroup,
|
||||
val groupSendEndorsementsResponse: GroupSendEndorsementsResponse?
|
||||
)
|
||||
@@ -1,11 +1,12 @@
|
||||
package org.whispersystems.signalservice.api.groupsv2
|
||||
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket.GroupHistory
|
||||
|
||||
/**
|
||||
* Wraps result of group history fetch with it's associated paging data.
|
||||
*/
|
||||
data class GroupHistoryPage(val changeLogs: List<DecryptedGroupChangeLog>, val pagingData: PagingData) {
|
||||
data class GroupHistoryPage(val changeLogs: List<DecryptedGroupChangeLog>, val groupSendEndorsementsResponse: GroupSendEndorsementsResponse?, val pagingData: PagingData) {
|
||||
|
||||
data class PagingData(val hasMorePages: Boolean, val nextPageRevision: Int) {
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.groupsv2
|
||||
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Helper container for all data needed to send with group send endorsements.
|
||||
*/
|
||||
data class GroupSendEndorsements(
|
||||
val expirationMs: Long,
|
||||
val endorsements: Map<ServiceId.ACI, GroupSendEndorsement>,
|
||||
val sealedSenderCertificate: SenderCertificate,
|
||||
val groupSecretParams: GroupSecretParams
|
||||
) {
|
||||
|
||||
private val expiration: Instant by lazy { Instant.ofEpochMilli(expirationMs) }
|
||||
private val combinedEndorsement: GroupSendEndorsement by lazy { GroupSendEndorsement.combine(endorsements.values) }
|
||||
|
||||
fun serialize(): ByteArray {
|
||||
return combinedEndorsement.toFullToken(groupSecretParams, expiration).serialize()
|
||||
}
|
||||
|
||||
fun forIndividuals(addresses: List<SignalServiceAddress>): List<GroupSendFullToken?> {
|
||||
return addresses
|
||||
.map { a -> endorsements[a.serviceId] }
|
||||
.map { e -> e?.toFullToken(groupSecretParams, expiration) }
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,16 @@ import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations;
|
||||
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
|
||||
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
|
||||
import org.signal.storageservice.protos.groups.Group;
|
||||
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupChangeResponse;
|
||||
import org.signal.storageservice.protos.groups.GroupChanges;
|
||||
import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
||||
import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.GroupResponse;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
@@ -31,6 +34,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
@@ -73,9 +77,9 @@ public class GroupsV2Api {
|
||||
return new GroupsV2AuthorizationString(groupSecretParams, authCredentialPresentation);
|
||||
}
|
||||
|
||||
public void putNewGroup(GroupsV2Operations.NewGroup newGroup,
|
||||
GroupsV2AuthorizationString authorization)
|
||||
throws IOException
|
||||
public DecryptedGroupResponse putNewGroup(GroupsV2Operations.NewGroup newGroup,
|
||||
GroupsV2AuthorizationString authorization)
|
||||
throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException
|
||||
{
|
||||
Group group = newGroup.getNewGroupMessage();
|
||||
|
||||
@@ -83,34 +87,38 @@ public class GroupsV2Api {
|
||||
String cdnKey = uploadAvatar(newGroup.getAvatar().get(), newGroup.getGroupSecretParams(), authorization);
|
||||
|
||||
group = group.newBuilder()
|
||||
.avatar(cdnKey)
|
||||
.build();
|
||||
.avatar(cdnKey)
|
||||
.build();
|
||||
}
|
||||
|
||||
socket.putNewGroupsV2Group(group, authorization);
|
||||
GroupResponse response = socket.putNewGroupsV2Group(group, authorization);
|
||||
|
||||
return groupsOperations.forGroup(newGroup.getGroupSecretParams())
|
||||
.decryptGroup(Objects.requireNonNull(response.group), response.groupSendEndorsementsResponse.toByteArray());
|
||||
}
|
||||
|
||||
public NetworkResult<DecryptedGroup> getGroupAsResult(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) {
|
||||
public NetworkResult<DecryptedGroupResponse> getGroupAsResult(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) {
|
||||
return NetworkResult.fromFetch(() -> getGroup(groupSecretParams, authorization));
|
||||
}
|
||||
|
||||
public DecryptedGroup getGroup(GroupSecretParams groupSecretParams,
|
||||
GroupsV2AuthorizationString authorization)
|
||||
throws IOException, InvalidGroupStateException, VerificationFailedException
|
||||
public DecryptedGroupResponse getGroup(GroupSecretParams groupSecretParams,
|
||||
GroupsV2AuthorizationString authorization)
|
||||
throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException
|
||||
{
|
||||
Group group = socket.getGroupsV2Group(authorization);
|
||||
GroupResponse response = socket.getGroupsV2Group(authorization);
|
||||
|
||||
return groupsOperations.forGroup(groupSecretParams)
|
||||
.decryptGroup(group);
|
||||
.decryptGroup(Objects.requireNonNull(response.group), response.groupSendEndorsementsResponse.toByteArray());
|
||||
}
|
||||
|
||||
public GroupHistoryPage getGroupHistoryPage(GroupSecretParams groupSecretParams,
|
||||
int fromRevision,
|
||||
GroupsV2AuthorizationString authorization,
|
||||
boolean includeFirstState)
|
||||
throws IOException, InvalidGroupStateException, VerificationFailedException
|
||||
boolean includeFirstState,
|
||||
long sendEndorsementsExpirationMs)
|
||||
throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException
|
||||
{
|
||||
PushServiceSocket.GroupHistory group = socket.getGroupsV2GroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState);
|
||||
PushServiceSocket.GroupHistory group = socket.getGroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState, sendEndorsementsExpirationMs);
|
||||
List<DecryptedGroupChangeLog> result = new ArrayList<>(group.getGroupChanges().groupChanges.size());
|
||||
GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);
|
||||
|
||||
@@ -121,7 +129,10 @@ public class GroupsV2Api {
|
||||
result.add(new DecryptedGroupChangeLog(decryptedGroup, decryptedChange));
|
||||
}
|
||||
|
||||
return new GroupHistoryPage(result, GroupHistoryPage.PagingData.forGroupHistory(group));
|
||||
byte[] groupSendEndorsementsResponseBytes = group.getGroupChanges().groupSendEndorsementsResponse.toByteArray();
|
||||
GroupSendEndorsementsResponse groupSendEndorsementsResponse = groupSendEndorsementsResponseBytes.length > 0 ? new GroupSendEndorsementsResponse(groupSendEndorsementsResponseBytes) : null;
|
||||
|
||||
return new GroupHistoryPage(result, groupSendEndorsementsResponse, GroupHistoryPage.PagingData.forGroupHistory(group));
|
||||
}
|
||||
|
||||
public NetworkResult<Integer> getGroupJoinedAt(@Nonnull GroupsV2AuthorizationString authorization) {
|
||||
@@ -162,9 +173,9 @@ public class GroupsV2Api {
|
||||
return form.key;
|
||||
}
|
||||
|
||||
public GroupChange patchGroup(GroupChange.Actions groupChange,
|
||||
GroupsV2AuthorizationString authorization,
|
||||
Optional<byte[]> groupLinkPassword)
|
||||
public GroupChangeResponse patchGroup(GroupChange.Actions groupChange,
|
||||
GroupsV2AuthorizationString authorization,
|
||||
Optional<byte[]> groupLinkPassword)
|
||||
throws IOException
|
||||
{
|
||||
return socket.patchGroupsV2Group(groupChange, authorization.toString(), groupLinkPassword);
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groups.ProfileKeyCiphertext;
|
||||
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
@@ -53,6 +54,9 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
@@ -154,7 +158,7 @@ public final class GroupsV2Operations {
|
||||
private final GroupSecretParams groupSecretParams;
|
||||
private final ClientZkGroupCipher clientZkGroupCipher;
|
||||
|
||||
private GroupOperations(GroupSecretParams groupSecretParams) {
|
||||
public GroupOperations(GroupSecretParams groupSecretParams) {
|
||||
this.groupSecretParams = groupSecretParams;
|
||||
this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
|
||||
}
|
||||
@@ -425,6 +429,15 @@ public final class GroupsV2Operations {
|
||||
return new PendingMember.Builder().member(member);
|
||||
}
|
||||
|
||||
public @Nonnull DecryptedGroupResponse decryptGroup(@Nonnull Group group, @Nonnull byte[] groupSendEndorsementsBytes)
|
||||
throws VerificationFailedException, InvalidGroupStateException, InvalidInputException
|
||||
{
|
||||
DecryptedGroup decryptedGroup = decryptGroup(group);
|
||||
GroupSendEndorsementsResponse groupSendEndorsementsResponse = groupSendEndorsementsBytes.length > 0 ? new GroupSendEndorsementsResponse(groupSendEndorsementsBytes) : null;
|
||||
|
||||
return new DecryptedGroupResponse(decryptedGroup, groupSendEndorsementsResponse);
|
||||
}
|
||||
|
||||
public DecryptedGroup decryptGroup(Group group)
|
||||
throws VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
@@ -1019,6 +1032,46 @@ public final class GroupsV2Operations {
|
||||
return ids;
|
||||
}
|
||||
|
||||
public @Nullable ReceivedGroupSendEndorsements receiveGroupSendEndorsements(@Nonnull ACI selfAci,
|
||||
@Nonnull DecryptedGroup decryptedGroup,
|
||||
@Nullable ByteString groupSendEndorsementsResponse)
|
||||
{
|
||||
if (groupSendEndorsementsResponse != null && groupSendEndorsementsResponse.size() > 0) {
|
||||
try {
|
||||
return receiveGroupSendEndorsements(selfAci, decryptedGroup, new GroupSendEndorsementsResponse(groupSendEndorsementsResponse.toByteArray()));
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, "Unable to parse send endorsements response", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable ReceivedGroupSendEndorsements receiveGroupSendEndorsements(@Nonnull ACI selfAci,
|
||||
@Nonnull DecryptedGroup decryptedGroup,
|
||||
@Nullable GroupSendEndorsementsResponse groupSendEndorsementsResponse)
|
||||
{
|
||||
if (groupSendEndorsementsResponse == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ACI> members = decryptedGroup.members.stream().map(m -> ACI.parseOrThrow(m.aciBytes)).collect(Collectors.toList());
|
||||
|
||||
GroupSendEndorsementsResponse.ReceivedEndorsements endorsements = null;
|
||||
try {
|
||||
endorsements = groupSendEndorsementsResponse.receive(
|
||||
members.stream().map(ACI::getLibSignalAci).collect(Collectors.toList()),
|
||||
selfAci.getLibSignalAci(),
|
||||
groupSecretParams,
|
||||
serverPublicParams
|
||||
);
|
||||
} catch (VerificationFailedException e) {
|
||||
Log.w(TAG, "Unable to receive send endorsements for group", e);
|
||||
}
|
||||
|
||||
return endorsements != null ? new ReceivedGroupSendEndorsements(groupSendEndorsementsResponse.getExpiration(), members, endorsements)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NewGroup {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.groupsv2
|
||||
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Group send endorsement data received from the server.
|
||||
*/
|
||||
data class ReceivedGroupSendEndorsements(
|
||||
val expirationMs: Long,
|
||||
val endorsements: Map<ServiceId.ACI, GroupSendEndorsement>
|
||||
) {
|
||||
constructor(
|
||||
expiration: Instant,
|
||||
members: List<ServiceId.ACI>,
|
||||
receivedEndorsements: GroupSendEndorsementsResponse.ReceivedEndorsements
|
||||
) : this(
|
||||
expirationMs = expiration.toEpochMilli(),
|
||||
endorsements = members.zip(receivedEndorsements.endorsements).toMap()
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.whispersystems.signalservice.api.services;
|
||||
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
@@ -19,13 +19,14 @@ import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper;
|
||||
import org.whispersystems.signalservice.internal.websocket.ResponseMapper;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage;
|
||||
import org.signal.core.util.Base64;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import okio.ByteString;
|
||||
@@ -42,7 +43,9 @@ public class MessagingService {
|
||||
this.signalWebSocket = signalWebSocket;
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<SendMessageResponse>> send(OutgoingPushMessageList list, Optional<UnidentifiedAccess> unidentifiedAccess, boolean story) {
|
||||
public Single<ServiceResponse<SendMessageResponse>> send(OutgoingPushMessageList list,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
boolean story) {
|
||||
List<String> headers = new LinkedList<String>() {{
|
||||
add("content-type:application/json");
|
||||
}};
|
||||
@@ -66,15 +69,15 @@ public class MessagingService {
|
||||
.withCustomError(404, (status, body, getHeader) -> new UnregisteredUserException(list.getDestination(), new NotFoundException("not found")))
|
||||
.build();
|
||||
|
||||
return signalWebSocket.request(requestMessage, unidentifiedAccess)
|
||||
return signalWebSocket.request(requestMessage, sealedSenderAccess)
|
||||
.map(responseMapper::map)
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<SendGroupMessageResponse>> sendToGroup(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent, boolean story) {
|
||||
public Single<ServiceResponse<SendGroupMessageResponse>> sendToGroup(byte[] body, @Nonnull SealedSenderAccess sealedSenderAccess, long timestamp, boolean online, boolean urgent, boolean story) {
|
||||
List<String> headers = new LinkedList<String>() {{
|
||||
add("content-type:application/vnd.signal-messenger.mrm");
|
||||
add("Unidentified-Access-Key:" + Base64.encodeWithPadding(joinedUnidentifiedAccess));
|
||||
add(sealedSenderAccess.getHeader());
|
||||
}};
|
||||
|
||||
String path = String.format(Locale.US, "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s&story=%s", timestamp, online, urgent, story);
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyVersion;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
@@ -44,6 +45,7 @@ import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import io.reactivex.rxjava3.annotations.NonNull;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
@@ -72,7 +74,7 @@ public final class ProfileService {
|
||||
|
||||
public Single<ServiceResponse<ProfileAndCredential>> getProfile(@Nonnull SignalServiceAddress address,
|
||||
@Nonnull Optional<ProfileKey> profileKey,
|
||||
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
@Nonnull SignalServiceProfile.RequestType requestType,
|
||||
@Nonnull Locale locale)
|
||||
{
|
||||
@@ -116,9 +118,9 @@ public final class ProfileService {
|
||||
.withResponseMapper(new ProfileResponseMapper(requestType, requestContext))
|
||||
.build();
|
||||
|
||||
return signalWebSocket.request(requestMessage, unidentifiedAccess)
|
||||
return signalWebSocket.request(requestMessage, sealedSenderAccess)
|
||||
.map(responseMapper::map)
|
||||
.onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, unidentifiedAccess, requestType, locale))
|
||||
.onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, sealedSenderAccess, requestType, locale))
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
@@ -139,19 +141,19 @@ public final class ProfileService {
|
||||
|
||||
ResponseMapper<IdentityCheckResponse> responseMapper = DefaultResponseMapper.getDefault(IdentityCheckResponse.class);
|
||||
|
||||
return signalWebSocket.request(builder.build(), Optional.empty())
|
||||
return signalWebSocket.request(builder.build(), SealedSenderAccess.NONE)
|
||||
.map(responseMapper::map)
|
||||
.onErrorResumeNext(t -> performIdentityCheckRestFallback(request, Optional.empty(), responseMapper))
|
||||
.onErrorResumeNext(t -> performIdentityCheckRestFallback(request, responseMapper))
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
private Single<ServiceResponse<ProfileAndCredential>> getProfileRestFallback(@Nonnull SignalServiceAddress address,
|
||||
@Nonnull Optional<ProfileKey> profileKey,
|
||||
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
@Nonnull SignalServiceProfile.RequestType requestType,
|
||||
@Nonnull Locale locale)
|
||||
{
|
||||
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType, locale), 10, TimeUnit.SECONDS)
|
||||
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, sealedSenderAccess, requestType, locale), 10, TimeUnit.SECONDS)
|
||||
.onErrorResumeNext(t -> {
|
||||
Throwable error;
|
||||
if (t instanceof ExecutionException && t.getCause() != null) {
|
||||
@@ -161,7 +163,7 @@ public final class ProfileService {
|
||||
}
|
||||
|
||||
if (error instanceof AuthorizationFailedException) {
|
||||
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, Optional.empty(), requestType, locale), 10, TimeUnit.SECONDS);
|
||||
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, null, requestType, locale), 10, TimeUnit.SECONDS);
|
||||
} else {
|
||||
return Single.error(t);
|
||||
}
|
||||
@@ -170,9 +172,8 @@ public final class ProfileService {
|
||||
}
|
||||
|
||||
private @NonNull Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheckRestFallback(@Nonnull IdentityCheckRequest request,
|
||||
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
|
||||
return receiver.performIdentityCheck(request, unidentifiedAccess, responseMapper)
|
||||
return receiver.performIdentityCheck(request, responseMapper)
|
||||
.onErrorResumeNext(t -> {
|
||||
Throwable error;
|
||||
if (t instanceof ExecutionException && t.getCause() != null) {
|
||||
@@ -182,7 +183,7 @@ public final class ProfileService {
|
||||
}
|
||||
|
||||
if (error instanceof AuthorizationFailedException) {
|
||||
return receiver.performIdentityCheck(request, Optional.empty(), responseMapper);
|
||||
return receiver.performIdentityCheck(request, responseMapper);
|
||||
} else {
|
||||
return Single.error(t);
|
||||
}
|
||||
|
||||
@@ -39,9 +39,11 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
|
||||
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
|
||||
import org.signal.storageservice.protos.groups.Group;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupChangeResponse;
|
||||
import org.signal.storageservice.protos.groups.GroupChanges;
|
||||
import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
||||
import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.GroupResponse;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||
@@ -60,7 +62,7 @@ import org.whispersystems.signalservice.api.archive.BatchArchiveMediaRequest;
|
||||
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse;
|
||||
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest;
|
||||
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
@@ -273,13 +275,13 @@ public class PushServiceSocket {
|
||||
private static final String STICKER_PATH = "stickers/%s/full/%d";
|
||||
|
||||
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&zkcCredential=true";
|
||||
private static final String GROUPSV2_GROUP = "/v1/groups/";
|
||||
private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s";
|
||||
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false";
|
||||
private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form";
|
||||
private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s";
|
||||
private static final String GROUPSV2_TOKEN = "/v1/groups/token";
|
||||
private static final String GROUPSV2_JOINED_AT = "/v1/groups/joined_at_version";
|
||||
private static final String GROUPSV2_GROUP = "/v2/groups/";
|
||||
private static final String GROUPSV2_GROUP_PASSWORD = "/v2/groups/?inviteLinkPassword=%s";
|
||||
private static final String GROUPSV2_GROUP_CHANGES = "/v2/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false";
|
||||
private static final String GROUPSV2_AVATAR_REQUEST = "/v2/groups/avatar/form";
|
||||
private static final String GROUPSV2_GROUP_JOIN = "/v2/groups/join/%s";
|
||||
private static final String GROUPSV2_TOKEN = "/v2/groups/token";
|
||||
private static final String GROUPSV2_JOINED_AT = "/v2/groups/joined_at_version";
|
||||
|
||||
private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions";
|
||||
|
||||
@@ -376,7 +378,7 @@ public class PushServiceSocket {
|
||||
|
||||
public RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc) throws IOException {
|
||||
final String jsonBody = JsonUtil.toJson(new VerificationSessionMetadataRequestBody(credentialsProvider.getE164(), pushToken, mcc, mnc));
|
||||
try (Response response = makeServiceRequest(VERIFICATION_SESSION_PATH, "POST", jsonRequestBody(jsonBody), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty(), false)) {
|
||||
try (Response response = makeServiceRequest(VERIFICATION_SESSION_PATH, "POST", jsonRequestBody(jsonBody), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) {
|
||||
return parseSessionMetadataResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -384,7 +386,7 @@ public class PushServiceSocket {
|
||||
public RegistrationSessionMetadataResponse getSessionStatus(String sessionId) throws IOException {
|
||||
String path = VERIFICATION_SESSION_PATH + "/" + sessionId;
|
||||
|
||||
try (Response response = makeServiceRequest(path, "GET", jsonRequestBody(null), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty(), false)) {
|
||||
try (Response response = makeServiceRequest(path, "GET", jsonRequestBody(null), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) {
|
||||
return parseSessionMetadataResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -393,7 +395,7 @@ public class PushServiceSocket {
|
||||
String path = VERIFICATION_SESSION_PATH + "/" + sessionId;
|
||||
|
||||
final UpdateVerificationSessionRequestBody requestBody = new UpdateVerificationSessionRequestBody(captchaToken, pushToken, pushChallengeToken, mcc, mnc);
|
||||
try (Response response = makeServiceRequest(path, "PATCH", jsonRequestBody(JsonUtil.toJson(requestBody)), NO_HEADERS, new PatchRegistrationSessionResponseHandler(), Optional.empty(), false)) {
|
||||
try (Response response = makeServiceRequest(path, "PATCH", jsonRequestBody(JsonUtil.toJson(requestBody)), NO_HEADERS, new PatchRegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) {
|
||||
return parseSessionMetadataResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -414,7 +416,7 @@ public class PushServiceSocket {
|
||||
|
||||
body.put("client", androidSmsRetriever ? "android-2021-03" : "android");
|
||||
|
||||
try (Response response = makeServiceRequest(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, new RegistrationCodeRequestResponseHandler(), Optional.empty(), false)) {
|
||||
try (Response response = makeServiceRequest(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, new RegistrationCodeRequestResponseHandler(), SealedSenderAccess.NONE, false)) {
|
||||
return parseSessionMetadataResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -423,7 +425,7 @@ public class PushServiceSocket {
|
||||
String path = String.format(VERIFICATION_CODE_PATH, sessionId);
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("code", verificationCode);
|
||||
try (Response response = makeServiceRequest(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, new RegistrationCodeSubmissionResponseHandler(), Optional.empty(), false)) {
|
||||
try (Response response = makeServiceRequest(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, new RegistrationCodeSubmissionResponseHandler(), SealedSenderAccess.NONE, false)) {
|
||||
return parseSessionMetadataResponse(response);
|
||||
}
|
||||
}
|
||||
@@ -470,7 +472,7 @@ public class PushServiceSocket {
|
||||
skipDeviceTransfer,
|
||||
true);
|
||||
|
||||
String response = makeServiceRequest(path, "POST", JsonUtil.toJson(body), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty());
|
||||
String response = makeServiceRequest(path, "POST", JsonUtil.toJson(body), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE);
|
||||
return JsonUtil.fromJson(response, VerifyAccountResponse.class);
|
||||
}
|
||||
|
||||
@@ -512,14 +514,14 @@ public class PushServiceSocket {
|
||||
long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime));
|
||||
long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7);
|
||||
|
||||
String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null, NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
|
||||
String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
|
||||
return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class);
|
||||
}
|
||||
|
||||
public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException {
|
||||
String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(request));
|
||||
makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
|
||||
makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
}
|
||||
|
||||
public void setArchivePublicKey(ECPublicKey publicKey, ArchiveCredentialPresentation credentialPresentation) throws IOException {
|
||||
@@ -706,7 +708,7 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
|
||||
}
|
||||
|
||||
public SendGroupMessageResponse sendGroupMessage(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent, boolean story)
|
||||
public SendGroupMessageResponse sendGroupMessage(byte[] body, @Nonnull SealedSenderAccess sealedSenderAccess, long timestamp, boolean online, boolean urgent, boolean story)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
|
||||
{
|
||||
ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random);
|
||||
@@ -716,7 +718,7 @@ public class PushServiceSocket {
|
||||
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.encodeWithPadding(joinedUnidentifiedAccess));
|
||||
requestBuilder.addHeader(sealedSenderAccess.getHeaderName(), sealedSenderAccess.getHeaderValue());
|
||||
|
||||
if (signalAgent != null) {
|
||||
requestBuilder.addHeader("X-Signal-Agent", signalAgent);
|
||||
@@ -762,14 +764,14 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional<UnidentifiedAccess> unidentifiedAccess, boolean story)
|
||||
public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, @Nullable SealedSenderAccess sealedSenderAccess, boolean story)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
String responseText = makeServiceRequest(String.format("/v1/messages/%s?story=%s", bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, NO_HANDLER, unidentifiedAccess);
|
||||
String responseText = makeServiceRequest(String.format("/v1/messages/%s?story=%s", bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, NO_HANDLER, sealedSenderAccess);
|
||||
SendMessageResponse response = JsonUtil.fromJson(responseText, SendMessageResponse.class);
|
||||
|
||||
response.setSentUnidentfied(unidentifiedAccess.isPresent());
|
||||
response.setSentUnidentfied(sealedSenderAccess != null);
|
||||
|
||||
return response;
|
||||
} catch (NotFoundException nfe) {
|
||||
@@ -780,7 +782,7 @@ public class PushServiceSocket {
|
||||
public SignalServiceMessagesResult getMessages(boolean allowStories) throws IOException {
|
||||
Map<String, String> headers = Collections.singletonMap("X-Signal-Receive-Stories", allowStories ? "true" : "false");
|
||||
|
||||
try (Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, headers, NO_HANDLER, Optional.empty(), false)) {
|
||||
try (Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, headers, NO_HANDLER, SealedSenderAccess.NONE, false)) {
|
||||
validateServiceResponse(response);
|
||||
|
||||
List<SignalServiceEnvelopeEntity> envelopes = readBodyJson(response.body(), SignalServiceEnvelopeEntityList.class).getMessages();
|
||||
@@ -870,18 +872,18 @@ public class PushServiceSocket {
|
||||
* for all devices. If it is not a primary, it will only contain the prekeys for that specific device.
|
||||
*/
|
||||
public List<PreKeyBundle> getPreKeys(SignalServiceAddress destination,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
int deviceId)
|
||||
throws IOException
|
||||
{
|
||||
return getPreKeysBySpecifier(destination, unidentifiedAccess, deviceId == 1 ? "*" : String.valueOf(deviceId));
|
||||
return getPreKeysBySpecifier(destination, sealedSenderAccess, deviceId == 1 ? "*" : String.valueOf(deviceId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a prekey for a specific device.
|
||||
*/
|
||||
public PreKeyBundle getPreKey(SignalServiceAddress destination, int deviceId) throws IOException {
|
||||
List<PreKeyBundle> bundles = getPreKeysBySpecifier(destination, Optional.empty(), String.valueOf(deviceId));
|
||||
List<PreKeyBundle> bundles = getPreKeysBySpecifier(destination, null, String.valueOf(deviceId));
|
||||
|
||||
if (bundles.size() > 0) {
|
||||
return bundles.get(0);
|
||||
@@ -891,7 +893,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
private List<PreKeyBundle> getPreKeysBySpecifier(SignalServiceAddress destination,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
String deviceSpecifier)
|
||||
throws IOException
|
||||
{
|
||||
@@ -900,7 +902,7 @@ public class PushServiceSocket {
|
||||
|
||||
Log.d(TAG, "Fetching prekeys for " + destination.getIdentifier() + "." + deviceSpecifier + ", i.e. GET " + path);
|
||||
|
||||
String responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, NO_HANDLER, unidentifiedAccess);
|
||||
String responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, NO_HANDLER, sealedSenderAccess);
|
||||
PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class);
|
||||
List<PreKeyBundle> bundles = new LinkedList<>();
|
||||
|
||||
@@ -958,7 +960,7 @@ public class PushServiceSocket {
|
||||
if (responseCode == 409) {
|
||||
throw new NonSuccessfulResponseCodeException(409);
|
||||
}
|
||||
}, Optional.empty());
|
||||
}, null);
|
||||
}
|
||||
|
||||
public void retrieveBackup(int cdnNumber, Map<String, String> headers, String cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
|
||||
@@ -1012,8 +1014,8 @@ public class PushServiceSocket {
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
public ListenableFuture<SignalServiceProfile> retrieveProfile(SignalServiceAddress target, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) {
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, target.getIdentifier()), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess);
|
||||
public ListenableFuture<SignalServiceProfile> retrieveProfile(SignalServiceAddress target, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, target.getIdentifier()), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
|
||||
|
||||
return FutureTransformers.map(response, body -> {
|
||||
try {
|
||||
@@ -1025,7 +1027,7 @@ public class PushServiceSocket {
|
||||
});
|
||||
}
|
||||
|
||||
public ListenableFuture<ProfileAndCredential> retrieveVersionedProfileAndCredential(ACI target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) {
|
||||
public ListenableFuture<ProfileAndCredential> retrieveVersionedProfileAndCredential(ACI target, ProfileKey profileKey, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
|
||||
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci());
|
||||
ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target.getLibSignalAci(), profileKey);
|
||||
ProfileKeyCredentialRequest request = requestContext.getRequest();
|
||||
@@ -1035,7 +1037,7 @@ public class PushServiceSocket {
|
||||
String subPath = String.format("%s/%s/%s?credentialType=expiringProfileKey", target, version, credentialRequest);
|
||||
|
||||
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess);
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
|
||||
|
||||
return FutureTransformers.map(response, body -> formatProfileAndCredentialBody(requestContext, body));
|
||||
}
|
||||
@@ -1061,12 +1063,12 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public ListenableFuture<SignalServiceProfile> retrieveVersionedProfile(ACI target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) {
|
||||
public ListenableFuture<SignalServiceProfile> retrieveVersionedProfile(ACI target, ProfileKey profileKey, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
|
||||
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci());
|
||||
|
||||
String version = profileKeyIdentifier.serialize();
|
||||
String subPath = String.format("%s/%s", target, version);
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess);
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
|
||||
|
||||
return FutureTransformers.map(response, body -> {
|
||||
try {
|
||||
@@ -1102,7 +1104,7 @@ public class PushServiceSocket {
|
||||
requestBody,
|
||||
NO_HEADERS,
|
||||
PaymentsRegionException::responseCodeHandler,
|
||||
Optional.empty());
|
||||
SealedSenderAccess.NONE);
|
||||
|
||||
if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) {
|
||||
try {
|
||||
@@ -1126,13 +1128,12 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request,
|
||||
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nonnull ResponseMapper<IdentityCheckResponse> responseMapper)
|
||||
{
|
||||
Single<ServiceResponse<IdentityCheckResponse>> requestSingle = Single.fromCallable(() -> {
|
||||
try (Response response = getServiceConnection(PROFILE_BATCH_CHECK_PATH, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), unidentifiedAccess, false)) {
|
||||
try (Response response = getServiceConnection(PROFILE_BATCH_CHECK_PATH, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), SealedSenderAccess.NONE, false)) {
|
||||
String body = response.body() != null ? readBodyString(response.body()): "";
|
||||
return responseMapper.map(response.code(), body, response::header, unidentifiedAccess.isPresent());
|
||||
return responseMapper.map(response.code(), body, response::header, false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1146,7 +1147,7 @@ public class PushServiceSocket {
|
||||
@Nonnull ResponseMapper<BackupV2AuthCheckResponse> responseMapper)
|
||||
{
|
||||
Single<ServiceResponse<BackupV2AuthCheckResponse>> requestSingle = Single.fromCallable(() -> {
|
||||
try (Response response = getServiceConnection(BACKUP_AUTH_CHECK_V2, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) {
|
||||
try (Response response = getServiceConnection(BACKUP_AUTH_CHECK_V2, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), SealedSenderAccess.NONE, false)) {
|
||||
String body = response.body() != null ? readBodyString(response.body()): "";
|
||||
return responseMapper.map(response.code(), body, response::header, false);
|
||||
}
|
||||
@@ -1159,12 +1160,12 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
public BackupV2AuthCheckResponse checkSvr2AuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
|
||||
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V2, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
|
||||
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V2, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
return JsonUtil.fromJson(response, BackupV2AuthCheckResponse.class);
|
||||
}
|
||||
|
||||
public BackupV3AuthCheckResponse checkSvr3AuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
|
||||
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V3, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
|
||||
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V3, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
return JsonUtil.fromJson(response, BackupV3AuthCheckResponse.class);
|
||||
}
|
||||
|
||||
@@ -1218,7 +1219,7 @@ public class PushServiceSocket {
|
||||
case 422: throw new UsernameMalformedException();
|
||||
case 409: throw new UsernameTakenException();
|
||||
}
|
||||
}, Optional.empty());
|
||||
}, SealedSenderAccess.NONE);
|
||||
|
||||
return JsonUtil.fromJsonResponse(responseString, ReserveUsernameResponse.class);
|
||||
}
|
||||
@@ -1249,7 +1250,7 @@ public class PushServiceSocket {
|
||||
case 410:
|
||||
throw new UsernameTakenException();
|
||||
}
|
||||
}, Optional.empty());
|
||||
}, SealedSenderAccess.NONE);
|
||||
|
||||
return JsonUtil.fromJson(response, ConfirmUsernameResponse.class).getUsernameLinkHandle();
|
||||
} catch (BaseUsernameException e) {
|
||||
@@ -1303,7 +1304,7 @@ public class PushServiceSocket {
|
||||
if (responseCode == 428) {
|
||||
throw new CaptchaRejectedException();
|
||||
}
|
||||
}, Optional.empty());
|
||||
}, SealedSenderAccess.NONE);
|
||||
|
||||
}
|
||||
|
||||
@@ -2139,7 +2140,7 @@ public class PushServiceSocket {
|
||||
private String makeServiceRequestWithoutAuthentication(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
|
||||
{
|
||||
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, Optional.empty(), true)) {
|
||||
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, SealedSenderAccess.NONE, true)) {
|
||||
return readBodyString(response);
|
||||
}
|
||||
}
|
||||
@@ -2147,13 +2148,13 @@ public class PushServiceSocket {
|
||||
private String makeServiceRequest(String urlFragment, String method, String jsonBody)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
|
||||
{
|
||||
return makeServiceRequest(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER, Optional.empty());
|
||||
return makeServiceRequest(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER, SealedSenderAccess.NONE);
|
||||
}
|
||||
|
||||
private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler, Optional<UnidentifiedAccess> unidentifiedAccessKey)
|
||||
private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler, @Nullable SealedSenderAccess sealedSenderAccess)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
|
||||
{
|
||||
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, unidentifiedAccessKey, false)) {
|
||||
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, sealedSenderAccess, false)) {
|
||||
return readBodyString(response);
|
||||
}
|
||||
}
|
||||
@@ -2172,10 +2173,10 @@ public class PushServiceSocket {
|
||||
String method,
|
||||
String jsonBody,
|
||||
Map<String, String> headers,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccessKey)
|
||||
@Nullable SealedSenderAccess sealedSenderAccess)
|
||||
{
|
||||
OkHttpClient okHttpClient = buildOkHttpClient(unidentifiedAccessKey.isPresent());
|
||||
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, unidentifiedAccessKey, false));
|
||||
OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null);
|
||||
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, sealedSenderAccess, false));
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
@@ -2208,13 +2209,13 @@ public class PushServiceSocket {
|
||||
RequestBody body,
|
||||
Map<String, String> headers,
|
||||
ResponseCodeHandler responseCodeHandler,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccessKey,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
boolean doNotAddAuthenticationOrUnidentifiedAccessKey)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
|
||||
{
|
||||
Response response = null;
|
||||
try {
|
||||
response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey, doNotAddAuthenticationOrUnidentifiedAccessKey);
|
||||
response = getServiceConnection(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey);
|
||||
responseCodeHandler.handle(response.code(), response.body());
|
||||
return validateServiceResponse(response);
|
||||
} catch (Exception e) {
|
||||
@@ -2288,13 +2289,13 @@ public class PushServiceSocket {
|
||||
String method,
|
||||
RequestBody body,
|
||||
Map<String, String> headers,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
boolean doNotAddAuthenticationOrUnidentifiedAccessKey)
|
||||
throws PushNetworkException
|
||||
{
|
||||
try {
|
||||
OkHttpClient okHttpClient = buildOkHttpClient(unidentifiedAccess.isPresent());
|
||||
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, body, headers, unidentifiedAccess, doNotAddAuthenticationOrUnidentifiedAccessKey));
|
||||
OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null);
|
||||
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey));
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
@@ -2327,14 +2328,11 @@ public class PushServiceSocket {
|
||||
String method,
|
||||
RequestBody body,
|
||||
Map<String, String> headers,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
boolean doNotAddAuthenticationOrUnidentifiedAccessKey) {
|
||||
|
||||
ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random);
|
||||
|
||||
// Log.d(TAG, "Push service URL: " + connectionHolder.getUrl());
|
||||
// Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), urlFragment));
|
||||
|
||||
Request.Builder request = new Request.Builder();
|
||||
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
|
||||
request.method(method, body);
|
||||
@@ -2344,8 +2342,8 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
if (!headers.containsKey("Authorization") && !doNotAddAuthenticationOrUnidentifiedAccessKey) {
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
request.addHeader("Unidentified-Access-Key", Base64.encodeWithPadding(unidentifiedAccess.get().getUnidentifiedAccessKey()));
|
||||
if (sealedSenderAccess != null) {
|
||||
request.addHeader(sealedSenderAccess.getHeaderName(), sealedSenderAccess.getHeaderValue());
|
||||
} else if (credentialsProvider.getPassword() != null) {
|
||||
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
|
||||
}
|
||||
@@ -2364,6 +2362,12 @@ public class PushServiceSocket {
|
||||
|
||||
private Response makeStorageRequest(String authorization, String path, String method, RequestBody body, ResponseCodeHandler responseCodeHandler)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
return makeStorageRequest(authorization, path, method, body, NO_HEADERS, responseCodeHandler);
|
||||
}
|
||||
|
||||
private Response makeStorageRequest(String authorization, String path, String method, RequestBody body, Map<String, String> headers, ResponseCodeHandler responseCodeHandler)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(storageClients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
@@ -2372,8 +2376,6 @@ public class PushServiceSocket {
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
// Log.d(TAG, "Opening URL: " + connectionHolder.getUrl());
|
||||
|
||||
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
|
||||
request.method(method, body);
|
||||
|
||||
@@ -2385,6 +2387,10 @@ public class PushServiceSocket {
|
||||
request.addHeader("Authorization", authorization);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
request.addHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(request.build());
|
||||
|
||||
synchronized (connections) {
|
||||
@@ -2784,7 +2790,7 @@ public class PushServiceSocket {
|
||||
null,
|
||||
NO_HEADERS,
|
||||
NO_HANDLER,
|
||||
Optional.empty());
|
||||
SealedSenderAccess.NONE);
|
||||
|
||||
return JsonUtil.fromJson(response, CredentialResponse.class);
|
||||
}
|
||||
@@ -2816,8 +2822,8 @@ public class PushServiceSocket {
|
||||
}
|
||||
};
|
||||
|
||||
public void putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
public GroupResponse putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
|
||||
{
|
||||
try (Response response = makeStorageRequest(authorization.toString(),
|
||||
GROUPSV2_GROUP,
|
||||
@@ -2825,11 +2831,11 @@ public class PushServiceSocket {
|
||||
protobufRequestBody(group),
|
||||
GROUPS_V2_PUT_RESPONSE_HANDLER))
|
||||
{
|
||||
return;
|
||||
return GroupResponse.ADAPTER.decode(readBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
public Group getGroupsV2Group(GroupsV2AuthorizationString authorization)
|
||||
public GroupResponse getGroupsV2Group(GroupsV2AuthorizationString authorization)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
|
||||
{
|
||||
try (Response response = makeStorageRequest(authorization.toString(),
|
||||
@@ -2838,7 +2844,7 @@ public class PushServiceSocket {
|
||||
null,
|
||||
GROUPS_V2_GET_CURRENT_HANDLER))
|
||||
{
|
||||
return Group.ADAPTER.decode(readBodyBytes(response));
|
||||
return GroupResponse.ADAPTER.decode(readBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2855,7 +2861,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public GroupChange patchGroupsV2Group(GroupChange.Actions groupChange, String authorization, Optional<byte[]> groupLinkPassword)
|
||||
public GroupChangeResponse patchGroupsV2Group(GroupChange.Actions groupChange, String authorization, Optional<byte[]> groupLinkPassword)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
|
||||
{
|
||||
String path;
|
||||
@@ -2872,17 +2878,21 @@ public class PushServiceSocket {
|
||||
protobufRequestBody(groupChange),
|
||||
GROUPS_V2_PATCH_RESPONSE_HANDLER))
|
||||
{
|
||||
return GroupChange.ADAPTER.decode(readBodyBytes(response));
|
||||
return GroupChangeResponse.ADAPTER.decode(readBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
public GroupHistory getGroupsV2GroupHistory(int fromVersion, GroupsV2AuthorizationString authorization, int highestKnownEpoch, boolean includeFirstState)
|
||||
public GroupHistory getGroupHistory(int fromVersion, GroupsV2AuthorizationString authorization, int highestKnownEpoch, boolean includeFirstState, long sendEndorsementsExpirationMs)
|
||||
throws IOException
|
||||
{
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Cached-Send-Endorsements", Long.toString(TimeUnit.MILLISECONDS.toSeconds(sendEndorsementsExpirationMs)));
|
||||
|
||||
try (Response response = makeStorageRequest(authorization.toString(),
|
||||
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion, highestKnownEpoch, includeFirstState),
|
||||
"GET",
|
||||
null,
|
||||
headers,
|
||||
GROUPS_V2_GET_LOGS_HANDLER))
|
||||
{
|
||||
|
||||
@@ -2890,12 +2900,7 @@ public class PushServiceSocket {
|
||||
throw new PushNetworkException("No body!");
|
||||
}
|
||||
|
||||
GroupChanges groupChanges;
|
||||
try (InputStream input = response.body().byteStream()) {
|
||||
groupChanges = GroupChanges.ADAPTER.decode(input);
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
}
|
||||
GroupChanges groupChanges = GroupChanges.ADAPTER.decode(readBodyBytes(response));
|
||||
|
||||
if (response.code() == 206) {
|
||||
String contentRangeHeader = response.header("Content-Range");
|
||||
|
||||
@@ -213,13 +213,24 @@ message GroupChange {
|
||||
uint32 changeEpoch = 3;
|
||||
}
|
||||
|
||||
message GroupResponse {
|
||||
Group group = 1;
|
||||
bytes groupSendEndorsementsResponse = 2;
|
||||
}
|
||||
|
||||
message GroupChanges {
|
||||
message GroupChangeState {
|
||||
GroupChange groupChange = 1;
|
||||
Group groupState = 2;
|
||||
}
|
||||
|
||||
repeated GroupChangeState groupChanges = 1;
|
||||
repeated GroupChangeState groupChanges = 1;
|
||||
bytes groupSendEndorsementsResponse = 2;
|
||||
}
|
||||
|
||||
message GroupChangeResponse {
|
||||
GroupChange groupChange = 1;
|
||||
bytes groupSendEndorsementsResponse = 2;
|
||||
}
|
||||
|
||||
message GroupAttributeBlob {
|
||||
|
||||
Reference in New Issue
Block a user