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

@ -314,7 +314,7 @@ class GroupTableTest {
.revision(0) .revision(0)
.build() .build()
return groupTable.create(groupMasterKey, decryptedGroupState)!! return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
} }
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId { private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
@ -339,6 +339,6 @@ class GroupTableTest {
.revision(0) .revision(0)
.build() .build()
return groupTable.create(groupMasterKey, decryptedGroupState)!! return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
} }
} }

View file

@ -16,7 +16,7 @@ import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.Curve import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.ecc.ECKeyPair import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.AliceClient import org.thoughtcrime.securesms.testing.AliceClient
@ -55,8 +55,8 @@ class MessageProcessingPerformanceTest {
@Before @Before
fun setup() { fun setup() {
mockkStatic(UnidentifiedAccessUtil::class) mockkStatic(SealedSenderAccessUtil::class)
every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator every { SealedSenderAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
mockkObject(MessageContentProcessor) mockkObject(MessageContentProcessor)
every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application) every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application)
@ -64,7 +64,7 @@ class MessageProcessingPerformanceTest {
@After @After
fun after() { fun after() {
unmockkStatic(UnidentifiedAccessUtil::class) unmockkStatic(SealedSenderAccessUtil::class)
unmockkStatic(MessageContentProcessor::class) unmockkStatic(MessageContentProcessor::class)
} }

View file

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.testing
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.ECKeyPair import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
@ -50,7 +49,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
fun encrypt(now: Long, destination: Recipient): Envelope { fun encrypt(now: Long, destination: Recipient): Envelope {
return AppDependencies.signalServiceMessageSender.getEncryptedMessage( return AppDependencies.signalServiceMessageSender.getEncryptedMessage(
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()), SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate), FakeClientHelpers.getSealedSenderAccess(ProfileKey(destination.profileKey), aliceSenderCertificate),
1, 1,
FakeClientHelpers.encryptedTextMessage(now), FakeClientHelpers.encryptedTextMessage(now),
false false

View file

@ -17,7 +17,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.signal.libsignal.protocol.util.KeyHelper import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
import org.thoughtcrime.securesms.database.OneTimePreKeyTable import org.thoughtcrime.securesms.database.OneTimePreKeyTable
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignedPreKeyTable import org.thoughtcrime.securesms.database.SignedPreKeyTable
@ -25,14 +25,13 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
import org.whispersystems.signalservice.api.SignalSessionLock import org.whispersystems.signalservice.api.SignalSessionLock
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.push.DistributionId import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.Envelope import org.whispersystems.signalservice.internal.push.Envelope
import java.util.Optional
import java.util.UUID import java.util.UUID
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -75,7 +74,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
} }
fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) { fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) {
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator()) val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, SealedSenderAccessUtil.getCertificateValidator())
cipher.decrypt(envelope, serverDeliveredTimestamp) cipher.decrypt(envelope, serverDeliveredTimestamp)
} }
@ -126,8 +125,8 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
return ProfileKeyUtil.getSelfProfileKey() return ProfileKeyUtil.getSelfProfileKey()
} }
private fun getAliceUnidentifiedAccess(): Optional<UnidentifiedAccess> { private fun getAliceUnidentifiedAccess(): SealedSenderAccess? {
return FakeClientHelpers.getTargetUnidentifiedAccess(profileKey, getAliceProfileKey(), senderCertificate) return FakeClientHelpers.getSealedSenderAccess(getAliceProfileKey(), senderCertificate)
} }
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore { private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {

View file

@ -14,8 +14,8 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.whispersystems.signalservice.api.crypto.ContentHint import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.crypto.EnvelopeContent import org.whispersystems.signalservice.api.crypto.EnvelopeContent
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.Content import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage import org.whispersystems.signalservice.internal.push.DataMessage
@ -46,11 +46,10 @@ object FakeClientHelpers {
} }
} }
fun getTargetUnidentifiedAccess(myProfileKey: ProfileKey, theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): Optional<UnidentifiedAccess> { fun getSealedSenderAccess(theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): SealedSenderAccess? {
val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey) val themUnidentifiedAccessKey = UnidentifiedAccess(UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey), senderCertificate.serialized, false)
val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized, false), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized, false)).targetUnidentifiedAccess return SealedSenderAccess.forIndividual(themUnidentifiedAccessKey)
} }
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent { fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {

View file

@ -33,7 +33,7 @@ object GroupTestingUtils {
.title(MessageContentFuzzer.string()) .title(MessageContentFuzzer.string())
.build() .build()
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!! val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState, null)!!
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId) val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
SignalDatabase.recipients.setProfileSharing(groupRecipientId, true) SignalDatabase.recipients.setProfileSharing(groupRecipientId, true)

View file

@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver; import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver;
import org.thoughtcrime.securesms.messages.GroupSendEndorsementInternalNotifier;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule; import org.thoughtcrime.securesms.mms.SignalGlideModule;
@ -222,6 +223,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(GroupRingCleanupJob::enqueue) .addPostRender(GroupRingCleanupJob::enqueue)
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary) .addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
.addPostRender(() -> ActiveCallManager.clearNotifications(this)) .addPostRender(() -> ActiveCallManager.clearNotifications(this))
.addPostRender(() -> GroupSendEndorsementInternalNotifier.init())
.execute(); .execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");

View file

@ -241,7 +241,7 @@ fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
} }
val recipientId = writableDatabase.insert(RecipientTable.TABLE_NAME, null, values) val recipientId = writableDatabase.insert(RecipientTable.TABLE_NAME, null, values)
val restoredId = SignalDatabase.groups.create(masterKey, decryptedState) val restoredId = SignalDatabase.groups.create(masterKey, decryptedState, groupSendEndorsements = null)
if (restoredId != null) { if (restoredId != null) {
SignalDatabase.groups.setShowAsStoryState(restoredId, group.storySendMode.toGroupShowAsStoryState()) SignalDatabase.groups.setShowAsStoryState(restoredId, group.storySendMode.toGroupShowAsStoryState())
} }

View file

@ -129,7 +129,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
textPref( textPref(
title = DSLSettingsText.from("Sealed Sender Mode"), title = DSLSettingsText.from("Sealed Sender Mode"),
summary = DSLSettingsText.from(recipient.unidentifiedAccessMode.toString()) summary = DSLSettingsText.from(recipient.sealedSenderAccessMode.toString())
) )
textPref( textPref(

View file

@ -1,17 +1,17 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.signal.core.util.Base64;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
@ -21,10 +21,8 @@ import org.thoughtcrime.securesms.keyvalue.CertificateType;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.Base64; import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
@ -35,9 +33,9 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class UnidentifiedAccessUtil { public class SealedSenderAccessUtil {
private static final String TAG = Log.tag(UnidentifiedAccessUtil.class); private static final String TAG = Log.tag(SealedSenderAccessUtil.class);
private static final byte[] UNRESTRICTED_KEY = new byte[16]; private static final byte[] UNRESTRICTED_KEY = new byte[16];
@ -46,28 +44,34 @@ public class UnidentifiedAccessUtil {
} }
@WorkerThread @WorkerThread
public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context, @NonNull Recipient recipient) { public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull Recipient recipient) {
return getAccessFor(context, recipient, true); return getSealedSenderAccessFor(recipient, true);
} }
@WorkerThread @WorkerThread
public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context, @NonNull Recipient recipient, boolean log) { public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull Recipient recipient, boolean log) {
return getAccessFor(context, Collections.singletonList(recipient), log).get(0); return SealedSenderAccess.forIndividual(getAccessFor(recipient, log));
}
public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull Recipient recipient, @Nullable SealedSenderAccess.CreateGroupSendToken createGroupSendToken) {
return SealedSenderAccess.forIndividualWithGroupFallback(getAccessFor(recipient, true), getSealedSenderCertificate(), createGroupSendToken);
} }
@WorkerThread @WorkerThread
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients) { private static @Nullable UnidentifiedAccess getAccessFor(@NonNull Recipient recipient, boolean log) {
return getAccessFor(context, recipients, true); return getAccessFor(Collections.singletonList(recipient), false, log)
.get(0)
.orElse(null);
} }
@WorkerThread @WorkerThread
public static Map<RecipientId, Optional<UnidentifiedAccessPair>> getAccessMapFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean isForStory) { public static Map<RecipientId, Optional<UnidentifiedAccess>> getAccessMapFor(@NonNull List<Recipient> recipients, boolean isForStory) {
List<Optional<UnidentifiedAccessPair>> accessList = getAccessFor(context, recipients, isForStory, true); List<Optional<UnidentifiedAccess>> accessList = getAccessFor(recipients, isForStory, true);
Iterator<Recipient> recipientIterator = recipients.iterator(); Iterator<Recipient> recipientIterator = recipients.iterator();
Iterator<Optional<UnidentifiedAccessPair>> accessIterator = accessList.iterator(); Iterator<Optional<UnidentifiedAccess>> accessIterator = accessList.iterator();
Map<RecipientId, Optional<UnidentifiedAccessPair>> accessMap = new HashMap<>(recipients.size()); Map<RecipientId, Optional<UnidentifiedAccess>> accessMap = new HashMap<>(recipients.size());
while (recipientIterator.hasNext()) { while (recipientIterator.hasNext()) {
accessMap.put(recipientIterator.next().getId(), accessIterator.next()); accessMap.put(recipientIterator.next().getId(), accessIterator.next());
@ -77,40 +81,22 @@ public class UnidentifiedAccessUtil {
} }
@WorkerThread @WorkerThread
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean log) { private static List<Optional<UnidentifiedAccess>> getAccessFor(@NonNull List<Recipient> recipients, boolean isForStory, boolean log) {
return getAccessFor(context, recipients, false, log);
}
@WorkerThread
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean isForStory, boolean log) {
final byte[] ourUnidentifiedAccessKey;
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = UNRESTRICTED_KEY;
} else {
ourUnidentifiedAccessKey = ProfileKeyUtil.getSelfProfileKey().deriveAccessKey();
}
CertificateType certificateType = getUnidentifiedAccessCertificateType(); CertificateType certificateType = getUnidentifiedAccessCertificateType();
byte[] ourUnidentifiedAccessCertificate = SignalStore.certificate().getUnidentifiedAccessCertificate(certificateType); byte[] ourUnidentifiedAccessCertificate = SignalStore.certificate().getUnidentifiedAccessCertificate(certificateType);
List<Optional<UnidentifiedAccessPair>> access = recipients.parallelStream().map(recipient -> { List<Optional<UnidentifiedAccess>> access = recipients.parallelStream().map(recipient -> {
UnidentifiedAccessPair unidentifiedAccessPair = null; UnidentifiedAccess unidentifiedAccess = null;
if (ourUnidentifiedAccessCertificate != null) { if (ourUnidentifiedAccessCertificate != null) {
try { try {
UnidentifiedAccess theirAccess = getTargetUnidentifiedAccess(recipient, ourUnidentifiedAccessCertificate, isForStory); unidentifiedAccess = getTargetUnidentifiedAccess(recipient, ourUnidentifiedAccessCertificate, isForStory);
UnidentifiedAccess ourAccess = new UnidentifiedAccess(ourUnidentifiedAccessKey, ourUnidentifiedAccessCertificate, false);
if (theirAccess != null) {
unidentifiedAccessPair = new UnidentifiedAccessPair(theirAccess, ourAccess);
}
} catch (InvalidCertificateException e) { } catch (InvalidCertificateException e) {
Log.w(TAG, "Invalid unidentified access certificate!", e); Log.w(TAG, "Invalid unidentified access certificate!", e);
} }
} else { } else {
Log.w(TAG, "Missing unidentified access certificate!"); Log.w(TAG, "Missing our unidentified access certificate!");
} }
return Optional.ofNullable(unidentifiedAccessPair); return Optional.ofNullable(unidentifiedAccess);
}).collect(Collectors.toList()); }).collect(Collectors.toList());
int unidentifiedCount = Stream.of(access).filter(Optional::isPresent).toList().size(); int unidentifiedCount = Stream.of(access).filter(Optional::isPresent).toList().size();
@ -123,28 +109,17 @@ public class UnidentifiedAccessUtil {
return access; return access;
} }
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) { public static @Nullable SenderCertificate getSealedSenderCertificate() {
byte[] unidentifiedAccessCertificate = getUnidentifiedAccessCertificate();
if (unidentifiedAccessCertificate == null) {
return null;
}
try { try {
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); return new SenderCertificate(unidentifiedAccessCertificate);
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate();
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = UNRESTRICTED_KEY;
}
if (ourUnidentifiedAccessCertificate != null) {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate,
false),
new UnidentifiedAccess(ourUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate,
false)));
}
return Optional.empty();
} catch (InvalidCertificateException e) { } catch (InvalidCertificateException e) {
Log.w(TAG, e); Log.w(TAG, e);
return Optional.empty(); return null;
} }
} }
@ -166,7 +141,7 @@ public class UnidentifiedAccessUtil {
byte[] accessKey; byte[] accessKey;
switch (recipient.resolve().getUnidentifiedAccessMode()) { switch (recipient.resolve().getSealedSenderAccessMode()) {
case UNKNOWN: case UNKNOWN:
if (theirProfileKey == null) { if (theirProfileKey == null) {
if (isForStory) { if (isForStory) {
@ -192,7 +167,7 @@ public class UnidentifiedAccessUtil {
accessKey = UNRESTRICTED_KEY; accessKey = UNRESTRICTED_KEY;
break; break;
default: default:
throw new AssertionError("Unknown mode: " + recipient.getUnidentifiedAccessMode().getMode()); throw new AssertionError("Unknown mode: " + recipient.getSealedSenderAccessMode().getMode());
} }
if (accessKey == null && isForStory) { if (accessKey == null && isForStory) {

View file

@ -19,7 +19,9 @@ import org.signal.core.util.isAbsent
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.optionalString import org.signal.core.util.optionalString
import org.signal.core.util.readToList import org.signal.core.util.readToList
import org.signal.core.util.readToMap
import org.signal.core.util.readToSingleInt import org.signal.core.util.readToSingleInt
import org.signal.core.util.readToSingleLong
import org.signal.core.util.readToSingleObject import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBlob import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean import org.signal.core.util.requireBoolean
@ -30,7 +32,11 @@ import org.signal.core.util.requireString
import org.signal.core.util.select import org.signal.core.util.select
import org.signal.core.util.update import org.signal.core.util.update
import org.signal.core.util.withinTransaction import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken
import org.signal.storageservice.protos.groups.Member import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
@ -39,6 +45,7 @@ import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterat
import org.thoughtcrime.securesms.crypto.SenderKeyUtil import org.thoughtcrime.securesms.crypto.SenderKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.database.model.GroupRecord import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.GroupSendEndorsementRecords
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.BadGroupIdException import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
@ -50,6 +57,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
import org.whispersystems.signalservice.api.groupsv2.ReceivedGroupSendEndorsements
import org.whispersystems.signalservice.api.groupsv2.findMemberByAci import org.whispersystems.signalservice.api.groupsv2.findMemberByAci
import org.whispersystems.signalservice.api.groupsv2.findPendingByServiceId import org.whispersystems.signalservice.api.groupsv2.findPendingByServiceId
import org.whispersystems.signalservice.api.groupsv2.findRequestingByAci import org.whispersystems.signalservice.api.groupsv2.findRequestingByAci
@ -63,6 +71,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.io.Closeable import java.io.Closeable
import java.security.SecureRandom import java.security.SecureRandom
import java.time.Instant
import java.util.Optional import java.util.Optional
import java.util.stream.Collectors import java.util.stream.Collectors
import javax.annotation.CheckReturnValue import javax.annotation.CheckReturnValue
@ -94,6 +103,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
const val DISTRIBUTION_ID = "distribution_id" const val DISTRIBUTION_ID = "distribution_id"
const val SHOW_AS_STORY_STATE = "show_as_story_state" const val SHOW_AS_STORY_STATE = "show_as_story_state"
const val LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp" const val LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp"
const val GROUP_SEND_ENDORSEMENTS_EXPIRATION = "group_send_endorsements_expiration"
/** 32 bytes serialized [GroupMasterKey] */ /** 32 bytes serialized [GroupMasterKey] */
const val V2_MASTER_KEY = "master_key" const val V2_MASTER_KEY = "master_key"
@ -125,12 +135,13 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
$UNMIGRATED_V1_MEMBERS TEXT DEFAULT NULL, $UNMIGRATED_V1_MEMBERS TEXT DEFAULT NULL,
$DISTRIBUTION_ID TEXT UNIQUE DEFAULT NULL, $DISTRIBUTION_ID TEXT UNIQUE DEFAULT NULL,
$SHOW_AS_STORY_STATE INTEGER DEFAULT ${ShowAsStoryState.IF_ACTIVE.code}, $SHOW_AS_STORY_STATE INTEGER DEFAULT ${ShowAsStoryState.IF_ACTIVE.code},
$LAST_FORCE_UPDATE_TIMESTAMP INTEGER DEFAULT 0 $LAST_FORCE_UPDATE_TIMESTAMP INTEGER DEFAULT 0,
$GROUP_SEND_ENDORSEMENTS_EXPIRATION INTEGER DEFAULT 0
) )
""" """
@JvmField @JvmField
val CREATE_INDEXS = MembershipTable.CREATE_INDEXES val CREATE_INDEXS: Array<String> = MembershipTable.CREATE_INDEXES
private val GROUP_PROJECTION = arrayOf( private val GROUP_PROJECTION = arrayOf(
GROUP_ID, GROUP_ID,
@ -147,7 +158,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
V2_MASTER_KEY, V2_MASTER_KEY,
V2_REVISION, V2_REVISION,
V2_DECRYPTED_GROUP, V2_DECRYPTED_GROUP,
LAST_FORCE_UPDATE_TIMESTAMP LAST_FORCE_UPDATE_TIMESTAMP,
GROUP_SEND_ENDORSEMENTS_EXPIRATION
) )
val TYPED_GROUP_PROJECTION = GROUP_PROJECTION val TYPED_GROUP_PROJECTION = GROUP_PROJECTION
@ -165,6 +177,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
const val ID = "_id" const val ID = "_id"
const val GROUP_ID = "group_id" const val GROUP_ID = "group_id"
const val RECIPIENT_ID = "recipient_id" const val RECIPIENT_ID = "recipient_id"
const val ENDORSEMENT = "endorsement"
//language=sql //language=sql
const val CREATE_TABLE = """ const val CREATE_TABLE = """
@ -172,6 +185,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
$ID INTEGER PRIMARY KEY, $ID INTEGER PRIMARY KEY,
$GROUP_ID TEXT NOT NULL REFERENCES ${GroupTable.TABLE_NAME} (${GroupTable.GROUP_ID}) ON DELETE CASCADE, $GROUP_ID TEXT NOT NULL REFERENCES ${GroupTable.TABLE_NAME} (${GroupTable.GROUP_ID}) ON DELETE CASCADE,
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE, $RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
$ENDORSEMENT BLOB DEFAULT NULL,
UNIQUE($GROUP_ID, $RECIPIENT_ID) UNIQUE($GROUP_ID, $RECIPIENT_ID)
) )
""" """
@ -568,19 +582,19 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
throw LegacyGroupInsertException(groupId) throw LegacyGroupInsertException(groupId)
} }
return create(groupId, title, members, avatar, null, null) return create(groupId, title, members, avatar, null, null, null)
} }
@CheckReturnValue @CheckReturnValue
fun create(groupId: GroupId.Mms, title: String?, members: Collection<RecipientId>): Boolean { fun create(groupId: GroupId.Mms, title: String?, members: Collection<RecipientId>): Boolean {
return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null) return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null, null)
} }
@CheckReturnValue @CheckReturnValue
fun create(groupMasterKey: GroupMasterKey, groupState: DecryptedGroup): GroupId.V2? { fun create(groupMasterKey: GroupMasterKey, groupState: DecryptedGroup, groupSendEndorsements: ReceivedGroupSendEndorsements?): GroupId.V2? {
val groupId = GroupId.v2(groupMasterKey) val groupId = GroupId.v2(groupMasterKey)
return if (create(groupId = groupId, title = groupState.title, memberCollection = emptyList(), avatar = null, groupMasterKey = groupMasterKey, groupState = groupState)) { return if (create(groupId = groupId, title = groupState.title, memberCollection = emptyList(), avatar = null, groupMasterKey = groupMasterKey, groupState = groupState, receivedGroupSendEndorsements = groupSendEndorsements)) {
groupId groupId
} else { } else {
null null
@ -604,10 +618,9 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
if (updated < 1) { if (updated < 1) {
Log.w(TAG, "No group entry. Creating restore placeholder for $groupId") Log.w(TAG, "No group entry. Creating restore placeholder for $groupId")
create( create(
groupMasterKey, groupMasterKey = groupMasterKey,
DecryptedGroup.Builder() groupState = DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION),
.revision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) groupSendEndorsements = null
.build()
) )
} else { } else {
Log.w(TAG, "Had a group entry, but it was missing a master key. Updated.") Log.w(TAG, "Had a group entry, but it was missing a master key. Updated.")
@ -628,7 +641,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
memberCollection: Collection<RecipientId>, memberCollection: Collection<RecipientId>,
avatar: SignalServiceAttachmentPointer?, avatar: SignalServiceAttachmentPointer?,
groupMasterKey: GroupMasterKey?, groupMasterKey: GroupMasterKey?,
groupState: DecryptedGroup? groupState: DecryptedGroup?,
receivedGroupSendEndorsements: ReceivedGroupSendEndorsements?
): Boolean { ): Boolean {
val membershipValues = mutableListOf<ContentValues>() val membershipValues = mutableListOf<ContentValues>()
val groupRecipientId = recipients.getOrInsertFromGroupId(groupId) val groupRecipientId = recipients.getOrInsertFromGroupId(groupId)
@ -640,7 +654,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
values.put(RECIPIENT_ID, groupRecipientId.serialize()) values.put(RECIPIENT_ID, groupRecipientId.serialize())
values.put(GROUP_ID, groupId.toString()) values.put(GROUP_ID, groupId.toString())
values.put(TITLE, title) values.put(TITLE, title)
membershipValues.addAll(members.toContentValues(groupId)) membershipValues.addAll(members.toContentValues(groupId, receivedGroupSendEndorsements?.toGroupSendEndorsementRecords()))
values.put(MMS, groupId.isMms) values.put(MMS, groupId.isMms)
if (avatar != null) { if (avatar != null) {
@ -657,6 +671,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
if (groupId.isV2) { if (groupId.isV2) {
values.put(ACTIVE, if (groupState != null && gv2GroupActive(groupState)) 1 else 0) values.put(ACTIVE, if (groupState != null && gv2GroupActive(groupState)) 1 else 0)
values.put(DISTRIBUTION_ID, DistributionId.create().toString()) values.put(DISTRIBUTION_ID, DistributionId.create().toString())
values.put(GROUP_SEND_ENDORSEMENTS_EXPIRATION, receivedGroupSendEndorsements?.expirationMs ?: 0)
} else if (groupId.isV1) { } else if (groupId.isV1) {
values.put(ACTIVE, 1) values.put(ACTIVE, 1)
values.put(EXPECTED_V2_ID, groupId.requireV1().deriveV2MigrationGroupId().toString()) values.put(EXPECTED_V2_ID, groupId.requireV1().deriveV2MigrationGroupId().toString())
@ -676,7 +691,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
values.put(V2_REVISION, groupState.revision) values.put(V2_REVISION, groupState.revision)
values.put(V2_DECRYPTED_GROUP, groupState.encode()) values.put(V2_DECRYPTED_GROUP, groupState.encode())
membershipValues.clear() membershipValues.clear()
membershipValues.addAll(groupMembers.toContentValues(groupId)) membershipValues.addAll(groupMembers.toContentValues(groupId, receivedGroupSendEndorsements?.toGroupSendEndorsementRecords()))
} else { } else {
if (groupId.isV2) { if (groupId.isV2) {
throw AssertionError("V2 group id but no master key") throw AssertionError("V2 group id but no master key")
@ -691,9 +706,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
return false return false
} }
for (query in SqlUtil.buildBulkInsert(MembershipTable.TABLE_NAME, arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID), membershipValues)) { for (query in SqlUtil.buildBulkInsert(MembershipTable.TABLE_NAME, arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID, MembershipTable.ENDORSEMENT), membershipValues)) {
writableDatabase.execSQL(query.where, query.whereArgs) writableDatabase.execSQL(query.where, query.whereArgs)
} }
writableDatabase.setTransactionSuccessful() writableDatabase.setTransactionSuccessful()
} finally { } finally {
writableDatabase.endTransaction() writableDatabase.endTransaction()
@ -745,11 +761,11 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
notifyConversationListListeners() notifyConversationListListeners()
} }
fun update(groupMasterKey: GroupMasterKey, decryptedGroup: DecryptedGroup) { fun update(groupMasterKey: GroupMasterKey, decryptedGroup: DecryptedGroup, groupSendEndorsements: ReceivedGroupSendEndorsements?) {
update(GroupId.v2(groupMasterKey), decryptedGroup) update(GroupId.v2(groupMasterKey), decryptedGroup, groupSendEndorsements)
} }
fun update(groupId: GroupId.V2, decryptedGroup: DecryptedGroup) { fun update(groupId: GroupId.V2, decryptedGroup: DecryptedGroup, receivedGroupSendEndorsements: ReceivedGroupSendEndorsements?) {
val groupRecipientId: RecipientId = recipients.getOrInsertFromGroupId(groupId) val groupRecipientId: RecipientId = recipients.getOrInsertFromGroupId(groupId)
val existingGroup: Optional<GroupRecord> = getGroup(groupId) val existingGroup: Optional<GroupRecord> = getGroup(groupId)
val title: String = decryptedGroup.title val title: String = decryptedGroup.title
@ -760,6 +776,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.encode()) contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.encode())
contentValues.put(ACTIVE, if (gv2GroupActive(decryptedGroup)) 1 else 0) contentValues.put(ACTIVE, if (gv2GroupActive(decryptedGroup)) 1 else 0)
if (receivedGroupSendEndorsements != null) {
contentValues.put(GROUP_SEND_ENDORSEMENTS_EXPIRATION, receivedGroupSendEndorsements.expirationMs)
}
if (existingGroup.isPresent && existingGroup.get().unmigratedV1Members.isNotEmpty() && existingGroup.get().isV2Group) { if (existingGroup.isPresent && existingGroup.get().unmigratedV1Members.isNotEmpty() && existingGroup.get().isV2Group) {
val unmigratedV1Members: MutableSet<RecipientId> = existingGroup.get().unmigratedV1Members.toMutableSet() val unmigratedV1Members: MutableSet<RecipientId> = existingGroup.get().unmigratedV1Members.toMutableSet()
@ -781,6 +801,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
} }
val groupMembers = getV2GroupMembers(decryptedGroup, true) val groupMembers = getV2GroupMembers(decryptedGroup, true)
var groupSendEndorsementRecords: GroupSendEndorsementRecords? = receivedGroupSendEndorsements?.toGroupSendEndorsementRecords() ?: getGroupSendEndorsements(groupId)
val addedMembers: List<RecipientId> = if (existingGroup.isPresent && existingGroup.get().isV2Group) { val addedMembers: List<RecipientId> = if (existingGroup.isPresent && existingGroup.get().isV2Group) {
val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup) val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
@ -800,6 +821,12 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
) )
} }
if (receivedGroupSendEndorsements == null && (removed.isNotEmpty() || change.newMembers.isNotEmpty())) {
Log.v(TAG, "Members were removed or added, and no new endorsements, clearing endorsements and GSE expiration")
contentValues.put(GROUP_SEND_ENDORSEMENTS_EXPIRATION, 0)
groupSendEndorsementRecords = null
}
change.newMembers.toAciList().toRecipientIds() change.newMembers.toAciList().toRecipientIds()
} else { } else {
groupMembers groupMembers
@ -812,7 +839,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
.where("$GROUP_ID = ?", groupId.toString()) .where("$GROUP_ID = ?", groupId.toString())
.run() .run()
performMembershipUpdate(database, groupId, groupMembers) performMembershipUpdate(database, groupId, groupMembers, groupSendEndorsementRecords)
} }
if (decryptedGroup.disappearingMessagesTimer != null) { if (decryptedGroup.disappearingMessagesTimer != null) {
@ -867,7 +894,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
.toMutableList() .toMutableList()
} }
private fun performMembershipUpdate(database: SQLiteDatabase, groupId: GroupId, members: Collection<RecipientId>) { private fun performMembershipUpdate(database: SQLiteDatabase, groupId: GroupId, members: Collection<RecipientId>, groupSendEndorsementRecords: GroupSendEndorsementRecords?) {
check(database.inTransaction()) check(database.inTransaction())
database database
.delete(MembershipTable.TABLE_NAME) .delete(MembershipTable.TABLE_NAME)
@ -876,8 +903,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
val inserts = SqlUtil.buildBulkInsert( val inserts = SqlUtil.buildBulkInsert(
MembershipTable.TABLE_NAME, MembershipTable.TABLE_NAME,
arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID), arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID, MembershipTable.ENDORSEMENT),
members.toSet().toContentValues(groupId) members.toSet().toContentValues(groupId, groupSendEndorsementRecords)
) )
inserts.forEach { inserts.forEach {
@ -906,6 +933,94 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
.run() .run()
} }
fun getGroupSendEndorsementsExpiration(groupId: GroupId): Long {
return writableDatabase
.select(GROUP_SEND_ENDORSEMENTS_EXPIRATION)
.from(TABLE_NAME)
.where("$GROUP_ID = ?", groupId)
.run()
.readToSingleLong(0L)
}
fun updateGroupSendEndorsements(groupId: GroupId.V2, receivedGroupSendEndorsements: ReceivedGroupSendEndorsements) {
val endorsements: Map<RecipientId, GroupSendEndorsement?> = receivedGroupSendEndorsements.toGroupSendEndorsementRecords().endorsements
writableDatabase.withinTransaction { db ->
db.update(MembershipTable.TABLE_NAME, contentValuesOf(MembershipTable.ENDORSEMENT to null), "${MembershipTable.GROUP_ID} = ?", arrayOf(groupId.serialize()))
for ((recipientId, endorsement) in endorsements) {
db.update(MembershipTable.TABLE_NAME)
.values(MembershipTable.ENDORSEMENT to endorsement?.serialize())
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, recipientId)
.run()
}
writableDatabase
.update(TABLE_NAME)
.values(GROUP_SEND_ENDORSEMENTS_EXPIRATION to receivedGroupSendEndorsements.expirationMs)
.where("$GROUP_ID = ?", groupId)
.run()
}
}
fun getGroupSendEndorsements(groupId: GroupId): GroupSendEndorsementRecords {
val allEndorsements: Map<RecipientId, GroupSendEndorsement?> = readableDatabase
.select(MembershipTable.RECIPIENT_ID, MembershipTable.ENDORSEMENT)
.from(MembershipTable.TABLE_NAME)
.where("${MembershipTable.GROUP_ID} = ?", groupId)
.run()
.readToMap { cursor ->
val recipientId = RecipientId.from(cursor.requireLong(MembershipTable.RECIPIENT_ID))
val endorsement = cursor.requireBlob(MembershipTable.ENDORSEMENT)?.let { endorsementBlob ->
try {
GroupSendEndorsement(endorsementBlob)
} catch (e: InvalidInputException) {
Log.w(TAG, "Unable to parse group send endorsement for $recipientId", e)
null
}
}
recipientId to endorsement
}
return GroupSendEndorsementRecords(allEndorsements)
}
fun getGroupSendFullToken(threadId: Long, recipientId: RecipientId): GroupSendFullToken? {
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
if (threadRecipient == null || !threadRecipient.isGroup) {
return null
}
return getGroupSendFullToken(threadRecipient.requireGroupId().requireV2(), recipientId)
}
fun getGroupSendFullToken(groupId: GroupId.V2, recipientId: RecipientId): GroupSendFullToken? {
val groupRecord = SignalDatabase.groups.getGroup(groupId).orElse(null) ?: return null
val endorsement = SignalDatabase.groups.getGroupSendEndorsement(groupId, recipientId) ?: return null
val groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupRecord.requireV2GroupProperties().groupMasterKey)
return endorsement.toFullToken(groupSecretParams, Instant.ofEpochMilli(groupRecord.groupSendEndorsementExpiration))
}
private fun getGroupSendEndorsement(groupId: GroupId, recipientId: RecipientId): GroupSendEndorsement? {
return readableDatabase
.select(MembershipTable.ENDORSEMENT)
.from(MembershipTable.TABLE_NAME)
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, recipientId)
.run()
.readToSingleObject { c ->
c.requireBlob(MembershipTable.ENDORSEMENT)?.let { endorsementBlob ->
try {
GroupSendEndorsement(endorsementBlob)
} catch (e: InvalidInputException) {
Log.w(TAG, "Unable to parse group send endorsement for $recipientId", e)
null
}
}
}
}
@WorkerThread @WorkerThread
fun isCurrentMember(groupId: Push, recipientId: RecipientId): Boolean { fun isCurrentMember(groupId: Push, recipientId: RecipientId): Boolean {
return readableDatabase return readableDatabase
@ -1008,7 +1123,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
groupRevision = cursor.requireInt(V2_REVISION), groupRevision = cursor.requireInt(V2_REVISION),
decryptedGroupBytes = cursor.requireBlob(V2_DECRYPTED_GROUP), decryptedGroupBytes = cursor.requireBlob(V2_DECRYPTED_GROUP),
distributionId = cursor.optionalString(DISTRIBUTION_ID).map { id -> DistributionId.from(id) }.orElse(null), distributionId = cursor.optionalString(DISTRIBUTION_ID).map { id -> DistributionId.from(id) }.orElse(null),
lastForceUpdateTimestamp = cursor.requireLong(LAST_FORCE_UPDATE_TIMESTAMP) lastForceUpdateTimestamp = cursor.requireLong(LAST_FORCE_UPDATE_TIMESTAMP),
groupSendEndorsementExpiration = cursor.requireLong(GROUP_SEND_ENDORSEMENTS_EXPIRATION)
) )
} }
} }
@ -1220,15 +1336,20 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
return RecipientId.toSerializedList(this) return RecipientId.toSerializedList(this)
} }
private fun Collection<RecipientId>.toContentValues(groupId: GroupId): List<ContentValues> { private fun Collection<RecipientId>.toContentValues(groupId: GroupId, groupSendEndorsementRecords: GroupSendEndorsementRecords?): List<ContentValues> {
return map { return map {
contentValuesOf( contentValuesOf(
MembershipTable.GROUP_ID to groupId.serialize(), MembershipTable.GROUP_ID to groupId.serialize(),
MembershipTable.RECIPIENT_ID to it.serialize() MembershipTable.RECIPIENT_ID to it.serialize(),
MembershipTable.ENDORSEMENT to groupSendEndorsementRecords?.endorsements?.get(it)?.serialize()
) )
} }
} }
private fun ReceivedGroupSendEndorsements.toGroupSendEndorsementRecords(): GroupSendEndorsementRecords {
return GroupSendEndorsementRecords(endorsements.map { (aci, endorsement) -> RecipientId.from(aci) to endorsement }.toMap())
}
private fun serviceIdsToRecipientIds(serviceIds: Sequence<ServiceId>): MutableList<RecipientId> { private fun serviceIdsToRecipientIds(serviceIds: Sequence<ServiceId>): MutableList<RecipientId> {
return serviceIds return serviceIds
.map { serviceId -> .map { serviceId ->

View file

@ -958,10 +958,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
Log.i(TAG, "Creating restore placeholder for $groupId") Log.i(TAG, "Creating restore placeholder for $groupId")
val createdId = groups.create( val createdId = groups.create(
masterKey, groupMasterKey = masterKey,
DecryptedGroup.Builder() groupState = DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION),
.revision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) groupSendEndorsements = null
.build()
) )
if (createdId == null) { if (createdId == null) {
@ -1469,9 +1468,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
} }
} }
fun setUnidentifiedAccessMode(id: RecipientId, unidentifiedAccessMode: UnidentifiedAccessMode) { fun setSealedSenderAccessMode(id: RecipientId, sealedSenderAccessMode: SealedSenderAccessMode) {
val values = ContentValues(1).apply { val values = ContentValues(1).apply {
put(SEALED_SENDER_MODE, unidentifiedAccessMode.mode) put(SEALED_SENDER_MODE, sealedSenderAccessMode.mode)
} }
if (update(id, values)) { if (update(id, values)) {
AppDependencies.databaseObserver.notifyRecipientChanged(id) AppDependencies.databaseObserver.notifyRecipientChanged(id)
@ -1554,7 +1553,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
val valuesToSet = ContentValues(3).apply { val valuesToSet = ContentValues(3).apply {
put(PROFILE_KEY, encodedProfileKey) put(PROFILE_KEY, encodedProfileKey)
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL) putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
put(SEALED_SENDER_MODE, UnidentifiedAccessMode.UNKNOWN.mode) put(SEALED_SENDER_MODE, SealedSenderAccessMode.UNKNOWN.mode)
} }
val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare) val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare)
@ -1586,7 +1585,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
val valuesToSet = ContentValues(3).apply { val valuesToSet = ContentValues(3).apply {
put(PROFILE_KEY, Base64.encodeWithPadding(profileKey.serialize())) put(PROFILE_KEY, Base64.encodeWithPadding(profileKey.serialize()))
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL) putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
put(SEALED_SENDER_MODE, UnidentifiedAccessMode.UNKNOWN.mode) put(SEALED_SENDER_MODE, SealedSenderAccessMode.UNKNOWN.mode)
} }
if (writableDatabase.update(TABLE_NAME, valuesToSet, selection, args) > 0) { if (writableDatabase.update(TABLE_NAME, valuesToSet, selection, args) > 0) {
@ -4610,14 +4609,14 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
} }
} }
enum class UnidentifiedAccessMode(val mode: Int) { enum class SealedSenderAccessMode(val mode: Int) {
UNKNOWN(0), UNKNOWN(0),
DISABLED(1), DISABLED(1),
ENABLED(2), ENABLED(2),
UNRESTRICTED(3); UNRESTRICTED(3);
companion object { companion object {
fun fromMode(mode: Int): UnidentifiedAccessMode { fun fromMode(mode: Int): SealedSenderAccessMode {
return values()[mode] return values()[mode]
} }
} }

View file

@ -149,7 +149,7 @@ object RecipientTableCursorUtil {
profileSharing = cursor.requireBoolean(RecipientTable.PROFILE_SHARING), profileSharing = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
lastProfileFetch = cursor.requireLong(RecipientTable.LAST_PROFILE_FETCH), lastProfileFetch = cursor.requireLong(RecipientTable.LAST_PROFILE_FETCH),
notificationChannel = cursor.requireString(RecipientTable.NOTIFICATION_CHANNEL), notificationChannel = cursor.requireString(RecipientTable.NOTIFICATION_CHANNEL),
unidentifiedAccessMode = RecipientTable.UnidentifiedAccessMode.fromMode(cursor.requireInt(RecipientTable.SEALED_SENDER_MODE)), sealedSenderAccessMode = RecipientTable.SealedSenderAccessMode.fromMode(cursor.requireInt(RecipientTable.SEALED_SENDER_MODE)),
capabilities = readCapabilities(cursor), capabilities = readCapabilities(cursor),
storageId = Base64.decodeNullableOrThrow(cursor.requireString(RecipientTable.STORAGE_SERVICE_ID)), storageId = Base64.decodeNullableOrThrow(cursor.requireString(RecipientTable.STORAGE_SERVICE_ID)),
mentionSetting = RecipientTable.MentionSetting.fromId(cursor.requireInt(RecipientTable.MENTION_SETTING)), mentionSetting = RecipientTable.MentionSetting.fromId(cursor.requireInt(RecipientTable.MENTION_SETTING)),

View file

@ -95,6 +95,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V234_ThumbnailResto
import org.thoughtcrime.securesms.database.helpers.migration.V235_AttachmentUuidColumn import org.thoughtcrime.securesms.database.helpers.migration.V235_AttachmentUuidColumn
import org.thoughtcrime.securesms.database.helpers.migration.V236_FixInAppSubscriberCurrencyIfAble import org.thoughtcrime.securesms.database.helpers.migration.V236_FixInAppSubscriberCurrencyIfAble
import org.thoughtcrime.securesms.database.helpers.migration.V237_ResetGroupForceUpdateTimestamps import org.thoughtcrime.securesms.database.helpers.migration.V237_ResetGroupForceUpdateTimestamps
import org.thoughtcrime.securesms.database.helpers.migration.V238_AddGroupSendEndorsementsColumns
/** /**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@ -192,10 +193,11 @@ object SignalDatabaseMigrations {
234 to V234_ThumbnailRestoreStateColumn, 234 to V234_ThumbnailRestoreStateColumn,
235 to V235_AttachmentUuidColumn, 235 to V235_AttachmentUuidColumn,
236 to V236_FixInAppSubscriberCurrencyIfAble, 236 to V236_FixInAppSubscriberCurrencyIfAble,
237 to V237_ResetGroupForceUpdateTimestamps 237 to V237_ResetGroupForceUpdateTimestamps,
238 to V238_AddGroupSendEndorsementsColumns
) )
const val DATABASE_VERSION = 237 const val DATABASE_VERSION = 238
@JvmStatic @JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

View file

@ -0,0 +1,21 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
/**
* Add columns to group and group membership tables needed for group send endorsements.
*/
@Suppress("ClassName")
object V238_AddGroupSendEndorsementsColumns : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE groups ADD COLUMN group_send_endorsements_expiration INTEGER DEFAULT 0")
db.execSQL("ALTER TABLE group_membership ADD COLUMN endorsement BLOB DEFAULT NULL")
}
}

View file

@ -31,7 +31,8 @@ class GroupRecord(
groupRevision: Int, groupRevision: Int,
decryptedGroupBytes: ByteArray?, decryptedGroupBytes: ByteArray?,
val distributionId: DistributionId?, val distributionId: DistributionId?,
val lastForceUpdateTimestamp: Long val lastForceUpdateTimestamp: Long,
val groupSendEndorsementExpiration: Long
) { ) {
val members: List<RecipientId> by lazy { val members: List<RecipientId> by lazy {

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.model
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Contains the individual group send endorsements for a specific group
* source from our local db.
*/
data class GroupSendEndorsementRecords(val endorsements: Map<RecipientId, GroupSendEndorsement?>) {
fun getEndorsement(recipientId: RecipientId): GroupSendEndorsement? {
return endorsements[recipientId]
}
fun isMissingAnyEndorsements(): Boolean {
return endorsements.values.any { it == null }
}
}

View file

@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting
import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState
import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState
import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode
import org.thoughtcrime.securesms.database.RecipientTable.VibrateState import org.thoughtcrime.securesms.database.RecipientTable.VibrateState
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.profiles.ProfileName
@ -60,7 +60,7 @@ data class RecipientRecord(
val profileSharing: Boolean, val profileSharing: Boolean,
val lastProfileFetch: Long, val lastProfileFetch: Long,
val notificationChannel: String?, val notificationChannel: String?,
val unidentifiedAccessMode: UnidentifiedAccessMode, val sealedSenderAccessMode: SealedSenderAccessMode,
val capabilities: Capabilities, val capabilities: Capabilities,
val storageId: ByteArray?, val storageId: ByteArray?,
val mentionSetting: MentionSetting, val mentionSetting: MentionSetting,

View file

@ -172,6 +172,16 @@ public final class GroupManager {
} }
} }
@WorkerThread
public static void updateGroupSendEndorsements(@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey)
throws GroupChangeBusyException, IOException, GroupNotAMemberException
{
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
updater.updateGroupSendEndorsements();
}
}
@WorkerThread @WorkerThread
public static void setMemberAdmin(@NonNull Context context, public static void setMemberAdmin(@NonNull Context context,
@NonNull GroupId.V2 groupId, @NonNull GroupId.V2 groupId,

View file

@ -18,6 +18,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChangeResponse;
import org.signal.storageservice.protos.groups.GroupExternalCredential; import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
@ -49,6 +50,8 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.ProfileUtil; import org.thoughtcrime.securesms.util.ProfileUtil;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse;
import org.whispersystems.signalservice.api.groupsv2.ReceivedGroupSendEndorsements;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct; import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
@ -219,16 +222,18 @@ final class GroupManagerV2 {
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
{ {
GroupSecretParams groupSecretParams = GroupSecretParams.generate(); GroupSecretParams groupSecretParams = GroupSecretParams.generate();
DecryptedGroup decryptedGroup; DecryptedGroupResponse createGroupResponse;
try { try {
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessagesTimer); createGroupResponse = createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessagesTimer);
} catch (GroupAlreadyExistsException e) { } catch (GroupAlreadyExistsException e) {
throw new GroupChangeFailedException(e); throw new GroupChangeFailedException(e);
} }
GroupMasterKey masterKey = groupSecretParams.getMasterKey(); DecryptedGroup decryptedGroup = createGroupResponse.getGroup();
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup); GroupMasterKey masterKey = groupSecretParams.getMasterKey();
ReceivedGroupSendEndorsements groupSendEndorsements = groupsV2Operations.forGroup(groupSecretParams).receiveGroupSendEndorsements(selfAci, decryptedGroup, createGroupResponse.getGroupSendEndorsementsResponse());
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup, groupSendEndorsements);
if (groupId == null) { if (groupId == null) {
throw new GroupChangeFailedException("Unable to create group, group already exists"); throw new GroupChangeFailedException("Unable to create group, group already exists");
@ -670,7 +675,8 @@ final class GroupManagerV2 {
previousGroupState = v2GroupProperties.getDecryptedGroup(); previousGroupState = v2GroupProperties.getDecryptedGroup();
GroupChange signedGroupChange = commitToServer(changeActions); GroupChangeResponse changeResponse = commitToServer(changeActions);
GroupChange signedGroupChange = changeResponse.groupChange;
try { try {
//noinspection OptionalGetWithoutIsPresent //noinspection OptionalGetWithoutIsPresent
decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get(); decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
@ -680,7 +686,7 @@ final class GroupManagerV2 {
throw new IOException(e); throw new IOException(e);
} }
groupDatabase.update(groupId, decryptedGroupState); groupDatabase.update(groupId, decryptedGroupState, groupsV2Operations.forGroup(groupSecretParams).receiveGroupSendEndorsements(selfAci, decryptedGroupState, changeResponse.groupSendEndorsementsResponse));
GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState); GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState);
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange, sendToMembers); RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange, sendToMembers);
@ -690,7 +696,7 @@ final class GroupManagerV2 {
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers); return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers);
} }
private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change) private @NonNull GroupChangeResponse commitToServer(@NonNull GroupChange.Actions change)
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
{ {
try { try {
@ -747,6 +753,14 @@ final class GroupManagerV2 {
.forceSanityUpdateFromServer(timestamp); .forceSanityUpdateFromServer(timestamp);
} }
@WorkerThread
void updateGroupSendEndorsements()
throws IOException, GroupNotAMemberException
{
GroupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
.updateGroupSendEndorsements();
}
private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) { private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
if (signedGroupChange != null && signedGroupChange.length > 0) { if (signedGroupChange != null && signedGroupChange.length > 0) {
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey)); GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
@ -764,11 +778,11 @@ final class GroupManagerV2 {
} }
@WorkerThread @WorkerThread
private @NonNull DecryptedGroup createGroupOnServer(@NonNull GroupSecretParams groupSecretParams, private @NonNull DecryptedGroupResponse createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
@Nullable String name, @Nullable String name,
@Nullable byte[] avatar, @Nullable byte[] avatar,
@NonNull Collection<RecipientId> members, @NonNull Collection<RecipientId> members,
int disappearingMessageTimerSeconds) int disappearingMessageTimerSeconds)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
{ {
if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(members)) { if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(members)) {
@ -797,15 +811,10 @@ final class GroupManagerV2 {
disappearingMessageTimerSeconds); disappearingMessageTimerSeconds);
try { try {
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); DecryptedGroupResponse groupResponse = groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, AppDependencies.getGroupsV2Authorization().getAuthorizationForToday(serviceIds, groupSecretParams)); return groupResponse;
if (decryptedGroup == null) { } catch (VerificationFailedException | InvalidGroupStateException | InvalidInputException e) {
throw new GroupChangeFailedException();
}
return decryptedGroup;
} catch (VerificationFailedException | InvalidGroupStateException e) {
throw new GroupChangeFailedException(e); throw new GroupChangeFailedException(e);
} catch (GroupExistsException e) { } catch (GroupExistsException e) {
throw new GroupAlreadyExistsException(e); throw new GroupAlreadyExistsException(e);
@ -837,8 +846,9 @@ final class GroupManagerV2 {
@Nullable byte[] avatar) @Nullable byte[] avatar)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException
{ {
boolean requestToJoin = joinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR; boolean requestToJoin = joinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR;
boolean alreadyAMember = false; boolean alreadyAMember = false;
boolean groupAlreadyExists = false;
if (requestToJoin) { if (requestToJoin) {
Log.i(TAG, "Requesting to join " + groupId); Log.i(TAG, "Requesting to join " + groupId);
@ -846,10 +856,13 @@ final class GroupManagerV2 {
Log.i(TAG, "Joining " + groupId); Log.i(TAG, "Joining " + groupId);
} }
GroupChange signedGroupChange = null; GroupChangeResponse groupChangeResponse = null;
DecryptedGroupChange decryptedChange = null; GroupChange signedGroupChange = null;
DecryptedGroupChange decryptedChange = null;
try { try {
signedGroupChange = joinGroupOnServer(requestToJoin, joinInfo.revision); groupChangeResponse = joinGroupOnServer(requestToJoin, joinInfo.revision);
signedGroupChange = groupChangeResponse.groupChange;
if (requestToJoin) { if (requestToJoin) {
Log.i(TAG, String.format("Successfully requested to join %s on server", groupId)); Log.i(TAG, String.format("Successfully requested to join %s on server", groupId));
@ -857,7 +870,7 @@ final class GroupManagerV2 {
Log.i(TAG, String.format("Successfully added self to %s on server", groupId)); Log.i(TAG, String.format("Successfully added self to %s on server", groupId));
} }
decryptedChange = decryptChange(signedGroupChange); decryptedChange = decryptChange(Objects.requireNonNull(signedGroupChange));
} catch (GroupJoinAlreadyAMemberException e) { } catch (GroupJoinAlreadyAMemberException e) {
Log.i(TAG, "Server reports that we are already a member of " + groupId); Log.i(TAG, "Server reports that we are already a member of " + groupId);
alreadyAMember = true; alreadyAMember = true;
@ -869,28 +882,37 @@ final class GroupManagerV2 {
if (group.isPresent()) { if (group.isPresent()) {
Log.i(TAG, "Group already present locally"); Log.i(TAG, "Group already present locally");
if (decryptedChange != null) { groupAlreadyExists = true;
try {
GroupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
.updateLocalGroupToRevision(decryptedChange.revision, System.currentTimeMillis(), decryptedChange);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Unable to apply join change to existing group", e);
}
}
} else { } else {
GroupId.V2 groupId = groupDatabase.create(groupMasterKey, decryptedGroup); GroupId.V2 groupId = groupDatabase.create(groupMasterKey, decryptedGroup, null);
if (groupId != null) { if (groupId != null) {
Log.i(TAG, "Created local group with placeholder"); Log.i(TAG, "Created local group with placeholder");
} else { } else {
Log.i(TAG, "Create placeholder failed, group suddenly present locally, attempting to apply change"); Log.i(TAG, "Create placeholder failed, group suddenly present locally");
if (decryptedChange != null) { groupAlreadyExists = true;
try { }
GroupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey) }
.updateLocalGroupToRevision(decryptedChange.revision, System.currentTimeMillis(), decryptedChange);
} catch (GroupNotAMemberException e) { if (groupAlreadyExists) {
Log.w(TAG, "Unable to apply join change to existing group", e); Log.i(TAG, "Attempting to update local group with change/server");
} try {
} GroupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
.updateLocalGroupToRevision(decryptedChange != null ? decryptedChange.revision : GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), decryptedChange);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Despite adding self to group, change/server says we are not a member, scheduling refresh of group info " + groupId, e);
AppDependencies.getJobManager()
.add(new RequestGroupV2InfoJob(groupId));
throw new GroupChangeFailedException(e);
} catch (IOException e) {
Log.w(TAG, "Group data fetch failed, scheduling refresh of group info " + groupId, e);
AppDependencies.getJobManager()
.add(new RequestGroupV2InfoJob(groupId));
throw e;
} }
} }
@ -1006,7 +1028,7 @@ final class GroupManagerV2 {
return group.build(); return group.build();
} }
private @NonNull GroupChange joinGroupOnServer(boolean requestToJoin, int currentRevision) private @NonNull GroupChangeResponse joinGroupOnServer(boolean requestToJoin, int currentRevision)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException
{ {
if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(Collections.singleton(Recipient.self().getId()))) { if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(Collections.singleton(Recipient.self().getId()))) {
@ -1029,7 +1051,7 @@ final class GroupManagerV2 {
return commitJoinChangeWithConflictResolution(currentRevision, change); return commitJoinChangeWithConflictResolution(currentRevision, change);
} }
private @NonNull GroupChange commitJoinChangeWithConflictResolution(int currentRevision, @NonNull GroupChange.Actions.Builder change) private @NonNull GroupChangeResponse commitJoinChangeWithConflictResolution(int currentRevision, @NonNull GroupChange.Actions.Builder change)
throws GroupChangeFailedException, IOException, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException throws GroupChangeFailedException, IOException, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException
{ {
for (int attempt = 0; attempt < 5; attempt++) { for (int attempt = 0; attempt < 5; attempt++) {
@ -1038,10 +1060,10 @@ final class GroupManagerV2 {
.build(); .build();
Log.i(TAG, "Trying to join group at V" + changeActions.revision); Log.i(TAG, "Trying to join group at V" + changeActions.revision);
GroupChange signedGroupChange = commitJoinToServer(changeActions); GroupChangeResponse changeResponse = commitJoinToServer(changeActions);
Log.i(TAG, "Successfully joined group at V" + changeActions.revision); Log.i(TAG, "Successfully joined group at V" + changeActions.revision);
return signedGroupChange; return changeResponse;
} catch (GroupPatchNotAcceptedException e) { } catch (GroupPatchNotAcceptedException e) {
Log.w(TAG, "Patch not accepted", e); Log.w(TAG, "Patch not accepted", e);
@ -1051,7 +1073,7 @@ final class GroupManagerV2 {
} else { } else {
throw new GroupChangeFailedException(e); throw new GroupChangeFailedException(e);
} }
} catch (VerificationFailedException | InvalidGroupStateException ex) { } catch (VerificationFailedException | InvalidGroupStateException | InvalidInputException ex) {
throw new GroupChangeFailedException(ex); throw new GroupChangeFailedException(ex);
} }
} catch (ConflictException e) { } catch (ConflictException e) {
@ -1064,7 +1086,7 @@ final class GroupManagerV2 {
throw new GroupChangeFailedException("Unable to join group after conflicts"); throw new GroupChangeFailedException("Unable to join group after conflicts");
} }
private @NonNull GroupChange commitJoinToServer(@NonNull GroupChange.Actions change) private @NonNull GroupChangeResponse commitJoinToServer(@NonNull GroupChange.Actions change)
throws GroupChangeFailedException, IOException, GroupLinkNotActiveException throws GroupChangeFailedException, IOException, GroupLinkNotActiveException
{ {
try { try {
@ -1109,7 +1131,7 @@ final class GroupManagerV2 {
} }
private boolean testGroupMembership() private boolean testGroupMembership()
throws IOException, VerificationFailedException, InvalidGroupStateException throws IOException, VerificationFailedException, InvalidGroupStateException, InvalidInputException
{ {
try { try {
groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
@ -1140,7 +1162,7 @@ final class GroupManagerV2 {
DecryptedGroupChange decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get(); DecryptedGroupChange decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
DecryptedGroup newGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(decryptedGroup, decryptedChange); DecryptedGroup newGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(decryptedGroup, decryptedChange);
groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.revision)); groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.revision), null);
sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(decryptedGroup, decryptedChange, newGroup), signedGroupChange, false); sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(decryptedGroup, decryptedChange, newGroup), signedGroupChange, false);
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
@ -1165,7 +1187,7 @@ final class GroupManagerV2 {
.build(); .build();
Log.i(TAG, "Trying to cancel request group at V" + changeActions.revision); Log.i(TAG, "Trying to cancel request group at V" + changeActions.revision);
GroupChange signedGroupChange = commitJoinToServer(changeActions); GroupChange signedGroupChange = commitJoinToServer(changeActions).groupChange;
Log.i(TAG, "Successfully cancelled group join at V" + changeActions.revision); Log.i(TAG, "Successfully cancelled group join at V" + changeActions.revision);
return signedGroupChange; return signedGroupChange;

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.groups.v2.processing package org.thoughtcrime.securesms.groups.v2.processing
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
import org.signal.storageservice.protos.groups.local.DecryptedGroup import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog
@ -9,10 +10,15 @@ import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog
*/ */
class GroupStateDiff( class GroupStateDiff(
val previousGroupState: DecryptedGroup?, val previousGroupState: DecryptedGroup?,
val serverHistory: List<DecryptedGroupChangeLog> val serverHistory: List<DecryptedGroupChangeLog>,
val groupSendEndorsementsResponse: GroupSendEndorsementsResponse?
) { ) {
constructor(previousGroupState: DecryptedGroup?, changedGroupState: DecryptedGroup?, change: DecryptedGroupChange?) : this(previousGroupState, listOf(DecryptedGroupChangeLog(changedGroupState, change))) constructor(
previousGroupState: DecryptedGroup?,
changedGroupState: DecryptedGroup?,
change: DecryptedGroupChange?
) : this(previousGroupState, listOf(DecryptedGroupChangeLog(changedGroupState, change)), null)
val earliestRevisionNumber: Int val earliestRevisionNumber: Int
get() { get() {

View file

@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.orNull import org.signal.core.util.orNull
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.VerificationFailedException import org.signal.libsignal.zkgroup.VerificationFailedException
import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.groups.GroupSecretParams import org.signal.libsignal.zkgroup.groups.GroupSecretParams
@ -43,6 +44,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException
import org.whispersystems.signalservice.api.groupsv2.ReceivedGroupSendEndorsements
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceIds import org.whispersystems.signalservice.api.push.ServiceIds
@ -95,8 +97,9 @@ class GroupsV2StateProcessor private constructor(
} }
} }
private val groupsApi = AppDependencies.signalServiceAccountManager.groupsV2Api private val groupsApi = AppDependencies.signalServiceAccountManager.getGroupsV2Api()
private val groupsV2Authorization = AppDependencies.groupsV2Authorization private val groupsV2Authorization = AppDependencies.groupsV2Authorization
private val groupOperations = AppDependencies.groupsV2Operations.forGroup(groupSecretParams)
private val groupId = GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier()) private val groupId = GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier())
private val profileAndMessageHelper = ProfileAndMessageHelper.create(serviceIds.aci, groupMasterKey, groupId) private val profileAndMessageHelper = ProfileAndMessageHelper.create(serviceIds.aci, groupMasterKey, groupId)
@ -124,6 +127,36 @@ class GroupsV2StateProcessor private constructor(
} }
} }
/**
* Fetch and save the latest group send endorsements from the server. This endorsements returned may
* not match our local view of the membership if the membership has changed on the server and we haven't updated the
* group state yet. This is only an issue when trying to send to a group member that has been removed and should be handled
* gracefully as a fallback in the sending flow.
*/
@WorkerThread
@Throws(IOException::class, GroupNotAMemberException::class)
fun updateGroupSendEndorsements() {
val result = groupsApi.getGroupAsResult(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams))
val groupResponse = when (result) {
is NetworkResult.Success -> result.result
else -> when (val cause = result.getCause()!!) {
is NotInGroupException, is GroupNotFoundException -> throw GroupNotAMemberException(cause)
is IOException -> throw cause
else -> throw IOException(cause)
}
}
val receivedGroupSendEndorsements = groupOperations.receiveGroupSendEndorsements(serviceIds.aci, groupResponse.group, groupResponse.groupSendEndorsementsResponse)
if (receivedGroupSendEndorsements != null) {
Log.i(TAG, "$logPrefix Updating group send endorsements")
SignalDatabase.groups.updateGroupSendEndorsements(groupId, receivedGroupSendEndorsements)
} else {
Log.w(TAG, "$logPrefix No group send endorsements on response")
}
}
/** /**
* Using network where required, will attempt to bring the local copy of the group up to the revision specified. * Using network where required, will attempt to bring the local copy of the group up to the revision specified.
* *
@ -233,7 +266,8 @@ class GroupsV2StateProcessor private constructor(
return saveGroupUpdate( return saveGroupUpdate(
timestamp = timestamp, timestamp = timestamp,
serverGuid = serverGuid, serverGuid = serverGuid,
groupStateDiff = groupStateDiff groupStateDiff = groupStateDiff,
groupSendEndorsements = null
) )
} }
@ -259,6 +293,8 @@ class GroupsV2StateProcessor private constructor(
else -> return InternalUpdateResult.from(result.getCause()!!) else -> return InternalUpdateResult.from(result.getCause()!!)
} }
val sendEndorsementExpiration = groupRecord.map { it.groupSendEndorsementExpiration }.orElse(0L)
var includeFirstState = currentLocalState == null || var includeFirstState = currentLocalState == null ||
currentLocalState.revision < 0 || currentLocalState.revision < 0 ||
currentLocalState.revision == joinedAtRevision || currentLocalState.revision == joinedAtRevision ||
@ -273,20 +309,30 @@ class GroupsV2StateProcessor private constructor(
var hasRemainingRemoteChanges = false var hasRemainingRemoteChanges = false
while (hasMore) { while (hasMore) {
Log.i(TAG, "$logPrefix Requesting change logs from server, currentRevision=${currentLocalState?.revision ?: "null"} logsNeededFrom=$logsNeededFrom includeFirstState=$includeFirstState") Log.i(TAG, "$logPrefix Requesting change logs from server, currentRevision=${currentLocalState?.revision ?: "null"} logsNeededFrom=$logsNeededFrom includeFirstState=$includeFirstState sendEndorsementExpiration=${sendEndorsementExpiration > 0}")
val (remoteGroupStateDiff, pagingData) = getGroupChangeLogs(currentLocalState, logsNeededFrom, includeFirstState) val (remoteGroupStateDiff, pagingData) = getGroupChangeLogs(currentLocalState, logsNeededFrom, includeFirstState, sendEndorsementExpiration)
val applyGroupStateDiffResult: AdvanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(remoteGroupStateDiff, targetRevision) val applyGroupStateDiffResult: AdvanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(remoteGroupStateDiff, targetRevision)
val updatedGroupState: DecryptedGroup? = applyGroupStateDiffResult.updatedGroupState val updatedGroupState: DecryptedGroup? = applyGroupStateDiffResult.updatedGroupState
if (updatedGroupState == null || updatedGroupState == remoteGroupStateDiff.previousGroupState) { if (updatedGroupState == null || updatedGroupState == remoteGroupStateDiff.previousGroupState) {
Log.i(TAG, "$logPrefix Local state is at or later than server revision: ${currentLocalState?.revision ?: "null"}") Log.i(TAG, "$logPrefix Local state is at or later than server revision: ${currentLocalState?.revision ?: "null"}")
if (currentLocalState != null) {
val endorsements = groupOperations.receiveGroupSendEndorsements(serviceIds.aci, currentLocalState, remoteGroupStateDiff.groupSendEndorsementsResponse)
if (endorsements != null) {
Log.i(TAG, "$logPrefix Received updated send endorsements, saving")
SignalDatabase.groups.updateGroupSendEndorsements(groupId, endorsements)
}
}
return InternalUpdateResult.NoUpdateNeeded return InternalUpdateResult.NoUpdateNeeded
} }
Log.i(TAG, "$logPrefix Saving updated group state at revision: ${updatedGroupState.revision}") Log.i(TAG, "$logPrefix Saving updated group state at revision: ${updatedGroupState.revision}")
saveGroupState(remoteGroupStateDiff, updatedGroupState) saveGroupState(remoteGroupStateDiff, updatedGroupState, groupOperations.receiveGroupSendEndorsements(serviceIds.aci, updatedGroupState, remoteGroupStateDiff.groupSendEndorsementsResponse))
if (addMessagesForAllUpdates) { if (addMessagesForAllUpdates) {
Log.d(TAG, "$logPrefix Inserting group changes into chat history") Log.d(TAG, "$logPrefix Inserting group changes into chat history")
@ -320,7 +366,7 @@ class GroupsV2StateProcessor private constructor(
} }
if (!addMessagesForAllUpdates) { if (!addMessagesForAllUpdates) {
Log.i(TAG, "Inserting single update message for restore placeholder") Log.i(TAG, "$logPrefix Inserting single update message for restore placeholder")
profileAndMessageHelper.insertUpdateMessages(runningTimestamp, null, setOf(AppliedGroupChangeLog(currentLocalState!!, null)), serverGuid) profileAndMessageHelper.insertUpdateMessages(runningTimestamp, null, setOf(AppliedGroupChangeLog(currentLocalState!!, null)), serverGuid)
} }
@ -341,19 +387,20 @@ class GroupsV2StateProcessor private constructor(
private fun updateToLatestViaServer(timestamp: Long, currentLocalState: DecryptedGroup?, reconstructChange: Boolean): InternalUpdateResult { private fun updateToLatestViaServer(timestamp: Long, currentLocalState: DecryptedGroup?, reconstructChange: Boolean): InternalUpdateResult {
val result = groupsApi.getGroupAsResult(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams)) val result = groupsApi.getGroupAsResult(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams))
val serverState = if (result is NetworkResult.Success) { val groupResponse = if (result is NetworkResult.Success) {
result.result result.result
} else { } else {
return InternalUpdateResult.from(result.getCause()!!) return InternalUpdateResult.from(result.getCause()!!)
} }
val completeGroupChange = if (reconstructChange) GroupChangeReconstruct.reconstructGroupChange(currentLocalState, serverState) else null val completeGroupChange = if (reconstructChange) GroupChangeReconstruct.reconstructGroupChange(currentLocalState, groupResponse.group) else null
val remoteGroupStateDiff = GroupStateDiff(currentLocalState, serverState, completeGroupChange) val remoteGroupStateDiff = GroupStateDiff(currentLocalState, groupResponse.group, completeGroupChange)
return saveGroupUpdate( return saveGroupUpdate(
timestamp = timestamp, timestamp = timestamp,
serverGuid = null, serverGuid = null,
groupStateDiff = remoteGroupStateDiff groupStateDiff = remoteGroupStateDiff,
groupSendEndorsements = groupOperations.receiveGroupSendEndorsements(serviceIds.aci, groupResponse.group, groupResponse.groupSendEndorsementsResponse)
) )
} }
@ -403,22 +450,30 @@ class GroupsV2StateProcessor private constructor(
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun getGroupChangeLogs(localState: DecryptedGroup?, logsNeededFromRevision: Int, includeFirstState: Boolean): Pair<GroupStateDiff, GroupHistoryPage.PagingData> { private fun getGroupChangeLogs(
localState: DecryptedGroup?,
logsNeededFromRevision: Int,
includeFirstState: Boolean,
sendEndorsementsExpirationMs: Long
): Pair<GroupStateDiff, GroupHistoryPage.PagingData> {
try { try {
val groupHistoryPage = groupsApi.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), includeFirstState) val groupHistoryPage = groupsApi.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), includeFirstState, sendEndorsementsExpirationMs)
return GroupStateDiff(localState, groupHistoryPage.changeLogs) to groupHistoryPage.pagingData return GroupStateDiff(localState, groupHistoryPage.changeLogs, groupHistoryPage.groupSendEndorsementsResponse) to groupHistoryPage.pagingData
} catch (e: InvalidGroupStateException) { } catch (e: InvalidGroupStateException) {
throw IOException(e) throw IOException(e)
} catch (e: VerificationFailedException) { } catch (e: VerificationFailedException) {
throw IOException(e) throw IOException(e)
} catch (e: InvalidInputException) {
throw IOException(e)
} }
} }
private fun saveGroupUpdate( private fun saveGroupUpdate(
timestamp: Long, timestamp: Long,
serverGuid: String?, serverGuid: String?,
groupStateDiff: GroupStateDiff groupStateDiff: GroupStateDiff,
groupSendEndorsements: ReceivedGroupSendEndorsements?
): InternalUpdateResult { ): InternalUpdateResult {
val currentLocalState: DecryptedGroup? = groupStateDiff.previousGroupState val currentLocalState: DecryptedGroup? = groupStateDiff.previousGroupState
val applyGroupStateDiffResult = GroupStatePatcher.applyGroupStateDiff(groupStateDiff, GroupStatePatcher.LATEST) val applyGroupStateDiffResult = GroupStatePatcher.applyGroupStateDiff(groupStateDiff, GroupStatePatcher.LATEST)
@ -426,12 +481,18 @@ class GroupsV2StateProcessor private constructor(
if (updatedGroupState == null || updatedGroupState == groupStateDiff.previousGroupState) { if (updatedGroupState == null || updatedGroupState == groupStateDiff.previousGroupState) {
Log.i(TAG, "$logPrefix Local state and server state are equal") Log.i(TAG, "$logPrefix Local state and server state are equal")
if (groupSendEndorsements != null) {
Log.i(TAG, "$logPrefix Saving new send endorsements")
SignalDatabase.groups.updateGroupSendEndorsements(groupId, groupSendEndorsements)
}
return InternalUpdateResult.NoUpdateNeeded return InternalUpdateResult.NoUpdateNeeded
} else { } else {
Log.i(TAG, "$logPrefix Local state (revision: ${currentLocalState?.revision}) does not match, updating to ${updatedGroupState.revision}") Log.i(TAG, "$logPrefix Local state (revision: ${currentLocalState?.revision}) does not match, updating to ${updatedGroupState.revision}")
} }
saveGroupState(groupStateDiff, updatedGroupState) saveGroupState(groupStateDiff, updatedGroupState, groupSendEndorsements)
if (currentLocalState == null || currentLocalState.revision == RESTORE_PLACEHOLDER_REVISION) { if (currentLocalState == null || currentLocalState.revision == RESTORE_PLACEHOLDER_REVISION) {
Log.i(TAG, "$logPrefix Inserting single update message for no local state or restore placeholder") Log.i(TAG, "$logPrefix Inserting single update message for no local state or restore placeholder")
@ -453,20 +514,24 @@ class GroupsV2StateProcessor private constructor(
return InternalUpdateResult.Updated(updatedGroupState) return InternalUpdateResult.Updated(updatedGroupState)
} }
private fun saveGroupState(groupStateDiff: GroupStateDiff, updatedGroupState: DecryptedGroup) { private fun saveGroupState(groupStateDiff: GroupStateDiff, updatedGroupState: DecryptedGroup, groupSendEndorsements: ReceivedGroupSendEndorsements?) {
val previousGroupState = groupStateDiff.previousGroupState val previousGroupState = groupStateDiff.previousGroupState
if (groupSendEndorsements != null) {
Log.i(TAG, "$logPrefix Updating send endorsements")
}
val needsAvatarFetch = if (previousGroupState == null) { val needsAvatarFetch = if (previousGroupState == null) {
val groupId = SignalDatabase.groups.create(groupMasterKey, updatedGroupState) val groupId = SignalDatabase.groups.create(groupMasterKey, updatedGroupState, groupSendEndorsements)
if (groupId == null) { if (groupId == null) {
Log.w(TAG, "$logPrefix Group create failed, trying to update") Log.w(TAG, "$logPrefix Group create failed, trying to update")
SignalDatabase.groups.update(groupMasterKey, updatedGroupState) SignalDatabase.groups.update(groupMasterKey, updatedGroupState, groupSendEndorsements)
} }
updatedGroupState.avatar.isNotEmpty() updatedGroupState.avatar.isNotEmpty()
} else { } else {
SignalDatabase.groups.update(groupMasterKey, updatedGroupState) SignalDatabase.groups.update(groupMasterKey, updatedGroupState, groupSendEndorsements)
updatedGroupState.avatar != previousGroupState.avatar updatedGroupState.avatar != previousGroupState.avatar
} }

View file

@ -4,7 +4,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime; import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime;
@ -18,14 +18,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.RemoteConfig;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -136,12 +134,11 @@ public class AutomaticSessionResetJob extends BaseJob {
return; return;
} }
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
try { try {
messageSender.sendNullMessage(address, unidentifiedAccess); messageSender.sendNullMessage(address, SealedSenderAccessUtil.getSealedSenderAccessFor(recipient));
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
Log.w(TAG, "Unable to send null message."); Log.w(TAG, "Unable to send null message.");
} }

View file

@ -18,7 +18,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException
import org.whispersystems.signalservice.internal.push.SyncMessage.CallLinkUpdate import org.whispersystems.signalservice.internal.push.SyncMessage.CallLinkUpdate
import java.util.Optional
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@ -83,7 +82,7 @@ class CallLinkUpdateSendJob private constructor(
) )
AppDependencies.signalServiceMessageSender AppDependencies.signalServiceMessageSender
.sendSyncMessage(SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate), Optional.empty()) .sendSyncMessage(SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate))
if (callLinkUpdateType == CallLinkUpdate.Type.DELETE) { if (callLinkUpdateType == CallLinkUpdate.Type.DELETE) {
SignalDatabase.callLinks.deleteCallLink(callLinkRoomId) SignalDatabase.callLinks.deleteCallLink(callLinkRoomId)

View file

@ -17,7 +17,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException
import org.whispersystems.signalservice.internal.push.SyncMessage import org.whispersystems.signalservice.internal.push.SyncMessage
import java.util.Optional
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@ -98,10 +97,7 @@ class CallLogEventSendJob private constructor(
override fun onRun() { override fun onRun() {
AppDependencies.signalServiceMessageSender AppDependencies.signalServiceMessageSender
.sendSyncMessage( .sendSyncMessage(SignalServiceSyncMessage.forCallLogEvent(callLogEvent))
SignalServiceSyncMessage.forCallLogEvent(callLogEvent),
Optional.empty()
)
} }
override fun onShouldRetry(e: Exception): Boolean { override fun onShouldRetry(e: Exception): Boolean {

View file

@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer
import org.thoughtcrime.securesms.service.webrtc.CallEventSyncMessageUtil import org.thoughtcrime.securesms.service.webrtc.CallEventSyncMessageUtil
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
import org.whispersystems.signalservice.internal.push.SyncMessage import org.whispersystems.signalservice.internal.push.SyncMessage
import java.util.Optional
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@ -142,7 +141,7 @@ class CallSyncEventJob private constructor(
val syncMessage = createSyncMessage(syncTimestamp, callSyncEvent, call.type) val syncMessage = createSyncMessage(syncTimestamp, callSyncEvent, call.type)
return try { return try {
AppDependencies.signalServiceMessageSender.sendSyncMessage(SignalServiceSyncMessage.forCallEvent(syncMessage), Optional.empty()) AppDependencies.signalServiceMessageSender.sendSyncMessage(SignalServiceSyncMessage.forCallEvent(syncMessage))
null null
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Unable to send call event sync message for ${callSyncEvent.callId}", e) Log.w(TAG, "Unable to send call event sync message for ${callSyncEvent.callId}", e)

View file

@ -10,11 +10,11 @@ import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PaymentTable; import org.thoughtcrime.securesms.database.PaymentTable;
import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode; import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -37,7 +37,6 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalServiceMessageSender.IndividualSendEvents; import org.whispersystems.signalservice.api.SignalServiceMessageSender.IndividualSendEvents;
import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@ -160,7 +159,7 @@ public class IndividualSendJob extends PushSendJob {
Recipient recipient = message.getThreadRecipient().fresh(); Recipient recipient = message.getThreadRecipient().fresh();
byte[] profileKey = recipient.getProfileKey(); byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode(); SealedSenderAccessMode accessMode = recipient.getSealedSenderAccessMode();
boolean unidentified = deliver(message, originalEditedMessage); boolean unidentified = deliver(message, originalEditedMessage);
@ -177,15 +176,15 @@ public class IndividualSendJob extends PushSendJob {
SignalDatabase.messages().incrementViewedReceiptCount(message.getSentTimeMillis(), recipient.getId(), System.currentTimeMillis()); SignalDatabase.messages().incrementViewedReceiptCount(message.getSentTimeMillis(), recipient.getId(), System.currentTimeMillis());
} }
if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) { if (unidentified && accessMode == SealedSenderAccessMode.UNKNOWN && profileKey == null) {
log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-unrestricted following a UD send."); log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-unrestricted following a UD send.");
SignalDatabase.recipients().setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.UNRESTRICTED); SignalDatabase.recipients().setSealedSenderAccessMode(recipient.getId(), SealedSenderAccessMode.UNRESTRICTED);
} else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) { } else if (unidentified && accessMode == SealedSenderAccessMode.UNKNOWN) {
log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-enabled following a UD send."); log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-enabled following a UD send.");
SignalDatabase.recipients().setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.ENABLED); SignalDatabase.recipients().setSealedSenderAccessMode(recipient.getId(), SealedSenderAccessMode.ENABLED);
} else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) { } else if (!unidentified && accessMode != SealedSenderAccessMode.DISABLED) {
log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-disabled following a non-UD send."); log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-disabled following a non-UD send.");
SignalDatabase.recipients().setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.DISABLED); SignalDatabase.recipients().setSealedSenderAccessMode(recipient.getId(), SealedSenderAccessMode.DISABLED);
} }
if (originalEditedMessage != null && originalEditedMessage.getExpireStarted() > 0) { if (originalEditedMessage != null && originalEditedMessage.getExpireStarted() > 0) {
@ -306,25 +305,35 @@ public class IndividualSendJob extends PushSendJob {
if (originalEditedMessage != null) { if (originalEditedMessage != null) {
if (Util.equals(SignalStore.account().getAci(), address.getServiceId())) { if (Util.equals(SignalStore.account().getAci(), address.getServiceId())) {
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SendMessageResult result = messageSender.sendSelfSyncEditMessage(new SignalServiceEditMessage(originalEditedMessage.getDateSent(), mediaMessage)); SendMessageResult result = messageSender.sendSelfSyncEditMessage(new SignalServiceEditMessage(originalEditedMessage.getDateSent(), mediaMessage));
SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false); SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false);
return syncAccess.isPresent(); return SealedSenderAccessUtil.getSealedSenderCertificate() != null;
} else { } else {
SendMessageResult result = messageSender.sendEditMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage, IndividualSendEvents.EMPTY, message.isUrgent(), originalEditedMessage.getDateSent()); SendMessageResult result = messageSender.sendEditMessage(address,
SealedSenderAccessUtil.getSealedSenderAccessFor(messageRecipient),
ContentHint.RESENDABLE,
mediaMessage,
IndividualSendEvents.EMPTY,
message.isUrgent(),
originalEditedMessage.getDateSent());
SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false); SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false);
return result.getSuccess().isUnidentified(); return result.getSuccess().isUnidentified();
} }
} else if (Util.equals(SignalStore.account().getAci(), address.getServiceId())) { } else if (Util.equals(SignalStore.account().getAci(), address.getServiceId())) {
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SendMessageResult result = messageSender.sendSyncMessage(mediaMessage); SendMessageResult result = messageSender.sendSyncMessage(mediaMessage);
SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false); SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false);
return syncAccess.isPresent(); return SealedSenderAccessUtil.getSealedSenderCertificate() != null;
} else { } else {
SignalLocalMetrics.IndividualMessageSend.onDeliveryStarted(messageId, message.getSentTimeMillis()); SignalLocalMetrics.IndividualMessageSend.onDeliveryStarted(messageId, message.getSentTimeMillis());
SendMessageResult result = messageSender.sendDataMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage, new MetricEventListener(messageId), message.isUrgent(), messageRecipient.getNeedsPniSignature()); SendMessageResult result = messageSender.sendDataMessage(address,
SealedSenderAccessUtil.getSealedSenderAccessFor(messageRecipient),
ContentHint.RESENDABLE,
mediaMessage,
new MetricEventListener(messageId),
message.isUrgent(),
messageRecipient.getNeedsPniSignature());
SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), message.isUrgent()); SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), message.isUrgent());

View file

@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.RecipientTable.RecipientReader; import org.thoughtcrime.securesms.database.RecipientTable.RecipientReader;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
@ -88,8 +87,8 @@ public class MultiDeviceBlockedUpdateJob extends BaseJob {
} }
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
messageSender.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(blockedIndividuals, blockedGroups)), messageSender.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(blockedIndividuals, blockedGroups))
UnidentifiedAccessUtil.getAccessForSync(context)); );
} }
} }

View file

@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.internal.push.SyncMessage.CallLinkUpdate import org.whispersystems.signalservice.internal.push.SyncMessage.CallLinkUpdate
import java.util.Optional
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
/** /**
@ -57,7 +56,7 @@ class MultiDeviceCallLinkSyncJob private constructor(
val syncMessage = SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate) val syncMessage = SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate)
try { try {
AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessage, Optional.empty()) AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessage)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Unable to send call link update message.", e) Log.w(TAG, "Unable to send call link update message.", e)
throw e throw e

View file

@ -5,10 +5,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
@ -106,8 +105,8 @@ public class MultiDeviceConfigurationUpdateJob extends BaseJob {
messageSender.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(readReceiptsEnabled), messageSender.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(readReceiptsEnabled),
Optional.of(unidentifiedDeliveryIndicatorsEnabled), Optional.of(unidentifiedDeliveryIndicatorsEnabled),
Optional.of(typingIndicatorsEnabled), Optional.of(typingIndicatorsEnabled),
Optional.of(linkPreviewsEnabled))), Optional.of(linkPreviewsEnabled)))
UnidentifiedAccessUtil.getAccessForSync(context)); );
} }
@Override @Override

View file

@ -15,13 +15,12 @@ import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper; import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
@ -298,8 +297,8 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
.withLength(length) .withLength(length)
.withResumableUploadSpec(messageSender.getResumableUploadSpec()); .withResumableUploadSpec(messageSender.getResumableUploadSpec());
messageSender.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)), messageSender.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete))
UnidentifiedAccessUtil.getAccessForSync(context)); );
} catch (IOException ioe) { } catch (IOException ioe) {
throw new NetworkException(ioe); throw new NetworkException(ioe);
} }

View file

@ -5,7 +5,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
@ -73,8 +72,8 @@ public class MultiDeviceKeysUpdateJob extends BaseJob {
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey(); StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey();
messageSender.sendSyncMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.ofNullable(storageServiceKey), Optional.of(SignalStore.svr().getOrCreateMasterKey()))), messageSender.sendSyncMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.ofNullable(storageServiceKey), Optional.of(SignalStore.svr().getOrCreateMasterKey())))
UnidentifiedAccessUtil.getAccessForSync(context)); );
} }
@Override @Override

View file

@ -5,10 +5,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -122,8 +121,8 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob {
} }
if (response != null) { if (response != null) {
messageSender.sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response), messageSender.sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response)
UnidentifiedAccessUtil.getAccessForSync(context)); );
} else { } else {
Log.w(TAG, recipient.getId() + " not registered!"); Log.w(TAG, recipient.getId() + " not registered!");
} }

View file

@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.PaymentTable; import org.thoughtcrime.securesms.database.PaymentTable;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
@ -117,8 +116,8 @@ public final class MultiDeviceOutgoingPaymentSyncJob extends BaseJob {
AppDependencies.getSignalServiceMessageSender() AppDependencies.getSignalServiceMessageSender()
.sendSyncMessage(SignalServiceSyncMessage.forOutgoingPayment(outgoingPaymentMessage), .sendSyncMessage(SignalServiceSyncMessage.forOutgoingPayment(outgoingPaymentMessage)
UnidentifiedAccessUtil.getAccessForSync(context)); );
} }
@Override @Override

View file

@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
@ -58,8 +57,8 @@ public class MultiDeviceProfileContentUpdateJob extends BaseJob {
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE), messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)
UnidentifiedAccessUtil.getAccessForSync(context)); );
} }
@Override @Override

View file

@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
@ -98,7 +97,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, false)); SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, false));
messageSender.sendSyncMessage(syncMessage, UnidentifiedAccessUtil.getAccessForSync(context)); messageSender.sendSyncMessage(syncMessage);
} }
@Override @Override

View file

@ -8,12 +8,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.signal.core.util.ListUtil; import org.signal.core.util.ListUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -121,7 +120,7 @@ public class MultiDeviceReadUpdateJob extends BaseJob {
} }
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
messageSender.sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages), UnidentifiedAccessUtil.getAccessForSync(context)); messageSender.sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages));
} }
@Override @Override

View file

@ -3,13 +3,12 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.signal.core.util.Hex; import org.signal.core.util.Hex;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
@ -93,8 +92,8 @@ public class MultiDeviceStickerPackOperationJob extends BaseJob {
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
StickerPackOperationMessage stickerPackOperation = new StickerPackOperationMessage(packIdBytes, packKeyBytes, remoteType); StickerPackOperationMessage stickerPackOperation = new StickerPackOperationMessage(packIdBytes, packKeyBytes, remoteType);
messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(Collections.singletonList(stickerPackOperation)), messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(Collections.singletonList(stickerPackOperation))
UnidentifiedAccessUtil.getAccessForSync(context)); );
} }
@Override @Override

View file

@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.Hex;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader; import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader;
import org.thoughtcrime.securesms.database.model.StickerPackRecord; import org.thoughtcrime.securesms.database.model.StickerPackRecord;
@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.signal.core.util.Hex;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
@ -80,8 +79,8 @@ public class MultiDeviceStickerPackSyncJob extends BaseJob {
} }
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(operations), messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(operations)
UnidentifiedAccessUtil.getAccessForSync(context)); );
} }
@Override @Override

View file

@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
@ -58,8 +57,7 @@ public class MultiDeviceStorageSyncRequestJob extends BaseJob {
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST), messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST));
UnidentifiedAccessUtil.getAccessForSync(context));
} }
@Override @Override

View file

@ -11,7 +11,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRe
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.lang.Exception
import java.util.Optional import java.util.Optional
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -57,7 +56,7 @@ class MultiDeviceStorySendSyncJob private constructor(parameters: Parameters, pr
val updateManifest = SignalDatabase.storySends.getLocalManifest(sentTimestamp) val updateManifest = SignalDatabase.storySends.getLocalManifest(sentTimestamp)
val recipientsSet: Set<SignalServiceStoryMessageRecipient> = updateManifest.toRecipientsSet() val recipientsSet: Set<SignalServiceStoryMessageRecipient> = updateManifest.toRecipientsSet()
val transcriptMessage: SignalServiceSyncMessage = SignalServiceSyncMessage.forSentTranscript(buildSentTranscript(recipientsSet)) val transcriptMessage: SignalServiceSyncMessage = SignalServiceSyncMessage.forSentTranscript(buildSentTranscript(recipientsSet))
val sendMessageResult = AppDependencies.signalServiceMessageSender.sendSyncMessage(transcriptMessage, Optional.empty()) val sendMessageResult = AppDependencies.signalServiceMessageSender.sendSyncMessage(transcriptMessage)
Log.i(TAG, "Sent transcript message with ${recipientsSet.size} recipients") Log.i(TAG, "Sent transcript message with ${recipientsSet.size} recipients")

View file

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.jobs package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
@ -57,10 +56,7 @@ class MultiDeviceSubscriptionSyncRequestJob private constructor(parameters: Para
val messageSender = AppDependencies.signalServiceMessageSender val messageSender = AppDependencies.signalServiceMessageSender
messageSender.sendSyncMessage( messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.SUBSCRIPTION_STATUS))
SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.SUBSCRIPTION_STATUS),
UnidentifiedAccessUtil.getAccessForSync(context)
)
} }
override fun onShouldRetry(e: Exception): Boolean { override fun onShouldRetry(e: Exception): Boolean {

View file

@ -4,20 +4,19 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.Base64;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus; import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.signal.core.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
@ -116,8 +115,8 @@ public class MultiDeviceVerifiedUpdateJob extends BaseJob {
SignalServiceAddress verifiedAddress = RecipientUtil.toSignalServiceAddress(context, recipient); SignalServiceAddress verifiedAddress = RecipientUtil.toSignalServiceAddress(context, recipient);
VerifiedMessage verifiedMessage = new VerifiedMessage(verifiedAddress, new IdentityKey(identityKey, 0), verifiedState, timestamp); VerifiedMessage verifiedMessage = new VerifiedMessage(verifiedAddress, new IdentityKey(identityKey, 0), verifiedState, timestamp);
messageSender.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage), messageSender.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)
UnidentifiedAccessUtil.getAccessFor(context, recipient)); );
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new IOException(e); throw new IOException(e);
} }

View file

@ -6,11 +6,10 @@ import androidx.annotation.Nullable;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -92,7 +91,7 @@ public class MultiDeviceViewOnceOpenJob extends BaseJob {
ViewOnceOpenMessage openMessage = new ViewOnceOpenMessage(RecipientUtil.getOrFetchServiceId(context, recipient), messageId.timestamp); ViewOnceOpenMessage openMessage = new ViewOnceOpenMessage(RecipientUtil.getOrFetchServiceId(context, recipient), messageId.timestamp);
messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewOnceOpen(openMessage), UnidentifiedAccessUtil.getAccessForSync(context)); messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewOnceOpen(openMessage));
} }
@Override @Override

View file

@ -8,12 +8,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.signal.core.util.ListUtil; import org.signal.core.util.ListUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -121,7 +120,7 @@ public class MultiDeviceViewedUpdateJob extends BaseJob {
} }
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages), UnidentifiedAccessUtil.getAccessForSync(context)); messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages));
} }
@Override @Override

View file

@ -4,21 +4,19 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -72,12 +70,11 @@ public class NullMessageSendJob extends BaseJob {
Log.w(TAG, recipient.getId() + " not registered!"); Log.w(TAG, recipient.getId() + " not registered!");
} }
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
try { try {
messageSender.sendNullMessage(address, unidentifiedAccess); messageSender.sendNullMessage(address, SealedSenderAccessUtil.getSealedSenderAccessFor(recipient));
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
Log.w(TAG, "Unable to send null message."); Log.w(TAG, "Unable to send null message.");
} }

View file

@ -4,12 +4,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.PaymentTable; import org.thoughtcrime.securesms.database.PaymentTable;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
@ -18,14 +18,12 @@ import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalServiceMessageSender.IndividualSendEvents; import org.whispersystems.signalservice.api.SignalServiceMessageSender.IndividualSendEvents;
import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
public final class PaymentNotificationSendJob extends BaseJob { public final class PaymentNotificationSendJob extends BaseJob {
@ -81,9 +79,8 @@ public final class PaymentNotificationSendJob extends BaseJob {
return; return;
} }
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
PaymentTable.PaymentTransaction payment = paymentDatabase.getPayment(uuid); PaymentTable.PaymentTransaction payment = paymentDatabase.getPayment(uuid);
@ -101,7 +98,13 @@ public final class PaymentNotificationSendJob extends BaseJob {
.withPayment(new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote()), null)) .withPayment(new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote()), null))
.build(); .build();
SendMessageResult sendMessageResult = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.DEFAULT, dataMessage, IndividualSendEvents.EMPTY, false, recipient.getNeedsPniSignature()); SendMessageResult sendMessageResult = messageSender.sendDataMessage(address,
SealedSenderAccessUtil.getSealedSenderAccessFor(recipient),
ContentHint.DEFAULT,
dataMessage,
IndividualSendEvents.EMPTY,
false,
recipient.getNeedsPniSignature());
if (recipient.getNeedsPniSignature()) { if (recipient.getNeedsPniSignature()) {
SignalDatabase.pendingPniSignatureMessages().insertIfNecessary(recipientId, dataMessage.getTimestamp(), sendMessageResult); SignalDatabase.pendingPniSignatureMessages().insertIfNecessary(recipientId, dataMessage.getTimestamp(), sendMessageResult);

View file

@ -7,7 +7,7 @@ import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage; import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListRecord; import org.thoughtcrime.securesms.database.model.DistributionListRecord;
import org.thoughtcrime.securesms.database.model.GroupRecord; import org.thoughtcrime.securesms.database.model.GroupRecord;
@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -141,9 +141,9 @@ public class ResendMessageJob extends BaseJob {
return; return;
} }
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccessPair> access = UnidentifiedAccessUtil.getAccessFor(context, recipient); Content contentToSend = content;
Content contentToSend = content; SealedSenderAccess.CreateGroupSendToken createGroupSendToken = null;
if (distributionId != null) { if (distributionId != null) {
if (groupId != null) { if (groupId != null) {
@ -157,6 +157,8 @@ public class ResendMessageJob extends BaseJob {
Log.w(TAG, "The target user is no longer in the group! Skipping message send."); Log.w(TAG, "The target user is no longer in the group! Skipping message send.");
return; return;
} }
createGroupSendToken = () -> SignalDatabase.groups().getGroupSendFullToken(groupId, recipientId);
} else { } else {
Log.d(TAG, "GroupId is not present. Assuming this is a message for a distribution list."); Log.d(TAG, "GroupId is not present. Assuming this is a message for a distribution list.");
DistributionListRecord listRecord = SignalDatabase.distributionLists().getListByDistributionId(distributionId); DistributionListRecord listRecord = SignalDatabase.distributionLists().getListByDistributionId(distributionId);
@ -178,6 +180,8 @@ public class ResendMessageJob extends BaseJob {
SendMessageResult result; SendMessageResult result;
SealedSenderAccess access = SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, createGroupSendToken);
try { try {
result = messageSender.resendContent(address, access, sentTimestamp, contentToSend, contentHint, Optional.ofNullable(groupId).map(GroupId::getDecodedId), urgent); result = messageSender.resendContent(address, access, sentTimestamp, contentToSend, contentHint, Optional.ofNullable(groupId).map(GroupId::getDecodedId), urgent);
} catch (IllegalStateException e) { } catch (IllegalStateException e) {

View file

@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTable.Companion.maskCapabilitiesToLong import org.thoughtcrime.securesms.database.RecipientTable.Companion.maskCapabilitiesToLong
import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState
import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.RecipientRecord import org.thoughtcrime.securesms.database.model.RecipientRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
@ -222,7 +222,7 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
unrestrictedUnidentifiedAccess = remoteProfile.isUnrestrictedUnidentifiedAccess unrestrictedUnidentifiedAccess = remoteProfile.isUnrestrictedUnidentifiedAccess
) )
if (localRecipientRecord.unidentifiedAccessMode != accessMode) { if (localRecipientRecord.sealedSenderAccessMode != accessMode) {
return true return true
} }
@ -322,8 +322,8 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
val profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.profileKey) val profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.profileKey)
val newMode = deriveUnidentifiedAccessMode(profileKey, unidentifiedAccessVerifier, unrestrictedUnidentifiedAccess) val newMode = deriveUnidentifiedAccessMode(profileKey, unidentifiedAccessVerifier, unrestrictedUnidentifiedAccess)
if (recipient.unidentifiedAccessMode !== newMode) { if (recipient.sealedSenderAccessMode !== newMode) {
if (newMode === UnidentifiedAccessMode.UNRESTRICTED) { if (newMode === SealedSenderAccessMode.UNRESTRICTED) {
Log.i(TAG, "Marking recipient UD status as unrestricted.") Log.i(TAG, "Marking recipient UD status as unrestricted.")
} else if (profileKey == null || unidentifiedAccessVerifier == null) { } else if (profileKey == null || unidentifiedAccessVerifier == null) {
Log.i(TAG, "Marking recipient UD status as disabled.") Log.i(TAG, "Marking recipient UD status as disabled.")
@ -331,15 +331,15 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
Log.i(TAG, "Marking recipient UD status as " + newMode.name + " after verification.") Log.i(TAG, "Marking recipient UD status as " + newMode.name + " after verification.")
} }
SignalDatabase.recipients.setUnidentifiedAccessMode(recipient.id, newMode) SignalDatabase.recipients.setSealedSenderAccessMode(recipient.id, newMode)
} }
} }
private fun deriveUnidentifiedAccessMode(profileKey: ProfileKey?, unidentifiedAccessVerifier: String?, unrestrictedUnidentifiedAccess: Boolean): UnidentifiedAccessMode { private fun deriveUnidentifiedAccessMode(profileKey: ProfileKey?, unidentifiedAccessVerifier: String?, unrestrictedUnidentifiedAccess: Boolean): SealedSenderAccessMode {
return if (unrestrictedUnidentifiedAccess && unidentifiedAccessVerifier != null) { return if (unrestrictedUnidentifiedAccess && unidentifiedAccessVerifier != null) {
UnidentifiedAccessMode.UNRESTRICTED SealedSenderAccessMode.UNRESTRICTED
} else if (profileKey == null || unidentifiedAccessVerifier == null) { } else if (profileKey == null || unidentifiedAccessVerifier == null) {
UnidentifiedAccessMode.DISABLED SealedSenderAccessMode.DISABLED
} else { } else {
val profileCipher = ProfileCipher(profileKey) val profileCipher = ProfileCipher(profileKey)
val verifiedUnidentifiedAccess: Boolean = try { val verifiedUnidentifiedAccess: Boolean = try {
@ -350,9 +350,9 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
} }
if (verifiedUnidentifiedAccess) { if (verifiedUnidentifiedAccess) {
UnidentifiedAccessMode.ENABLED SealedSenderAccessMode.ENABLED
} else { } else {
UnidentifiedAccessMode.DISABLED SealedSenderAccessMode.DISABLED
} }
} }
} }

View file

@ -5,12 +5,13 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -123,7 +124,7 @@ public class SendDeliveryReceiptJob extends BaseJob {
timestamp); timestamp);
SendMessageResult result = messageSender.sendReceipt(remoteAddress, SendMessageResult result = messageSender.sendReceipt(remoteAddress,
UnidentifiedAccessUtil.getAccessFor(context, recipient), SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, this::getGroupSendFullToken),
receiptMessage, receiptMessage,
recipient.getNeedsPniSignature()); recipient.getNeedsPniSignature());
@ -132,6 +133,19 @@ public class SendDeliveryReceiptJob extends BaseJob {
} }
} }
private @Nullable GroupSendFullToken getGroupSendFullToken() {
if (messageId == null) {
return null;
}
long threadId = SignalDatabase.messages().getThreadIdForMessage(messageId.getId());
if (threadId == -1) {
return null;
}
return SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId);
}
@Override @Override
public boolean onShouldRetry(@NonNull Exception e) { public boolean onShouldRetry(@NonNull Exception e) {
if (e instanceof ServerRejectedException) return false; if (e instanceof ServerRejectedException) return false;

View file

@ -9,14 +9,14 @@ import androidx.annotation.VisibleForTesting;
import org.signal.core.util.ListUtil; import org.signal.core.util.ListUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -188,7 +188,8 @@ public class SendReadReceiptJob extends BaseJob {
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageSentTimestamps, timestamp); SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageSentTimestamps, timestamp);
SendMessageResult result = messageSender.sendReceipt(remoteAddress, SendMessageResult result = messageSender.sendReceipt(remoteAddress,
UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
receiptMessage, receiptMessage,
recipient.getNeedsPniSignature()); recipient.getNeedsPniSignature());

View file

@ -4,19 +4,18 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
@ -87,12 +86,11 @@ public final class SendRetryReceiptJob extends BaseJob {
return; return;
} }
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccessPair> access = UnidentifiedAccessUtil.getAccessFor(context, recipient); Optional<byte[]> group = groupId.map(GroupId::getDecodedId);
Optional<byte[]> group = groupId.map(GroupId::getDecodedId);
Log.i(TAG, "Sending retry receipt for " + errorMessage.getTimestamp() + " to " + recipientId + ", device: " + errorMessage.getDeviceId()); Log.i(TAG, "Sending retry receipt for " + errorMessage.getTimestamp() + " to " + recipientId + ", device: " + errorMessage.getDeviceId());
AppDependencies.getSignalServiceMessageSender().sendRetryReceipt(address, access, group, errorMessage); AppDependencies.getSignalServiceMessageSender().sendRetryReceipt(address, SealedSenderAccessUtil.getSealedSenderAccessFor(recipient), group, errorMessage);
} }
@Override @Override

View file

@ -8,15 +8,15 @@ import androidx.annotation.Nullable;
import org.signal.core.util.ListUtil; import org.signal.core.util.ListUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
@ -208,7 +208,8 @@ public class SendViewedReceiptJob extends BaseJob {
timestamp); timestamp);
SendMessageResult result = messageSender.sendReceipt(remoteAddress, SendMessageResult result = messageSender.sendReceipt(remoteAddress,
UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), SealedSenderAccessUtil.getSealedSenderAccessFor(recipient,
() -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)),
receiptMessage, receiptMessage,
recipient.getNeedsPniSignature()); recipient.getNeedsPniSignature());

View file

@ -6,19 +6,19 @@ import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage; import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListRecord; import org.thoughtcrime.securesms.database.model.DistributionListRecord;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -85,12 +85,14 @@ public final class SenderKeyDistributionSendJob extends BaseJob {
return; return;
} }
GroupId.V2 groupId; GroupId.V2 groupId;
DistributionId distributionId; DistributionId distributionId;
SealedSenderAccess.CreateGroupSendToken createGroupSendFullToken = null;
if (threadRecipient.isPushV2Group()) { if (threadRecipient.isPushV2Group()) {
groupId = threadRecipient.requireGroupId().requireV2(); groupId = threadRecipient.requireGroupId().requireV2();
distributionId = SignalDatabase.groups().getOrCreateDistributionId(groupId); distributionId = SignalDatabase.groups().getOrCreateDistributionId(groupId);
createGroupSendFullToken = () -> SignalDatabase.groups().getGroupSendFullToken(groupId, targetRecipientId);
} else if (threadRecipient.isDistributionList()) { } else if (threadRecipient.isDistributionList()) {
groupId = null; groupId = null;
distributionId = SignalDatabase.distributionLists().getDistributionId(threadRecipientId); distributionId = SignalDatabase.distributionLists().getDistributionId(threadRecipientId);
@ -116,10 +118,10 @@ public final class SenderKeyDistributionSendJob extends BaseJob {
} }
} }
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, targetRecipient)); List<SignalServiceAddress> address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, targetRecipient));
SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId); SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId);
List<Optional<UnidentifiedAccessPair>> access = UnidentifiedAccessUtil.getAccessFor(context, Collections.singletonList(targetRecipient)); List<SealedSenderAccess> access = Collections.singletonList(SealedSenderAccessUtil.getSealedSenderAccessFor(targetRecipient, createGroupSendFullToken));
SendMessageResult result = messageSender.sendSenderKeyDistributionMessage(distributionId, address, access, message, Optional.ofNullable(groupId).map(GroupId::getDecodedId), false, false).get(0); SendMessageResult result = messageSender.sendSenderKeyDistributionMessage(distributionId, address, access, message, Optional.ofNullable(groupId).map(GroupId::getDecodedId), false, false).get(0);

View file

@ -12,7 +12,6 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.signal.core.util.Stopwatch; import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.UnknownStorageIdTable; import org.thoughtcrime.securesms.database.UnknownStorageIdTable;
@ -209,7 +208,7 @@ public class StorageSyncJob extends BaseJob {
} else { } else {
Log.w(TAG, "Failed to decrypt remote storage! Requesting new keys from primary.", e); Log.w(TAG, "Failed to decrypt remote storage! Requesting new keys from primary.", e);
SignalStore.storageService().clearStorageKeyFromPrimary(); SignalStore.storageService().clearStorageKeyFromPrimary();
AppDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forRequest(RequestMessage.forType(SyncMessage.Request.Type.KEYS)), UnidentifiedAccessUtil.getAccessForSync(context)); AppDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forRequest(RequestMessage.forType(SyncMessage.Request.Type.KEYS)));
} }
} }
} }

View file

@ -0,0 +1,116 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
/**
* Internal user only notifier when "bad" things happen with group send endorsement sends.
*/
object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListener {
private const val TAG = "GSENotifier"
private var lastGroupSendNotify: Duration = 0.milliseconds
private var skippedGroupSendNotifies = 0
private var lastMissingNotify: Duration = 0.milliseconds
private var lastFallbackNotify: Duration = 0.milliseconds
@JvmStatic
fun init() {
if (RemoteConfig.internalUser) {
SealedSenderAccess.fallbackListener = this
}
}
override fun onAccessToTokenFallback() {
Log.w(TAG, "Fallback from access key to token", Throwable())
postFallbackError(AppDependencies.application)
}
override fun onTokenToAccessFallback(hasAccessKeyFallback: Boolean) {
Log.w(TAG, "Fallback from token hasAccessKey=$hasAccessKeyFallback", Throwable())
postFallbackError(AppDependencies.application)
}
@JvmStatic
fun postGroupSendFallbackError(context: Context) {
val now = System.currentTimeMillis().milliseconds
if (lastGroupSendNotify + 5.minutes > now && skippedGroupSendNotifies < 5) {
skippedGroupSendNotifies++
return
}
val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("[Internal-only] GSE failed for group send")
.setContentText("Please tap to send a debug log")
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable()))
.build()
NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR, notification)
lastGroupSendNotify = now
skippedGroupSendNotifies = 0
}
@JvmStatic
fun postMissingGroupSendEndorsement(context: Context) {
val now = System.currentTimeMillis().milliseconds
if (lastMissingNotify + 5.minutes > now) {
return
}
val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("[Internal-only] GSE missing for recipient")
.setContentText("Please tap to send a debug log")
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable()))
.build()
NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR, notification)
lastMissingNotify = now
}
@JvmStatic
fun postFallbackError(context: Context) {
val now = System.currentTimeMillis().milliseconds
if (lastFallbackNotify + 5.minutes > now) {
return
}
val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("[Internal-only] GSE fallback occurred!")
.setContentText("Please tap to send a debug log")
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable()))
.build()
NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR, notification)
lastFallbackNotify = now
}
}

View file

@ -7,34 +7,44 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidRegistrationIdException; import org.signal.libsignal.protocol.InvalidRegistrationIdException;
import org.signal.libsignal.protocol.NoSessionException; import org.signal.libsignal.protocol.NoSessionException;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.MessageSendLogTables; import org.thoughtcrime.securesms.database.MessageSendLogTables;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListId; import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.model.GroupSendEndorsementRecords;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeException;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.RecipientAccessList; import org.thoughtcrime.securesms.util.RecipientAccessList;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.CancelationException; import org.whispersystems.signalservice.api.CancelationException;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalServiceMessageSender.LegacyGroupEvents; import org.whispersystems.signalservice.api.SignalServiceMessageSender.LegacyGroupEvents;
import org.whispersystems.signalservice.api.SignalServiceMessageSender.SenderKeyGroupEvents; import org.whispersystems.signalservice.api.SignalServiceMessageSender.SenderKeyGroupEvents;
import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; 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.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
@ -43,6 +53,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRe
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.Preconditions;
@ -52,6 +63,7 @@ import org.whispersystems.signalservice.internal.push.http.PartialSendBatchCompl
import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener; import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener;
import java.io.IOException; import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -248,28 +260,81 @@ public final class GroupSendUtil {
Set<Recipient> unregisteredTargets = allTargets.stream().filter(Recipient::isUnregistered).collect(Collectors.toSet()); Set<Recipient> unregisteredTargets = allTargets.stream().filter(Recipient::isUnregistered).collect(Collectors.toSet());
List<Recipient> registeredTargets = allTargets.stream().filter(r -> !unregisteredTargets.contains(r)).collect(Collectors.toList()); List<Recipient> registeredTargets = allTargets.stream().filter(r -> !unregisteredTargets.contains(r)).collect(Collectors.toList());
RecipientData recipients = new RecipientData(context, registeredTargets, isStorySend); RecipientData recipients = new RecipientData(context, registeredTargets, isStorySend);
Optional<GroupRecord> groupRecord = groupId != null ? SignalDatabase.groups().getGroup(groupId) : Optional.empty(); Optional<GroupRecord> groupRecord = groupId != null ? SignalDatabase.groups().getGroup(groupId) : Optional.empty();
GroupSendEndorsementRecords groupSendEndorsementRecords = groupRecord.filter(GroupRecord::isV2Group).map(g -> SignalDatabase.groups().getGroupSendEndorsements(g.getId())).orElse(null);
long groupSendEndorsementExpiration = groupRecord.map(GroupRecord::getGroupSendEndorsementExpiration).orElse(0L);
SenderCertificate senderCertificate = SealedSenderAccessUtil.getSealedSenderCertificate();
boolean useGroupSendEndorsements = groupSendEndorsementRecords != null;
if (useGroupSendEndorsements && senderCertificate == null) {
Log.w(TAG, "Can't use group send endorsements without a sealed sender certificate, falling back to access key");
useGroupSendEndorsements = false;
} else if (useGroupSendEndorsements) {
boolean refreshGroupSendEndorsements = false;
if (groupSendEndorsementExpiration == 0) {
Log.i(TAG, "No group send endorsements expiration set, need to refresh");
refreshGroupSendEndorsements = true;
} else if (groupSendEndorsementExpiration - TimeUnit.HOURS.toMillis(2) < System.currentTimeMillis()) {
Log.i(TAG, "Group send endorsements are expired or expire imminently, refresh. Expires in " + (groupSendEndorsementExpiration - System.currentTimeMillis()) + "ms");
refreshGroupSendEndorsements = true;
} else if (groupSendEndorsementRecords.isMissingAnyEndorsements()) {
Log.i(TAG, "Missing group send endorsements for some members, refresh.");
refreshGroupSendEndorsements = true;
}
if (refreshGroupSendEndorsements) {
try {
GroupManager.updateGroupSendEndorsements(context, groupRecord.get().requireV2GroupProperties().getGroupMasterKey());
groupSendEndorsementExpiration = SignalDatabase.groups().getGroupSendEndorsementsExpiration(groupId);
groupSendEndorsementRecords = SignalDatabase.groups().getGroupSendEndorsements(groupId);
} catch (GroupChangeException | IOException e) {
if (groupSendEndorsementExpiration == 0) {
Log.w(TAG, "Unable to update group send endorsements, falling back to access key", e);
useGroupSendEndorsements = false;
groupSendEndorsementRecords = new GroupSendEndorsementRecords(Collections.emptyMap());
} else {
Log.w(TAG, "Unable to update group send endorsements, using what we have", e);
}
}
Log.d(TAG, "Refresh all group state because we needed to refresh gse");
AppDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId));
}
}
List<Recipient> senderKeyTargets = new LinkedList<>(); List<Recipient> senderKeyTargets = new LinkedList<>();
List<Recipient> legacyTargets = new LinkedList<>(); List<Recipient> legacyTargets = new LinkedList<>();
for (Recipient recipient : registeredTargets) { for (Recipient recipient : registeredTargets) {
Optional<UnidentifiedAccessPair> access = recipients.getAccessPair(recipient.getId()); Optional<UnidentifiedAccess> access = recipients.getAccessPair(recipient.getId());
boolean validMembership = true; boolean validMembership = groupId == null || (groupRecord.isPresent() && groupRecord.get().getMembers().contains(recipient.getId()));
if (groupId != null && (!groupRecord.isPresent() || !groupRecord.get().getMembers().contains(recipient.getId()))) { if (useGroupSendEndorsements) {
validMembership = false; GroupSendEndorsement groupSendEndorsement = groupSendEndorsementRecords.getEndorsement(recipient.getId());
} if (groupSendEndorsement != null && recipient.getHasAci() && validMembership) {
senderKeyTargets.add(recipient);
if (recipient.getHasServiceId() && } else {
access.isPresent() && legacyTargets.add(recipient);
access.get().getTargetUnidentifiedAccess().isPresent() && if (validMembership) {
validMembership) Log.w(TAG, "Should be using group send endorsement but not found for " + recipient.getId());
{ if (RemoteConfig.internalUser()) {
senderKeyTargets.add(recipient); GroupSendEndorsementInternalNotifier.postMissingGroupSendEndorsement(context);
}
}
}
} else { } else {
legacyTargets.add(recipient); // Use sender key
if (recipient.getHasServiceId() &&
access.isPresent() &&
validMembership)
{
senderKeyTargets.add(recipient);
} else {
legacyTargets.add(recipient);
}
} }
} }
@ -299,7 +364,7 @@ public final class GroupSendUtil {
List<SendMessageResult> allResults = new ArrayList<>(allTargets.size()); List<SendMessageResult> allResults = new ArrayList<>(allTargets.size());
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
if (senderKeyTargets.size() > 0 && distributionId != null) { if (Util.hasItems(senderKeyTargets) && distributionId != null) {
long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(distributionId); long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(distributionId);
long keyAge = System.currentTimeMillis() - keyCreateTime; long keyAge = System.currentTimeMillis() - keyCreateTime;
@ -309,14 +374,36 @@ public final class GroupSendUtil {
} }
try { try {
List<SignalServiceAddress> targets = senderKeyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList()); List<SignalServiceAddress> targets = new ArrayList<>(senderKeyTargets.size());
List<UnidentifiedAccess> access = senderKeyTargets.stream().map(r -> recipients.requireAccess(r.getId())).collect(Collectors.toList()); List<UnidentifiedAccess> access = new ArrayList<>(senderKeyTargets.size());
Map<ServiceId.ACI, GroupSendEndorsement> senderKeyEndorsements = new HashMap<>(senderKeyTargets.size());
GroupSendEndorsements groupSendEndorsements = null;
for (Recipient recipient : senderKeyTargets) {
targets.add(recipients.getAddress(recipient.getId()));
if (useGroupSendEndorsements) {
senderKeyEndorsements.put(recipient.requireAci(), groupSendEndorsementRecords.getEndorsement(recipient.getId()));
access.add(recipients.getAccess(recipient.getId()));
} else {
access.add(recipients.requireAccess(recipient.getId()));
}
}
if (useGroupSendEndorsements) {
groupSendEndorsements = new GroupSendEndorsements(
groupSendEndorsementExpiration,
senderKeyEndorsements,
senderCertificate,
GroupSecretParams.deriveFromMasterKey(groupRecord.get().requireV2GroupProperties().getGroupMasterKey())
);
}
final MessageSendLogTables messageLogDatabase = SignalDatabase.messageLog(); final MessageSendLogTables messageLogDatabase = SignalDatabase.messageLog();
final AtomicLong entryId = new AtomicLong(-1); final AtomicLong entryId = new AtomicLong(-1);
final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog(); final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog();
List<SendMessageResult> results = sendOperation.sendWithSenderKey(messageSender, distributionId, targets, access, isRecipientUpdate, partialResults -> { List<SendMessageResult> results = sendOperation.sendWithSenderKey(messageSender, distributionId, targets, access, groupSendEndorsements, isRecipientUpdate, partialResults -> {
if (!includeInMessageLog) { if (!includeInMessageLog) {
return; return;
} }
@ -343,6 +430,10 @@ public final class GroupSendUtil {
} catch (InvalidUnidentifiedAccessHeaderException e) { } catch (InvalidUnidentifiedAccessHeaderException e) {
Log.w(TAG, "Someone had a bad UD header. Falling back to legacy sends.", e); Log.w(TAG, "Someone had a bad UD header. Falling back to legacy sends.", e);
legacyTargets.addAll(senderKeyTargets); legacyTargets.addAll(senderKeyTargets);
if (useGroupSendEndorsements && RemoteConfig.internalUser()) {
GroupSendEndorsementInternalNotifier.postGroupSendFallbackError(context);
}
} catch (NoSessionException e) { } catch (NoSessionException e) {
Log.w(TAG, "No session. Falling back to legacy sends.", e); Log.w(TAG, "No session. Falling back to legacy sends.", e);
legacyTargets.addAll(senderKeyTargets); legacyTargets.addAll(senderKeyTargets);
@ -377,15 +468,32 @@ public final class GroupSendUtil {
Log.i(TAG, "Need to do a legacy send to send a sync message for a group of only ourselves."); Log.i(TAG, "Need to do a legacy send to send a sync message for a group of only ourselves.");
} }
List<SignalServiceAddress> targets = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList()); List<SignalServiceAddress> legacyTargetAddresses = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList());
List<Optional<UnidentifiedAccessPair>> access = legacyTargets.stream().map(r -> recipients.getAccessPair(r.getId())).collect(Collectors.toList()); List<UnidentifiedAccess> legacyTargetAccesses = legacyTargets.stream().map(r -> recipients.getAccess(r.getId())).collect(Collectors.toList());
boolean recipientUpdate = isRecipientUpdate || allResults.size() > 0; List<GroupSendFullToken> groupSendTokens = null;
boolean recipientUpdate = isRecipientUpdate || allResults.isEmpty();
if (useGroupSendEndorsements) {
Instant expiration = Instant.ofEpochMilli(groupSendEndorsementExpiration);
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupRecord.get().requireV2GroupProperties().getGroupMasterKey());
groupSendTokens = new ArrayList<>(legacyTargetAddresses.size());
for (Recipient r : legacyTargets) {
GroupSendEndorsement endorsement = groupSendEndorsementRecords.getEndorsement(r.getId());
if (r.getHasAci() && endorsement != null) {
groupSendTokens.add(endorsement.toFullToken(groupSecretParams, expiration));
} else {
groupSendTokens.add(null);
}
}
}
final MessageSendLogTables messageLogDatabase = SignalDatabase.messageLog(); final MessageSendLogTables messageLogDatabase = SignalDatabase.messageLog();
final AtomicLong entryId = new AtomicLong(-1); final AtomicLong entryId = new AtomicLong(-1);
final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog(); final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog();
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, targets, legacyTargets, access, recipientUpdate, result -> { List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, legacyTargetAddresses, legacyTargets, SealedSenderAccess.forFanOutGroupSend(groupSendTokens, SealedSenderAccessUtil.getSealedSenderCertificate(), legacyTargetAccesses), recipientUpdate, result -> {
if (!includeInMessageLog) { if (!includeInMessageLog) {
return; return;
} }
@ -402,7 +510,7 @@ public final class GroupSendUtil {
allResults.addAll(results); allResults.addAll(results);
int successCount = (int) results.stream().filter(SendMessageResult::isSuccess).count(); int successCount = (int) results.stream().filter(SendMessageResult::isSuccess).count();
Log.d(TAG, "Successfully sent using 1:1 to " + successCount + "/" + targets.size() + " legacy targets."); Log.d(TAG, "Successfully sent using 1:1 to " + successCount + "/" + legacyTargetAddresses.size() + " legacy targets.");
} else if (relatedMessageId != null) { } else if (relatedMessageId != null) {
SignalLocalMetrics.GroupMessageSend.onLegacyMessageSent(relatedMessageId.getId()); SignalLocalMetrics.GroupMessageSend.onLegacyMessageSent(relatedMessageId.getId());
SignalLocalMetrics.GroupMessageSend.onLegacySyncFinished(relatedMessageId.getId()); SignalLocalMetrics.GroupMessageSend.onLegacySyncFinished(relatedMessageId.getId());
@ -448,6 +556,7 @@ public final class GroupSendUtil {
@NonNull DistributionId distributionId, @NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> access, @NonNull List<UnidentifiedAccess> access,
@Nullable GroupSendEndorsements groupSendEndorsements,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendBatchCompleteListener partialListener) @Nullable PartialSendBatchCompleteListener partialListener)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException; throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException;
@ -455,7 +564,7 @@ public final class GroupSendUtil {
@NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients, @NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access, @NonNull List<SealedSenderAccess> sealedSenderAccesses,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener, @Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal) @Nullable CancelationSignal cancelationSignal)
@ -504,19 +613,20 @@ public final class GroupSendUtil {
@NonNull DistributionId distributionId, @NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> access, @NonNull List<UnidentifiedAccess> access,
@Nullable GroupSendEndorsements groupSendEndorsements,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendBatchCompleteListener partialListener) @Nullable PartialSendBatchCompleteListener partialListener)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
{ {
SenderKeyGroupEvents listener = relatedMessageId != null ? new SenderKeyMetricEventListener(relatedMessageId.getId()) : SenderKeyGroupEvents.EMPTY; SenderKeyGroupEvents listener = relatedMessageId != null ? new SenderKeyMetricEventListener(relatedMessageId.getId()) : SenderKeyGroupEvents.EMPTY;
return messageSender.sendGroupDataMessage(distributionId, targets, access, isRecipientUpdate, contentHint, message, listener, urgent, isForStory, editMessage, partialListener); return messageSender.sendGroupDataMessage(distributionId, targets, access, groupSendEndorsements, isRecipientUpdate, contentHint, message, listener, urgent, isForStory, editMessage, partialListener);
} }
@Override @Override
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender, public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients, @NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access, @NonNull List<SealedSenderAccess> sealedSenderAccesses,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener, @Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal) @Nullable CancelationSignal cancelationSignal)
@ -524,13 +634,14 @@ public final class GroupSendUtil {
{ {
// PniSignatures are only needed for 1:1 messages, but some message jobs use the GroupSendUtil methods to send 1:1 // PniSignatures are only needed for 1:1 messages, but some message jobs use the GroupSendUtil methods to send 1:1
if (targets.size() == 1 && relatedMessageId == null) { if (targets.size() == 1 && relatedMessageId == null) {
Recipient targetRecipient = targetRecipients.get(0); Recipient targetRecipient = targetRecipients.get(0);
SendMessageResult result; SealedSenderAccess sealedSenderAccess = sealedSenderAccesses.get(0);
SendMessageResult result;
if (editMessage != null) { if (editMessage != null) {
result = messageSender.sendEditMessage(targets.get(0), access.get(0), contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, editMessage.getTargetSentTimestamp()); result = messageSender.sendEditMessage(targets.get(0), sealedSenderAccess, contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, editMessage.getTargetSentTimestamp());
} else { } else {
result = messageSender.sendDataMessage(targets.get(0), access.get(0), contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, targetRecipient.getNeedsPniSignature()); result = messageSender.sendDataMessage(targets.get(0), sealedSenderAccess, contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, targetRecipient.getNeedsPniSignature());
} }
if (targetRecipient.getNeedsPniSignature()) { if (targetRecipient.getNeedsPniSignature()) {
@ -540,10 +651,11 @@ public final class GroupSendUtil {
return Collections.singletonList(result); return Collections.singletonList(result);
} else { } else {
LegacyGroupEvents listener = relatedMessageId != null ? new LegacyMetricEventListener(relatedMessageId.getId()) : LegacyGroupEvents.EMPTY; LegacyGroupEvents listener = relatedMessageId != null ? new LegacyMetricEventListener(relatedMessageId.getId()) : LegacyGroupEvents.EMPTY;
if (editMessage != null) { if (editMessage != null) {
return messageSender.sendEditMessage(targets, access, isRecipientUpdate, contentHint, message, listener, partialListener, cancelationSignal, urgent, editMessage.getTargetSentTimestamp()); return messageSender.sendEditMessage(targets, sealedSenderAccesses, isRecipientUpdate, contentHint, message, listener, partialListener, cancelationSignal, urgent, editMessage.getTargetSentTimestamp());
} else { } else {
return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message, listener, partialListener, cancelationSignal, urgent); return messageSender.sendDataMessage(targets, sealedSenderAccesses, isRecipientUpdate, contentHint, message, listener, partialListener, cancelationSignal, urgent);
} }
} }
} }
@ -591,11 +703,12 @@ public final class GroupSendUtil {
@NonNull DistributionId distributionId, @NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> access, @NonNull List<UnidentifiedAccess> access,
@Nullable GroupSendEndorsements groupSendEndorsements,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendBatchCompleteListener partialListener) @Nullable PartialSendBatchCompleteListener partialListener)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
{ {
messageSender.sendGroupTyping(distributionId, targets, access, message); messageSender.sendGroupTyping(distributionId, targets, access, groupSendEndorsements, message);
List<SendMessageResult> results = targets.stream().map(a -> SendMessageResult.success(a, Collections.emptyList(), true, false, -1, Optional.empty())).collect(Collectors.toList()); List<SendMessageResult> results = targets.stream().map(a -> SendMessageResult.success(a, Collections.emptyList(), true, false, -1, Optional.empty())).collect(Collectors.toList());
if (partialListener != null) { if (partialListener != null) {
@ -609,13 +722,13 @@ public final class GroupSendUtil {
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender, public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients, @NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access, @NonNull List<SealedSenderAccess> sealedSenderAccesses,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener, @Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal) @Nullable CancelationSignal cancelationSignal)
throws IOException throws IOException
{ {
messageSender.sendTyping(targets, access, message, cancelationSignal); messageSender.sendTyping(targets, sealedSenderAccesses, message, cancelationSignal);
return targets.stream().map(a -> SendMessageResult.success(a, Collections.emptyList(), true, false, -1, Optional.empty())).collect(Collectors.toList()); return targets.stream().map(a -> SendMessageResult.success(a, Collections.emptyList(), true, false, -1, Optional.empty())).collect(Collectors.toList());
} }
@ -658,24 +771,25 @@ public final class GroupSendUtil {
@NonNull DistributionId distributionId, @NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> access, @NonNull List<UnidentifiedAccess> access,
@Nullable GroupSendEndorsements groupSendEndorsements,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendBatchCompleteListener partialSendListener) @Nullable PartialSendBatchCompleteListener partialSendListener)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
{ {
return messageSender.sendCallMessage(distributionId, targets, access, message, partialSendListener); return messageSender.sendCallMessage(distributionId, targets, access, groupSendEndorsements, message, partialSendListener);
} }
@Override @Override
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender, public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients, @NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access, @NonNull List<SealedSenderAccess> sealedSenderAccesses,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener, @Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal) @Nullable CancelationSignal cancelationSignal)
throws IOException throws IOException
{ {
return messageSender.sendCallMessage(targets, access, message); return messageSender.sendCallMessage(targets, sealedSenderAccesses, message);
} }
@Override @Override
@ -730,18 +844,19 @@ public final class GroupSendUtil {
@NonNull DistributionId distributionId, @NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> access, @NonNull List<UnidentifiedAccess> access,
@Nullable GroupSendEndorsements groupSendEndorsements,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendBatchCompleteListener partialListener) @Nullable PartialSendBatchCompleteListener partialListener)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
{ {
return messageSender.sendGroupStory(distributionId, Optional.ofNullable(groupId).map(GroupId::getDecodedId), targets, access, isRecipientUpdate, message, getSentTimestamp(), manifest, partialListener); return messageSender.sendGroupStory(distributionId, Optional.ofNullable(groupId).map(GroupId::getDecodedId), targets, access, groupSendEndorsements, isRecipientUpdate, message, getSentTimestamp(), manifest, partialListener);
} }
@Override @Override
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender, public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets, @NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients, @NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access, @NonNull List<SealedSenderAccess> sealedSenderAccesses,
boolean isRecipientUpdate, boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener, @Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal) @Nullable CancelationSignal cancelationSignal)
@ -839,12 +954,12 @@ public final class GroupSendUtil {
*/ */
private static final class RecipientData { private static final class RecipientData {
private final Map<RecipientId, Optional<UnidentifiedAccessPair>> accessById; private final Map<RecipientId, Optional<UnidentifiedAccess>> accessById;
private final Map<RecipientId, SignalServiceAddress> addressById; private final Map<RecipientId, SignalServiceAddress> addressById;
private final RecipientAccessList accessList; private final RecipientAccessList accessList;
RecipientData(@NonNull Context context, @NonNull List<Recipient> recipients, boolean isForStory) throws IOException { RecipientData(@NonNull Context context, @NonNull List<Recipient> recipients, boolean isForStory) throws IOException {
this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients, isForStory); this.accessById = SealedSenderAccessUtil.getAccessMapFor(recipients, isForStory);
this.addressById = mapAddresses(context, recipients); this.addressById = mapAddresses(context, recipients);
this.accessList = new RecipientAccessList(recipients); this.accessList = new RecipientAccessList(recipients);
} }
@ -853,22 +968,22 @@ public final class GroupSendUtil {
return Objects.requireNonNull(addressById.get(id)); return Objects.requireNonNull(addressById.get(id));
} }
@NonNull Optional<UnidentifiedAccessPair> getAccessPair(@NonNull RecipientId id) { @NonNull Optional<UnidentifiedAccess> getAccessPair(@NonNull RecipientId id) {
return Objects.requireNonNull(accessById.get(id)); return Objects.requireNonNull(accessById.get(id));
} }
@Nullable UnidentifiedAccess getAccess(@NonNull RecipientId id) {
return Objects.requireNonNull(accessById.get(id)).orElse(null);
}
@NonNull UnidentifiedAccess requireAccess(@NonNull RecipientId id) { @NonNull UnidentifiedAccess requireAccess(@NonNull RecipientId id) {
return Objects.requireNonNull(accessById.get(id)).get().getTargetUnidentifiedAccess().get(); return Objects.requireNonNull(accessById.get(id)).get();
} }
@NonNull RecipientId requireRecipientId(@NonNull SignalServiceAddress address) { @NonNull RecipientId requireRecipientId(@NonNull SignalServiceAddress address) {
return accessList.requireIdByAddress(address); return accessList.requireIdByAddress(address);
} }
@NonNull List<RecipientId> requireRecipientIds(@NonNull List<SignalServiceAddress> addresses) {
return addresses.stream().map(accessList::requireIdByAddress).collect(Collectors.toList());
}
private static @NonNull Map<RecipientId, SignalServiceAddress> mapAddresses(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException { private static @NonNull Map<RecipientId, SignalServiceAddress> mapAddresses(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException {
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients); List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);

View file

@ -32,7 +32,7 @@ import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage
import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock import org.thoughtcrime.securesms.crypto.ReentrantSessionLock
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.BadGroupIdException import org.thoughtcrime.securesms.groups.BadGroupIdException
@ -137,7 +137,7 @@ object MessageDecryptor {
val bufferedStore = bufferedProtocolStore.get(destination) val bufferedStore = bufferedProtocolStore.get(destination)
val localAddress = SignalServiceAddress(selfAci, SignalStore.account.e164) val localAddress = SignalServiceAddress(selfAci, SignalStore.account.e164)
val cipher = SignalServiceCipher(localAddress, SignalStore.account.deviceId, bufferedStore, ReentrantSessionLock.INSTANCE, UnidentifiedAccessUtil.getCertificateValidator()) val cipher = SignalServiceCipher(localAddress, SignalStore.account.deviceId, bufferedStore, ReentrantSessionLock.INSTANCE, SealedSenderAccessUtil.getCertificateValidator())
return try { return try {
val startTimeNanos = System.nanoTime() val startTimeNanos = System.nanoTime()

View file

@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting
import org.thoughtcrime.securesms.database.RecipientTable.MissingRecipientException import org.thoughtcrime.securesms.database.RecipientTable.MissingRecipientException
import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState
import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState
import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode
import org.thoughtcrime.securesms.database.RecipientTable.VibrateState import org.thoughtcrime.securesms.database.RecipientTable.VibrateState
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListId
@ -98,7 +98,7 @@ class Recipient(
val hiddenState: HiddenState = HiddenState.NOT_HIDDEN, val hiddenState: HiddenState = HiddenState.NOT_HIDDEN,
val lastProfileFetchTime: Long = 0, val lastProfileFetchTime: Long = 0,
private val notificationChannelValue: String? = null, private val notificationChannelValue: String? = null,
private val unidentifiedAccessModeValue: UnidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN, private val sealedSenderAccessModeValue: SealedSenderAccessMode = SealedSenderAccessMode.UNKNOWN,
private val capabilities: RecipientRecord.Capabilities = RecipientRecord.Capabilities.UNKNOWN, private val capabilities: RecipientRecord.Capabilities = RecipientRecord.Capabilities.UNKNOWN,
val storageId: ByteArray? = null, val storageId: ByteArray? = null,
val mentionSetting: MentionSetting = MentionSetting.ALWAYS_NOTIFY, val mentionSetting: MentionSetting = MentionSetting.ALWAYS_NOTIFY,
@ -318,10 +318,10 @@ class Recipient(
val deleteSyncCapability: Capability = capabilities.deleteSync val deleteSyncCapability: Capability = capabilities.deleteSync
/** The state around whether we can send sealed sender to this user. */ /** The state around whether we can send sealed sender to this user. */
val unidentifiedAccessMode: UnidentifiedAccessMode = if (pni.isPresent && pni == serviceId) { val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) {
UnidentifiedAccessMode.DISABLED SealedSenderAccessMode.DISABLED
} else { } else {
unidentifiedAccessModeValue sealedSenderAccessModeValue
} }
/** The wallpaper to render as the chat background, if present. */ /** The wallpaper to render as the chat background, if present. */
@ -760,7 +760,7 @@ class Recipient(
systemProfileName == other.systemProfileName && systemProfileName == other.systemProfileName &&
profileAvatar == other.profileAvatar && profileAvatar == other.profileAvatar &&
notificationChannelValue == other.notificationChannelValue && notificationChannelValue == other.notificationChannelValue &&
unidentifiedAccessModeValue == other.unidentifiedAccessModeValue && sealedSenderAccessModeValue == other.sealedSenderAccessModeValue &&
storageId.contentEquals(other.storageId) && storageId.contentEquals(other.storageId) &&
mentionSetting == other.mentionSetting && mentionSetting == other.mentionSetting &&
wallpaperValue == other.wallpaperValue && wallpaperValue == other.wallpaperValue &&

View file

@ -162,7 +162,7 @@ object RecipientCreator {
lastProfileFetchTime = record.lastProfileFetch, lastProfileFetchTime = record.lastProfileFetch,
isSelf = isSelf, isSelf = isSelf,
notificationChannelValue = record.notificationChannel, notificationChannelValue = record.notificationChannel,
unidentifiedAccessModeValue = record.unidentifiedAccessMode, sealedSenderAccessModeValue = record.sealedSenderAccessMode,
capabilities = record.capabilities, capabilities = record.capabilities,
storageId = record.storageId, storageId = record.storageId,
mentionSetting = record.mentionSetting, mentionSetting = record.mentionSetting,

View file

@ -36,7 +36,7 @@ import org.signal.ringrtc.PeekInfo;
import org.signal.ringrtc.Remote; import org.signal.ringrtc.Remote;
import org.signal.storageservice.protos.groups.GroupExternalCredential; import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.thoughtcrime.securesms.WebRtcCallActivity; import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.CallLinkTable; import org.thoughtcrime.securesms.database.CallLinkTable;
import org.thoughtcrime.securesms.database.CallTable; import org.thoughtcrime.securesms.database.CallTable;
import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.GroupTable;
@ -66,8 +66,8 @@ import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.RecipientAccessList; import org.thoughtcrime.securesms.util.RecipientAccessList;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.rx.RxStore; import org.thoughtcrime.securesms.util.rx.RxStore;
@ -75,6 +75,7 @@ import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager; import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.calls.CallingResponse; import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
@ -780,8 +781,8 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
try { try {
AppDependencies.getSignalServiceMessageSender() AppDependencies.getSignalServiceMessageSender()
.sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient), .sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
recipient.isSelf() ? Optional.empty() : UnidentifiedAccessUtil.getAccessFor(context, recipient), recipient.isSelf() ? SealedSenderAccess.NONE : SealedSenderAccessUtil.getSealedSenderAccessFor(recipient),
callMessage); callMessage);
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
Log.i(TAG, "onSendCallMessage onFailure: ", e); Log.i(TAG, "onSendCallMessage onFailure: ", e);
RetrieveProfileJob.enqueue(recipient.getId()); RetrieveProfileJob.enqueue(recipient.getId());
@ -1129,8 +1130,8 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
try { try {
AppDependencies.getSignalServiceMessageSender() AppDependencies.getSignalServiceMessageSender()
.sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient), .sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
UnidentifiedAccessUtil.getAccessFor(context, recipient), SealedSenderAccessUtil.getSealedSenderAccessFor(recipient),
callMessage); callMessage);
process((s, p) -> p.handleMessageSentSuccess(s, remotePeer.getCallId())); process((s, p) -> p.handleMessageSentSuccess(s, remotePeer.getCallId()));
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
RetrieveProfileJob.enqueue(remotePeer.getId()); RetrieveProfileJob.enqueue(remotePeer.getId());
@ -1158,7 +1159,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
networkExecutor.execute(() -> { networkExecutor.execute(() -> {
try { try {
SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, isVideoCall); SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, isVideoCall);
AppDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent), Optional.empty()); AppDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent));
} catch (IOException | UntrustedIdentityException e) { } catch (IOException | UntrustedIdentityException e) {
Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e); Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e);
} }
@ -1175,7 +1176,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
networkExecutor.execute(() -> { networkExecutor.execute(() -> {
try { try {
SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createNotAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, isVideoCall); SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createNotAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, isVideoCall);
AppDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent), Optional.empty()); AppDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent));
} catch (IOException | UntrustedIdentityException e) { } catch (IOException | UntrustedIdentityException e) {
Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e); Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e);
} }
@ -1188,7 +1189,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
networkExecutor.execute(() -> { networkExecutor.execute(() -> {
try { try {
SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createNotAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, true); SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createNotAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, true);
AppDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent), Optional.empty()); AppDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent));
} catch (IOException | UntrustedIdentityException e) { } catch (IOException | UntrustedIdentityException e) {
Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e); Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e);
} }

View file

@ -17,7 +17,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
@ -38,8 +38,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.profiles.AvatarUploadParams; import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
@ -118,7 +117,7 @@ public final class ProfileUtil {
ServiceResponse<ProfileAndCredential> response = Single ServiceResponse<ProfileAndCredential> response = Single
.fromCallable(() -> new SignalServiceAddress(pni)) .fromCallable(() -> new SignalServiceAddress(pni))
.flatMap(address -> profileService.getProfile(address, Optional.empty(), Optional.empty(), requestType, Locale.getDefault())) .flatMap(address -> profileService.getProfile(address, Optional.empty(), SealedSenderAccess.NONE, requestType, Locale.getDefault()))
.onErrorReturn(t -> ServiceResponse.forUnknownError(t)) .onErrorReturn(t -> ServiceResponse.forUnknownError(t))
.blockingGet(); .blockingGet();
@ -137,12 +136,12 @@ public final class ProfileUtil {
@NonNull SignalServiceProfile.RequestType requestType, @NonNull SignalServiceProfile.RequestType requestType,
boolean allowUnidentifiedAccess) boolean allowUnidentifiedAccess)
{ {
ProfileService profileService = AppDependencies.getProfileService(); ProfileService profileService = AppDependencies.getProfileService();
Optional<UnidentifiedAccess> unidentifiedAccess = allowUnidentifiedAccess ? getUnidentifiedAccess(context, recipient) : Optional.empty(); SealedSenderAccess sealedSenderAccess = allowUnidentifiedAccess ? SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, false) : SealedSenderAccess.NONE;
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()); Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
return Single.fromCallable(() -> toSignalServiceAddress(context, recipient)) return Single.fromCallable(() -> toSignalServiceAddress(context, recipient))
.flatMap(address -> profileService.getProfile(address, profileKey, unidentifiedAccess, requestType, Locale.getDefault()).map(p -> new Pair<>(recipient, p))) .flatMap(address -> profileService.getProfile(address, profileKey, sealedSenderAccess, requestType, Locale.getDefault()).map(p -> new Pair<>(recipient, p)))
.onErrorReturn(t -> new Pair<>(recipient, ServiceResponse.forUnknownError(t))); .onErrorReturn(t -> new Pair<>(recipient, ServiceResponse.forUnknownError(t)));
} }
@ -397,16 +396,6 @@ public final class ProfileUtil {
} }
} }
private static Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) {
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient, false);
if (unidentifiedAccess.isPresent()) {
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
}
return Optional.empty();
}
private static @NonNull SignalServiceAddress toSignalServiceAddress(@NonNull Context context, @NonNull Recipient recipient) throws IOException { private static @NonNull SignalServiceAddress toSignalServiceAddress(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
if (recipient.getRegistered() == RecipientTable.RegisteredState.NOT_REGISTERED) { if (recipient.getRegistered() == RecipientTable.RegisteredState.NOT_REGISTERED) {
if (recipient.getHasServiceId()) { if (recipient.getHasServiceId()) {

View file

@ -67,7 +67,7 @@ class ChangeSet {
} }
fun toApiResponse(): GroupHistoryPage { fun toApiResponse(): GroupHistoryPage {
return GroupHistoryPage(changeSet.map { DecryptedGroupChangeLog(it.groupSnapshot, it.groupChange) }, GroupHistoryPage.PagingData.NONE) return GroupHistoryPage(changeSet.map { DecryptedGroupChangeLog(it.groupSnapshot, it.groupChange) }, null, GroupHistoryPage.PagingData.NONE)
} }
} }
@ -193,7 +193,8 @@ fun groupRecord(
decryptedGroup.revision, decryptedGroup.revision,
decryptedGroup.encode(), decryptedGroup.encode(),
distributionId, distributionId,
System.currentTimeMillis() System.currentTimeMillis(),
0
) )
) )
} }

View file

@ -58,7 +58,7 @@ object RecipientDatabaseTestUtils {
profileSharing: Boolean = false, profileSharing: Boolean = false,
lastProfileFetch: Long = 0L, lastProfileFetch: Long = 0L,
notificationChannel: String? = null, notificationChannel: String? = null,
unidentifiedAccessMode: RecipientTable.UnidentifiedAccessMode = RecipientTable.UnidentifiedAccessMode.UNKNOWN, sealedSenderAccessMode: RecipientTable.SealedSenderAccessMode = RecipientTable.SealedSenderAccessMode.UNKNOWN,
capabilities: Long = 0L, capabilities: Long = 0L,
storageId: ByteArray? = null, storageId: ByteArray? = null,
mentionSetting: RecipientTable.MentionSetting = RecipientTable.MentionSetting.ALWAYS_NOTIFY, mentionSetting: RecipientTable.MentionSetting = RecipientTable.MentionSetting.ALWAYS_NOTIFY,
@ -121,7 +121,7 @@ object RecipientDatabaseTestUtils {
profileSharing = profileSharing, profileSharing = profileSharing,
lastProfileFetch = lastProfileFetch, lastProfileFetch = lastProfileFetch,
notificationChannel = notificationChannel, notificationChannel = notificationChannel,
unidentifiedAccessMode = unidentifiedAccessMode, sealedSenderAccessMode = sealedSenderAccessMode,
capabilities = RecipientRecord.Capabilities( capabilities = RecipientRecord.Capabilities(
rawBits = capabilities, rawBits = capabilities,
deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt()) deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt())

View file

@ -24,6 +24,7 @@ import org.signal.libsignal.protocol.logging.SignalProtocolLogger
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.groups.GroupSecretParams import org.signal.libsignal.zkgroup.groups.GroupSecretParams
import org.signal.storageservice.protos.groups.GroupChangeResponse
import org.signal.storageservice.protos.groups.Member import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember import org.signal.storageservice.protos.groups.local.DecryptedMember
@ -111,9 +112,9 @@ class GroupManagerV2Test_edit {
every { groupTable.getGroup(groupId) } returns data.groupRecord every { groupTable.getGroup(groupId) } returns data.groupRecord
every { groupTable.requireGroup(groupId) } returns data.groupRecord.get() every { groupTable.requireGroup(groupId) } returns data.groupRecord.get()
every { groupTable.update(any<GroupId.V2>(), any()) } returns Unit every { groupTable.update(any<GroupId.V2>(), any(), any()) } returns Unit
every { sendGroupUpdateHelper.sendGroupUpdate(masterKey, any(), any(), any()) } returns GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1) every { sendGroupUpdateHelper.sendGroupUpdate(masterKey, any(), any(), any()) } returns GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1)
every { groupsV2API.patchGroup(any(), any(), any()) } returns data.groupChange!! every { groupsV2API.patchGroup(any(), any(), any()) } returns GroupChangeResponse(groupChange = data.groupChange!!)
} }
private fun editGroup(perform: GroupManagerV2.GroupEditor.() -> Unit) { private fun editGroup(perform: GroupManagerV2.GroupEditor.() -> Unit) {
@ -122,7 +123,7 @@ class GroupManagerV2Test_edit {
private fun then(then: (DecryptedGroup) -> Unit) { private fun then(then: (DecryptedGroup) -> Unit) {
val decryptedGroupArg = slot<DecryptedGroup>() val decryptedGroupArg = slot<DecryptedGroup>()
verify { groupTable.update(groupId, capture(decryptedGroupArg)) } verify { groupTable.update(groupId, capture(decryptedGroupArg), any()) }
then(decryptedGroupArg.captured) then(decryptedGroupArg.captured)
} }

View file

@ -41,7 +41,7 @@ public final class GroupStatePatcherTest {
@Test @Test
public void unknown_group_with_no_states_to_update() { public void unknown_group_with_no_states_to_update() {
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, emptyList()), 10); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, emptyList(), null), 10);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList())); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -52,7 +52,7 @@ public final class GroupStatePatcherTest {
public void known_group_with_no_states_to_update() { public void known_group_with_no_states_to_update() {
DecryptedGroup currentState = state(0); DecryptedGroup currentState = state(0);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, emptyList()), 10); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, emptyList(), null), 10);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList())); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -63,7 +63,7 @@ public final class GroupStatePatcherTest {
public void unknown_group_single_state_to_update() { public void unknown_group_single_state_to_update() {
DecryptedGroupChangeLog log0 = serverLogEntry(0); DecryptedGroupChangeLog log0 = serverLogEntry(0);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, singletonList(log0)), 10); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, singletonList(log0), null), 10);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -75,7 +75,7 @@ public final class GroupStatePatcherTest {
DecryptedGroup currentState = state(0); DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntry(1); DecryptedGroupChangeLog log1 = serverLogEntry(1);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log1)), 1); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log1), null), 1);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log1)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log1))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -88,7 +88,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log1 = serverLogEntry(1); DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log2 = serverLogEntry(2); DecryptedGroupChangeLog log2 = serverLogEntry(2);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2)), 2); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2), null), 2);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -101,7 +101,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log1 = serverLogEntry(1); DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log2 = serverLogEntry(2); DecryptedGroupChangeLog log2 = serverLogEntry(2);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2)), 2); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2), null), 2);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log2)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log2))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -115,7 +115,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log2 = serverLogEntry(2); DecryptedGroupChangeLog log2 = serverLogEntry(2);
DecryptedGroupChangeLog log3 = serverLogEntry(3); DecryptedGroupChangeLog log3 = serverLogEntry(3);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3)), 2); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3), null), 2);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
assertNewState(log2.getGroup(), singletonList(log3), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); assertNewState(log2.getGroup(), singletonList(log3), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
@ -129,7 +129,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log2 = serverLogEntry(2); DecryptedGroupChangeLog log2 = serverLogEntry(2);
DecryptedGroupChangeLog log3 = serverLogEntry(3); DecryptedGroupChangeLog log3 = serverLogEntry(3);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2), asLocal(log3)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2), asLocal(log3))));
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
@ -142,7 +142,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log1 = serverLogEntry(Integer.MAX_VALUE - 1); DecryptedGroupChangeLog log1 = serverLogEntry(Integer.MAX_VALUE - 1);
DecryptedGroupChangeLog log2 = serverLogEntry(Integer.MAX_VALUE); DecryptedGroupChangeLog log2 = serverLogEntry(Integer.MAX_VALUE);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log2))));
assertNewState(log2.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); assertNewState(log2.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
@ -153,7 +153,7 @@ public final class GroupStatePatcherTest {
public void unknown_group_single_state_to_update_with_missing_change() { public void unknown_group_single_state_to_update_with_missing_change() {
DecryptedGroupChangeLog log0 = serverLogEntryWholeStateOnly(0); DecryptedGroupChangeLog log0 = serverLogEntryWholeStateOnly(0);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, singletonList(log0)), 10); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(null, singletonList(log0), null), 10);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log0))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -165,7 +165,7 @@ public final class GroupStatePatcherTest {
DecryptedGroup currentState = state(0); DecryptedGroup currentState = state(0);
DecryptedGroupChangeLog log1 = serverLogEntryWholeStateOnly(1); DecryptedGroupChangeLog log1 = serverLogEntryWholeStateOnly(1);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log1)), 1); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log1), null), 1);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(localLogEntryNoEditor(1)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(localLogEntryNoEditor(1))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -179,7 +179,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log2 = serverLogEntryWholeStateOnly(2); DecryptedGroupChangeLog log2 = serverLogEntryWholeStateOnly(2);
DecryptedGroupChangeLog log3 = serverLogEntry(3); DecryptedGroupChangeLog log3 = serverLogEntry(3);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2, log3), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), localLogEntryNoEditor(2), asLocal(log3)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), localLogEntryNoEditor(2), asLocal(log3))));
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
@ -192,7 +192,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log1 = serverLogEntry(1); DecryptedGroupChangeLog log1 = serverLogEntry(1);
DecryptedGroupChangeLog log3 = serverLogEntry(3); DecryptedGroupChangeLog log3 = serverLogEntry(3);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log3)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log3), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log3)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), asLocal(log3))));
assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
@ -220,7 +220,7 @@ public final class GroupStatePatcherTest {
.build(); .build();
DecryptedGroupChangeLog log4 = new DecryptedGroupChangeLog(state4, change(4)); DecryptedGroupChangeLog log4 = new DecryptedGroupChangeLog(state4, change(4));
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log3, log4)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log3, log4), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
new AppliedGroupChangeLog(state3a, log3.getChange()), new AppliedGroupChangeLog(state3a, log3.getChange()),
@ -241,7 +241,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log7 = serverLogEntryWholeStateOnly(7); DecryptedGroupChangeLog log7 = serverLogEntryWholeStateOnly(7);
DecryptedGroupChangeLog log8 = serverLogEntryWholeStateOnly(8); DecryptedGroupChangeLog log8 = serverLogEntryWholeStateOnly(8);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log6, log7, log8)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log6, log7, log8), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(localLogEntryNoEditor(6), localLogEntryNoEditor(7), localLogEntryNoEditor(8)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(localLogEntryNoEditor(6), localLogEntryNoEditor(7), localLogEntryNoEditor(8))));
assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
@ -255,7 +255,7 @@ public final class GroupStatePatcherTest {
DecryptedGroupChangeLog log8 = logEntryMissingState(8); DecryptedGroupChangeLog log8 = logEntryMissingState(8);
DecryptedGroupChangeLog log9 = logEntryMissingState(9); DecryptedGroupChangeLog log9 = logEntryMissingState(9);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log7, log8, log9)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log7, log8, log9), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(serverLogEntry(7)), asLocal(serverLogEntry(8)), asLocal(serverLogEntry(9))))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(serverLogEntry(7)), asLocal(serverLogEntry(8)), asLocal(serverLogEntry(9)))));
assertNewState(state(9), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); assertNewState(state(9), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
@ -287,7 +287,7 @@ public final class GroupStatePatcherTest {
.build(), .build(),
change(9)); change(9));
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log7, log8, log9)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log7, log8, log9), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log7), assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log7),
new AppliedGroupChangeLog(state7b, log8.getChange()), new AppliedGroupChangeLog(state7b, log8.getChange()),
@ -305,7 +305,7 @@ public final class GroupStatePatcherTest {
DecryptedGroup currentState = state(6); DecryptedGroup currentState = state(6);
DecryptedGroupChangeLog log6 = serverLogEntryWholeStateOnly(6); DecryptedGroupChangeLog log6 = serverLogEntryWholeStateOnly(6);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log6)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log6), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList())); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
assertNewState(state(6), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); assertNewState(state(6), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges());
@ -320,7 +320,7 @@ public final class GroupStatePatcherTest {
.build(); .build();
DecryptedGroupChangeLog log6 = serverLogEntry(6); DecryptedGroupChangeLog log6 = serverLogEntry(6);
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log6)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log6), null), LATEST);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log6)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(asLocal(log6))));
assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty());
@ -352,7 +352,7 @@ public final class GroupStatePatcherTest {
.newMembers(Collections.singletonList(newMember)) .newMembers(Collections.singletonList(newMember))
.build()); .build());
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8), null), LATEST);
assertNotNull(log8.getGroup()); assertNotNull(log8.getGroup());
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList())); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList()));
@ -392,7 +392,7 @@ public final class GroupStatePatcherTest {
.newMembers(Collections.singletonList(newMember)) .newMembers(Collections.singletonList(newMember))
.build(); .build();
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8), null), LATEST);
assertNotNull(log8.getGroup()); assertNotNull(log8.getGroup());
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new AppliedGroupChangeLog(log8.getGroup(), expectedChange)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new AppliedGroupChangeLog(log8.getGroup(), expectedChange))));
@ -433,7 +433,7 @@ public final class GroupStatePatcherTest {
.newAvatar(new DecryptedString.Builder().value_("Group Avatar " + 8).build()) .newAvatar(new DecryptedString.Builder().value_("Group Avatar " + 8).build())
.build(); .build();
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8)), LATEST); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, singletonList(log8), null), LATEST);
assertNotNull(log8.getGroup()); assertNotNull(log8.getGroup());
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new AppliedGroupChangeLog(log8.getGroup(), expectedChange)))); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(singletonList(new AppliedGroupChangeLog(log8.getGroup(), expectedChange))));
@ -454,7 +454,7 @@ public final class GroupStatePatcherTest {
.newTitle(new DecryptedString.Builder().value_(log1.getGroup().title).build()) .newTitle(new DecryptedString.Builder().value_(log1.getGroup().title).build())
.build()); .build());
AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2)), 2); AdvanceGroupStateResult advanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(new GroupStateDiff(currentState, asList(log1, log2), null), 2);
assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1), assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(asList(asLocal(log1),
new AppliedGroupChangeLog(log2.getGroup(), new DecryptedGroupChange.Builder() new AppliedGroupChangeLog(log2.getGroup(), new DecryptedGroupChange.Builder()

View file

@ -56,9 +56,12 @@ import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
import org.thoughtcrime.securesms.testutil.SystemOutLogger import org.thoughtcrime.securesms.testutil.SystemOutLogger
import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException
import org.whispersystems.signalservice.api.groupsv2.ReceivedGroupSendEndorsements
import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.push.ServiceIds import org.whispersystems.signalservice.api.push.ServiceIds
@ -88,6 +91,7 @@ class GroupsV2StateProcessorTest {
private lateinit var recipientTable: RecipientTable private lateinit var recipientTable: RecipientTable
private lateinit var groupsV2API: GroupsV2Api private lateinit var groupsV2API: GroupsV2Api
private lateinit var groupsV2Authorization: GroupsV2Authorization private lateinit var groupsV2Authorization: GroupsV2Authorization
private lateinit var groupsV2Operations: GroupsV2Operations
private lateinit var profileAndMessageHelper: ProfileAndMessageHelper private lateinit var profileAndMessageHelper: ProfileAndMessageHelper
private lateinit var jobManager: JobManager private lateinit var jobManager: JobManager
@ -104,6 +108,7 @@ class GroupsV2StateProcessorTest {
groupTable = mockk() groupTable = mockk()
recipientTable = mockk() recipientTable = mockk()
groupsV2API = mockk() groupsV2API = mockk()
groupsV2Operations = mockk()
groupsV2Authorization = mockk() groupsV2Authorization = mockk()
profileAndMessageHelper = spyk(ProfileAndMessageHelper(serviceIds.aci, masterKey, groupId)) profileAndMessageHelper = spyk(ProfileAndMessageHelper(serviceIds.aci, masterKey, groupId))
jobManager = mockk() jobManager = mockk()
@ -112,6 +117,7 @@ class GroupsV2StateProcessorTest {
every { AppDependencies.jobManager } returns jobManager every { AppDependencies.jobManager } returns jobManager
every { AppDependencies.signalServiceAccountManager.getGroupsV2Api() } returns groupsV2API every { AppDependencies.signalServiceAccountManager.getGroupsV2Api() } returns groupsV2API
every { AppDependencies.groupsV2Authorization } returns groupsV2Authorization every { AppDependencies.groupsV2Authorization } returns groupsV2Authorization
every { AppDependencies.groupsV2Operations } returns groupsV2Operations
mockkObject(SignalDatabase) mockkObject(SignalDatabase)
every { SignalDatabase.groups } returns groupTable every { SignalDatabase.groups } returns groupTable
@ -120,6 +126,8 @@ class GroupsV2StateProcessorTest {
mockkObject(ProfileAndMessageHelper) mockkObject(ProfileAndMessageHelper)
every { ProfileAndMessageHelper.create(any(), any(), any()) } returns profileAndMessageHelper every { ProfileAndMessageHelper.create(any(), any(), any()) } returns profileAndMessageHelper
every { groupsV2Operations.forGroup(secretParams) } answers { callOriginal() }
processor = GroupsV2StateProcessor.forGroup(serviceIds, masterKey, secretParams) processor = GroupsV2StateProcessor.forGroup(serviceIds, masterKey, secretParams)
} }
@ -142,11 +150,11 @@ class GroupsV2StateProcessorTest {
every { groupsV2Authorization.getAuthorizationForToday(serviceIds, secretParams) } returns null every { groupsV2Authorization.getAuthorizationForToday(serviceIds, secretParams) } returns null
if (data.expectTableUpdate) { if (data.expectTableUpdate) {
justRun { groupTable.update(any<GroupMasterKey>(), any<DecryptedGroup>()) } justRun { groupTable.update(any<GroupMasterKey>(), any<DecryptedGroup>(), any<ReceivedGroupSendEndorsements>()) }
} }
if (data.expectTableCreate) { if (data.expectTableCreate) {
every { groupTable.create(any<GroupMasterKey>(), any<DecryptedGroup>()) } returns groupId every { groupTable.create(any<GroupMasterKey>(), any<DecryptedGroup>(), any<ReceivedGroupSendEndorsements>()) } returns groupId
} }
if (data.expectTableUpdate || data.expectTableCreate) { if (data.expectTableUpdate || data.expectTableCreate) {
@ -155,11 +163,11 @@ class GroupsV2StateProcessorTest {
} }
data.serverState?.let { serverState -> data.serverState?.let { serverState ->
every { groupsV2API.getGroup(any(), any()) } returns serverState every { groupsV2API.getGroup(any(), any()) } returns DecryptedGroupResponse(serverState, null)
} }
data.changeSet?.let { changeSet -> data.changeSet?.let { changeSet ->
every { groupsV2API.getGroupHistoryPage(any(), data.requestedRevision, any(), data.includeFirst) } returns changeSet.toApiResponse() every { groupsV2API.getGroupHistoryPage(any(), data.requestedRevision, any(), data.includeFirst, 0) } returns changeSet.toApiResponse()
} }
every { groupsV2API.getGroupAsResult(any(), any()) } answers { callOriginal() } every { groupsV2API.getGroupAsResult(any(), any()) } answers { callOriginal() }
@ -241,7 +249,7 @@ class GroupsV2StateProcessorTest {
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
assertThat("title changed to match server", result.latestServer!!.title, `is`("Asdf")) assertThat("title changed to match server", result.latestServer!!.title, `is`("Asdf"))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -279,7 +287,7 @@ class GroupsV2StateProcessorTest {
assertThat("revision matches server", result.latestServer!!.revision, `is`(7)) assertThat("revision matches server", result.latestServer!!.revision, `is`(7))
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("Asdf!")) assertThat("title changed on server to final result", result.latestServer!!.title, `is`("Asdf!"))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -323,7 +331,7 @@ class GroupsV2StateProcessorTest {
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("And beyond")) assertThat("title changed on server to final result", result.latestServer!!.title, `is`("And beyond"))
assertThat("Description updated in change after full snapshot", result.latestServer!!.description, `is`("Description")) assertThat("Description updated in change after full snapshot", result.latestServer!!.description, `is`("Description"))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -351,7 +359,7 @@ class GroupsV2StateProcessorTest {
assertThat("revision matches peer change", result.latestServer!!.revision, `is`(6)) assertThat("revision matches peer change", result.latestServer!!.revision, `is`(6))
assertThat("timer changed by peer change", result.latestServer!!.disappearingMessagesTimer!!.duration, `is`(5000)) assertThat("timer changed by peer change", result.latestServer!!.disappearingMessagesTimer!!.duration, `is`(5000))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -383,7 +391,7 @@ class GroupsV2StateProcessorTest {
assertThat("member promoted by peer change", result.latestServer!!.members.map { it.aciBytes }, hasItem(selfAci.toByteString())) assertThat("member promoted by peer change", result.latestServer!!.members.map { it.aciBytes }, hasItem(selfAci.toByteString()))
verify { jobManager.add(ofType(DirectoryRefreshJob::class)) } verify { jobManager.add(ofType(DirectoryRefreshJob::class)) }
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -426,7 +434,7 @@ class GroupsV2StateProcessorTest {
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
assertThat("revision matches server", result.latestServer!!.revision, `is`(2)) assertThat("revision matches server", result.latestServer!!.revision, `is`(2))
verify { groupsV2API.getGroupHistoryPage(secretParams, 1, any(), false) } verify { groupsV2API.getGroupHistoryPage(secretParams, 1, any(), false, 0) }
unmockkStatic(DecryptedGroupUtil::class) unmockkStatic(DecryptedGroupUtil::class)
} }
@ -485,7 +493,7 @@ class GroupsV2StateProcessorTest {
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
assertThat("revision matches server", result.latestServer!!.revision, `is`(3)) assertThat("revision matches server", result.latestServer!!.revision, `is`(3))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -513,7 +521,7 @@ class GroupsV2StateProcessorTest {
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
assertThat("revision matches server", result.latestServer!!.revision, `is`(2)) assertThat("revision matches server", result.latestServer!!.revision, `is`(2))
verify { groupTable.create(masterKey, result.latestServer!!) } verify { groupTable.create(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -551,7 +559,7 @@ class GroupsV2StateProcessorTest {
assertThat("title matches that as it was in revision added", result.latestServer!!.title, `is`("Baking Signal for Science")) assertThat("title matches that as it was in revision added", result.latestServer!!.title, `is`("Baking Signal for Science"))
verify { jobManager.add(ofType(RequestGroupV2InfoJob::class)) } verify { jobManager.add(ofType(RequestGroupV2InfoJob::class)) }
verify { groupTable.create(masterKey, result.latestServer!!) } verify { groupTable.create(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -577,7 +585,7 @@ class GroupsV2StateProcessorTest {
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
assertThat("revision matches latest server", result.latestServer!!.revision, `is`(10)) assertThat("revision matches latest server", result.latestServer!!.revision, `is`(10))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -613,7 +621,7 @@ class GroupsV2StateProcessorTest {
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
assertThat("revision matches server", result.latestServer!!.revision, `is`(3)) assertThat("revision matches server", result.latestServer!!.revision, `is`(3))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -662,7 +670,7 @@ class GroupsV2StateProcessorTest {
assertThat("title matches revision approved at", result.latestServer!!.title, `is`("Beam me up")) assertThat("title matches revision approved at", result.latestServer!!.title, `is`("Beam me up"))
verify { jobManager.add(ofType(RequestGroupV2InfoJob::class)) } verify { jobManager.add(ofType(RequestGroupV2InfoJob::class)) }
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
@Test @Test
@ -705,7 +713,7 @@ class GroupsV2StateProcessorTest {
assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED))
assertThat("revision matches latest revision on server", result.latestServer!!.revision, `is`(101)) assertThat("revision matches latest revision on server", result.latestServer!!.revision, `is`(101))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
/** /**
@ -756,7 +764,7 @@ class GroupsV2StateProcessorTest {
assertThat("group update messages contains new member add", updateMessageContextArgs.map { it.change!!.newMembers }, hasItem(hasItem(secondOther))) assertThat("group update messages contains new member add", updateMessageContextArgs.map { it.change!!.newMembers }, hasItem(hasItem(secondOther)))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
/** /**
@ -792,7 +800,7 @@ class GroupsV2StateProcessorTest {
assertThat("group update messages contains new member add", updateMessageContextArgs.map { it.change!!.newMembers }, hasItem(hasItem(secondOther))) assertThat("group update messages contains new member add", updateMessageContextArgs.map { it.change!!.newMembers }, hasItem(hasItem(secondOther)))
assertThat("group update messages contains title change", updateMessageContextArgs.mapNotNull { it.change!!.newTitle }.any { it.value_ == "Changed" }) assertThat("group update messages contains title change", updateMessageContextArgs.mapNotNull { it.change!!.newTitle }.any { it.value_ == "Changed" })
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
/** /**
@ -874,6 +882,6 @@ class GroupsV2StateProcessorTest {
assertThat("revision matches server", result.latestServer!!.revision, `is`(10)) assertThat("revision matches server", result.latestServer!!.revision, `is`(10))
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("Asdf!")) assertThat("title changed on server to final result", result.latestServer!!.title, `is`("Asdf!"))
verify { groupTable.update(masterKey, result.latestServer!!) } verify { groupTable.update(masterKey, result.latestServer!!, null) }
} }
} }

View file

@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api;
import com.squareup.wire.FieldEncoding; import com.squareup.wire.FieldEncoding;
import org.signal.core.util.Base64;
import org.signal.libsignal.net.Network; import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException; 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.archive.ArchiveApi;
import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; 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.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; 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.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper; import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper;
import org.signal.core.util.Base64;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -757,7 +758,7 @@ public class SignalServiceAccountManager {
throws NonSuccessfulResponseCodeException, PushNetworkException throws NonSuccessfulResponseCodeException, PushNetworkException
{ {
try { 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(); return credential.getExpiringProfileKeyCredential();
} catch (InterruptedException | TimeoutException e) { } catch (InterruptedException | TimeoutException e) {
throw new PushNetworkException(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.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream; 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.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@ -98,7 +98,7 @@ public class SignalServiceMessageReceiver {
public ListenableFuture<ProfileAndCredential> retrieveProfile(SignalServiceAddress address, public ListenableFuture<ProfileAndCredential> retrieveProfile(SignalServiceAddress address,
Optional<ProfileKey> profileKey, Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
SignalServiceProfile.RequestType requestType, SignalServiceProfile.RequestType requestType,
Locale locale) Locale locale)
{ {
@ -115,16 +115,16 @@ public class SignalServiceMessageReceiver {
} }
if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) { if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) {
return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), unidentifiedAccess, locale); return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), sealedSenderAccess, locale);
} else { } 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, return new ProfileAndCredential(profile,
SignalServiceProfile.RequestType.PROFILE, SignalServiceProfile.RequestType.PROFILE,
Optional.empty()); Optional.empty());
}); });
} }
} else { } else {
return FutureTransformers.map(socket.retrieveProfile(address, unidentifiedAccess, locale), profile -> { return FutureTransformers.map(socket.retrieveProfile(address, sealedSenderAccess, locale), profile -> {
return new ProfileAndCredential(profile, return new ProfileAndCredential(profile,
SignalServiceProfile.RequestType.PROFILE, SignalServiceProfile.RequestType.PROFILE,
Optional.empty()); Optional.empty());
@ -146,8 +146,8 @@ public class SignalServiceMessageReceiver {
return new FileInputStream(destination); return new FileInputStream(destination);
} }
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull Optional<UnidentifiedAccess> unidentifiedAccess, @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) { public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
return socket.performIdentityCheck(request, unidentifiedAccess, 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.PreKeyBundle;
import org.signal.libsignal.protocol.state.SessionRecord; import org.signal.libsignal.protocol.state.SessionRecord;
import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.protocol.util.Pair;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.EnvelopeContent; 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.SignalGroupSessionBuilder;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder; import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; 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.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; 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.AttachmentDigest;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import org.whispersystems.signalservice.internal.push.AttachmentPointer; 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.AttachmentUploadForm;
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
import org.whispersystems.signalservice.internal.push.BodyRange; import org.whispersystems.signalservice.internal.push.BodyRange;
import org.whispersystems.signalservice.internal.push.CallMessage; import org.whispersystems.signalservice.internal.push.CallMessage;
import org.whispersystems.signalservice.internal.push.Content; 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.PartialSendCompleteListener;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.ByteArrayUtil;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -145,10 +146,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -223,7 +222,7 @@ public class SignalServiceMessageSender {
* @param message The read receipt to deliver. * @param message The read receipt to deliver.
*/ */
public SendMessageResult sendReceipt(SignalServiceAddress recipient, public SendMessageResult sendReceipt(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
SignalServiceReceiptMessage message, SignalServiceReceiptMessage message,
boolean includePniSignature) boolean includePniSignature)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
@ -240,14 +239,14 @@ public class SignalServiceMessageSender {
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); 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. * Send a retry receipt for a bad-encrypted envelope.
*/ */
public void sendRetryReceipt(SignalServiceAddress recipient, public void sendRetryReceipt(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
Optional<byte[]> groupId, Optional<byte[]> groupId,
DecryptionErrorMessage errorMessage) DecryptionErrorMessage errorMessage)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
@ -258,16 +257,16 @@ public class SignalServiceMessageSender {
PlaintextContent content = new PlaintextContent(errorMessage); PlaintextContent content = new PlaintextContent(errorMessage);
EnvelopeContent envelopeContent = EnvelopeContent.plaintext(content, groupId); 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. * 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, public void sendTyping(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess, List<SealedSenderAccess> sealedSenderAccesses,
SignalServiceTypingMessage message, SignalServiceTypingMessage message,
CancelationSignal cancelationSignal) CancelationSignal cancelationSignal)
throws IOException throws IOException
{ {
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a typing message to " + recipients.size() + " recipient(s) using 1:1 messages."); 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); Content content = createTypingContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); 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. * 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, public void sendGroupTyping(DistributionId distributionId,
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess, List<UnidentifiedAccess> unidentifiedAccess,
SignalServiceTypingMessage message) @Nullable GroupSendEndorsements groupSendEndorsements,
SignalServiceTypingMessage message)
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
{ {
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a typing message to " + recipients.size() + " recipient(s) using sender key."); Log.d(TAG, "[" + message.getTimestamp() + "] Sending a typing message to " + recipients.size() + " recipient(s) using sender key.");
Content content = createTypingContent(message); 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); 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 * 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*" * method named "sendGroup*"
*/ */
public List<SendMessageResult> sendGroupStory(DistributionId distributionId, public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
Optional<byte[]> groupId, Optional<byte[]> groupId,
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess, List<UnidentifiedAccess> unidentifiedAccess,
boolean isRecipientUpdate, @Nullable GroupSendEndorsements groupSendEndorsements,
SignalServiceStoryMessage message, boolean isRecipientUpdate,
long timestamp, SignalServiceStoryMessage message,
long timestamp,
Set<SignalServiceStoryMessageRecipient> manifest, Set<SignalServiceStoryMessageRecipient> manifest,
PartialSendBatchCompleteListener partialListener) PartialSendBatchCompleteListener partialListener)
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
{ {
Log.d(TAG, "[" + timestamp + "] Sending a story."); Log.d(TAG, "[" + timestamp + "] Sending a story.");
Content content = createStoryContent(message); 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) { if (partialListener != null) {
partialListener.onPartialSendComplete(sendMessageResults); partialListener.onPartialSendComplete(sendMessageResults);
@ -354,7 +355,7 @@ public class SignalServiceMessageSender {
* @throws IOException * @throws IOException
*/ */
public void sendCallMessage(SignalServiceAddress recipient, public void sendCallMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
SignalServiceCallMessage message) SignalServiceCallMessage message)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
@ -364,11 +365,11 @@ public class SignalServiceMessageSender {
Content content = createCallContent(message); Content content = createCallContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.empty()); 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, public List<SendMessageResult> sendCallMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess, List<SealedSenderAccess> sealedSenderAccesses,
SignalServiceCallMessage message) SignalServiceCallMessage message)
throws IOException throws IOException
{ {
@ -378,12 +379,13 @@ public class SignalServiceMessageSender {
Content content = createCallContent(message); Content content = createCallContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.empty()); 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, public List<SendMessageResult> sendCallMessage(DistributionId distributionId,
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess, List<UnidentifiedAccess> unidentifiedAccess,
@Nullable GroupSendEndorsements groupSendEndorsements,
SignalServiceCallMessage message, SignalServiceCallMessage message,
PartialSendBatchCompleteListener partialListener) PartialSendBatchCompleteListener partialListener)
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
@ -392,7 +394,7 @@ public class SignalServiceMessageSender {
Content content = createCallContent(message); 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) { if (partialListener != null) {
partialListener.onPartialSendComplete(results); partialListener.onPartialSendComplete(results);
@ -423,27 +425,27 @@ public class SignalServiceMessageSender {
* @throws UntrustedIdentityException * @throws UntrustedIdentityException
* @throws IOException * @throws IOException
*/ */
public SendMessageResult sendDataMessage(SignalServiceAddress recipient, public SendMessageResult sendDataMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
ContentHint contentHint, ContentHint contentHint,
SignalServiceDataMessage message, SignalServiceDataMessage message,
IndividualSendEvents sendEvents, IndividualSendEvents sendEvents,
boolean urgent, boolean urgent,
boolean includePniSignature) boolean includePniSignature)
throws UntrustedIdentityException, IOException throws UntrustedIdentityException, IOException
{ {
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message."); Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message.");
Content content = createMessageContent(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. * Send an edit message to a single recipient.
*/ */
public SendMessageResult sendEditMessage(SignalServiceAddress recipient, public SendMessageResult sendEditMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
ContentHint contentHint, ContentHint contentHint,
SignalServiceDataMessage message, SignalServiceDataMessage message,
IndividualSendEvents sendEvents, IndividualSendEvents sendEvents,
@ -455,14 +457,14 @@ public class SignalServiceMessageSender {
Content content = createEditMessageContent(new SignalServiceEditMessage(targetSentTimestamp, message)); 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. * Sends content to a single recipient.
*/ */
private SendMessageResult sendContent(SignalServiceAddress recipient, private SendMessageResult sendContent(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
ContentHint contentHint, ContentHint contentHint,
SignalServiceDataMessage message, SignalServiceDataMessage message,
IndividualSendEvents sendEvents, IndividualSendEvents sendEvents,
@ -481,7 +483,7 @@ public class SignalServiceMessageSender {
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
long timestamp = message.getTimestamp(); 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(); sendEvents.onMessageSent();
@ -489,7 +491,7 @@ public class SignalServiceMessageSender {
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false, Collections.emptySet()); Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false, Collections.emptySet());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); 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(); sendEvents.onSyncMessageSent();
@ -509,13 +511,13 @@ public class SignalServiceMessageSender {
/** /**
* Sends the provided {@link SenderKeyDistributionMessage} to the specified recipients. * Sends the provided {@link SenderKeyDistributionMessage} to the specified recipients.
*/ */
public List<SendMessageResult> sendSenderKeyDistributionMessage(DistributionId distributionId, public List<SendMessageResult> sendSenderKeyDistributionMessage(DistributionId distributionId,
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess, List<SealedSenderAccess> sealedSenderAccesses,
SenderKeyDistributionMessage message, SenderKeyDistributionMessage message,
Optional<byte[]> groupId, Optional<byte[]> groupId,
boolean urgent, boolean urgent,
boolean story) boolean story)
throws IOException throws IOException
{ {
ByteString distributionBytes = ByteString.of(message.serialize()); 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); 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. * Resend a previously-sent message.
*/ */
public SendMessageResult resendContent(SignalServiceAddress address, public SendMessageResult resendContent(SignalServiceAddress address,
Optional<UnidentifiedAccessPair> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
long timestamp, long timestamp,
Content content, Content content,
ContentHint contentHint, ContentHint contentHint,
@ -543,13 +545,12 @@ public class SignalServiceMessageSender {
Log.d(TAG, "[" + timestamp + "] Resending content."); Log.d(TAG, "[" + timestamp + "] Resending content.");
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, groupId); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, groupId);
Optional<UnidentifiedAccess> access = unidentifiedAccess.isPresent() ? unidentifiedAccess.get().getTargetUnidentifiedAccess() : Optional.empty();
if (address.getServiceId().equals(localAddress.getServiceId())) { 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, public List<SendMessageResult> sendGroupDataMessage(DistributionId distributionId,
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess, List<UnidentifiedAccess> unidentifiedAccess,
@Nullable GroupSendEndorsements groupSendEndorsements,
boolean isRecipientUpdate, boolean isRecipientUpdate,
ContentHint contentHint, ContentHint contentHint,
SignalServiceDataMessage message, SignalServiceDataMessage message,
@ -579,7 +581,7 @@ public class SignalServiceMessageSender {
} }
Optional<byte[]> groupId = message.getGroupId(); 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) { if (partialListener != null) {
partialListener.onPartialSendComplete(results); partialListener.onPartialSendComplete(results);
@ -591,7 +593,7 @@ public class SignalServiceMessageSender {
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate, Collections.emptySet()); Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate, Collections.emptySet());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); 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(); 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* * @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. * the calling thread.
*/ */
public List<SendMessageResult> sendDataMessage(List<SignalServiceAddress> recipients, public List<SendMessageResult> sendDataMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess, List<SealedSenderAccess> sealedSenderAccesses,
boolean isRecipientUpdate, boolean isRecipientUpdate,
ContentHint contentHint, ContentHint contentHint,
SignalServiceDataMessage message, SignalServiceDataMessage message,
LegacyGroupEvents sendEvents, LegacyGroupEvents sendEvents,
PartialSendCompleteListener partialListener, PartialSendCompleteListener partialListener,
CancelationSignal cancelationSignal, CancelationSignal cancelationSignal,
boolean urgent) boolean urgent)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message to " + recipients.size() + " recipients."); Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message to " + recipients.size() + " recipients.");
@ -621,7 +623,7 @@ public class SignalServiceMessageSender {
Content content = createMessageContent(message); Content content = createMessageContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
long timestamp = message.getTimestamp(); 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; boolean needsSyncInResults = false;
sendEvents.onMessageSent(); sendEvents.onMessageSent();
@ -642,7 +644,7 @@ public class SignalServiceMessageSender {
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet()); Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); 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(); 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* * @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. * the calling thread.
*/ */
public List<SendMessageResult> sendEditMessage(List<SignalServiceAddress> recipients, public List<SendMessageResult> sendEditMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess, List<SealedSenderAccess> sealedSenderAccesses,
boolean isRecipientUpdate, boolean isRecipientUpdate,
ContentHint contentHint, ContentHint contentHint,
SignalServiceDataMessage message, SignalServiceDataMessage message,
LegacyGroupEvents sendEvents, LegacyGroupEvents sendEvents,
PartialSendCompleteListener partialListener, PartialSendCompleteListener partialListener,
CancelationSignal cancelationSignal, CancelationSignal cancelationSignal,
boolean urgent, boolean urgent,
long targetSentTimestamp) long targetSentTimestamp)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
Log.d(TAG, "[" + message.getTimestamp() + "] Sending a edit message to " + recipients.size() + " recipients."); 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)); Content content = createEditMessageContent(new SignalServiceEditMessage(targetSentTimestamp, message));
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
long timestamp = message.getTimestamp(); 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; boolean needsSyncInResults = false;
sendEvents.onMessageSent(); sendEvents.onMessageSent();
@ -694,7 +696,7 @@ public class SignalServiceMessageSender {
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet()); Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); 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(); sendEvents.onSyncMessageSent();
@ -706,17 +708,17 @@ public class SignalServiceMessageSender {
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
Log.d(TAG, "[" + dataMessage.getTimestamp() + "] Sending self-sync message."); Log.d(TAG, "[" + dataMessage.getTimestamp() + "] Sending self-sync message.");
return sendSyncMessage(createSelfSendSyncMessage(dataMessage), Optional.empty()); return sendSyncMessage(createSelfSendSyncMessage(dataMessage));
} }
public SendMessageResult sendSelfSyncEditMessage(SignalServiceEditMessage editMessage) public SendMessageResult sendSelfSyncEditMessage(SignalServiceEditMessage editMessage)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
Log.d(TAG, "[" + editMessage.getDataMessage().getTimestamp() + "] Sending self-sync edit message for " + editMessage.getTargetSentTimestamp() + "."); 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 throws IOException, UntrustedIdentityException
{ {
Content content; Content content;
@ -736,7 +738,7 @@ public class SignalServiceMessageSender {
} else if (message.getConfiguration().isPresent()) { } else if (message.getConfiguration().isPresent()) {
content = createMultiDeviceConfigurationContent(message.getConfiguration().get()); content = createMultiDeviceConfigurationContent(message.getConfiguration().get());
} else if (message.getSent().isPresent()) { } else if (message.getSent().isPresent()) {
content = createMultiDeviceSentTranscriptContent(message.getSent().get(), unidentifiedAccess.isPresent()); content = createMultiDeviceSentTranscriptContent(message.getSent().get());
} else if (message.getStickerPackOperations().isPresent()) { } else if (message.getStickerPackOperations().isPresent()) {
content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get()); content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get());
} else if (message.getFetchType().isPresent()) { } else if (message.getFetchType().isPresent()) {
@ -774,7 +776,7 @@ public class SignalServiceMessageSender {
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); 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()); Content.Builder content = new Content.Builder().syncMessage(syncMessage.build());
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content.build(), ContentHint.IMPLICIT, Optional.empty()); 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() { public void cancelInFlightRequests() {
@ -927,19 +929,19 @@ public class SignalServiceMessageSender {
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); 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()) { if (result.getSuccess().isNeedsSync()) {
Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.encode()); Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.encode());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); 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; return result;
} }
public SendMessageResult sendNullMessage(SignalServiceAddress address, Optional<UnidentifiedAccessPair> unidentifiedAccess) public SendMessageResult sendNullMessage(SignalServiceAddress address, @Nullable SealedSenderAccess sealedSenderAccess)
throws UntrustedIdentityException, IOException throws UntrustedIdentityException, IOException
{ {
byte[] nullMessageBody = new DataMessage.Builder() byte[] nullMessageBody = new DataMessage.Builder()
@ -957,7 +959,7 @@ public class SignalServiceMessageSender {
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); 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() { private PniSignatureMessage createPniSignatureMessage() {
@ -1380,10 +1382,10 @@ public class SignalServiceMessageSender {
return container.syncMessage(builder.build()).build(); 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(); SignalServiceAddress address = transcript.getDestination().get();
Content content = createMessageContent(transcript); 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, return createMultiDeviceSentTranscriptContent(content,
@ -1424,7 +1426,7 @@ public class SignalServiceMessageSender {
unidentifiedDeliveryStatuses.add(new SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder() unidentifiedDeliveryStatuses.add(new SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder()
.destinationServiceId(result.getAddress().getServiceId().toString()) .destinationServiceId(result.getAddress().getServiceId().toString())
.unidentified(result.getSuccess().isUnidentified()) .unidentified(false)
.destinationIdentityKey(identity) .destinationIdentityKey(identity)
.build()); .build());
} }
@ -1933,15 +1935,15 @@ public class SignalServiceMessageSender {
return SignalServiceSyncMessage.forSentTranscript(transcript); return SignalServiceSyncMessage.forSentTranscript(transcript);
} }
private SendMessageResult sendMessage(SignalServiceAddress recipient, private SendMessageResult sendMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
long timestamp, long timestamp,
EnvelopeContent content, EnvelopeContent content,
boolean online, boolean online,
CancelationSignal cancelationSignal, CancelationSignal cancelationSignal,
SendEvents sendEvents, SendEvents sendEvents,
boolean urgent, boolean urgent,
boolean story) boolean story)
throws UntrustedIdentityException, IOException throws UntrustedIdentityException, IOException
{ {
enforceMaxContentSize(content); enforceMaxContentSize(content);
@ -1954,7 +1956,13 @@ public class SignalServiceMessageSender {
} }
try { 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) { if (i == 0 && sendEvents != null) {
sendEvents.onMessageEncrypted(); sendEvents.onMessageEncrypted();
} }
@ -1969,52 +1977,41 @@ public class SignalServiceMessageSender {
throw new CancelationException(); throw new CancelationException();
} }
if (!unidentifiedAccess.isPresent()) { try {
try { SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, sealedSenderAccess, story).blockingGet()).getResultOrThrow();
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());
return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); } catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) {
} catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) { // Non-technical failures shouldn't be retried with socket
// Non-technical failures shouldn't be retried with socket throw e;
throw e; } catch (WebSocketUnavailableException 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() + ")"); Log.i(TAG, "[sendMessage][" + timestamp + "] " + pipe + " unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); String pipe = sealedSenderAccess == null ? "Pipe" : "Unidentified pipe";
Log.w(TAG, "[sendMessage][" + timestamp + "] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); Throwable cause = e;
} if (e.getCause() != null) {
} else if (unidentifiedAccess.isPresent()) { cause = e.getCause();
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() + ")");
} }
Log.w(TAG, "[sendMessage][" + timestamp + "] " + pipe + " failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")");
} }
if (cancelationSignal != null && cancelationSignal.isCanceled()) { if (cancelationSignal != null && cancelationSignal.isCanceled()) {
throw new CancelationException(); 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()); return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent());
} catch (InvalidKeyException ike) { } catch (InvalidKeyException ike) {
Log.w(TAG, ike); Log.w(TAG, ike);
unidentifiedAccess = Optional.empty(); if (sealedSenderAccess != null) {
sealedSenderAccess = sealedSenderAccess.switchToFallback();
}
} catch (AuthorizationFailedException afe) { } catch (AuthorizationFailedException afe) {
if (unidentifiedAccess.isPresent()) { if (sealedSenderAccess != null) {
Log.w(TAG, "Got an AuthorizationFailedException when trying to send using sealed sender. Falling back."); Log.w(TAG, "Got an AuthorizationFailedException when trying to send using sealed sender. Falling back.");
unidentifiedAccess = Optional.empty(); sealedSenderAccess = sealedSenderAccess.switchToFallback();
} else { } else {
Log.w(TAG, "Got an AuthorizationFailedException without using sealed sender!", afe); Log.w(TAG, "Got an AuthorizationFailedException without using sealed sender!", afe);
throw afe; throw afe;
@ -2038,7 +2035,7 @@ public class SignalServiceMessageSender {
* @throws IOException - Unknown failure or a failure not representable by an unsuccessful {@code SendMessageResult}. * @throws IOException - Unknown failure or a failure not representable by an unsuccessful {@code SendMessageResult}.
*/ */
private List<SendMessageResult> sendMessage(List<SignalServiceAddress> recipients, private List<SendMessageResult> sendMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccess>> unidentifiedAccess, List<SealedSenderAccess> sealedSenderAccesses,
long timestamp, long timestamp,
EnvelopeContent content, EnvelopeContent content,
boolean online, boolean online,
@ -2052,15 +2049,16 @@ public class SignalServiceMessageSender {
Log.d(TAG, "[" + timestamp + "] Sending to " + recipients.size() + " recipients."); Log.d(TAG, "[" + timestamp + "] Sending to " + recipients.size() + " recipients.");
enforceMaxContentSize(content); enforceMaxContentSize(content);
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
List<Observable<SendMessageResult>> singleResults = new LinkedList<>(); List<Observable<SendMessageResult>> singleResults = new LinkedList<>();
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator(); Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator(); Iterator<SealedSenderAccess> sealedSenderAccessIterator = sealedSenderAccesses.iterator();
while (recipientIterator.hasNext()) { while (recipientIterator.hasNext()) {
SignalServiceAddress recipient = recipientIterator.next(); SignalServiceAddress recipient = recipientIterator.next();
Optional<UnidentifiedAccess> access = unidentifiedAccessIterator.next(); SealedSenderAccess sealedSenderAccess = sealedSenderAccessIterator.next();
singleResults.add(sendMessageRx(recipient, access, timestamp, content, online, cancelationSignal, sendEvents, urgent, story, 0).toObservable());
singleResults.add(sendMessageRx(recipient, sealedSenderAccess, timestamp, content, online, cancelationSignal, sendEvents, urgent, story, 0).toObservable());
} }
List<SendMessageResult> results; List<SendMessageResult> results;
@ -2126,7 +2124,7 @@ public class SignalServiceMessageSender {
* errors via {@code onError} * errors via {@code onError}
*/ */
private Single<SendMessageResult> sendMessageRx(SignalServiceAddress recipient, private Single<SendMessageResult> sendMessageRx(SignalServiceAddress recipient,
final Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
long timestamp, long timestamp,
EnvelopeContent content, EnvelopeContent content,
boolean online, boolean online,
@ -2140,7 +2138,7 @@ public class SignalServiceMessageSender {
enforceMaxContentSize(content); enforceMaxContentSize(content);
Single<OutgoingPushMessageList> messagesSingle = Single.fromCallable(() -> { 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) { if (retryCount == 0 && sendEvents != null) {
sendEvents.onMessageEncrypted(); sendEvents.onMessageEncrypted();
@ -2161,7 +2159,7 @@ public class SignalServiceMessageSender {
return Single.error(new CancelationException()); return Single.error(new CancelationException());
} }
return messagingService.send(messages, unidentifiedAccess, story) return messagingService.send(messages, sealedSenderAccess, story)
.map(r -> new kotlin.Pair<>(messages, r)); .map(r -> new kotlin.Pair<>(messages, r));
}) })
.observeOn(scheduler) .observeOn(scheduler)
@ -2196,14 +2194,14 @@ public class SignalServiceMessageSender {
// Non-technical failures shouldn't be retried with socket // Non-technical failures shouldn't be retried with socket
return Single.error(throwable); return Single.error(throwable);
} else if (throwable instanceof WebSocketUnavailableException) { } 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) { } else if (throwable instanceof IOException) {
Throwable cause = throwable.getCause() != null ? throwable.getCause() : throwable; 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(() -> { return Single.fromCallable(() -> {
SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess, story); SendMessageResponse response = socket.sendMessage(messages, sealedSenderAccess, story);
return SendMessageResult.success( return SendMessageResult.success(
recipient, recipient,
messages.getDevices(), messages.getDevices(),
@ -2229,7 +2227,7 @@ public class SignalServiceMessageSender {
Log.w(TAG, t); Log.w(TAG, t);
return sendMessageRx( return sendMessageRx(
recipient, recipient,
Optional.empty(), SealedSenderAccess.NONE,
timestamp, timestamp,
content, content,
online, online,
@ -2240,11 +2238,11 @@ public class SignalServiceMessageSender {
retryCount + 1 retryCount + 1
); );
} else if (t instanceof AuthorizationFailedException) { } 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."); Log.w(TAG, "Got an AuthorizationFailedException when trying to send using sealed sender. Falling back.");
return sendMessageRx( return sendMessageRx(
recipient, recipient,
Optional.empty(), sealedSenderAccess.switchToFallback(),
timestamp, timestamp,
content, content,
online, online,
@ -2268,7 +2266,7 @@ public class SignalServiceMessageSender {
}) })
.flatMap(unused -> sendMessageRx( .flatMap(unused -> sendMessageRx(
recipient, recipient,
unidentifiedAccess, sealedSenderAccess,
timestamp, timestamp,
content, content,
online, online,
@ -2288,7 +2286,7 @@ public class SignalServiceMessageSender {
}) })
.flatMap(unused -> sendMessageRx( .flatMap(unused -> sendMessageRx(
recipient, recipient,
unidentifiedAccess, sealedSenderAccess,
timestamp, timestamp,
content, content,
online, online,
@ -2336,17 +2334,18 @@ public class SignalServiceMessageSender {
* *
* This method will handle sending out SenderKeyDistributionMessages as necessary. * 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<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess, List<UnidentifiedAccess> unidentifiedAccess,
long timestamp, @Nullable GroupSendEndorsements groupSendEndorsements,
Content content, long timestamp,
ContentHint contentHint, Content content,
Optional<byte[]> groupId, ContentHint contentHint,
boolean online, Optional<byte[]> groupId,
SenderKeyGroupEvents sendEvents, boolean online,
boolean urgent, SenderKeyGroupEvents sendEvents,
boolean story) boolean urgent,
boolean story)
throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException
{ {
if (recipients.isEmpty()) { if (recipients.isEmpty()) {
@ -2364,34 +2363,36 @@ public class SignalServiceMessageSender {
accessBySid.put(addressIterator.next().getServiceId(), accessIterator.next()); accessBySid.put(addressIterator.next().getServiceId(), accessIterator.next());
} }
SealedSenderAccess sealedSenderAccess = SealedSenderAccess.forGroupSend(groupSendEndorsements, unidentifiedAccess, story);
for (int i = 0; i < RETRY_COUNT; i++) { for (int i = 0; i < RETRY_COUNT; i++) {
GroupTargetInfo targetInfo = buildGroupTargetInfo(recipients); GroupTargetInfo targetInfo = buildGroupTargetInfo(recipients);
final GroupTargetInfo targetInfoSnapshot = targetInfo; final GroupTargetInfo targetInfoSnapshot = targetInfo;
Set<SignalProtocolAddress> sharedWith = aciStore.getSenderKeySharedWith(distributionId); Set<SignalProtocolAddress> sharedWith = aciStore.getSenderKeySharedWith(distributionId);
List<SignalServiceAddress> needsSenderKey = targetInfo.destinations.stream() List<SignalServiceAddress> needsSenderKeyTargets = targetInfo.destinations.stream()
.filter(a -> !sharedWith.contains(a) || targetInfoSnapshot.sessions.get(a) == null) .filter(a -> !sharedWith.contains(a) || targetInfoSnapshot.sessions.get(a) == null)
.map(a -> ServiceId.parseOrThrow(a.getName())) .map(a -> ServiceId.parseOrThrow(a.getName()))
.distinct() .distinct()
.map(SignalServiceAddress::new) .map(SignalServiceAddress::new)
.collect(Collectors.toList()); .collect(Collectors.toList());
if (needsSenderKey.size() > 0) { if (needsSenderKeyTargets.size() > 0) {
Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Need to send the distribution message to " + needsSenderKey.size() + " addresses."); Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Need to send the distribution message to " + needsSenderKeyTargets.size() + " addresses.");
SenderKeyDistributionMessage message = getOrCreateNewGroupSession(distributionId); SenderKeyDistributionMessage senderKeyDistributionMessage = getOrCreateNewGroupSession(distributionId);
List<Optional<UnidentifiedAccessPair>> access = needsSenderKey.stream() List<UnidentifiedAccess> needsSenderKeyAccesses = needsSenderKeyTargets.stream()
.map(r -> { .map(r -> accessBySid.get(r.getServiceId()))
UnidentifiedAccess targetAccess = accessBySid.get(r.getServiceId()); .collect(Collectors.toList());
return Optional.of(new UnidentifiedAccessPair(targetAccess, targetAccess));
}) List<GroupSendFullToken> needsSenderKeyGroupSendTokens = groupSendEndorsements != null ? groupSendEndorsements.forIndividuals(needsSenderKeyTargets) : null;
.collect(Collectors.toList()); List<SealedSenderAccess> needsSenderKeySealedSenderAccesses = SealedSenderAccess.forFanOutGroupSend(needsSenderKeyGroupSendTokens, sealedSenderAccess.getSenderCertificate(), needsSenderKeyAccesses);
List<SendMessageResult> results = sendSenderKeyDistributionMessage(distributionId, List<SendMessageResult> results = sendSenderKeyDistributionMessage(distributionId,
needsSenderKey, needsSenderKeyTargets,
access, needsSenderKeySealedSenderAccesses,
message, senderKeyDistributionMessage,
groupId, groupId,
urgent, 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() List<SignalServiceAddress> successes = results.stream()
.filter(SendMessageResult::isSuccess) .filter(SendMessageResult::isSuccess)
@ -2403,7 +2404,7 @@ public class SignalServiceMessageSender {
aciStore.markSenderKeySharedWith(distributionId, successAddresses); 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(); int failureCount = results.size() - successes.size();
if (failureCount > 0) { if (failureCount > 0) {
@ -2434,26 +2435,20 @@ public class SignalServiceMessageSender {
sendEvents.onSenderKeyShared(); sendEvents.onSenderKeyShared();
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, aciStore, sessionLock, null); SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, aciStore, sessionLock, null);
SenderCertificate senderCertificate = unidentifiedAccess.get(0).getUnidentifiedCertificate();
byte[] ciphertext; byte[] ciphertext;
try { 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) { } catch (org.signal.libsignal.protocol.UntrustedIdentityException e) {
throw new UntrustedIdentityException("Untrusted during group encrypt", e.getName(), e.getUntrustedIdentity()); throw new UntrustedIdentityException("Untrusted during group encrypt", e.getName(), e.getUntrustedIdentity());
} }
sendEvents.onMessageEncrypted(); sendEvents.onMessageEncrypted();
byte[] joinedUnidentifiedAccess = new byte[16];
for (UnidentifiedAccess access : unidentifiedAccess) {
joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.getUnidentifiedAccessKey());
}
try { try {
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); return transformGroupResponseToMessageResults(targetInfo.devices, response, content);
} catch (InvalidUnidentifiedAccessHeaderException | NotFoundException | GroupMismatchedDevicesException | GroupStaleDevicesException e) { } catch (InvalidUnidentifiedAccessHeaderException | NotFoundException | GroupMismatchedDevicesException | GroupStaleDevicesException e) {
// Non-technical failures shouldn't be retried with socket // 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() + ")"); 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); return transformGroupResponseToMessageResults(targetInfo.devices, response, content);
} catch (GroupMismatchedDevicesException e) { } catch (GroupMismatchedDevicesException e) {
Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling mismatched devices. (" + e.getMessage() + ")"); 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()); SignalServiceAddress address = new SignalServiceAddress(ServiceId.parseOrThrow(stale.getUuid()), Optional.empty());
handleStaleDevices(address, stale.getDevices()); 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 + ")"); Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Attempt failed (i = " + i + ")");
@ -2637,7 +2639,7 @@ public class SignalServiceMessageSender {
} }
private OutgoingPushMessageList getEncryptedMessages(SignalServiceAddress recipient, private OutgoingPushMessageList getEncryptedMessages(SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
long timestamp, long timestamp,
EnvelopeContent plaintext, EnvelopeContent plaintext,
boolean online, boolean online,
@ -2659,7 +2661,7 @@ public class SignalServiceMessageSender {
for (int deviceId : deviceIds) { for (int deviceId : deviceIds) {
if (deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID || aciStore.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) { 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 // Visible for testing only
public OutgoingPushMessage getEncryptedMessage(SignalServiceAddress recipient, public OutgoingPushMessage getEncryptedMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
int deviceId, int deviceId,
EnvelopeContent plaintext, EnvelopeContent plaintext,
boolean story) boolean story)
@ -2679,7 +2681,7 @@ public class SignalServiceMessageSender {
if (!aciStore.containsSession(signalProtocolAddress)) { if (!aciStore.containsSession(signalProtocolAddress)) {
try { try {
List<PreKeyBundle> preKeys = getPreKeys(recipient, unidentifiedAccess, deviceId, story); List<PreKeyBundle> preKeys = getPreKeys(recipient, sealedSenderAccess, deviceId, story);
for (PreKeyBundle preKey : preKeys) { for (PreKeyBundle preKey : preKeys) {
Log.d(TAG, "Initializing prekey session for " + signalProtocolAddress); Log.d(TAG, "Initializing prekey session for " + signalProtocolAddress);
@ -2702,24 +2704,24 @@ public class SignalServiceMessageSender {
} }
try { try {
return cipher.encrypt(signalProtocolAddress, unidentifiedAccess, plaintext); return cipher.encrypt(signalProtocolAddress, sealedSenderAccess, plaintext);
} catch (org.signal.libsignal.protocol.UntrustedIdentityException e) { } catch (org.signal.libsignal.protocol.UntrustedIdentityException e) {
throw new UntrustedIdentityException("Untrusted on send", recipient.getIdentifier(), e.getUntrustedIdentity()); 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 { try {
// If it's only unrestricted because it's a story send, then we know it'll fail // If it's only unrestricted because it's a story send, then we know it'll fail
if (story && unidentifiedAccess.isPresent() && unidentifiedAccess.get().isUnrestrictedForStory()) { if (story && SealedSenderAccess.isUnrestrictedForStory(sealedSenderAccess)) {
unidentifiedAccess = Optional.empty(); sealedSenderAccess = null;
} }
return socket.getPreKeys(recipient, unidentifiedAccess, deviceId); return socket.getPreKeys(recipient, sealedSenderAccess, deviceId);
} catch (NonSuccessfulResponseCodeException e) { } catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 401 && story) { if (e.getCode() == 401 && story) {
Log.d(TAG, "Got 401 when fetching prekey for story. Trying without UD."); 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 { } else {
throw e; throw e;
} }
@ -2782,25 +2784,6 @@ public class SignalServiceMessageSender {
return addresses; 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) { private EnvelopeContent enforceMaxContentSize(EnvelopeContent content) {
int size = content.size(); int size = content.size();

View file

@ -1,7 +1,7 @@
package org.whispersystems.signalservice.api; package org.whispersystems.signalservice.api;
import org.signal.libsignal.protocol.logging.Log; 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.messages.EnvelopeResponse;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import org.whispersystems.signalservice.api.websocket.WebSocketFactory; 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.WebSocketRequestMessage;
import org.whispersystems.signalservice.internal.websocket.WebSocketResponseMessage; import org.whispersystems.signalservice.internal.websocket.WebSocketResponseMessage;
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse; import org.whispersystems.signalservice.internal.websocket.WebsocketResponse;
import org.signal.core.util.Base64;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -19,6 +18,8 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -198,10 +199,11 @@ public final class SignalWebSocket {
} }
} }
public Single<WebsocketResponse> request(WebSocketRequestMessage requestMessage, Optional<UnidentifiedAccess> unidentifiedAccess) { public Single<WebsocketResponse> request(WebSocketRequestMessage requestMessage, @Nullable SealedSenderAccess sealedSenderAccess) {
if (unidentifiedAccess.isPresent()) { if (sealedSenderAccess != null) {
List<String> headers = new ArrayList<>(requestMessage.headers); 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() WebSocketRequestMessage message = requestMessage.newBuilder()
.headers(headers) .headers(headers)
.build(); .build();
@ -209,7 +211,7 @@ public final class SignalWebSocket {
return getUnidentifiedWebSocket().sendRequest(message) return getUnidentifiedWebSocket().sendRequest(message)
.flatMap(r -> { .flatMap(r -> {
if (r.getStatus() == 401) { if (r.getStatus() == 401) {
return request(requestMessage); return request(requestMessage, sealedSenderAccess.switchToFallback());
} }
return Single.just(r); 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.Map;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Nullable;
/** /**
* This is used to encrypt + decrypt received envelopes. * This is used to encrypt + decrypt received envelopes.
*/ */
@ -110,17 +112,17 @@ public class SignalServiceCipher {
} }
public OutgoingPushMessage encrypt(SignalProtocolAddress destination, public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
EnvelopeContent content) EnvelopeContent content)
throws UntrustedIdentityException, InvalidKeyException throws UntrustedIdentityException, InvalidKeyException
{ {
try { try {
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination)); 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() SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getServiceId().getRawUuid(), localAddress.getNumber()
.orElse(null), localDeviceId)); .orElse(null), localDeviceId));
return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, unidentifiedAccess.get().getUnidentifiedCertificate()); return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, sealedSenderAccess.getSenderCertificate());
} else { } else {
return content.processUnsealedSender(sessionCipher, destination); return content.processUnsealedSender(sessionCipher, destination);
} }

View file

@ -65,7 +65,6 @@ public class UnidentifiedAccess {
} }
} }
private static byte[] createEmptyByteArray(int length) { private static byte[] createEmptyByteArray(int length) {
return new byte[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 package org.whispersystems.signalservice.api.groupsv2
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
import org.whispersystems.signalservice.internal.push.PushServiceSocket.GroupHistory import org.whispersystems.signalservice.internal.push.PushServiceSocket.GroupHistory
/** /**
* Wraps result of group history fetch with it's associated paging data. * 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) { data class PagingData(val hasMorePages: Boolean, val nextPageRevision: Int) {
companion object { 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.calllinks.CallLinkAuthCredentialResponse;
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher; import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams; 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.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group; import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupAttributeBlob; import org.signal.storageservice.protos.groups.GroupAttributeBlob;
import org.signal.storageservice.protos.groups.GroupChange; 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.GroupChanges;
import org.signal.storageservice.protos.groups.GroupExternalCredential; import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.GroupJoinInfo; 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.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
@ -31,6 +34,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -73,9 +77,9 @@ public class GroupsV2Api {
return new GroupsV2AuthorizationString(groupSecretParams, authCredentialPresentation); return new GroupsV2AuthorizationString(groupSecretParams, authCredentialPresentation);
} }
public void putNewGroup(GroupsV2Operations.NewGroup newGroup, public DecryptedGroupResponse putNewGroup(GroupsV2Operations.NewGroup newGroup,
GroupsV2AuthorizationString authorization) GroupsV2AuthorizationString authorization)
throws IOException throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException
{ {
Group group = newGroup.getNewGroupMessage(); Group group = newGroup.getNewGroupMessage();
@ -83,34 +87,38 @@ public class GroupsV2Api {
String cdnKey = uploadAvatar(newGroup.getAvatar().get(), newGroup.getGroupSecretParams(), authorization); String cdnKey = uploadAvatar(newGroup.getAvatar().get(), newGroup.getGroupSecretParams(), authorization);
group = group.newBuilder() group = group.newBuilder()
.avatar(cdnKey) .avatar(cdnKey)
.build(); .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)); return NetworkResult.fromFetch(() -> getGroup(groupSecretParams, authorization));
} }
public DecryptedGroup getGroup(GroupSecretParams groupSecretParams, public DecryptedGroupResponse getGroup(GroupSecretParams groupSecretParams,
GroupsV2AuthorizationString authorization) GroupsV2AuthorizationString authorization)
throws IOException, InvalidGroupStateException, VerificationFailedException throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException
{ {
Group group = socket.getGroupsV2Group(authorization); GroupResponse response = socket.getGroupsV2Group(authorization);
return groupsOperations.forGroup(groupSecretParams) return groupsOperations.forGroup(groupSecretParams)
.decryptGroup(group); .decryptGroup(Objects.requireNonNull(response.group), response.groupSendEndorsementsResponse.toByteArray());
} }
public GroupHistoryPage getGroupHistoryPage(GroupSecretParams groupSecretParams, public GroupHistoryPage getGroupHistoryPage(GroupSecretParams groupSecretParams,
int fromRevision, int fromRevision,
GroupsV2AuthorizationString authorization, GroupsV2AuthorizationString authorization,
boolean includeFirstState) boolean includeFirstState,
throws IOException, InvalidGroupStateException, VerificationFailedException 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()); List<DecryptedGroupChangeLog> result = new ArrayList<>(group.getGroupChanges().groupChanges.size());
GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams); GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);
@ -121,7 +129,10 @@ public class GroupsV2Api {
result.add(new DecryptedGroupChangeLog(decryptedGroup, decryptedChange)); 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) { public NetworkResult<Integer> getGroupJoinedAt(@Nonnull GroupsV2AuthorizationString authorization) {
@ -162,9 +173,9 @@ public class GroupsV2Api {
return form.key; return form.key;
} }
public GroupChange patchGroup(GroupChange.Actions groupChange, public GroupChangeResponse patchGroup(GroupChange.Actions groupChange,
GroupsV2AuthorizationString authorization, GroupsV2AuthorizationString authorization,
Optional<byte[]> groupLinkPassword) Optional<byte[]> groupLinkPassword)
throws IOException throws IOException
{ {
return socket.patchGroupsV2Group(groupChange, authorization.toString(), groupLinkPassword); 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.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.ProfileKeyCiphertext; import org.signal.libsignal.zkgroup.groups.ProfileKeyCiphertext;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext; 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.ClientZkProfileOperations;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
@ -53,6 +54,9 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import okio.ByteString; import okio.ByteString;
/** /**
@ -154,7 +158,7 @@ public final class GroupsV2Operations {
private final GroupSecretParams groupSecretParams; private final GroupSecretParams groupSecretParams;
private final ClientZkGroupCipher clientZkGroupCipher; private final ClientZkGroupCipher clientZkGroupCipher;
private GroupOperations(GroupSecretParams groupSecretParams) { public GroupOperations(GroupSecretParams groupSecretParams) {
this.groupSecretParams = groupSecretParams; this.groupSecretParams = groupSecretParams;
this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams); this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
} }
@ -425,6 +429,15 @@ public final class GroupsV2Operations {
return new PendingMember.Builder().member(member); 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) public DecryptedGroup decryptGroup(Group group)
throws VerificationFailedException, InvalidGroupStateException throws VerificationFailedException, InvalidGroupStateException
{ {
@ -1019,6 +1032,46 @@ public final class GroupsV2Operations {
return ids; 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 { 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; package org.whispersystems.signalservice.api.services;
import org.whispersystems.signalservice.api.SignalWebSocket; 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.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.internal.ServiceResponse; 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.DefaultResponseMapper;
import org.whispersystems.signalservice.internal.websocket.ResponseMapper; import org.whispersystems.signalservice.internal.websocket.ResponseMapper;
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage; import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage;
import org.signal.core.util.Base64;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import okio.ByteString; import okio.ByteString;
@ -42,7 +43,9 @@ public class MessagingService {
this.signalWebSocket = signalWebSocket; 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>() {{ List<String> headers = new LinkedList<String>() {{
add("content-type:application/json"); 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"))) .withCustomError(404, (status, body, getHeader) -> new UnregisteredUserException(list.getDestination(), new NotFoundException("not found")))
.build(); .build();
return signalWebSocket.request(requestMessage, unidentifiedAccess) return signalWebSocket.request(requestMessage, sealedSenderAccess)
.map(responseMapper::map) .map(responseMapper::map)
.onErrorReturn(ServiceResponse::forUnknownError); .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>() {{ List<String> headers = new LinkedList<String>() {{
add("content-type:application/vnd.signal-messenger.mrm"); 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); 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.signal.libsignal.zkgroup.profiles.ProfileKeyVersion;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalWebSocket; 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.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
@ -44,6 +45,7 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
@ -72,7 +74,7 @@ public final class ProfileService {
public Single<ServiceResponse<ProfileAndCredential>> getProfile(@Nonnull SignalServiceAddress address, public Single<ServiceResponse<ProfileAndCredential>> getProfile(@Nonnull SignalServiceAddress address,
@Nonnull Optional<ProfileKey> profileKey, @Nonnull Optional<ProfileKey> profileKey,
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
@Nonnull SignalServiceProfile.RequestType requestType, @Nonnull SignalServiceProfile.RequestType requestType,
@Nonnull Locale locale) @Nonnull Locale locale)
{ {
@ -116,9 +118,9 @@ public final class ProfileService {
.withResponseMapper(new ProfileResponseMapper(requestType, requestContext)) .withResponseMapper(new ProfileResponseMapper(requestType, requestContext))
.build(); .build();
return signalWebSocket.request(requestMessage, unidentifiedAccess) return signalWebSocket.request(requestMessage, sealedSenderAccess)
.map(responseMapper::map) .map(responseMapper::map)
.onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, unidentifiedAccess, requestType, locale)) .onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, sealedSenderAccess, requestType, locale))
.onErrorReturn(ServiceResponse::forUnknownError); .onErrorReturn(ServiceResponse::forUnknownError);
} }
@ -139,19 +141,19 @@ public final class ProfileService {
ResponseMapper<IdentityCheckResponse> responseMapper = DefaultResponseMapper.getDefault(IdentityCheckResponse.class); ResponseMapper<IdentityCheckResponse> responseMapper = DefaultResponseMapper.getDefault(IdentityCheckResponse.class);
return signalWebSocket.request(builder.build(), Optional.empty()) return signalWebSocket.request(builder.build(), SealedSenderAccess.NONE)
.map(responseMapper::map) .map(responseMapper::map)
.onErrorResumeNext(t -> performIdentityCheckRestFallback(request, Optional.empty(), responseMapper)) .onErrorResumeNext(t -> performIdentityCheckRestFallback(request, responseMapper))
.onErrorReturn(ServiceResponse::forUnknownError); .onErrorReturn(ServiceResponse::forUnknownError);
} }
private Single<ServiceResponse<ProfileAndCredential>> getProfileRestFallback(@Nonnull SignalServiceAddress address, private Single<ServiceResponse<ProfileAndCredential>> getProfileRestFallback(@Nonnull SignalServiceAddress address,
@Nonnull Optional<ProfileKey> profileKey, @Nonnull Optional<ProfileKey> profileKey,
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
@Nonnull SignalServiceProfile.RequestType requestType, @Nonnull SignalServiceProfile.RequestType requestType,
@Nonnull Locale locale) @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 -> { .onErrorResumeNext(t -> {
Throwable error; Throwable error;
if (t instanceof ExecutionException && t.getCause() != null) { if (t instanceof ExecutionException && t.getCause() != null) {
@ -161,7 +163,7 @@ public final class ProfileService {
} }
if (error instanceof AuthorizationFailedException) { 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 { } else {
return Single.error(t); return Single.error(t);
} }
@ -170,9 +172,8 @@ public final class ProfileService {
} }
private @NonNull Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheckRestFallback(@Nonnull IdentityCheckRequest request, private @NonNull Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheckRestFallback(@Nonnull IdentityCheckRequest request,
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
@Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) { @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
return receiver.performIdentityCheck(request, unidentifiedAccess, responseMapper) return receiver.performIdentityCheck(request, responseMapper)
.onErrorResumeNext(t -> { .onErrorResumeNext(t -> {
Throwable error; Throwable error;
if (t instanceof ExecutionException && t.getCause() != null) { if (t instanceof ExecutionException && t.getCause() != null) {
@ -182,7 +183,7 @@ public final class ProfileService {
} }
if (error instanceof AuthorizationFailedException) { if (error instanceof AuthorizationFailedException) {
return receiver.performIdentityCheck(request, Optional.empty(), responseMapper); return receiver.performIdentityCheck(request, responseMapper);
} else { } else {
return Single.error(t); 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.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group; import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupChange; 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.GroupChanges;
import org.signal.storageservice.protos.groups.GroupExternalCredential; import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.GroupJoinInfo; import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.storageservice.protos.groups.GroupResponse;
import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.Member;
import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; 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.BatchArchiveMediaResponse;
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest; import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest;
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse; 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.CredentialResponse;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; 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 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_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 = "/v2/groups/";
private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s"; private static final String GROUPSV2_GROUP_PASSWORD = "/v2/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_GROUP_CHANGES = "/v2/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_AVATAR_REQUEST = "/v2/groups/avatar/form";
private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s"; private static final String GROUPSV2_GROUP_JOIN = "/v2/groups/join/%s";
private static final String GROUPSV2_TOKEN = "/v1/groups/token"; private static final String GROUPSV2_TOKEN = "/v2/groups/token";
private static final String GROUPSV2_JOINED_AT = "/v1/groups/joined_at_version"; private static final String GROUPSV2_JOINED_AT = "/v2/groups/joined_at_version";
private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions"; 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 { 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)); 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); return parseSessionMetadataResponse(response);
} }
} }
@ -384,7 +386,7 @@ public class PushServiceSocket {
public RegistrationSessionMetadataResponse getSessionStatus(String sessionId) throws IOException { public RegistrationSessionMetadataResponse getSessionStatus(String sessionId) throws IOException {
String path = VERIFICATION_SESSION_PATH + "/" + sessionId; 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); return parseSessionMetadataResponse(response);
} }
} }
@ -393,7 +395,7 @@ public class PushServiceSocket {
String path = VERIFICATION_SESSION_PATH + "/" + sessionId; String path = VERIFICATION_SESSION_PATH + "/" + sessionId;
final UpdateVerificationSessionRequestBody requestBody = new UpdateVerificationSessionRequestBody(captchaToken, pushToken, pushChallengeToken, mcc, mnc); 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); return parseSessionMetadataResponse(response);
} }
} }
@ -414,7 +416,7 @@ public class PushServiceSocket {
body.put("client", androidSmsRetriever ? "android-2021-03" : "android"); 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); return parseSessionMetadataResponse(response);
} }
} }
@ -423,7 +425,7 @@ public class PushServiceSocket {
String path = String.format(VERIFICATION_CODE_PATH, sessionId); String path = String.format(VERIFICATION_CODE_PATH, sessionId);
Map<String, String> body = new HashMap<>(); Map<String, String> body = new HashMap<>();
body.put("code", verificationCode); 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); return parseSessionMetadataResponse(response);
} }
} }
@ -470,7 +472,7 @@ public class PushServiceSocket {
skipDeviceTransfer, skipDeviceTransfer,
true); 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); return JsonUtil.fromJson(response, VerifyAccountResponse.class);
} }
@ -512,14 +514,14 @@ public class PushServiceSocket {
long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime)); long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime));
long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7); 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); return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class);
} }
public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException { public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException {
String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(request)); 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 { public void setArchivePublicKey(ECPublicKey publicKey, ArchiveCredentialPresentation credentialPresentation) throws IOException {
@ -706,7 +708,7 @@ public class PushServiceSocket {
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); 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 throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{ {
ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random); ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random);
@ -716,7 +718,7 @@ public class PushServiceSocket {
Request.Builder requestBuilder = new Request.Builder(); Request.Builder requestBuilder = new Request.Builder();
requestBuilder.url(String.format("%s%s", connectionHolder.getUrl(), path)); requestBuilder.url(String.format("%s%s", connectionHolder.getUrl(), path));
requestBuilder.put(RequestBody.create(MediaType.get("application/vnd.signal-messenger.mrm"), body)); 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) { if (signalAgent != null) {
requestBuilder.addHeader("X-Signal-Agent", signalAgent); 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 throws IOException
{ {
try { 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); SendMessageResponse response = JsonUtil.fromJson(responseText, SendMessageResponse.class);
response.setSentUnidentfied(unidentifiedAccess.isPresent()); response.setSentUnidentfied(sealedSenderAccess != null);
return response; return response;
} catch (NotFoundException nfe) { } catch (NotFoundException nfe) {
@ -780,7 +782,7 @@ public class PushServiceSocket {
public SignalServiceMessagesResult getMessages(boolean allowStories) throws IOException { public SignalServiceMessagesResult getMessages(boolean allowStories) throws IOException {
Map<String, String> headers = Collections.singletonMap("X-Signal-Receive-Stories", allowStories ? "true" : "false"); 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); validateServiceResponse(response);
List<SignalServiceEnvelopeEntity> envelopes = readBodyJson(response.body(), SignalServiceEnvelopeEntityList.class).getMessages(); 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. * 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, public List<PreKeyBundle> getPreKeys(SignalServiceAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
int deviceId) int deviceId)
throws IOException 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. * Retrieves a prekey for a specific device.
*/ */
public PreKeyBundle getPreKey(SignalServiceAddress destination, int deviceId) throws IOException { 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) { if (bundles.size() > 0) {
return bundles.get(0); return bundles.get(0);
@ -891,7 +893,7 @@ public class PushServiceSocket {
} }
private List<PreKeyBundle> getPreKeysBySpecifier(SignalServiceAddress destination, private List<PreKeyBundle> getPreKeysBySpecifier(SignalServiceAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
String deviceSpecifier) String deviceSpecifier)
throws IOException throws IOException
{ {
@ -900,7 +902,7 @@ public class PushServiceSocket {
Log.d(TAG, "Fetching prekeys for " + destination.getIdentifier() + "." + deviceSpecifier + ", i.e. GET " + path); 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); PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class);
List<PreKeyBundle> bundles = new LinkedList<>(); List<PreKeyBundle> bundles = new LinkedList<>();
@ -958,7 +960,7 @@ public class PushServiceSocket {
if (responseCode == 409) { if (responseCode == 409) {
throw new NonSuccessfulResponseCodeException(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) 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(); return output.toByteArray();
} }
public ListenableFuture<SignalServiceProfile> retrieveProfile(SignalServiceAddress target, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) { 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), unidentifiedAccess); ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, target.getIdentifier()), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
return FutureTransformers.map(response, body -> { return FutureTransformers.map(response, body -> {
try { 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()); ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci());
ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target.getLibSignalAci(), profileKey); ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target.getLibSignalAci(), profileKey);
ProfileKeyCredentialRequest request = requestContext.getRequest(); ProfileKeyCredentialRequest request = requestContext.getRequest();
@ -1035,7 +1037,7 @@ public class PushServiceSocket {
String subPath = String.format("%s/%s/%s?credentialType=expiringProfileKey", target, version, credentialRequest); 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)); 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()); ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci());
String version = profileKeyIdentifier.serialize(); String version = profileKeyIdentifier.serialize();
String subPath = String.format("%s/%s", target, version); 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 -> { return FutureTransformers.map(response, body -> {
try { try {
@ -1102,7 +1104,7 @@ public class PushServiceSocket {
requestBody, requestBody,
NO_HEADERS, NO_HEADERS,
PaymentsRegionException::responseCodeHandler, PaymentsRegionException::responseCodeHandler,
Optional.empty()); SealedSenderAccess.NONE);
if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) { if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) {
try { try {
@ -1126,13 +1128,12 @@ public class PushServiceSocket {
} }
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request, public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request,
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
@Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper)
{ {
Single<ServiceResponse<IdentityCheckResponse>> requestSingle = Single.fromCallable(() -> { 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()): ""; 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) @Nonnull ResponseMapper<BackupV2AuthCheckResponse> responseMapper)
{ {
Single<ServiceResponse<BackupV2AuthCheckResponse>> requestSingle = Single.fromCallable(() -> { 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()): ""; String body = response.body() != null ? readBodyString(response.body()): "";
return responseMapper.map(response.code(), body, response::header, false); 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 { 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); return JsonUtil.fromJson(response, BackupV2AuthCheckResponse.class);
} }
public BackupV3AuthCheckResponse checkSvr3AuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException { 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); return JsonUtil.fromJson(response, BackupV3AuthCheckResponse.class);
} }
@ -1218,7 +1219,7 @@ public class PushServiceSocket {
case 422: throw new UsernameMalformedException(); case 422: throw new UsernameMalformedException();
case 409: throw new UsernameTakenException(); case 409: throw new UsernameTakenException();
} }
}, Optional.empty()); }, SealedSenderAccess.NONE);
return JsonUtil.fromJsonResponse(responseString, ReserveUsernameResponse.class); return JsonUtil.fromJsonResponse(responseString, ReserveUsernameResponse.class);
} }
@ -1249,7 +1250,7 @@ public class PushServiceSocket {
case 410: case 410:
throw new UsernameTakenException(); throw new UsernameTakenException();
} }
}, Optional.empty()); }, SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, ConfirmUsernameResponse.class).getUsernameLinkHandle(); return JsonUtil.fromJson(response, ConfirmUsernameResponse.class).getUsernameLinkHandle();
} catch (BaseUsernameException e) { } catch (BaseUsernameException e) {
@ -1303,7 +1304,7 @@ public class PushServiceSocket {
if (responseCode == 428) { if (responseCode == 428) {
throw new CaptchaRejectedException(); 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) private String makeServiceRequestWithoutAuthentication(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException 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); return readBodyString(response);
} }
} }
@ -2147,13 +2148,13 @@ public class PushServiceSocket {
private String makeServiceRequest(String urlFragment, String method, String jsonBody) private String makeServiceRequest(String urlFragment, String method, String jsonBody)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException 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 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); return readBodyString(response);
} }
} }
@ -2172,10 +2173,10 @@ public class PushServiceSocket {
String method, String method,
String jsonBody, String jsonBody,
Map<String, String> headers, Map<String, String> headers,
Optional<UnidentifiedAccess> unidentifiedAccessKey) @Nullable SealedSenderAccess sealedSenderAccess)
{ {
OkHttpClient okHttpClient = buildOkHttpClient(unidentifiedAccessKey.isPresent()); OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null);
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, unidentifiedAccessKey, false)); Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, sealedSenderAccess, false));
synchronized (connections) { synchronized (connections) {
connections.add(call); connections.add(call);
@ -2208,13 +2209,13 @@ public class PushServiceSocket {
RequestBody body, RequestBody body,
Map<String, String> headers, Map<String, String> headers,
ResponseCodeHandler responseCodeHandler, ResponseCodeHandler responseCodeHandler,
Optional<UnidentifiedAccess> unidentifiedAccessKey, @Nullable SealedSenderAccess sealedSenderAccess,
boolean doNotAddAuthenticationOrUnidentifiedAccessKey) boolean doNotAddAuthenticationOrUnidentifiedAccessKey)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{ {
Response response = null; Response response = null;
try { try {
response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey, doNotAddAuthenticationOrUnidentifiedAccessKey); response = getServiceConnection(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey);
responseCodeHandler.handle(response.code(), response.body()); responseCodeHandler.handle(response.code(), response.body());
return validateServiceResponse(response); return validateServiceResponse(response);
} catch (Exception e) { } catch (Exception e) {
@ -2288,13 +2289,13 @@ public class PushServiceSocket {
String method, String method,
RequestBody body, RequestBody body,
Map<String, String> headers, Map<String, String> headers,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
boolean doNotAddAuthenticationOrUnidentifiedAccessKey) boolean doNotAddAuthenticationOrUnidentifiedAccessKey)
throws PushNetworkException throws PushNetworkException
{ {
try { try {
OkHttpClient okHttpClient = buildOkHttpClient(unidentifiedAccess.isPresent()); OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null);
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, body, headers, unidentifiedAccess, doNotAddAuthenticationOrUnidentifiedAccessKey)); Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey));
synchronized (connections) { synchronized (connections) {
connections.add(call); connections.add(call);
@ -2327,14 +2328,11 @@ public class PushServiceSocket {
String method, String method,
RequestBody body, RequestBody body,
Map<String, String> headers, Map<String, String> headers,
Optional<UnidentifiedAccess> unidentifiedAccess, @Nullable SealedSenderAccess sealedSenderAccess,
boolean doNotAddAuthenticationOrUnidentifiedAccessKey) { boolean doNotAddAuthenticationOrUnidentifiedAccessKey) {
ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random); 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.Builder request = new Request.Builder();
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment)); request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
request.method(method, body); request.method(method, body);
@ -2344,8 +2342,8 @@ public class PushServiceSocket {
} }
if (!headers.containsKey("Authorization") && !doNotAddAuthenticationOrUnidentifiedAccessKey) { if (!headers.containsKey("Authorization") && !doNotAddAuthenticationOrUnidentifiedAccessKey) {
if (unidentifiedAccess.isPresent()) { if (sealedSenderAccess != null) {
request.addHeader("Unidentified-Access-Key", Base64.encodeWithPadding(unidentifiedAccess.get().getUnidentifiedAccessKey())); request.addHeader(sealedSenderAccess.getHeaderName(), sealedSenderAccess.getHeaderValue());
} else if (credentialsProvider.getPassword() != null) { } else if (credentialsProvider.getPassword() != null) {
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider)); 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) private Response makeStorageRequest(String authorization, String path, String method, RequestBody body, ResponseCodeHandler responseCodeHandler)
throws PushNetworkException, NonSuccessfulResponseCodeException 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); ConnectionHolder connectionHolder = getRandom(storageClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient() OkHttpClient okHttpClient = connectionHolder.getClient()
@ -2372,8 +2376,6 @@ public class PushServiceSocket {
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build(); .build();
// Log.d(TAG, "Opening URL: " + connectionHolder.getUrl());
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path); Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
request.method(method, body); request.method(method, body);
@ -2385,6 +2387,10 @@ public class PushServiceSocket {
request.addHeader("Authorization", authorization); request.addHeader("Authorization", authorization);
} }
for (Map.Entry<String, String> entry : headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
Call call = okHttpClient.newCall(request.build()); Call call = okHttpClient.newCall(request.build());
synchronized (connections) { synchronized (connections) {
@ -2784,7 +2790,7 @@ public class PushServiceSocket {
null, null,
NO_HEADERS, NO_HEADERS,
NO_HANDLER, NO_HANDLER,
Optional.empty()); SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, CredentialResponse.class); return JsonUtil.fromJson(response, CredentialResponse.class);
} }
@ -2816,8 +2822,8 @@ public class PushServiceSocket {
} }
}; };
public void putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization) public GroupResponse putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization)
throws NonSuccessfulResponseCodeException, PushNetworkException throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
{ {
try (Response response = makeStorageRequest(authorization.toString(), try (Response response = makeStorageRequest(authorization.toString(),
GROUPSV2_GROUP, GROUPSV2_GROUP,
@ -2825,11 +2831,11 @@ public class PushServiceSocket {
protobufRequestBody(group), protobufRequestBody(group),
GROUPS_V2_PUT_RESPONSE_HANDLER)) 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 throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
{ {
try (Response response = makeStorageRequest(authorization.toString(), try (Response response = makeStorageRequest(authorization.toString(),
@ -2838,7 +2844,7 @@ public class PushServiceSocket {
null, null,
GROUPS_V2_GET_CURRENT_HANDLER)) 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 throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
{ {
String path; String path;
@ -2872,17 +2878,21 @@ public class PushServiceSocket {
protobufRequestBody(groupChange), protobufRequestBody(groupChange),
GROUPS_V2_PATCH_RESPONSE_HANDLER)) 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 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(), try (Response response = makeStorageRequest(authorization.toString(),
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion, highestKnownEpoch, includeFirstState), String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion, highestKnownEpoch, includeFirstState),
"GET", "GET",
null, null,
headers,
GROUPS_V2_GET_LOGS_HANDLER)) GROUPS_V2_GET_LOGS_HANDLER))
{ {
@ -2890,12 +2900,7 @@ public class PushServiceSocket {
throw new PushNetworkException("No body!"); throw new PushNetworkException("No body!");
} }
GroupChanges groupChanges; GroupChanges groupChanges = GroupChanges.ADAPTER.decode(readBodyBytes(response));
try (InputStream input = response.body().byteStream()) {
groupChanges = GroupChanges.ADAPTER.decode(input);
} catch (IOException e) {
throw new PushNetworkException(e);
}
if (response.code() == 206) { if (response.code() == 206) {
String contentRangeHeader = response.header("Content-Range"); String contentRangeHeader = response.header("Content-Range");

View file

@ -213,13 +213,24 @@ message GroupChange {
uint32 changeEpoch = 3; uint32 changeEpoch = 3;
} }
message GroupResponse {
Group group = 1;
bytes groupSendEndorsementsResponse = 2;
}
message GroupChanges { message GroupChanges {
message GroupChangeState { message GroupChangeState {
GroupChange groupChange = 1; GroupChange groupChange = 1;
Group groupState = 2; Group groupState = 2;
} }
repeated GroupChangeState groupChanges = 1; repeated GroupChangeState groupChanges = 1;
bytes groupSendEndorsementsResponse = 2;
}
message GroupChangeResponse {
GroupChange groupChange = 1;
bytes groupSendEndorsementsResponse = 2;
} }
message GroupAttributeBlob { message GroupAttributeBlob {

View file

@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
import org.whispersystems.signalservice.api.SignalSessionLock import org.whispersystems.signalservice.api.SignalSessionLock
import org.whispersystems.signalservice.api.crypto.ContentHint import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.crypto.EnvelopeContent 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.SignalGroupSessionBuilder
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
@ -93,7 +94,7 @@ class SignalClient {
val outgoingPushMessage: OutgoingPushMessage = cipher.encrypt( val outgoingPushMessage: OutgoingPushMessage = cipher.encrypt(
SignalProtocolAddress(to.aci.toString(), 1), SignalProtocolAddress(to.aci.toString(), 1),
Optional.empty(), SealedSenderAccess.NONE,
EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty()) EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty())
) )
@ -124,7 +125,7 @@ class SignalClient {
val outgoingPushMessage: OutgoingPushMessage = cipher.encrypt( val outgoingPushMessage: OutgoingPushMessage = cipher.encrypt(
SignalProtocolAddress(to.aci.toString(), 1), SignalProtocolAddress(to.aci.toString(), 1),
Optional.of(UnidentifiedAccess(to.unidentifiedAccessKey, senderCertificate.serialized, false)), SealedSenderAccess.forIndividual(UnidentifiedAccess(to.unidentifiedAccessKey, senderCertificate.serialized, false)),
EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty()) EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty())
) )