Add Group Send Endorsements support.

This commit is contained in:
Cody Henthorne
2024-07-08 12:47:20 -04:00
parent 414368e251
commit f5abd7acdf
86 changed files with 1691 additions and 887 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,6 @@ public class UnidentifiedAccess {
}
}
private static byte[] createEmptyByteArray(int length) {
return new byte[length];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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