From f5abd7acdfd7f36f41405e76fc5da6972795dc91 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 8 Jul 2024 12:47:20 -0400 Subject: [PATCH] Add Group Send Endorsements support. --- .../securesms/database/GroupTableTest.kt | 4 +- .../MessageProcessingPerformanceTest.kt | 8 +- .../securesms/testing/AliceClient.kt | 3 +- .../securesms/testing/BobClient.kt | 11 +- .../securesms/testing/FakeClientHelpers.kt | 9 +- .../securesms/testing/GroupTestingUtils.kt | 2 +- .../securesms/ApplicationContext.java | 2 + .../RecipientTableBackupExtensions.kt | 2 +- .../InternalConversationSettingsFragment.kt | 2 +- ...sUtil.java => SealedSenderAccessUtil.java} | 101 ++-- .../securesms/database/GroupTable.kt | 171 ++++++- .../securesms/database/RecipientTable.kt | 19 +- .../database/RecipientTableCursorUtil.kt | 2 +- .../helpers/SignalDatabaseMigrations.kt | 6 +- .../V238_AddGroupSendEndorsementsColumns.kt | 21 + .../securesms/database/model/GroupRecord.kt | 3 +- .../model/GroupSendEndorsementRecords.kt | 23 + .../database/model/RecipientRecord.kt | 4 +- .../securesms/groups/GroupManager.java | 10 + .../securesms/groups/GroupManagerV2.java | 128 ++--- .../groups/v2/processing/GroupStateDiff.kt | 10 +- .../v2/processing/GroupsV2StateProcessor.kt | 103 +++- .../jobs/AutomaticSessionResetJob.java | 11 +- .../securesms/jobs/CallLinkUpdateSendJob.kt | 3 +- .../securesms/jobs/CallLogEventSendJob.kt | 6 +- .../securesms/jobs/CallSyncEventJob.kt | 3 +- .../securesms/jobs/IndividualSendJob.java | 41 +- .../jobs/MultiDeviceBlockedUpdateJob.java | 5 +- .../jobs/MultiDeviceCallLinkSyncJob.kt | 3 +- .../MultiDeviceConfigurationUpdateJob.java | 7 +- .../jobs/MultiDeviceContactUpdateJob.java | 7 +- .../jobs/MultiDeviceKeysUpdateJob.java | 5 +- .../MultiDeviceMessageRequestResponseJob.java | 7 +- .../MultiDeviceOutgoingPaymentSyncJob.java | 5 +- .../MultiDeviceProfileContentUpdateJob.java | 5 +- .../jobs/MultiDeviceProfileKeyUpdateJob.java | 3 +- .../jobs/MultiDeviceReadUpdateJob.java | 5 +- .../MultiDeviceStickerPackOperationJob.java | 15 +- .../jobs/MultiDeviceStickerPackSyncJob.java | 7 +- .../MultiDeviceStorageSyncRequestJob.java | 4 +- .../jobs/MultiDeviceStorySendSyncJob.kt | 3 +- .../MultiDeviceSubscriptionSyncRequestJob.kt | 6 +- .../jobs/MultiDeviceVerifiedUpdateJob.java | 9 +- .../jobs/MultiDeviceViewOnceOpenJob.java | 5 +- .../jobs/MultiDeviceViewedUpdateJob.java | 5 +- .../securesms/jobs/NullMessageSendJob.java | 13 +- .../jobs/PaymentNotificationSendJob.java | 19 +- .../securesms/jobs/ResendMessageJob.java | 14 +- .../securesms/jobs/RetrieveProfileJob.kt | 20 +- .../jobs/SendDeliveryReceiptJob.java | 20 +- .../securesms/jobs/SendReadReceiptJob.java | 7 +- .../securesms/jobs/SendRetryReceiptJob.java | 14 +- .../securesms/jobs/SendViewedReceiptJob.java | 7 +- .../jobs/SenderKeyDistributionSendJob.java | 24 +- .../securesms/jobs/StorageSyncJob.java | 3 +- .../GroupSendEndorsementInternalNotifier.kt | 116 +++++ .../securesms/messages/GroupSendUtil.java | 221 ++++++--- .../securesms/messages/MessageDecryptor.kt | 4 +- .../securesms/recipients/Recipient.kt | 12 +- .../securesms/recipients/RecipientCreator.kt | 2 +- .../service/webrtc/SignalCallManager.java | 19 +- .../securesms/util/ProfileUtil.java | 25 +- .../securesms/database/GroupTestUtil.kt | 5 +- .../database/RecipientDatabaseTestUtils.kt | 4 +- .../groups/GroupManagerV2Test_edit.kt | 7 +- .../v2/processing/GroupStatePatcherTest.java | 46 +- .../processing/GroupsV2StateProcessorTest.kt | 48 +- .../api/SignalServiceAccountManager.java | 5 +- .../api/SignalServiceMessageReceiver.java | 14 +- .../api/SignalServiceMessageSender.java | 439 +++++++++--------- .../signalservice/api/SignalWebSocket.java | 14 +- .../api/crypto/SealedSenderAccess.kt | 198 ++++++++ .../api/crypto/SignalServiceCipher.java | 8 +- .../api/crypto/UnidentifiedAccess.java | 1 - .../api/crypto/UnidentifiedAccessPair.java | 23 - .../api/groupsv2/DecryptedGroupResponse.kt | 18 + .../api/groupsv2/GroupHistoryPage.kt | 3 +- .../api/groupsv2/GroupSendEndorsements.kt | 38 ++ .../api/groupsv2/GroupsV2Api.java | 49 +- .../api/groupsv2/GroupsV2Operations.java | 55 ++- .../groupsv2/ReceivedGroupSendEndorsements.kt | 28 ++ .../api/services/MessagingService.java | 17 +- .../api/services/ProfileService.java | 23 +- .../internal/push/PushServiceSocket.java | 163 +++---- .../src/main/protowire/Groups.proto | 13 +- .../java/org/signal/util/SignalClient.kt | 5 +- 86 files changed, 1691 insertions(+), 887 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/crypto/{UnidentifiedAccessUtil.java => SealedSenderAccessUtil.java} (54%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V238_AddGroupSendEndorsementsColumns.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/GroupSendEndorsementRecords.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendEndorsementInternalNotifier.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SealedSenderAccess.kt delete mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccessPair.java create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupResponse.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupSendEndorsements.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ReceivedGroupSendEndorsements.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/GroupTableTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/GroupTableTest.kt index ccea2189e4..195ff7d8b3 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/GroupTableTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/GroupTableTest.kt @@ -314,7 +314,7 @@ class GroupTableTest { .revision(0) .build() - return groupTable.create(groupMasterKey, decryptedGroupState)!! + return groupTable.create(groupMasterKey, decryptedGroupState, null)!! } private fun insertPushGroupWithSelfAndOthers(others: List): GroupId { @@ -339,6 +339,6 @@ class GroupTableTest { .revision(0) .build() - return groupTable.create(groupMasterKey, decryptedGroupState)!! + return groupTable.create(groupMasterKey, decryptedGroupState, null)!! } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageProcessingPerformanceTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageProcessingPerformanceTest.kt index 07233e48f7..f51d478c35 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageProcessingPerformanceTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageProcessingPerformanceTest.kt @@ -16,7 +16,7 @@ import org.signal.core.util.logging.Log import org.signal.libsignal.protocol.ecc.Curve import org.signal.libsignal.protocol.ecc.ECKeyPair 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.recipients.Recipient import org.thoughtcrime.securesms.testing.AliceClient @@ -55,8 +55,8 @@ class MessageProcessingPerformanceTest { @Before fun setup() { - mockkStatic(UnidentifiedAccessUtil::class) - every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator + mockkStatic(SealedSenderAccessUtil::class) + every { SealedSenderAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator mockkObject(MessageContentProcessor) every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application) @@ -64,7 +64,7 @@ class MessageProcessingPerformanceTest { @After fun after() { - unmockkStatic(UnidentifiedAccessUtil::class) + unmockkStatic(SealedSenderAccessUtil::class) unmockkStatic(MessageContentProcessor::class) } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/AliceClient.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/AliceClient.kt index 578a44b7bb..b429745306 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/AliceClient.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/AliceClient.kt @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.testing import org.signal.core.util.logging.Log import org.signal.libsignal.protocol.ecc.ECKeyPair import org.signal.libsignal.zkgroup.profiles.ProfileKey -import org.thoughtcrime.securesms.crypto.ProfileKeyUtil import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore 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 { return AppDependencies.signalServiceMessageSender.getEncryptedMessage( SignalServiceAddress(destination.requireServiceId(), destination.requireE164()), - FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate), + FakeClientHelpers.getSealedSenderAccess(ProfileKey(destination.profileKey), aliceSenderCertificate), 1, FakeClientHelpers.encryptedTextMessage(now), false diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/BobClient.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/BobClient.kt index 4565bf8d58..3eb4d602be 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/BobClient.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/BobClient.kt @@ -17,7 +17,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord import org.signal.libsignal.protocol.util.KeyHelper import org.signal.libsignal.zkgroup.profiles.ProfileKey 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.SignalDatabase 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.whispersystems.signalservice.api.SignalServiceAccountDataStore 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.SignalSessionBuilder -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess 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.internal.push.Envelope -import java.util.Optional import java.util.UUID 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) { - val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator()) + val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, SealedSenderAccessUtil.getCertificateValidator()) cipher.decrypt(envelope, serverDeliveredTimestamp) } @@ -126,8 +125,8 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: return ProfileKeyUtil.getSelfProfileKey() } - private fun getAliceUnidentifiedAccess(): Optional { - return FakeClientHelpers.getTargetUnidentifiedAccess(profileKey, getAliceProfileKey(), senderCertificate) + private fun getAliceUnidentifiedAccess(): SealedSenderAccess? { + return FakeClientHelpers.getSealedSenderAccess(getAliceProfileKey(), senderCertificate) } private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore { diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/FakeClientHelpers.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/FakeClientHelpers.kt index d18eb79922..903c530611 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/FakeClientHelpers.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/FakeClientHelpers.kt @@ -14,8 +14,8 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith import org.whispersystems.signalservice.api.crypto.ContentHint import org.whispersystems.signalservice.api.crypto.EnvelopeContent +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.internal.push.Content import org.whispersystems.signalservice.internal.push.DataMessage @@ -46,11 +46,10 @@ object FakeClientHelpers { } } - fun getTargetUnidentifiedAccess(myProfileKey: ProfileKey, theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): Optional { - val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey) - val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey) + fun getSealedSenderAccess(theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): SealedSenderAccess? { + val themUnidentifiedAccessKey = UnidentifiedAccess(UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey), senderCertificate.serialized, false) - 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 { diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt index 0c0d366194..cecba043d0 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt @@ -33,7 +33,7 @@ object GroupTestingUtils { .title(MessageContentFuzzer.string()) .build() - val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!! + val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState, null)!! val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId) SignalDatabase.recipients.setProfileSharing(groupRecipientId, true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 88675d9fa1..de43af0da1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver; +import org.thoughtcrime.securesms.messages.GroupSendEndorsementInternalNotifier; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideModule; @@ -222,6 +223,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr .addPostRender(GroupRingCleanupJob::enqueue) .addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary) .addPostRender(() -> ActiveCallManager.clearNotifications(this)) + .addPostRender(() -> GroupSendEndorsementInternalNotifier.init()) .execute(); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt index c6a341715a..9ab7375132 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt @@ -241,7 +241,7 @@ fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId { } 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) { SignalDatabase.groups.setShowAsStoryState(restoredId, group.storySendMode.toGroupShowAsStoryState()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt index 279647cb47..6a4ffada0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt @@ -129,7 +129,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment( textPref( title = DSLSettingsText.from("Sealed Sender Mode"), - summary = DSLSettingsText.from(recipient.unidentifiedAccessMode.toString()) + summary = DSLSettingsText.from(recipient.sealedSenderAccessMode.toString()) ) textPref( diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/SealedSenderAccessUtil.java similarity index 54% rename from app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java rename to app/src/main/java/org/thoughtcrime/securesms/crypto/SealedSenderAccessUtil.java index 8ef92e8734..213104bbd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/SealedSenderAccessUtil.java @@ -1,17 +1,17 @@ package org.thoughtcrime.securesms.crypto; -import android.content.Context; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; +import org.signal.core.util.Base64; import org.signal.core.util.logging.Log; import org.signal.libsignal.metadata.certificate.CertificateValidator; 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.ecc.Curve; 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.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.signal.core.util.Base64; -import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import java.io.IOException; import java.util.Collections; @@ -35,9 +33,9 @@ import java.util.Map; import java.util.Optional; 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]; @@ -46,28 +44,34 @@ public class UnidentifiedAccessUtil { } @WorkerThread - public static Optional getAccessFor(@NonNull Context context, @NonNull Recipient recipient) { - return getAccessFor(context, recipient, true); + public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull Recipient recipient) { + return getSealedSenderAccessFor(recipient, true); } @WorkerThread - public static Optional getAccessFor(@NonNull Context context, @NonNull Recipient recipient, boolean log) { - return getAccessFor(context, Collections.singletonList(recipient), log).get(0); + public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull Recipient recipient, boolean log) { + 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 - public static List> getAccessFor(@NonNull Context context, @NonNull List recipients) { - return getAccessFor(context, recipients, true); + private static @Nullable UnidentifiedAccess getAccessFor(@NonNull Recipient recipient, boolean log) { + return getAccessFor(Collections.singletonList(recipient), false, log) + .get(0) + .orElse(null); } @WorkerThread - public static Map> getAccessMapFor(@NonNull Context context, @NonNull List recipients, boolean isForStory) { - List> accessList = getAccessFor(context, recipients, isForStory, true); + public static Map> getAccessMapFor(@NonNull List recipients, boolean isForStory) { + List> accessList = getAccessFor(recipients, isForStory, true); - Iterator recipientIterator = recipients.iterator(); - Iterator> accessIterator = accessList.iterator(); + Iterator recipientIterator = recipients.iterator(); + Iterator> accessIterator = accessList.iterator(); - Map> accessMap = new HashMap<>(recipients.size()); + Map> accessMap = new HashMap<>(recipients.size()); while (recipientIterator.hasNext()) { accessMap.put(recipientIterator.next().getId(), accessIterator.next()); @@ -77,40 +81,22 @@ public class UnidentifiedAccessUtil { } @WorkerThread - public static List> getAccessFor(@NonNull Context context, @NonNull List recipients, boolean log) { - return getAccessFor(context, recipients, false, log); - } - - @WorkerThread - public static List> getAccessFor(@NonNull Context context, @NonNull List recipients, boolean isForStory, boolean log) { - final byte[] ourUnidentifiedAccessKey; - - if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { - ourUnidentifiedAccessKey = UNRESTRICTED_KEY; - } else { - ourUnidentifiedAccessKey = ProfileKeyUtil.getSelfProfileKey().deriveAccessKey(); - } - + private static List> getAccessFor(@NonNull List recipients, boolean isForStory, boolean log) { CertificateType certificateType = getUnidentifiedAccessCertificateType(); byte[] ourUnidentifiedAccessCertificate = SignalStore.certificate().getUnidentifiedAccessCertificate(certificateType); - List> access = recipients.parallelStream().map(recipient -> { - UnidentifiedAccessPair unidentifiedAccessPair = null; + List> access = recipients.parallelStream().map(recipient -> { + UnidentifiedAccess unidentifiedAccess = null; if (ourUnidentifiedAccessCertificate != null) { try { - UnidentifiedAccess theirAccess = getTargetUnidentifiedAccess(recipient, ourUnidentifiedAccessCertificate, isForStory); - UnidentifiedAccess ourAccess = new UnidentifiedAccess(ourUnidentifiedAccessKey, ourUnidentifiedAccessCertificate, false); - - if (theirAccess != null) { - unidentifiedAccessPair = new UnidentifiedAccessPair(theirAccess, ourAccess); - } + unidentifiedAccess = getTargetUnidentifiedAccess(recipient, ourUnidentifiedAccessCertificate, isForStory); } catch (InvalidCertificateException e) { Log.w(TAG, "Invalid unidentified access certificate!", e); } } 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()); int unidentifiedCount = Stream.of(access).filter(Optional::isPresent).toList().size(); @@ -123,28 +109,17 @@ public class UnidentifiedAccessUtil { return access; } - public static Optional getAccessForSync(@NonNull Context context) { + public static @Nullable SenderCertificate getSealedSenderCertificate() { + byte[] unidentifiedAccessCertificate = getUnidentifiedAccessCertificate(); + if (unidentifiedAccessCertificate == null) { + return null; + } + try { - byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); - 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(); + return new SenderCertificate(unidentifiedAccessCertificate); } catch (InvalidCertificateException e) { Log.w(TAG, e); - return Optional.empty(); + return null; } } @@ -166,7 +141,7 @@ public class UnidentifiedAccessUtil { byte[] accessKey; - switch (recipient.resolve().getUnidentifiedAccessMode()) { + switch (recipient.resolve().getSealedSenderAccessMode()) { case UNKNOWN: if (theirProfileKey == null) { if (isForStory) { @@ -192,7 +167,7 @@ public class UnidentifiedAccessUtil { accessKey = UNRESTRICTED_KEY; break; default: - throw new AssertionError("Unknown mode: " + recipient.getUnidentifiedAccessMode().getMode()); + throw new AssertionError("Unknown mode: " + recipient.getSealedSenderAccessMode().getMode()); } if (accessKey == null && isForStory) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt index 7a9d3a9ba7..53f58215c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt @@ -19,7 +19,9 @@ import org.signal.core.util.isAbsent import org.signal.core.util.logging.Log import org.signal.core.util.optionalString import org.signal.core.util.readToList +import org.signal.core.util.readToMap import org.signal.core.util.readToSingleInt +import org.signal.core.util.readToSingleLong import org.signal.core.util.readToSingleObject import org.signal.core.util.requireBlob 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.update 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.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.local.DecryptedGroup 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.database.SignalDatabase.Companion.recipients import org.thoughtcrime.securesms.database.model.GroupRecord +import org.thoughtcrime.securesms.database.model.GroupSendEndorsementRecords import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.groups.BadGroupIdException import org.thoughtcrime.securesms.groups.GroupId @@ -50,6 +57,7 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil 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.findPendingByServiceId 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 java.io.Closeable import java.security.SecureRandom +import java.time.Instant import java.util.Optional import java.util.stream.Collectors import javax.annotation.CheckReturnValue @@ -94,6 +103,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT const val DISTRIBUTION_ID = "distribution_id" const val SHOW_AS_STORY_STATE = "show_as_story_state" const val LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp" + const val GROUP_SEND_ENDORSEMENTS_EXPIRATION = "group_send_endorsements_expiration" /** 32 bytes serialized [GroupMasterKey] */ const val V2_MASTER_KEY = "master_key" @@ -125,12 +135,13 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT $UNMIGRATED_V1_MEMBERS TEXT DEFAULT NULL, $DISTRIBUTION_ID TEXT UNIQUE DEFAULT NULL, $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 - val CREATE_INDEXS = MembershipTable.CREATE_INDEXES + val CREATE_INDEXS: Array = MembershipTable.CREATE_INDEXES private val GROUP_PROJECTION = arrayOf( GROUP_ID, @@ -147,7 +158,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP, - LAST_FORCE_UPDATE_TIMESTAMP + LAST_FORCE_UPDATE_TIMESTAMP, + GROUP_SEND_ENDORSEMENTS_EXPIRATION ) val TYPED_GROUP_PROJECTION = GROUP_PROJECTION @@ -165,6 +177,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT const val ID = "_id" const val GROUP_ID = "group_id" const val RECIPIENT_ID = "recipient_id" + const val ENDORSEMENT = "endorsement" //language=sql const val CREATE_TABLE = """ @@ -172,6 +185,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT $ID INTEGER PRIMARY KEY, $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, + $ENDORSEMENT BLOB DEFAULT NULL, UNIQUE($GROUP_ID, $RECIPIENT_ID) ) """ @@ -568,19 +582,19 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT throw LegacyGroupInsertException(groupId) } - return create(groupId, title, members, avatar, null, null) + return create(groupId, title, members, avatar, null, null, null) } @CheckReturnValue fun create(groupId: GroupId.Mms, title: String?, members: Collection): 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 - fun create(groupMasterKey: GroupMasterKey, groupState: DecryptedGroup): GroupId.V2? { + fun create(groupMasterKey: GroupMasterKey, groupState: DecryptedGroup, groupSendEndorsements: ReceivedGroupSendEndorsements?): GroupId.V2? { 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 } else { null @@ -604,10 +618,9 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT if (updated < 1) { Log.w(TAG, "No group entry. Creating restore placeholder for $groupId") create( - groupMasterKey, - DecryptedGroup.Builder() - .revision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) - .build() + groupMasterKey = groupMasterKey, + groupState = DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION), + groupSendEndorsements = null ) } else { 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, avatar: SignalServiceAttachmentPointer?, groupMasterKey: GroupMasterKey?, - groupState: DecryptedGroup? + groupState: DecryptedGroup?, + receivedGroupSendEndorsements: ReceivedGroupSendEndorsements? ): Boolean { val membershipValues = mutableListOf() val groupRecipientId = recipients.getOrInsertFromGroupId(groupId) @@ -640,7 +654,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT values.put(RECIPIENT_ID, groupRecipientId.serialize()) values.put(GROUP_ID, groupId.toString()) values.put(TITLE, title) - membershipValues.addAll(members.toContentValues(groupId)) + membershipValues.addAll(members.toContentValues(groupId, receivedGroupSendEndorsements?.toGroupSendEndorsementRecords())) values.put(MMS, groupId.isMms) if (avatar != null) { @@ -657,6 +671,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT if (groupId.isV2) { values.put(ACTIVE, if (groupState != null && gv2GroupActive(groupState)) 1 else 0) values.put(DISTRIBUTION_ID, DistributionId.create().toString()) + values.put(GROUP_SEND_ENDORSEMENTS_EXPIRATION, receivedGroupSendEndorsements?.expirationMs ?: 0) } else if (groupId.isV1) { values.put(ACTIVE, 1) 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_DECRYPTED_GROUP, groupState.encode()) membershipValues.clear() - membershipValues.addAll(groupMembers.toContentValues(groupId)) + membershipValues.addAll(groupMembers.toContentValues(groupId, receivedGroupSendEndorsements?.toGroupSendEndorsementRecords())) } else { if (groupId.isV2) { throw AssertionError("V2 group id but no master key") @@ -691,9 +706,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT 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.setTransactionSuccessful() } finally { writableDatabase.endTransaction() @@ -745,11 +761,11 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT notifyConversationListListeners() } - fun update(groupMasterKey: GroupMasterKey, decryptedGroup: DecryptedGroup) { - update(GroupId.v2(groupMasterKey), decryptedGroup) + fun update(groupMasterKey: GroupMasterKey, decryptedGroup: DecryptedGroup, groupSendEndorsements: ReceivedGroupSendEndorsements?) { + 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 existingGroup: Optional = getGroup(groupId) 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(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) { val unmigratedV1Members: MutableSet = existingGroup.get().unmigratedV1Members.toMutableSet() @@ -781,6 +801,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT } val groupMembers = getV2GroupMembers(decryptedGroup, true) + var groupSendEndorsementRecords: GroupSendEndorsementRecords? = receivedGroupSendEndorsements?.toGroupSendEndorsementRecords() ?: getGroupSendEndorsements(groupId) val addedMembers: List = if (existingGroup.isPresent && existingGroup.get().isV2Group) { 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() } else { groupMembers @@ -812,7 +839,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT .where("$GROUP_ID = ?", groupId.toString()) .run() - performMembershipUpdate(database, groupId, groupMembers) + performMembershipUpdate(database, groupId, groupMembers, groupSendEndorsementRecords) } if (decryptedGroup.disappearingMessagesTimer != null) { @@ -867,7 +894,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT .toMutableList() } - private fun performMembershipUpdate(database: SQLiteDatabase, groupId: GroupId, members: Collection) { + private fun performMembershipUpdate(database: SQLiteDatabase, groupId: GroupId, members: Collection, groupSendEndorsementRecords: GroupSendEndorsementRecords?) { check(database.inTransaction()) database .delete(MembershipTable.TABLE_NAME) @@ -876,8 +903,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT val inserts = SqlUtil.buildBulkInsert( MembershipTable.TABLE_NAME, - arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID), - members.toSet().toContentValues(groupId) + arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID, MembershipTable.ENDORSEMENT), + members.toSet().toContentValues(groupId, groupSendEndorsementRecords) ) inserts.forEach { @@ -906,6 +933,94 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT .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 = 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 = 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 fun isCurrentMember(groupId: Push, recipientId: RecipientId): Boolean { return readableDatabase @@ -1008,7 +1123,8 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT groupRevision = cursor.requireInt(V2_REVISION), decryptedGroupBytes = cursor.requireBlob(V2_DECRYPTED_GROUP), 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) } - private fun Collection.toContentValues(groupId: GroupId): List { + private fun Collection.toContentValues(groupId: GroupId, groupSendEndorsementRecords: GroupSendEndorsementRecords?): List { return map { contentValuesOf( 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): MutableList { return serviceIds .map { serviceId -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 6966061145..37cbee9333 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -958,10 +958,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da Log.i(TAG, "Creating restore placeholder for $groupId") val createdId = groups.create( - masterKey, - DecryptedGroup.Builder() - .revision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) - .build() + groupMasterKey = masterKey, + groupState = DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION), + groupSendEndorsements = 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 { - put(SEALED_SENDER_MODE, unidentifiedAccessMode.mode) + put(SEALED_SENDER_MODE, sealedSenderAccessMode.mode) } if (update(id, values)) { AppDependencies.databaseObserver.notifyRecipientChanged(id) @@ -1554,7 +1553,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da val valuesToSet = ContentValues(3).apply { put(PROFILE_KEY, encodedProfileKey) 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) @@ -1586,7 +1585,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da val valuesToSet = ContentValues(3).apply { put(PROFILE_KEY, Base64.encodeWithPadding(profileKey.serialize())) 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) { @@ -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), DISABLED(1), ENABLED(2), UNRESTRICTED(3); companion object { - fun fromMode(mode: Int): UnidentifiedAccessMode { + fun fromMode(mode: Int): SealedSenderAccessMode { return values()[mode] } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt index 8cde95e406..e5630d5667 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt @@ -149,7 +149,7 @@ object RecipientTableCursorUtil { profileSharing = cursor.requireBoolean(RecipientTable.PROFILE_SHARING), lastProfileFetch = cursor.requireLong(RecipientTable.LAST_PROFILE_FETCH), 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), storageId = Base64.decodeNullableOrThrow(cursor.requireString(RecipientTable.STORAGE_SERVICE_ID)), mentionSetting = RecipientTable.MentionSetting.fromId(cursor.requireInt(RecipientTable.MENTION_SETTING)), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index b2196ed161..e984809abf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -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.V236_FixInAppSubscriberCurrencyIfAble 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. @@ -192,10 +193,11 @@ object SignalDatabaseMigrations { 234 to V234_ThumbnailRestoreStateColumn, 235 to V235_AttachmentUuidColumn, 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 fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V238_AddGroupSendEndorsementsColumns.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V238_AddGroupSendEndorsementsColumns.kt new file mode 100644 index 0000000000..1214560604 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V238_AddGroupSendEndorsementsColumns.kt @@ -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") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt index f9ab21dd2a..3bc40bee70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt @@ -31,7 +31,8 @@ class GroupRecord( groupRevision: Int, decryptedGroupBytes: ByteArray?, val distributionId: DistributionId?, - val lastForceUpdateTimestamp: Long + val lastForceUpdateTimestamp: Long, + val groupSendEndorsementExpiration: Long ) { val members: List by lazy { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupSendEndorsementRecords.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupSendEndorsementRecords.kt new file mode 100644 index 0000000000..edb4e8298b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupSendEndorsementRecords.kt @@ -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) { + fun getEndorsement(recipientId: RecipientId): GroupSendEndorsement? { + return endorsements[recipientId] + } + + fun isMissingAnyEndorsements(): Boolean { + return endorsements.values.any { it == null } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index 8528cba394..7d6953c650 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState 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.groups.GroupId import org.thoughtcrime.securesms.profiles.ProfileName @@ -60,7 +60,7 @@ data class RecipientRecord( val profileSharing: Boolean, val lastProfileFetch: Long, val notificationChannel: String?, - val unidentifiedAccessMode: UnidentifiedAccessMode, + val sealedSenderAccessMode: SealedSenderAccessMode, val capabilities: Capabilities, val storageId: ByteArray?, val mentionSetting: MentionSetting, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index eb6c5a3f85..3ecb17bb75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -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 public static void setMemberAdmin(@NonNull Context context, @NonNull GroupId.V2 groupId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 65a7913e30..6f5eee8186 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -18,6 +18,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.storageservice.protos.groups.AccessControl; 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.Member; 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.sms.MessageSender; 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.GroupCandidate; import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct; @@ -219,16 +222,18 @@ final class GroupManagerV2 { throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception { GroupSecretParams groupSecretParams = GroupSecretParams.generate(); - DecryptedGroup decryptedGroup; + DecryptedGroupResponse createGroupResponse; try { - decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessagesTimer); + createGroupResponse = createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessagesTimer); } catch (GroupAlreadyExistsException e) { throw new GroupChangeFailedException(e); } - GroupMasterKey masterKey = groupSecretParams.getMasterKey(); - GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup); + DecryptedGroup decryptedGroup = createGroupResponse.getGroup(); + 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) { throw new GroupChangeFailedException("Unable to create group, group already exists"); @@ -670,7 +675,8 @@ final class GroupManagerV2 { previousGroupState = v2GroupProperties.getDecryptedGroup(); - GroupChange signedGroupChange = commitToServer(changeActions); + GroupChangeResponse changeResponse = commitToServer(changeActions); + GroupChange signedGroupChange = changeResponse.groupChange; try { //noinspection OptionalGetWithoutIsPresent decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get(); @@ -680,7 +686,7 @@ final class GroupManagerV2 { 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); 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); } - private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change) + private @NonNull GroupChangeResponse commitToServer(@NonNull GroupChange.Actions change) throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException { try { @@ -747,6 +753,14 @@ final class GroupManagerV2 { .forceSanityUpdateFromServer(timestamp); } + @WorkerThread + void updateGroupSendEndorsements() + throws IOException, GroupNotAMemberException + { + GroupsV2StateProcessor.forGroup(serviceIds, groupMasterKey) + .updateGroupSendEndorsements(); + } + private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) { if (signedGroupChange != null && signedGroupChange.length > 0) { GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey)); @@ -764,11 +778,11 @@ final class GroupManagerV2 { } @WorkerThread - private @NonNull DecryptedGroup createGroupOnServer(@NonNull GroupSecretParams groupSecretParams, - @Nullable String name, - @Nullable byte[] avatar, - @NonNull Collection members, - int disappearingMessageTimerSeconds) + private @NonNull DecryptedGroupResponse createGroupOnServer(@NonNull GroupSecretParams groupSecretParams, + @Nullable String name, + @Nullable byte[] avatar, + @NonNull Collection members, + int disappearingMessageTimerSeconds) throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException { if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(members)) { @@ -797,15 +811,10 @@ final class GroupManagerV2 { disappearingMessageTimerSeconds); 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)); - if (decryptedGroup == null) { - throw new GroupChangeFailedException(); - } - - return decryptedGroup; - } catch (VerificationFailedException | InvalidGroupStateException e) { + return groupResponse; + } catch (VerificationFailedException | InvalidGroupStateException | InvalidInputException e) { throw new GroupChangeFailedException(e); } catch (GroupExistsException e) { throw new GroupAlreadyExistsException(e); @@ -837,8 +846,9 @@ final class GroupManagerV2 { @Nullable byte[] avatar) throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException { - boolean requestToJoin = joinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR; - boolean alreadyAMember = false; + boolean requestToJoin = joinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR; + boolean alreadyAMember = false; + boolean groupAlreadyExists = false; if (requestToJoin) { Log.i(TAG, "Requesting to join " + groupId); @@ -846,10 +856,13 @@ final class GroupManagerV2 { Log.i(TAG, "Joining " + groupId); } - GroupChange signedGroupChange = null; - DecryptedGroupChange decryptedChange = null; + GroupChangeResponse groupChangeResponse = null; + GroupChange signedGroupChange = null; + DecryptedGroupChange decryptedChange = null; + try { - signedGroupChange = joinGroupOnServer(requestToJoin, joinInfo.revision); + groupChangeResponse = joinGroupOnServer(requestToJoin, joinInfo.revision); + signedGroupChange = groupChangeResponse.groupChange; if (requestToJoin) { 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)); } - decryptedChange = decryptChange(signedGroupChange); + decryptedChange = decryptChange(Objects.requireNonNull(signedGroupChange)); } catch (GroupJoinAlreadyAMemberException e) { Log.i(TAG, "Server reports that we are already a member of " + groupId); alreadyAMember = true; @@ -869,28 +882,37 @@ final class GroupManagerV2 { if (group.isPresent()) { Log.i(TAG, "Group already present locally"); - if (decryptedChange != null) { - 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); - } - } + groupAlreadyExists = true; } else { - GroupId.V2 groupId = groupDatabase.create(groupMasterKey, decryptedGroup); + GroupId.V2 groupId = groupDatabase.create(groupMasterKey, decryptedGroup, null); + if (groupId != null) { Log.i(TAG, "Created local group with placeholder"); } else { - Log.i(TAG, "Create placeholder failed, group suddenly present locally, attempting to apply change"); - if (decryptedChange != null) { - 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); - } - } + Log.i(TAG, "Create placeholder failed, group suddenly present locally"); + groupAlreadyExists = true; + } + } + + if (groupAlreadyExists) { + 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(); } - private @NonNull GroupChange joinGroupOnServer(boolean requestToJoin, int currentRevision) + private @NonNull GroupChangeResponse joinGroupOnServer(boolean requestToJoin, int currentRevision) throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException { if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(Collections.singleton(Recipient.self().getId()))) { @@ -1029,7 +1051,7 @@ final class GroupManagerV2 { 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 { for (int attempt = 0; attempt < 5; attempt++) { @@ -1038,10 +1060,10 @@ final class GroupManagerV2 { .build(); 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); - return signedGroupChange; + return changeResponse; } catch (GroupPatchNotAcceptedException e) { Log.w(TAG, "Patch not accepted", e); @@ -1051,7 +1073,7 @@ final class GroupManagerV2 { } else { throw new GroupChangeFailedException(e); } - } catch (VerificationFailedException | InvalidGroupStateException ex) { + } catch (VerificationFailedException | InvalidGroupStateException | InvalidInputException ex) { throw new GroupChangeFailedException(ex); } } catch (ConflictException e) { @@ -1064,7 +1086,7 @@ final class GroupManagerV2 { 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 { try { @@ -1109,7 +1131,7 @@ final class GroupManagerV2 { } private boolean testGroupMembership() - throws IOException, VerificationFailedException, InvalidGroupStateException + throws IOException, VerificationFailedException, InvalidGroupStateException, InvalidInputException { try { groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams)); @@ -1140,7 +1162,7 @@ final class GroupManagerV2 { DecryptedGroupChange decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get(); 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); } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { @@ -1165,7 +1187,7 @@ final class GroupManagerV2 { .build(); 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); return signedGroupChange; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateDiff.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateDiff.kt index 1d4ce43461..96a9973852 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateDiff.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateDiff.kt @@ -1,5 +1,6 @@ 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.DecryptedGroupChange import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog @@ -9,10 +10,15 @@ import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog */ class GroupStateDiff( val previousGroupState: DecryptedGroup?, - val serverHistory: List + val serverHistory: List, + 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 get() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt index d75d5f7e56..00382ee4a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.kt @@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import org.signal.core.util.logging.Log import org.signal.core.util.orNull +import org.signal.libsignal.zkgroup.InvalidInputException import org.signal.libsignal.zkgroup.VerificationFailedException import org.signal.libsignal.zkgroup.groups.GroupMasterKey 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.InvalidGroupStateException 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.ACI 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 groupOperations = AppDependencies.groupsV2Operations.forGroup(groupSecretParams) private val groupId = GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier()) 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. * @@ -233,7 +266,8 @@ class GroupsV2StateProcessor private constructor( return saveGroupUpdate( timestamp = timestamp, serverGuid = serverGuid, - groupStateDiff = groupStateDiff + groupStateDiff = groupStateDiff, + groupSendEndorsements = null ) } @@ -259,6 +293,8 @@ class GroupsV2StateProcessor private constructor( else -> return InternalUpdateResult.from(result.getCause()!!) } + val sendEndorsementExpiration = groupRecord.map { it.groupSendEndorsementExpiration }.orElse(0L) + var includeFirstState = currentLocalState == null || currentLocalState.revision < 0 || currentLocalState.revision == joinedAtRevision || @@ -273,20 +309,30 @@ class GroupsV2StateProcessor private constructor( var hasRemainingRemoteChanges = false 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 updatedGroupState: DecryptedGroup? = applyGroupStateDiffResult.updatedGroupState if (updatedGroupState == null || updatedGroupState == remoteGroupStateDiff.previousGroupState) { 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 } 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) { Log.d(TAG, "$logPrefix Inserting group changes into chat history") @@ -320,7 +366,7 @@ class GroupsV2StateProcessor private constructor( } 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) } @@ -341,19 +387,20 @@ class GroupsV2StateProcessor private constructor( private fun updateToLatestViaServer(timestamp: Long, currentLocalState: DecryptedGroup?, reconstructChange: Boolean): InternalUpdateResult { 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 } else { return InternalUpdateResult.from(result.getCause()!!) } - val completeGroupChange = if (reconstructChange) GroupChangeReconstruct.reconstructGroupChange(currentLocalState, serverState) else null - val remoteGroupStateDiff = GroupStateDiff(currentLocalState, serverState, completeGroupChange) + val completeGroupChange = if (reconstructChange) GroupChangeReconstruct.reconstructGroupChange(currentLocalState, groupResponse.group) else null + val remoteGroupStateDiff = GroupStateDiff(currentLocalState, groupResponse.group, completeGroupChange) return saveGroupUpdate( timestamp = timestamp, 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) - private fun getGroupChangeLogs(localState: DecryptedGroup?, logsNeededFromRevision: Int, includeFirstState: Boolean): Pair { + private fun getGroupChangeLogs( + localState: DecryptedGroup?, + logsNeededFromRevision: Int, + includeFirstState: Boolean, + sendEndorsementsExpirationMs: Long + ): Pair { 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) { throw IOException(e) } catch (e: VerificationFailedException) { throw IOException(e) + } catch (e: InvalidInputException) { + throw IOException(e) } } private fun saveGroupUpdate( timestamp: Long, serverGuid: String?, - groupStateDiff: GroupStateDiff + groupStateDiff: GroupStateDiff, + groupSendEndorsements: ReceivedGroupSendEndorsements? ): InternalUpdateResult { val currentLocalState: DecryptedGroup? = groupStateDiff.previousGroupState val applyGroupStateDiffResult = GroupStatePatcher.applyGroupStateDiff(groupStateDiff, GroupStatePatcher.LATEST) @@ -426,12 +481,18 @@ class GroupsV2StateProcessor private constructor( if (updatedGroupState == null || updatedGroupState == groupStateDiff.previousGroupState) { 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 } else { 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) { 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) } - private fun saveGroupState(groupStateDiff: GroupStateDiff, updatedGroupState: DecryptedGroup) { + private fun saveGroupState(groupStateDiff: GroupStateDiff, updatedGroupState: DecryptedGroup, groupSendEndorsements: ReceivedGroupSendEndorsements?) { val previousGroupState = groupStateDiff.previousGroupState + if (groupSendEndorsements != null) { + Log.i(TAG, "$logPrefix Updating send endorsements") + } + val needsAvatarFetch = if (previousGroupState == null) { - val groupId = SignalDatabase.groups.create(groupMasterKey, updatedGroupState) + val groupId = SignalDatabase.groups.create(groupMasterKey, updatedGroupState, groupSendEndorsements) if (groupId == null) { Log.w(TAG, "$logPrefix Group create failed, trying to update") - SignalDatabase.groups.update(groupMasterKey, updatedGroupState) + SignalDatabase.groups.update(groupMasterKey, updatedGroupState, groupSendEndorsements) } updatedGroupState.avatar.isNotEmpty() } else { - SignalDatabase.groups.update(groupMasterKey, updatedGroupState) + SignalDatabase.groups.update(groupMasterKey, updatedGroupState, groupSendEndorsements) updatedGroupState.avatar != previousGroupState.avatar } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java index 3ef6b35807..416b6d89f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java @@ -4,7 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.SignalDatabase; 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.util.RemoteConfig; 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.push.SignalServiceAddress; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.concurrent.TimeUnit; /** @@ -136,12 +134,11 @@ public class AutomaticSessionResetJob extends BaseJob { return; } - SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); - Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); + SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); + SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); try { - messageSender.sendNullMessage(address, unidentifiedAccess); + messageSender.sendNullMessage(address, SealedSenderAccessUtil.getSealedSenderAccessFor(recipient)); } catch (UntrustedIdentityException e) { Log.w(TAG, "Unable to send null message."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkUpdateSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkUpdateSendJob.kt index 2d36042241..0d09dc8280 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkUpdateSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkUpdateSendJob.kt @@ -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.ServerRejectedException import org.whispersystems.signalservice.internal.push.SyncMessage.CallLinkUpdate -import java.util.Optional import java.util.concurrent.TimeUnit /** @@ -83,7 +82,7 @@ class CallLinkUpdateSendJob private constructor( ) AppDependencies.signalServiceMessageSender - .sendSyncMessage(SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate), Optional.empty()) + .sendSyncMessage(SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate)) if (callLinkUpdateType == CallLinkUpdate.Type.DELETE) { SignalDatabase.callLinks.deleteCallLink(callLinkRoomId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt index a6ccff06b0..804fd54232 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLogEventSendJob.kt @@ -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.ServerRejectedException import org.whispersystems.signalservice.internal.push.SyncMessage -import java.util.Optional import java.util.concurrent.TimeUnit /** @@ -98,10 +97,7 @@ class CallLogEventSendJob private constructor( override fun onRun() { AppDependencies.signalServiceMessageSender - .sendSyncMessage( - SignalServiceSyncMessage.forCallLogEvent(callLogEvent), - Optional.empty() - ) + .sendSyncMessage(SignalServiceSyncMessage.forCallLogEvent(callLogEvent)) } override fun onShouldRetry(e: Exception): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallSyncEventJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallSyncEventJob.kt index b451a848ec..ca94d33af7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallSyncEventJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallSyncEventJob.kt @@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer import org.thoughtcrime.securesms.service.webrtc.CallEventSyncMessageUtil import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage import org.whispersystems.signalservice.internal.push.SyncMessage -import java.util.Optional import java.util.concurrent.TimeUnit /** @@ -142,7 +141,7 @@ class CallSyncEventJob private constructor( val syncMessage = createSyncMessage(syncTimestamp, callSyncEvent, call.type) return try { - AppDependencies.signalServiceMessageSender.sendSyncMessage(SignalServiceSyncMessage.forCallEvent(syncMessage), Optional.empty()) + AppDependencies.signalServiceMessageSender.sendSyncMessage(SignalServiceSyncMessage.forCallEvent(syncMessage)) null } catch (e: Exception) { Log.w(TAG, "Unable to send call event sync message for ${callSyncEvent.callId}", e) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java index e37271b06c..ce570b87f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java @@ -10,11 +10,11 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; 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.NoSuchMessageException; 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.model.MessageId; 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.IndividualSendEvents; 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.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -160,7 +159,7 @@ public class IndividualSendJob extends PushSendJob { Recipient recipient = message.getThreadRecipient().fresh(); byte[] profileKey = recipient.getProfileKey(); - UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode(); + SealedSenderAccessMode accessMode = recipient.getSealedSenderAccessMode(); boolean unidentified = deliver(message, originalEditedMessage); @@ -177,15 +176,15 @@ public class IndividualSendJob extends PushSendJob { 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."); - SignalDatabase.recipients().setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.UNRESTRICTED); - } else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) { + SignalDatabase.recipients().setSealedSenderAccessMode(recipient.getId(), SealedSenderAccessMode.UNRESTRICTED); + } else if (unidentified && accessMode == SealedSenderAccessMode.UNKNOWN) { log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-enabled following a UD send."); - SignalDatabase.recipients().setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.ENABLED); - } else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) { + SignalDatabase.recipients().setSealedSenderAccessMode(recipient.getId(), SealedSenderAccessMode.ENABLED); + } else if (!unidentified && accessMode != SealedSenderAccessMode.DISABLED) { 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) { @@ -306,25 +305,35 @@ public class IndividualSendJob extends PushSendJob { if (originalEditedMessage != null) { if (Util.equals(SignalStore.account().getAci(), address.getServiceId())) { - Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); SendMessageResult result = messageSender.sendSelfSyncEditMessage(new SignalServiceEditMessage(originalEditedMessage.getDateSent(), mediaMessage)); SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false); - return syncAccess.isPresent(); + return SealedSenderAccessUtil.getSealedSenderCertificate() != null; } 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); return result.getSuccess().isUnidentified(); } } else if (Util.equals(SignalStore.account().getAci(), address.getServiceId())) { - Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); SendMessageResult result = messageSender.sendSyncMessage(mediaMessage); SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false); - return syncAccess.isPresent(); + return SealedSenderAccessUtil.getSealedSenderCertificate() != null; } else { 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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java index 88b62b8f80..925ab0989b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.RecipientReader; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -88,8 +87,8 @@ public class MultiDeviceBlockedUpdateJob extends BaseJob { } SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(blockedIndividuals, blockedGroups)), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(blockedIndividuals, blockedGroups)) + ); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceCallLinkSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceCallLinkSyncJob.kt index edb40b854b..f3d7be2e73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceCallLinkSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceCallLinkSyncJob.kt @@ -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.push.exceptions.PushNetworkException import org.whispersystems.signalservice.internal.push.SyncMessage.CallLinkUpdate -import java.util.Optional import kotlin.time.Duration.Companion.days /** @@ -57,7 +56,7 @@ class MultiDeviceCallLinkSyncJob private constructor( val syncMessage = SignalServiceSyncMessage.forCallLinkUpdate(callLinkUpdate) try { - AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessage, Optional.empty()) + AppDependencies.signalServiceMessageSender.sendSyncMessage(syncMessage) } catch (e: Exception) { Log.w(TAG, "Unable to send call link update message.", e) throw e diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java index 67756f7cb5..35f4d6d26d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java @@ -5,10 +5,9 @@ import androidx.annotation.NonNull; 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.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.net.NotPushRegisteredException; @@ -106,8 +105,8 @@ public class MultiDeviceConfigurationUpdateJob extends BaseJob { messageSender.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(readReceiptsEnabled), Optional.of(unidentifiedDeliveryIndicatorsEnabled), Optional.of(typingIndicatorsEnabled), - Optional.of(linkPreviewsEnabled))), - UnidentifiedAccessUtil.getAccessForSync(context)); + Optional.of(linkPreviewsEnabled))) + ); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index 8a4fc0e8a6..458ea90049 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -15,13 +15,12 @@ import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.net.NotPushRegisteredException; @@ -298,8 +297,8 @@ public class MultiDeviceContactUpdateJob extends BaseJob { .withLength(length) .withResumableUploadSpec(messageSender.getResumableUploadSpec()); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)) + ); } catch (IOException ioe) { throw new NetworkException(ioe); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java index e2bdc25f00..e122e936d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java @@ -5,7 +5,6 @@ import androidx.annotation.NonNull; 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.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; @@ -73,8 +72,8 @@ public class MultiDeviceKeysUpdateJob extends BaseJob { SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey(); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.ofNullable(storageServiceKey), Optional.of(SignalStore.svr().getOrCreateMasterKey()))), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.ofNullable(storageServiceKey), Optional.of(SignalStore.svr().getOrCreateMasterKey()))) + ); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java index 9f67afe940..3fc50ca7ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java @@ -5,10 +5,9 @@ import androidx.annotation.NonNull; 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.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; @@ -122,8 +121,8 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob { } if (response != null) { - messageSender.sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response) + ); } else { Log.w(TAG, recipient.getId() + " not registered!"); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceOutgoingPaymentSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceOutgoingPaymentSyncJob.java index dd7f5b8c50..e9d2fd0ce1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceOutgoingPaymentSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceOutgoingPaymentSyncJob.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.PaymentTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.AppDependencies; @@ -117,8 +116,8 @@ public final class MultiDeviceOutgoingPaymentSyncJob extends BaseJob { AppDependencies.getSignalServiceMessageSender() - .sendSyncMessage(SignalServiceSyncMessage.forOutgoingPayment(outgoingPaymentMessage), - UnidentifiedAccessUtil.getAccessForSync(context)); + .sendSyncMessage(SignalServiceSyncMessage.forOutgoingPayment(outgoingPaymentMessage) + ); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileContentUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileContentUpdateJob.java index 813f682e82..a44de92d20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileContentUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileContentUpdateJob.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; 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.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; @@ -58,8 +57,8 @@ public class MultiDeviceProfileContentUpdateJob extends BaseJob { SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE) + ); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java index ef8e8f638d..16b7c21ab8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java @@ -7,7 +7,6 @@ import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; @@ -98,7 +97,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob { SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, false)); - messageSender.sendSyncMessage(syncMessage, UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(syncMessage); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java index 8102a3f483..76eeb9a69d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java @@ -8,12 +8,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.signal.core.util.ListUtil; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; @@ -121,7 +120,7 @@ public class MultiDeviceReadUpdateJob extends BaseJob { } SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages), UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackOperationJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackOperationJob.java index 3658bbdac5..b673612a18 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackOperationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackOperationJob.java @@ -3,13 +3,12 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; 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.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.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; @@ -93,8 +92,8 @@ public class MultiDeviceStickerPackOperationJob extends BaseJob { SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); StickerPackOperationMessage stickerPackOperation = new StickerPackOperationMessage(packIdBytes, packKeyBytes, remoteType); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(Collections.singletonList(stickerPackOperation)), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(Collections.singletonList(stickerPackOperation)) + ); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackSyncJob.java index 0a360557c7..fcbf3d253b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackSyncJob.java @@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.signal.core.util.Hex; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader; 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.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; -import org.signal.core.util.Hex; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; @@ -80,8 +79,8 @@ public class MultiDeviceStickerPackSyncJob extends BaseJob { } SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(operations), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(operations) + ); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorageSyncRequestJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorageSyncRequestJob.java index 02125a3220..81da6276a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorageSyncRequestJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorageSyncRequestJob.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; 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.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; @@ -58,8 +57,7 @@ public class MultiDeviceStorageSyncRequestJob extends BaseJob { SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorySendSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorySendSyncJob.kt index d061ab78e9..0764592c7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorySendSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorySendSyncJob.kt @@ -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.SignalServiceSyncMessage import org.whispersystems.signalservice.api.push.SignalServiceAddress -import java.lang.Exception import java.util.Optional import java.util.concurrent.TimeUnit @@ -57,7 +56,7 @@ class MultiDeviceStorySendSyncJob private constructor(parameters: Parameters, pr val updateManifest = SignalDatabase.storySends.getLocalManifest(sentTimestamp) val recipientsSet: Set = updateManifest.toRecipientsSet() 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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceSubscriptionSyncRequestJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceSubscriptionSyncRequestJob.kt index 273e07850f..b6e6ca1df7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceSubscriptionSyncRequestJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceSubscriptionSyncRequestJob.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.jobs import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint @@ -57,10 +56,7 @@ class MultiDeviceSubscriptionSyncRequestJob private constructor(parameters: Para val messageSender = AppDependencies.signalServiceMessageSender - messageSender.sendSyncMessage( - SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.SUBSCRIPTION_STATUS), - UnidentifiedAccessUtil.getAccessForSync(context) - ) + messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.SUBSCRIPTION_STATUS)) } override fun onShouldRetry(e: Exception): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java index 18b3e3395b..37f2522cd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java @@ -4,20 +4,19 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.signal.core.util.Base64; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; -import org.signal.core.util.Base64; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -116,8 +115,8 @@ public class MultiDeviceVerifiedUpdateJob extends BaseJob { SignalServiceAddress verifiedAddress = RecipientUtil.toSignalServiceAddress(context, recipient); VerifiedMessage verifiedMessage = new VerifiedMessage(verifiedAddress, new IdentityKey(identityKey, 0), verifiedState, timestamp); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage), - UnidentifiedAccessUtil.getAccessFor(context, recipient)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage) + ); } catch (InvalidKeyException e) { throw new IOException(e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewOnceOpenJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewOnceOpenJob.java index a1860019da..b8acf0e402 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewOnceOpenJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewOnceOpenJob.java @@ -6,11 +6,10 @@ import androidx.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonProperty; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; 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); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewOnceOpen(openMessage), UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewOnceOpen(openMessage)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewedUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewedUpdateJob.java index f74879289a..02d6231e18 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewedUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewedUpdateJob.java @@ -8,12 +8,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.signal.core.util.ListUtil; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; @@ -121,7 +120,7 @@ public class MultiDeviceViewedUpdateJob extends BaseJob { } SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages), UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/NullMessageSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/NullMessageSendJob.java index 5253a2c2c0..9e710ad6ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/NullMessageSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/NullMessageSendJob.java @@ -4,21 +4,19 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; 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.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; -import java.util.Optional; import java.util.concurrent.TimeUnit; /** @@ -72,12 +70,11 @@ public class NullMessageSendJob extends BaseJob { Log.w(TAG, recipient.getId() + " not registered!"); } - SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); - Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); + SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); + SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); try { - messageSender.sendNullMessage(address, unidentifiedAccess); + messageSender.sendNullMessage(address, SealedSenderAccessUtil.getSealedSenderAccessFor(recipient)); } catch (UntrustedIdentityException e) { Log.w(TAG, "Unable to send null message."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java index b2a0c517e5..83bcbc0630 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java @@ -4,12 +4,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.SignalDatabase; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; 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.IndividualSendEvents; 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.SignalServiceDataMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import java.io.IOException; -import java.util.Optional; import java.util.UUID; public final class PaymentNotificationSendJob extends BaseJob { @@ -81,9 +79,8 @@ public final class PaymentNotificationSendJob extends BaseJob { return; } - SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); - Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); + SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); + SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); 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)) .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()) { SignalDatabase.pendingPniSignatureMessages().insertIfNecessary(recipientId, dataMessage.getTimestamp(), sendMessageResult); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResendMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResendMessageJob.java index db7e6c4388..76ccd84351 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResendMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResendMessageJob.java @@ -7,7 +7,7 @@ import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.SignalProtocolAddress; 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.model.DistributionListRecord; 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.whispersystems.signalservice.api.SignalServiceMessageSender; 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.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -141,9 +141,9 @@ public class ResendMessageJob extends BaseJob { return; } - SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); - Optional access = UnidentifiedAccessUtil.getAccessFor(context, recipient); - Content contentToSend = content; + SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); + Content contentToSend = content; + SealedSenderAccess.CreateGroupSendToken createGroupSendToken = null; if (distributionId != 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."); return; } + + createGroupSendToken = () -> SignalDatabase.groups().getGroupSendFullToken(groupId, recipientId); } else { Log.d(TAG, "GroupId is not present. Assuming this is a message for a distribution list."); DistributionListRecord listRecord = SignalDatabase.distributionLists().getListByDistributionId(distributionId); @@ -178,6 +180,8 @@ public class ResendMessageJob extends BaseJob { SendMessageResult result; + SealedSenderAccess access = SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, createGroupSendToken); + try { result = messageSender.resendContent(address, access, sentTimestamp, contentToSend, contentHint, Optional.ofNullable(groupId).map(GroupId::getDecodedId), urgent); } catch (IllegalStateException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.kt index 9cb18e24dd..93ab2aaca9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.kt @@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.database.GroupTable import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.RecipientTable.Companion.maskCapabilitiesToLong 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.model.RecipientRecord import org.thoughtcrime.securesms.dependencies.AppDependencies @@ -222,7 +222,7 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val unrestrictedUnidentifiedAccess = remoteProfile.isUnrestrictedUnidentifiedAccess ) - if (localRecipientRecord.unidentifiedAccessMode != accessMode) { + if (localRecipientRecord.sealedSenderAccessMode != accessMode) { return true } @@ -322,8 +322,8 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val val profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.profileKey) val newMode = deriveUnidentifiedAccessMode(profileKey, unidentifiedAccessVerifier, unrestrictedUnidentifiedAccess) - if (recipient.unidentifiedAccessMode !== newMode) { - if (newMode === UnidentifiedAccessMode.UNRESTRICTED) { + if (recipient.sealedSenderAccessMode !== newMode) { + if (newMode === SealedSenderAccessMode.UNRESTRICTED) { Log.i(TAG, "Marking recipient UD status as unrestricted.") } else if (profileKey == null || unidentifiedAccessVerifier == null) { 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.") } - 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) { - UnidentifiedAccessMode.UNRESTRICTED + SealedSenderAccessMode.UNRESTRICTED } else if (profileKey == null || unidentifiedAccessVerifier == null) { - UnidentifiedAccessMode.DISABLED + SealedSenderAccessMode.DISABLED } else { val profileCipher = ProfileCipher(profileKey) val verifiedUnidentifiedAccess: Boolean = try { @@ -350,9 +350,9 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val } if (verifiedUnidentifiedAccess) { - UnidentifiedAccessMode.ENABLED + SealedSenderAccessMode.ENABLED } else { - UnidentifiedAccessMode.DISABLED + SealedSenderAccessMode.DISABLED } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java index 15c97081f0..a435164545 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java @@ -5,12 +5,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.model.MessageId; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; @@ -123,7 +124,7 @@ public class SendDeliveryReceiptJob extends BaseJob { timestamp); SendMessageResult result = messageSender.sendReceipt(remoteAddress, - UnidentifiedAccessUtil.getAccessFor(context, recipient), + SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, this::getGroupSendFullToken), receiptMessage, 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 public boolean onShouldRetry(@NonNull Exception e) { if (e instanceof ServerRejectedException) return false; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java index 34716e5c93..35f226810a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java @@ -9,14 +9,14 @@ import androidx.annotation.VisibleForTesting; import org.signal.core.util.ListUtil; 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.SignalDatabase; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; @@ -188,7 +188,8 @@ public class SendReadReceiptJob extends BaseJob { SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageSentTimestamps, timestamp); SendMessageResult result = messageSender.sendReceipt(remoteAddress, - UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), + SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, + () -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)), receiptMessage, recipient.getNeedsPniSignature()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendRetryReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendRetryReceiptJob.java index b1d2ddf8b2..37cec0fa99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendRetryReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendRetryReceiptJob.java @@ -4,19 +4,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.InvalidMessageException; 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.groups.GroupId; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; 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.exceptions.PushNetworkException; @@ -87,12 +86,11 @@ public final class SendRetryReceiptJob extends BaseJob { return; } - SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); - Optional access = UnidentifiedAccessUtil.getAccessFor(context, recipient); - Optional group = groupId.map(GroupId::getDecodedId); + SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); + Optional group = groupId.map(GroupId::getDecodedId); 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java index 317cb251e9..6d9cd00862 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java @@ -8,15 +8,15 @@ import androidx.annotation.Nullable; import org.signal.core.util.ListUtil; 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.SignalDatabase; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.net.NotPushRegisteredException; @@ -208,7 +208,8 @@ public class SendViewedReceiptJob extends BaseJob { timestamp); SendMessageResult result = messageSender.sendReceipt(remoteAddress, - UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), + SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, + () -> SignalDatabase.groups().getGroupSendFullToken(threadId, recipientId)), receiptMessage, recipient.getNeedsPniSignature()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java index 16583cd1a7..1bd0f8db24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java @@ -6,19 +6,19 @@ import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.SignalProtocolAddress; 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.model.DistributionListRecord; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; 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.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -85,12 +85,14 @@ public final class SenderKeyDistributionSendJob extends BaseJob { return; } - GroupId.V2 groupId; - DistributionId distributionId; + GroupId.V2 groupId; + DistributionId distributionId; + SealedSenderAccess.CreateGroupSendToken createGroupSendFullToken = null; if (threadRecipient.isPushV2Group()) { - groupId = threadRecipient.requireGroupId().requireV2(); - distributionId = SignalDatabase.groups().getOrCreateDistributionId(groupId); + groupId = threadRecipient.requireGroupId().requireV2(); + distributionId = SignalDatabase.groups().getOrCreateDistributionId(groupId); + createGroupSendFullToken = () -> SignalDatabase.groups().getGroupSendFullToken(groupId, targetRecipientId); } else if (threadRecipient.isDistributionList()) { groupId = null; distributionId = SignalDatabase.distributionLists().getDistributionId(threadRecipientId); @@ -116,10 +118,10 @@ public final class SenderKeyDistributionSendJob extends BaseJob { } } - SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - List address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, targetRecipient)); - SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId); - List> access = UnidentifiedAccessUtil.getAccessFor(context, Collections.singletonList(targetRecipient)); + SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); + List address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, targetRecipient)); + SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId); + List 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java index 154dfd398f..93d8c4d70e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -12,7 +12,6 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase; import org.signal.core.util.Stopwatch; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.InvalidKeyException; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.UnknownStorageIdTable; @@ -209,7 +208,7 @@ public class StorageSyncJob extends BaseJob { } else { Log.w(TAG, "Failed to decrypt remote storage! Requesting new keys from primary.", e); 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))); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendEndorsementInternalNotifier.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendEndorsementInternalNotifier.kt new file mode 100644 index 0000000000..4a91b05b4e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendEndorsementInternalNotifier.kt @@ -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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java index 4c7ecf2f99..663097bef2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java @@ -7,34 +7,44 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; 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.InvalidRegistrationIdException; 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.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.database.model.GroupRecord; +import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil; import org.thoughtcrime.securesms.database.MessageSendLogTables; import org.thoughtcrime.securesms.database.SignalDatabase; 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.dependencies.AppDependencies; +import org.thoughtcrime.securesms.groups.GroupChangeException; 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.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; -import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.RecipientAccessList; +import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.CancelationException; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender.LegacyGroupEvents; import org.whispersystems.signalservice.api.SignalServiceMessageSender.SenderKeyGroupEvents; 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.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; 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.calls.SignalServiceCallMessage; 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.exceptions.NotFoundException; 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 java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -248,28 +260,81 @@ public final class GroupSendUtil { Set unregisteredTargets = allTargets.stream().filter(Recipient::isUnregistered).collect(Collectors.toSet()); List registeredTargets = allTargets.stream().filter(r -> !unregisteredTargets.contains(r)).collect(Collectors.toList()); - RecipientData recipients = new RecipientData(context, registeredTargets, isStorySend); - Optional groupRecord = groupId != null ? SignalDatabase.groups().getGroup(groupId) : Optional.empty(); + RecipientData recipients = new RecipientData(context, registeredTargets, isStorySend); + Optional 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 senderKeyTargets = new LinkedList<>(); List legacyTargets = new LinkedList<>(); for (Recipient recipient : registeredTargets) { - Optional access = recipients.getAccessPair(recipient.getId()); - boolean validMembership = true; + Optional access = recipients.getAccessPair(recipient.getId()); + boolean validMembership = groupId == null || (groupRecord.isPresent() && groupRecord.get().getMembers().contains(recipient.getId())); - if (groupId != null && (!groupRecord.isPresent() || !groupRecord.get().getMembers().contains(recipient.getId()))) { - validMembership = false; - } - - if (recipient.getHasServiceId() && - access.isPresent() && - access.get().getTargetUnidentifiedAccess().isPresent() && - validMembership) - { - senderKeyTargets.add(recipient); + if (useGroupSendEndorsements) { + GroupSendEndorsement groupSendEndorsement = groupSendEndorsementRecords.getEndorsement(recipient.getId()); + if (groupSendEndorsement != null && recipient.getHasAci() && validMembership) { + senderKeyTargets.add(recipient); + } else { + legacyTargets.add(recipient); + if (validMembership) { + Log.w(TAG, "Should be using group send endorsement but not found for " + recipient.getId()); + if (RemoteConfig.internalUser()) { + GroupSendEndorsementInternalNotifier.postMissingGroupSendEndorsement(context); + } + } + } } 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 allResults = new ArrayList<>(allTargets.size()); SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender(); - if (senderKeyTargets.size() > 0 && distributionId != null) { + if (Util.hasItems(senderKeyTargets) && distributionId != null) { long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(distributionId); long keyAge = System.currentTimeMillis() - keyCreateTime; @@ -309,14 +374,36 @@ public final class GroupSendUtil { } try { - List targets = senderKeyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList()); - List access = senderKeyTargets.stream().map(r -> recipients.requireAccess(r.getId())).collect(Collectors.toList()); + List targets = new ArrayList<>(senderKeyTargets.size()); + List access = new ArrayList<>(senderKeyTargets.size()); + Map 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 AtomicLong entryId = new AtomicLong(-1); final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog(); - List results = sendOperation.sendWithSenderKey(messageSender, distributionId, targets, access, isRecipientUpdate, partialResults -> { + List results = sendOperation.sendWithSenderKey(messageSender, distributionId, targets, access, groupSendEndorsements, isRecipientUpdate, partialResults -> { if (!includeInMessageLog) { return; } @@ -343,6 +430,10 @@ public final class GroupSendUtil { } catch (InvalidUnidentifiedAccessHeaderException e) { Log.w(TAG, "Someone had a bad UD header. Falling back to legacy sends.", e); legacyTargets.addAll(senderKeyTargets); + + if (useGroupSendEndorsements && RemoteConfig.internalUser()) { + GroupSendEndorsementInternalNotifier.postGroupSendFallbackError(context); + } } catch (NoSessionException e) { Log.w(TAG, "No session. Falling back to legacy sends.", e); 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."); } - List targets = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList()); - List> access = legacyTargets.stream().map(r -> recipients.getAccessPair(r.getId())).collect(Collectors.toList()); - boolean recipientUpdate = isRecipientUpdate || allResults.size() > 0; + List legacyTargetAddresses = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList()); + List legacyTargetAccesses = legacyTargets.stream().map(r -> recipients.getAccess(r.getId())).collect(Collectors.toList()); + List 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 AtomicLong entryId = new AtomicLong(-1); final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog(); - List results = sendOperation.sendLegacy(messageSender, targets, legacyTargets, access, recipientUpdate, result -> { + List results = sendOperation.sendLegacy(messageSender, legacyTargetAddresses, legacyTargets, SealedSenderAccess.forFanOutGroupSend(groupSendTokens, SealedSenderAccessUtil.getSealedSenderCertificate(), legacyTargetAccesses), recipientUpdate, result -> { if (!includeInMessageLog) { return; } @@ -402,7 +510,7 @@ public final class GroupSendUtil { allResults.addAll(results); 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) { SignalLocalMetrics.GroupMessageSend.onLegacyMessageSent(relatedMessageId.getId()); SignalLocalMetrics.GroupMessageSend.onLegacySyncFinished(relatedMessageId.getId()); @@ -448,6 +556,7 @@ public final class GroupSendUtil { @NonNull DistributionId distributionId, @NonNull List targets, @NonNull List access, + @Nullable GroupSendEndorsements groupSendEndorsements, boolean isRecipientUpdate, @Nullable PartialSendBatchCompleteListener partialListener) throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException; @@ -455,7 +564,7 @@ public final class GroupSendUtil { @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, @NonNull List targetRecipients, - @NonNull List> access, + @NonNull List sealedSenderAccesses, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @Nullable CancelationSignal cancelationSignal) @@ -504,19 +613,20 @@ public final class GroupSendUtil { @NonNull DistributionId distributionId, @NonNull List targets, @NonNull List access, + @Nullable GroupSendEndorsements groupSendEndorsements, boolean isRecipientUpdate, @Nullable PartialSendBatchCompleteListener partialListener) throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException { 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 public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, @NonNull List targetRecipients, - @NonNull List> access, + @NonNull List sealedSenderAccesses, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @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 if (targets.size() == 1 && relatedMessageId == null) { - Recipient targetRecipient = targetRecipients.get(0); - SendMessageResult result; + Recipient targetRecipient = targetRecipients.get(0); + SealedSenderAccess sealedSenderAccess = sealedSenderAccesses.get(0); + SendMessageResult result; 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 { - 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()) { @@ -540,10 +651,11 @@ public final class GroupSendUtil { return Collections.singletonList(result); } else { LegacyGroupEvents listener = relatedMessageId != null ? new LegacyMetricEventListener(relatedMessageId.getId()) : LegacyGroupEvents.EMPTY; + 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 { - 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 List targets, @NonNull List access, + @Nullable GroupSendEndorsements groupSendEndorsements, boolean isRecipientUpdate, @Nullable PartialSendBatchCompleteListener partialListener) throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException { - messageSender.sendGroupTyping(distributionId, targets, access, message); + messageSender.sendGroupTyping(distributionId, targets, access, groupSendEndorsements, message); List results = targets.stream().map(a -> SendMessageResult.success(a, Collections.emptyList(), true, false, -1, Optional.empty())).collect(Collectors.toList()); if (partialListener != null) { @@ -609,13 +722,13 @@ public final class GroupSendUtil { public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, @NonNull List targetRecipients, - @NonNull List> access, + @NonNull List sealedSenderAccesses, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @Nullable CancelationSignal cancelationSignal) 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()); } @@ -658,24 +771,25 @@ public final class GroupSendUtil { @NonNull DistributionId distributionId, @NonNull List targets, @NonNull List access, + @Nullable GroupSendEndorsements groupSendEndorsements, boolean isRecipientUpdate, @Nullable PartialSendBatchCompleteListener partialSendListener) throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException { - return messageSender.sendCallMessage(distributionId, targets, access, message, partialSendListener); + return messageSender.sendCallMessage(distributionId, targets, access, groupSendEndorsements, message, partialSendListener); } @Override public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, @NonNull List targetRecipients, - @NonNull List> access, + @NonNull List sealedSenderAccesses, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @Nullable CancelationSignal cancelationSignal) throws IOException { - return messageSender.sendCallMessage(targets, access, message); + return messageSender.sendCallMessage(targets, sealedSenderAccesses, message); } @Override @@ -730,18 +844,19 @@ public final class GroupSendUtil { @NonNull DistributionId distributionId, @NonNull List targets, @NonNull List access, + @Nullable GroupSendEndorsements groupSendEndorsements, boolean isRecipientUpdate, @Nullable PartialSendBatchCompleteListener partialListener) 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 public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, @NonNull List targetRecipients, - @NonNull List> access, + @NonNull List sealedSenderAccesses, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @Nullable CancelationSignal cancelationSignal) @@ -839,12 +954,12 @@ public final class GroupSendUtil { */ private static final class RecipientData { - private final Map> accessById; + private final Map> accessById; private final Map addressById; private final RecipientAccessList accessList; RecipientData(@NonNull Context context, @NonNull List recipients, boolean isForStory) throws IOException { - this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients, isForStory); + this.accessById = SealedSenderAccessUtil.getAccessMapFor(recipients, isForStory); this.addressById = mapAddresses(context, recipients); this.accessList = new RecipientAccessList(recipients); } @@ -853,22 +968,22 @@ public final class GroupSendUtil { return Objects.requireNonNull(addressById.get(id)); } - @NonNull Optional getAccessPair(@NonNull RecipientId id) { + @NonNull Optional getAccessPair(@NonNull RecipientId 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) { - return Objects.requireNonNull(accessById.get(id)).get().getTargetUnidentifiedAccess().get(); + return Objects.requireNonNull(accessById.get(id)).get(); } @NonNull RecipientId requireRecipientId(@NonNull SignalServiceAddress address) { return accessList.requireIdByAddress(address); } - @NonNull List requireRecipientIds(@NonNull List addresses) { - return addresses.stream().map(accessList::requireIdByAddress).collect(Collectors.toList()); - } - private static @NonNull Map mapAddresses(@NonNull Context context, @NonNull List recipients) throws IOException { List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt index 5eebd2c344..3f97588e1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt @@ -32,7 +32,7 @@ import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.thoughtcrime.securesms.R 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.dependencies.AppDependencies import org.thoughtcrime.securesms.groups.BadGroupIdException @@ -137,7 +137,7 @@ object MessageDecryptor { val bufferedStore = bufferedProtocolStore.get(destination) 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 { val startTimeNanos = System.nanoTime() diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt index 7173867c5a..4e5e233829 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt @@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting import org.thoughtcrime.securesms.database.RecipientTable.MissingRecipientException import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState 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.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId @@ -98,7 +98,7 @@ class Recipient( val hiddenState: HiddenState = HiddenState.NOT_HIDDEN, val lastProfileFetchTime: Long = 0, 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, val storageId: ByteArray? = null, val mentionSetting: MentionSetting = MentionSetting.ALWAYS_NOTIFY, @@ -318,10 +318,10 @@ class Recipient( val deleteSyncCapability: Capability = capabilities.deleteSync /** The state around whether we can send sealed sender to this user. */ - val unidentifiedAccessMode: UnidentifiedAccessMode = if (pni.isPresent && pni == serviceId) { - UnidentifiedAccessMode.DISABLED + val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) { + SealedSenderAccessMode.DISABLED } else { - unidentifiedAccessModeValue + sealedSenderAccessModeValue } /** The wallpaper to render as the chat background, if present. */ @@ -760,7 +760,7 @@ class Recipient( systemProfileName == other.systemProfileName && profileAvatar == other.profileAvatar && notificationChannelValue == other.notificationChannelValue && - unidentifiedAccessModeValue == other.unidentifiedAccessModeValue && + sealedSenderAccessModeValue == other.sealedSenderAccessModeValue && storageId.contentEquals(other.storageId) && mentionSetting == other.mentionSetting && wallpaperValue == other.wallpaperValue && diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientCreator.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientCreator.kt index e3b9e09e3d..e689855f8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientCreator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientCreator.kt @@ -162,7 +162,7 @@ object RecipientCreator { lastProfileFetchTime = record.lastProfileFetch, isSelf = isSelf, notificationChannelValue = record.notificationChannel, - unidentifiedAccessModeValue = record.unidentifiedAccessMode, + sealedSenderAccessModeValue = record.sealedSenderAccessMode, capabilities = record.capabilities, storageId = record.storageId, mentionSetting = record.mentionSetting, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index cdefca2ec9..80ad3166cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -36,7 +36,7 @@ import org.signal.ringrtc.PeekInfo; import org.signal.ringrtc.Remote; import org.signal.storageservice.protos.groups.GroupExternalCredential; 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.CallTable; 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.WebRtcServiceState; import org.thoughtcrime.securesms.util.AppForegroundObserver; -import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.RecipientAccessList; +import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; 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.locks.LockManager; import org.webrtc.PeerConnection; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.calls.CallingResponse; @@ -780,8 +781,8 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. try { AppDependencies.getSignalServiceMessageSender() .sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient), - recipient.isSelf() ? Optional.empty() : UnidentifiedAccessUtil.getAccessFor(context, recipient), - callMessage); + recipient.isSelf() ? SealedSenderAccess.NONE : SealedSenderAccessUtil.getSealedSenderAccessFor(recipient), + callMessage); } catch (UntrustedIdentityException e) { Log.i(TAG, "onSendCallMessage onFailure: ", e); RetrieveProfileJob.enqueue(recipient.getId()); @@ -1129,8 +1130,8 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. try { AppDependencies.getSignalServiceMessageSender() .sendCallMessage(RecipientUtil.toSignalServiceAddress(context, recipient), - UnidentifiedAccessUtil.getAccessFor(context, recipient), - callMessage); + SealedSenderAccessUtil.getSealedSenderAccessFor(recipient), + callMessage); process((s, p) -> p.handleMessageSentSuccess(s, remotePeer.getCallId())); } catch (UntrustedIdentityException e) { RetrieveProfileJob.enqueue(remotePeer.getId()); @@ -1158,7 +1159,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. networkExecutor.execute(() -> { try { 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) { 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(() -> { try { 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) { 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(() -> { try { 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) { Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index 421b8a744c..f34781d546 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -17,7 +17,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.badges.models.Badge; 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.SignalDatabase; 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.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.profiles.AvatarUploadParams; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; @@ -118,7 +117,7 @@ public final class ProfileUtil { ServiceResponse response = Single .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)) .blockingGet(); @@ -137,12 +136,12 @@ public final class ProfileUtil { @NonNull SignalServiceProfile.RequestType requestType, boolean allowUnidentifiedAccess) { - ProfileService profileService = AppDependencies.getProfileService(); - Optional unidentifiedAccess = allowUnidentifiedAccess ? getUnidentifiedAccess(context, recipient) : Optional.empty(); - Optional profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()); + ProfileService profileService = AppDependencies.getProfileService(); + SealedSenderAccess sealedSenderAccess = allowUnidentifiedAccess ? SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, false) : SealedSenderAccess.NONE; + Optional profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()); 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))); } @@ -397,16 +396,6 @@ public final class ProfileUtil { } } - private static Optional getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) { - Optional 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 { if (recipient.getRegistered() == RecipientTable.RegisteredState.NOT_REGISTERED) { if (recipient.getHasServiceId()) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt index 9487b485a9..e69d878c80 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt @@ -67,7 +67,7 @@ class ChangeSet { } 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.encode(), distributionId, - System.currentTimeMillis() + System.currentTimeMillis(), + 0 ) ) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 211f8badad..749ef4a50a 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -58,7 +58,7 @@ object RecipientDatabaseTestUtils { profileSharing: Boolean = false, lastProfileFetch: Long = 0L, notificationChannel: String? = null, - unidentifiedAccessMode: RecipientTable.UnidentifiedAccessMode = RecipientTable.UnidentifiedAccessMode.UNKNOWN, + sealedSenderAccessMode: RecipientTable.SealedSenderAccessMode = RecipientTable.SealedSenderAccessMode.UNKNOWN, capabilities: Long = 0L, storageId: ByteArray? = null, mentionSetting: RecipientTable.MentionSetting = RecipientTable.MentionSetting.ALWAYS_NOTIFY, @@ -121,7 +121,7 @@ object RecipientDatabaseTestUtils { profileSharing = profileSharing, lastProfileFetch = lastProfileFetch, notificationChannel = notificationChannel, - unidentifiedAccessMode = unidentifiedAccessMode, + sealedSenderAccessMode = sealedSenderAccessMode, capabilities = RecipientRecord.Capabilities( rawBits = capabilities, deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt()) diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt index ab35a825da..54efd17911 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt @@ -24,6 +24,7 @@ import org.signal.libsignal.protocol.logging.SignalProtocolLogger import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider import org.signal.libsignal.zkgroup.groups.GroupMasterKey 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.local.DecryptedGroup import org.signal.storageservice.protos.groups.local.DecryptedMember @@ -111,9 +112,9 @@ class GroupManagerV2Test_edit { every { groupTable.getGroup(groupId) } returns data.groupRecord every { groupTable.requireGroup(groupId) } returns data.groupRecord.get() - every { groupTable.update(any(), any()) } returns Unit + every { groupTable.update(any(), any(), any()) } returns Unit 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) { @@ -122,7 +123,7 @@ class GroupManagerV2Test_edit { private fun then(then: (DecryptedGroup) -> Unit) { val decryptedGroupArg = slot() - verify { groupTable.update(groupId, capture(decryptedGroupArg)) } + verify { groupTable.update(groupId, capture(decryptedGroupArg), any()) } then(decryptedGroupArg.captured) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.java index 6f36d06430..3053d99f92 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStatePatcherTest.java @@ -41,7 +41,7 @@ public final class GroupStatePatcherTest { @Test 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())); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -52,7 +52,7 @@ public final class GroupStatePatcherTest { public void known_group_with_no_states_to_update() { 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())); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -63,7 +63,7 @@ public final class GroupStatePatcherTest { public void unknown_group_single_state_to_update() { 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)))); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -75,7 +75,7 @@ public final class GroupStatePatcherTest { DecryptedGroup currentState = state(0); 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)))); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -88,7 +88,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log1 = serverLogEntry(1); 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)))); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -101,7 +101,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log1 = serverLogEntry(1); 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)))); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -115,7 +115,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log2 = serverLogEntry(2); 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)))); assertNewState(log2.getGroup(), singletonList(log3), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); @@ -129,7 +129,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log2 = serverLogEntry(2); 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)))); assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); @@ -142,7 +142,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log1 = serverLogEntry(Integer.MAX_VALUE - 1); 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)))); 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() { 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)))); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -165,7 +165,7 @@ public final class GroupStatePatcherTest { DecryptedGroup currentState = state(0); 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)))); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -179,7 +179,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log2 = serverLogEntryWholeStateOnly(2); 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)))); assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); @@ -192,7 +192,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log1 = serverLogEntry(1); 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)))); assertNewState(log3.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); @@ -220,7 +220,7 @@ public final class GroupStatePatcherTest { .build(); 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), new AppliedGroupChangeLog(state3a, log3.getChange()), @@ -241,7 +241,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log7 = serverLogEntryWholeStateOnly(7); 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)))); assertNewState(log8.getGroup(), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); @@ -255,7 +255,7 @@ public final class GroupStatePatcherTest { DecryptedGroupChangeLog log8 = logEntryMissingState(8); 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))))); assertNewState(state(9), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); @@ -287,7 +287,7 @@ public final class GroupStatePatcherTest { .build(), 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), new AppliedGroupChangeLog(state7b, log8.getChange()), @@ -305,7 +305,7 @@ public final class GroupStatePatcherTest { DecryptedGroup currentState = state(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())); assertNewState(state(6), emptyList(), advanceGroupStateResult.getUpdatedGroupState(), advanceGroupStateResult.getRemainingRemoteGroupChanges()); @@ -320,7 +320,7 @@ public final class GroupStatePatcherTest { .build(); 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)))); assertTrue(advanceGroupStateResult.getRemainingRemoteGroupChanges().isEmpty()); @@ -352,7 +352,7 @@ public final class GroupStatePatcherTest { .newMembers(Collections.singletonList(newMember)) .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()); assertThat(advanceGroupStateResult.getProcessedLogEntries(), is(emptyList())); @@ -392,7 +392,7 @@ public final class GroupStatePatcherTest { .newMembers(Collections.singletonList(newMember)) .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()); 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()) .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()); 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()) .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), new AppliedGroupChangeLog(log2.getGroup(), new DecryptedGroupChange.Builder() diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt index 3fa1176d90..3ecf85d19e 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt @@ -56,9 +56,12 @@ import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger import org.thoughtcrime.securesms.testutil.SystemOutLogger 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.GroupsV2Api +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations 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.PNI import org.whispersystems.signalservice.api.push.ServiceIds @@ -88,6 +91,7 @@ class GroupsV2StateProcessorTest { private lateinit var recipientTable: RecipientTable private lateinit var groupsV2API: GroupsV2Api private lateinit var groupsV2Authorization: GroupsV2Authorization + private lateinit var groupsV2Operations: GroupsV2Operations private lateinit var profileAndMessageHelper: ProfileAndMessageHelper private lateinit var jobManager: JobManager @@ -104,6 +108,7 @@ class GroupsV2StateProcessorTest { groupTable = mockk() recipientTable = mockk() groupsV2API = mockk() + groupsV2Operations = mockk() groupsV2Authorization = mockk() profileAndMessageHelper = spyk(ProfileAndMessageHelper(serviceIds.aci, masterKey, groupId)) jobManager = mockk() @@ -112,6 +117,7 @@ class GroupsV2StateProcessorTest { every { AppDependencies.jobManager } returns jobManager every { AppDependencies.signalServiceAccountManager.getGroupsV2Api() } returns groupsV2API every { AppDependencies.groupsV2Authorization } returns groupsV2Authorization + every { AppDependencies.groupsV2Operations } returns groupsV2Operations mockkObject(SignalDatabase) every { SignalDatabase.groups } returns groupTable @@ -120,6 +126,8 @@ class GroupsV2StateProcessorTest { mockkObject(ProfileAndMessageHelper) every { ProfileAndMessageHelper.create(any(), any(), any()) } returns profileAndMessageHelper + every { groupsV2Operations.forGroup(secretParams) } answers { callOriginal() } + processor = GroupsV2StateProcessor.forGroup(serviceIds, masterKey, secretParams) } @@ -142,11 +150,11 @@ class GroupsV2StateProcessorTest { every { groupsV2Authorization.getAuthorizationForToday(serviceIds, secretParams) } returns null if (data.expectTableUpdate) { - justRun { groupTable.update(any(), any()) } + justRun { groupTable.update(any(), any(), any()) } } if (data.expectTableCreate) { - every { groupTable.create(any(), any()) } returns groupId + every { groupTable.create(any(), any(), any()) } returns groupId } if (data.expectTableUpdate || data.expectTableCreate) { @@ -155,11 +163,11 @@ class GroupsV2StateProcessorTest { } data.serverState?.let { serverState -> - every { groupsV2API.getGroup(any(), any()) } returns serverState + every { groupsV2API.getGroup(any(), any()) } returns DecryptedGroupResponse(serverState, null) } 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() } @@ -241,7 +249,7 @@ class GroupsV2StateProcessorTest { assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) 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 @@ -279,7 +287,7 @@ class GroupsV2StateProcessorTest { assertThat("revision matches server", result.latestServer!!.revision, `is`(7)) 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 @@ -323,7 +331,7 @@ class GroupsV2StateProcessorTest { 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")) - verify { groupTable.update(masterKey, result.latestServer!!) } + verify { groupTable.update(masterKey, result.latestServer!!, null) } } @Test @@ -351,7 +359,7 @@ class GroupsV2StateProcessorTest { assertThat("revision matches peer change", result.latestServer!!.revision, `is`(6)) 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 @@ -383,7 +391,7 @@ class GroupsV2StateProcessorTest { assertThat("member promoted by peer change", result.latestServer!!.members.map { it.aciBytes }, hasItem(selfAci.toByteString())) verify { jobManager.add(ofType(DirectoryRefreshJob::class)) } - verify { groupTable.update(masterKey, result.latestServer!!) } + verify { groupTable.update(masterKey, result.latestServer!!, null) } } @Test @@ -426,7 +434,7 @@ class GroupsV2StateProcessorTest { assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) 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) } @@ -485,7 +493,7 @@ class GroupsV2StateProcessorTest { assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("revision matches server", result.latestServer!!.revision, `is`(3)) - verify { groupTable.update(masterKey, result.latestServer!!) } + verify { groupTable.update(masterKey, result.latestServer!!, null) } } @Test @@ -513,7 +521,7 @@ class GroupsV2StateProcessorTest { assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("revision matches server", result.latestServer!!.revision, `is`(2)) - verify { groupTable.create(masterKey, result.latestServer!!) } + verify { groupTable.create(masterKey, result.latestServer!!, null) } } @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")) verify { jobManager.add(ofType(RequestGroupV2InfoJob::class)) } - verify { groupTable.create(masterKey, result.latestServer!!) } + verify { groupTable.create(masterKey, result.latestServer!!, null) } } @Test @@ -577,7 +585,7 @@ class GroupsV2StateProcessorTest { assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("revision matches latest server", result.latestServer!!.revision, `is`(10)) - verify { groupTable.update(masterKey, result.latestServer!!) } + verify { groupTable.update(masterKey, result.latestServer!!, null) } } @Test @@ -613,7 +621,7 @@ class GroupsV2StateProcessorTest { assertThat("local should update to server", result.updateStatus, `is`(GroupUpdateResult.UpdateStatus.GROUP_UPDATED)) assertThat("revision matches server", result.latestServer!!.revision, `is`(3)) - verify { groupTable.update(masterKey, result.latestServer!!) } + verify { groupTable.update(masterKey, result.latestServer!!, null) } } @Test @@ -662,7 +670,7 @@ class GroupsV2StateProcessorTest { assertThat("title matches revision approved at", result.latestServer!!.title, `is`("Beam me up")) verify { jobManager.add(ofType(RequestGroupV2InfoJob::class)) } - verify { groupTable.update(masterKey, result.latestServer!!) } + verify { groupTable.update(masterKey, result.latestServer!!, null) } } @Test @@ -705,7 +713,7 @@ class GroupsV2StateProcessorTest { 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)) - 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))) - 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 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("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) } } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 6f342fcf57..2dee1dcc5f 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api; import com.squareup.wire.FieldEncoding; +import org.signal.core.util.Base64; import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidKeyException; @@ -26,6 +27,7 @@ import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.archive.ArchiveApi; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; @@ -88,7 +90,6 @@ import org.whispersystems.signalservice.internal.storage.protos.WriteOperation; import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider; import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper; -import org.signal.core.util.Base64; import java.io.IOException; import java.security.MessageDigest; @@ -757,7 +758,7 @@ public class SignalServiceAccountManager { throws NonSuccessfulResponseCodeException, PushNetworkException { try { - ProfileAndCredential credential = this.pushServiceSocket.retrieveVersionedProfileAndCredential(serviceId, profileKey, Optional.empty(), locale).get(10, TimeUnit.SECONDS); + ProfileAndCredential credential = this.pushServiceSocket.retrieveVersionedProfileAndCredential(serviceId, profileKey, SealedSenderAccess.NONE, locale).get(10, TimeUnit.SECONDS); return credential.getExpiringProfileKeyCredential(); } catch (InterruptedException | TimeoutException e) { throw new PushNetworkException(e); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 18876cf8f8..b86c3472ae 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -17,7 +17,7 @@ import org.whispersystems.signalservice.api.backup.BackupKey; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -98,7 +98,7 @@ public class SignalServiceMessageReceiver { public ListenableFuture retrieveProfile(SignalServiceAddress address, Optional profileKey, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, SignalServiceProfile.RequestType requestType, Locale locale) { @@ -115,16 +115,16 @@ public class SignalServiceMessageReceiver { } if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) { - return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), unidentifiedAccess, locale); + return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), sealedSenderAccess, locale); } else { - return FutureTransformers.map(socket.retrieveVersionedProfile(aci, profileKey.get(), unidentifiedAccess, locale), profile -> { + return FutureTransformers.map(socket.retrieveVersionedProfile(aci, profileKey.get(), sealedSenderAccess, locale), profile -> { return new ProfileAndCredential(profile, SignalServiceProfile.RequestType.PROFILE, Optional.empty()); }); } } else { - return FutureTransformers.map(socket.retrieveProfile(address, unidentifiedAccess, locale), profile -> { + return FutureTransformers.map(socket.retrieveProfile(address, sealedSenderAccess, locale), profile -> { return new ProfileAndCredential(profile, SignalServiceProfile.RequestType.PROFILE, Optional.empty()); @@ -146,8 +146,8 @@ public class SignalServiceMessageReceiver { return new FileInputStream(destination); } - public Single> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull Optional unidentifiedAccess, @Nonnull ResponseMapper responseMapper) { - return socket.performIdentityCheck(request, unidentifiedAccess, responseMapper); + public Single> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull ResponseMapper responseMapper) { + return socket.performIdentityCheck(request, responseMapper); } /** diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 98132bd8e3..4fa97f85cb 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -22,16 +22,18 @@ import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage; import org.signal.libsignal.protocol.state.PreKeyBundle; import org.signal.libsignal.protocol.state.SessionRecord; import org.signal.libsignal.protocol.util.Pair; +import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.EnvelopeContent; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -92,8 +94,8 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import org.whispersystems.signalservice.internal.crypto.AttachmentDigest; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.AttachmentPointer; -import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes; import org.whispersystems.signalservice.internal.push.AttachmentUploadForm; +import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes; import org.whispersystems.signalservice.internal.push.BodyRange; import org.whispersystems.signalservice.internal.push.CallMessage; import org.whispersystems.signalservice.internal.push.Content; @@ -131,7 +133,6 @@ import org.whispersystems.signalservice.internal.push.http.PartialSendBatchCompl import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener; import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import org.whispersystems.signalservice.internal.util.Util; -import org.whispersystems.util.ByteArrayUtil; import java.io.IOException; import java.io.InputStream; @@ -145,10 +146,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -223,7 +222,7 @@ public class SignalServiceMessageSender { * @param message The read receipt to deliver. */ public SendMessageResult sendReceipt(SignalServiceAddress recipient, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, SignalServiceReceiptMessage message, boolean includePniSignature) throws IOException, UntrustedIdentityException @@ -240,14 +239,14 @@ public class SignalServiceMessageSender { EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - return sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null, null, false, false); + return sendMessage(recipient, sealedSenderAccess, message.getWhen(), envelopeContent, false, null, null, false, false); } /** * Send a retry receipt for a bad-encrypted envelope. */ public void sendRetryReceipt(SignalServiceAddress recipient, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, Optional groupId, DecryptionErrorMessage errorMessage) throws IOException, UntrustedIdentityException @@ -258,16 +257,16 @@ public class SignalServiceMessageSender { PlaintextContent content = new PlaintextContent(errorMessage); EnvelopeContent envelopeContent = EnvelopeContent.plaintext(content, groupId); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, null, false, false); + sendMessage(recipient, sealedSenderAccess, System.currentTimeMillis(), envelopeContent, false, null, null, false, false); } /** * Sends a typing indicator using client-side fanout. Doesn't bother with return results, since these are best-effort. */ - public void sendTyping(List recipients, - List> unidentifiedAccess, - SignalServiceTypingMessage message, - CancelationSignal cancelationSignal) + public void sendTyping(List recipients, + List sealedSenderAccesses, + SignalServiceTypingMessage message, + CancelationSignal cancelationSignal) throws IOException { Log.d(TAG, "[" + message.getTimestamp() + "] Sending a typing message to " + recipients.size() + " recipient(s) using 1:1 messages."); @@ -275,22 +274,23 @@ public class SignalServiceMessageSender { Content content = createTypingContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null, cancelationSignal, null, false, false); + sendMessage(recipients, sealedSenderAccesses, message.getTimestamp(), envelopeContent, true, null, cancelationSignal, null, false, false); } /** * Send a typing indicator to a group using sender key. Doesn't bother with return results, since these are best-effort. */ - public void sendGroupTyping(DistributionId distributionId, - List recipients, - List unidentifiedAccess, - SignalServiceTypingMessage message) + public void sendGroupTyping(DistributionId distributionId, + List recipients, + List unidentifiedAccess, + @Nullable GroupSendEndorsements groupSendEndorsements, + SignalServiceTypingMessage message) throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException { Log.d(TAG, "[" + message.getTimestamp() + "] Sending a typing message to " + recipients.size() + " recipient(s) using sender key."); Content content = createTypingContent(message); - sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY, false, false); + sendGroupMessage(distributionId, recipients, unidentifiedAccess, groupSendEndorsements, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY, false, false); } /** @@ -311,28 +311,29 @@ public class SignalServiceMessageSender { } SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, isRecipientUpdate, manifest); - sendSyncMessage(syncMessage, Optional.empty()); + sendSyncMessage(syncMessage); } /** * Send a story using sender key. Note: This is not just for group stories -- it's for any story. Just following the naming convention of making sender key * method named "sendGroup*" */ - public List sendGroupStory(DistributionId distributionId, - Optional groupId, - List recipients, - List unidentifiedAccess, - boolean isRecipientUpdate, - SignalServiceStoryMessage message, - long timestamp, + public List sendGroupStory(DistributionId distributionId, + Optional groupId, + List recipients, + List unidentifiedAccess, + @Nullable GroupSendEndorsements groupSendEndorsements, + boolean isRecipientUpdate, + SignalServiceStoryMessage message, + long timestamp, Set manifest, - PartialSendBatchCompleteListener partialListener) + PartialSendBatchCompleteListener partialListener) throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException { Log.d(TAG, "[" + timestamp + "] Sending a story."); Content content = createStoryContent(message); - List sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.IMPLICIT, groupId, false, SenderKeyGroupEvents.EMPTY, false, true); + List sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, groupSendEndorsements, timestamp, content, ContentHint.IMPLICIT, groupId, false, SenderKeyGroupEvents.EMPTY, false, true); if (partialListener != null) { partialListener.onPartialSendComplete(sendMessageResults); @@ -354,7 +355,7 @@ public class SignalServiceMessageSender { * @throws IOException */ public void sendCallMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, SignalServiceCallMessage message) throws IOException, UntrustedIdentityException { @@ -364,11 +365,11 @@ public class SignalServiceMessageSender { Content content = createCallContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.empty()); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, message.isUrgent(), false); + sendMessage(recipient, sealedSenderAccess, timestamp, envelopeContent, false, null, null, message.isUrgent(), false); } public List sendCallMessage(List recipients, - List> unidentifiedAccess, + List sealedSenderAccesses, SignalServiceCallMessage message) throws IOException { @@ -378,12 +379,13 @@ public class SignalServiceMessageSender { Content content = createCallContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.empty()); - return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, null, message.isUrgent(), false); + return sendMessage(recipients, sealedSenderAccesses, timestamp, envelopeContent, false, null, null, null, message.isUrgent(), false); } public List sendCallMessage(DistributionId distributionId, List recipients, List unidentifiedAccess, + @Nullable GroupSendEndorsements groupSendEndorsements, SignalServiceCallMessage message, PartialSendBatchCompleteListener partialListener) throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException @@ -392,7 +394,7 @@ public class SignalServiceMessageSender { Content content = createCallContent(message); - List results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId(), false, SenderKeyGroupEvents.EMPTY, message.isUrgent(), false); + List results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, groupSendEndorsements, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId(), false, SenderKeyGroupEvents.EMPTY, message.isUrgent(), false); if (partialListener != null) { partialListener.onPartialSendComplete(results); @@ -423,27 +425,27 @@ public class SignalServiceMessageSender { * @throws UntrustedIdentityException * @throws IOException */ - public SendMessageResult sendDataMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, - ContentHint contentHint, - SignalServiceDataMessage message, - IndividualSendEvents sendEvents, - boolean urgent, - boolean includePniSignature) + public SendMessageResult sendDataMessage(SignalServiceAddress recipient, + @Nullable SealedSenderAccess sealedSenderAccess, + ContentHint contentHint, + SignalServiceDataMessage message, + IndividualSendEvents sendEvents, + boolean urgent, + boolean includePniSignature) throws UntrustedIdentityException, IOException { Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message."); Content content = createMessageContent(message); - return sendContent(recipient, unidentifiedAccess, contentHint, message, sendEvents, urgent, includePniSignature, content); + return sendContent(recipient, sealedSenderAccess, contentHint, message, sendEvents, urgent, includePniSignature, content); } /** * Send an edit message to a single recipient. */ public SendMessageResult sendEditMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, ContentHint contentHint, SignalServiceDataMessage message, IndividualSendEvents sendEvents, @@ -455,14 +457,14 @@ public class SignalServiceMessageSender { Content content = createEditMessageContent(new SignalServiceEditMessage(targetSentTimestamp, message)); - return sendContent(recipient, unidentifiedAccess, contentHint, message, sendEvents, urgent, false, content); + return sendContent(recipient, sealedSenderAccess, contentHint, message, sendEvents, urgent, false, content); } /** * Sends content to a single recipient. */ private SendMessageResult sendContent(SignalServiceAddress recipient, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, ContentHint contentHint, SignalServiceDataMessage message, IndividualSendEvents sendEvents, @@ -481,7 +483,7 @@ public class SignalServiceMessageSender { EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); long timestamp = message.getTimestamp(); - SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, sendEvents, urgent, false); + SendMessageResult result = sendMessage(recipient, sealedSenderAccess, timestamp, envelopeContent, false, null, sendEvents, urgent, false); sendEvents.onMessageSent(); @@ -489,7 +491,7 @@ public class SignalServiceMessageSender { Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false, Collections.emptySet()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, null, false, false); + sendMessage(localAddress, SealedSenderAccess.NONE, timestamp, syncMessageContent, false, null, null, false, false); } sendEvents.onSyncMessageSent(); @@ -509,13 +511,13 @@ public class SignalServiceMessageSender { /** * Sends the provided {@link SenderKeyDistributionMessage} to the specified recipients. */ - public List sendSenderKeyDistributionMessage(DistributionId distributionId, - List recipients, - List> unidentifiedAccess, - SenderKeyDistributionMessage message, - Optional groupId, - boolean urgent, - boolean story) + public List sendSenderKeyDistributionMessage(DistributionId distributionId, + List recipients, + List sealedSenderAccesses, + SenderKeyDistributionMessage message, + Optional groupId, + boolean urgent, + boolean story) throws IOException { ByteString distributionBytes = ByteString.of(message.serialize()); @@ -525,14 +527,14 @@ public class SignalServiceMessageSender { Log.d(TAG, "[" + timestamp + "] Sending SKDM to " + recipients.size() + " recipients for DistributionId " + distributionId); - return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, null, urgent, story); + return sendMessage(recipients, sealedSenderAccesses, timestamp, envelopeContent, false, null, null, null, urgent, story); } /** * Resend a previously-sent message. */ public SendMessageResult resendContent(SignalServiceAddress address, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, long timestamp, Content content, ContentHint contentHint, @@ -543,13 +545,12 @@ public class SignalServiceMessageSender { Log.d(TAG, "[" + timestamp + "] Resending content."); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, groupId); - Optional access = unidentifiedAccess.isPresent() ? unidentifiedAccess.get().getTargetUnidentifiedAccess() : Optional.empty(); if (address.getServiceId().equals(localAddress.getServiceId())) { - access = Optional.empty(); + sealedSenderAccess = SealedSenderAccess.NONE; } - return sendMessage(address, access, timestamp, envelopeContent, false, null, null, urgent, false); + return sendMessage(address, sealedSenderAccess, timestamp, envelopeContent, false, null, null, urgent, false); } /** @@ -558,6 +559,7 @@ public class SignalServiceMessageSender { public List sendGroupDataMessage(DistributionId distributionId, List recipients, List unidentifiedAccess, + @Nullable GroupSendEndorsements groupSendEndorsements, boolean isRecipientUpdate, ContentHint contentHint, SignalServiceDataMessage message, @@ -579,7 +581,7 @@ public class SignalServiceMessageSender { } Optional groupId = message.getGroupId(); - List results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId, false, sendEvents, urgent, isForStory); + List results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, groupSendEndorsements, message.getTimestamp(), content, contentHint, groupId, false, sendEvents, urgent, isForStory); if (partialListener != null) { partialListener.onPartialSendComplete(results); @@ -591,7 +593,7 @@ public class SignalServiceMessageSender { Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate, Collections.emptySet()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null, null, false, false); + sendMessage(localAddress, SealedSenderAccess.NONE, message.getTimestamp(), syncMessageContent, false, null, null, false, false); } sendEvents.onSyncMessageSent(); @@ -605,15 +607,15 @@ public class SignalServiceMessageSender { * @param partialListener A listener that will be called when an individual send is completed. Will be invoked on an arbitrary background thread, *not* * the calling thread. */ - public List sendDataMessage(List recipients, - List> unidentifiedAccess, - boolean isRecipientUpdate, - ContentHint contentHint, - SignalServiceDataMessage message, - LegacyGroupEvents sendEvents, - PartialSendCompleteListener partialListener, - CancelationSignal cancelationSignal, - boolean urgent) + public List sendDataMessage(List recipients, + List sealedSenderAccesses, + boolean isRecipientUpdate, + ContentHint contentHint, + SignalServiceDataMessage message, + LegacyGroupEvents sendEvents, + PartialSendCompleteListener partialListener, + CancelationSignal cancelationSignal, + boolean urgent) throws IOException, UntrustedIdentityException { Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message to " + recipients.size() + " recipients."); @@ -621,7 +623,7 @@ public class SignalServiceMessageSender { Content content = createMessageContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); long timestamp = message.getTimestamp(); - List results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, partialListener, cancelationSignal, sendEvents, urgent, false); + List results = sendMessage(recipients, sealedSenderAccesses, timestamp, envelopeContent, false, partialListener, cancelationSignal, sendEvents, urgent, false); boolean needsSyncInResults = false; sendEvents.onMessageSent(); @@ -642,7 +644,7 @@ public class SignalServiceMessageSender { Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, null, false, false); + sendMessage(localAddress, SealedSenderAccess.NONE, timestamp, syncMessageContent, false, null, null, false, false); } sendEvents.onSyncMessageSent(); @@ -656,16 +658,16 @@ public class SignalServiceMessageSender { * @param partialListener A listener that will be called when an individual send is completed. Will be invoked on an arbitrary background thread, *not* * the calling thread. */ - public List sendEditMessage(List recipients, - List> unidentifiedAccess, - boolean isRecipientUpdate, - ContentHint contentHint, - SignalServiceDataMessage message, - LegacyGroupEvents sendEvents, - PartialSendCompleteListener partialListener, - CancelationSignal cancelationSignal, - boolean urgent, - long targetSentTimestamp) + public List sendEditMessage(List recipients, + List sealedSenderAccesses, + boolean isRecipientUpdate, + ContentHint contentHint, + SignalServiceDataMessage message, + LegacyGroupEvents sendEvents, + PartialSendCompleteListener partialListener, + CancelationSignal cancelationSignal, + boolean urgent, + long targetSentTimestamp) throws IOException, UntrustedIdentityException { Log.d(TAG, "[" + message.getTimestamp() + "] Sending a edit message to " + recipients.size() + " recipients."); @@ -673,7 +675,7 @@ public class SignalServiceMessageSender { Content content = createEditMessageContent(new SignalServiceEditMessage(targetSentTimestamp, message)); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); long timestamp = message.getTimestamp(); - List results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, partialListener, cancelationSignal, null, urgent, false); + List results = sendMessage(recipients, sealedSenderAccesses, timestamp, envelopeContent, false, partialListener, cancelationSignal, null, urgent, false); boolean needsSyncInResults = false; sendEvents.onMessageSent(); @@ -694,7 +696,7 @@ public class SignalServiceMessageSender { Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null, null, false, false); + sendMessage(localAddress, SealedSenderAccess.NONE, timestamp, syncMessageContent, false, null, null, false, false); } sendEvents.onSyncMessageSent(); @@ -706,17 +708,17 @@ public class SignalServiceMessageSender { throws IOException, UntrustedIdentityException { Log.d(TAG, "[" + dataMessage.getTimestamp() + "] Sending self-sync message."); - return sendSyncMessage(createSelfSendSyncMessage(dataMessage), Optional.empty()); + return sendSyncMessage(createSelfSendSyncMessage(dataMessage)); } public SendMessageResult sendSelfSyncEditMessage(SignalServiceEditMessage editMessage) throws IOException, UntrustedIdentityException { Log.d(TAG, "[" + editMessage.getDataMessage().getTimestamp() + "] Sending self-sync edit message for " + editMessage.getTargetSentTimestamp() + "."); - return sendSyncMessage(createSelfSendSyncEditMessage(editMessage), Optional.empty()); + return sendSyncMessage(createSelfSendSyncEditMessage(editMessage)); } - public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message, Optional unidentifiedAccess) + public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { Content content; @@ -736,7 +738,7 @@ public class SignalServiceMessageSender { } else if (message.getConfiguration().isPresent()) { content = createMultiDeviceConfigurationContent(message.getConfiguration().get()); } else if (message.getSent().isPresent()) { - content = createMultiDeviceSentTranscriptContent(message.getSent().get(), unidentifiedAccess.isPresent()); + content = createMultiDeviceSentTranscriptContent(message.getSent().get()); } else if (message.getStickerPackOperations().isPresent()) { content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get()); } else if (message.getFetchType().isPresent()) { @@ -774,7 +776,7 @@ public class SignalServiceMessageSender { EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - return sendMessage(localAddress, Optional.empty(), timestamp, envelopeContent, false, null, null, urgent, false); + return sendMessage(localAddress, SealedSenderAccess.NONE, timestamp, envelopeContent, false, null, null, urgent, false); } /** @@ -792,7 +794,7 @@ public class SignalServiceMessageSender { Content.Builder content = new Content.Builder().syncMessage(syncMessage.build()); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content.build(), ContentHint.IMPLICIT, Optional.empty()); - return getEncryptedMessage(localAddress, Optional.empty(), deviceId, envelopeContent, false); + return getEncryptedMessage(localAddress, SealedSenderAccess.NONE, deviceId, envelopeContent, false); } public void cancelInFlightRequests() { @@ -927,19 +929,19 @@ public class SignalServiceMessageSender { EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - SendMessageResult result = sendMessage(message.getDestination(), Optional.empty(), message.getTimestamp(), envelopeContent, false, null, null, false, false); + SendMessageResult result = sendMessage(message.getDestination(), null, message.getTimestamp(), envelopeContent, false, null, null, false, false); if (result.getSuccess().isNeedsSync()) { Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.encode()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); - sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null, null, false, false); + sendMessage(localAddress, SealedSenderAccess.NONE, message.getTimestamp(), syncMessageContent, false, null, null, false, false); } return result; } - public SendMessageResult sendNullMessage(SignalServiceAddress address, Optional unidentifiedAccess) + public SendMessageResult sendNullMessage(SignalServiceAddress address, @Nullable SealedSenderAccess sealedSenderAccess) throws UntrustedIdentityException, IOException { byte[] nullMessageBody = new DataMessage.Builder() @@ -957,7 +959,7 @@ public class SignalServiceMessageSender { EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); - return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, null, false, false); + return sendMessage(address, sealedSenderAccess, System.currentTimeMillis(), envelopeContent, false, null, null, false, false); } private PniSignatureMessage createPniSignatureMessage() { @@ -1380,10 +1382,10 @@ public class SignalServiceMessageSender { return container.syncMessage(builder.build()).build(); } - private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, boolean unidentifiedAccess) throws IOException { + private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript) throws IOException { SignalServiceAddress address = transcript.getDestination().get(); Content content = createMessageContent(transcript); - SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.ofNullable(content)); + SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), false, true, -1, Optional.ofNullable(content)); return createMultiDeviceSentTranscriptContent(content, @@ -1424,7 +1426,7 @@ public class SignalServiceMessageSender { unidentifiedDeliveryStatuses.add(new SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder() .destinationServiceId(result.getAddress().getServiceId().toString()) - .unidentified(result.getSuccess().isUnidentified()) + .unidentified(false) .destinationIdentityKey(identity) .build()); } @@ -1933,15 +1935,15 @@ public class SignalServiceMessageSender { return SignalServiceSyncMessage.forSentTranscript(transcript); } - private SendMessageResult sendMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, - long timestamp, - EnvelopeContent content, - boolean online, - CancelationSignal cancelationSignal, - SendEvents sendEvents, - boolean urgent, - boolean story) + private SendMessageResult sendMessage(SignalServiceAddress recipient, + @Nullable SealedSenderAccess sealedSenderAccess, + long timestamp, + EnvelopeContent content, + boolean online, + CancelationSignal cancelationSignal, + SendEvents sendEvents, + boolean urgent, + boolean story) throws UntrustedIdentityException, IOException { enforceMaxContentSize(content); @@ -1954,7 +1956,13 @@ public class SignalServiceMessageSender { } try { - OutgoingPushMessageList messages = getEncryptedMessages(recipient, unidentifiedAccess, timestamp, content, online, urgent, story); + OutgoingPushMessageList messages = getEncryptedMessages(recipient, + sealedSenderAccess, + timestamp, + content, + online, + urgent, + story); if (i == 0 && sendEvents != null) { sendEvents.onMessageEncrypted(); } @@ -1969,52 +1977,41 @@ public class SignalServiceMessageSender { throw new CancelationException(); } - if (!unidentifiedAccess.isPresent()) { - try { - SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, Optional.empty(), story).blockingGet()).getResultOrThrow(); - return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); - } catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) { - // Non-technical failures shouldn't be retried with socket - throw e; - } catch (WebSocketUnavailableException e) { - Log.i(TAG, "[sendMessage][" + timestamp + "] Pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); - } catch (IOException e) { - Log.w(TAG, e); - Log.w(TAG, "[sendMessage][" + timestamp + "] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); - } - } else if (unidentifiedAccess.isPresent()) { - try { - SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, unidentifiedAccess, story).blockingGet()).getResultOrThrow(); - return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); - } catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) { - // Non-technical failures shouldn't be retried with socket - throw e; - } catch (WebSocketUnavailableException e) { - Log.i(TAG, "[sendMessage][" + timestamp + "] Unidentified pipe unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); - } catch (IOException e) { - Throwable cause = e; - if (e.getCause() != null) { - cause = e.getCause(); - } - Log.w(TAG, "[sendMessage][" + timestamp + "] Unidentified pipe failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")"); + try { + SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, sealedSenderAccess, story).blockingGet()).getResultOrThrow(); + return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); + } catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) { + // Non-technical failures shouldn't be retried with socket + throw e; + } catch (WebSocketUnavailableException e) { + String pipe = sealedSenderAccess == null ? "Pipe" : "Unidentified pipe"; + Log.i(TAG, "[sendMessage][" + timestamp + "] " + pipe + " unavailable, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); + } catch (IOException e) { + String pipe = sealedSenderAccess == null ? "Pipe" : "Unidentified pipe"; + Throwable cause = e; + if (e.getCause() != null) { + cause = e.getCause(); } + Log.w(TAG, "[sendMessage][" + timestamp + "] " + pipe + " failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")"); } if (cancelationSignal != null && cancelationSignal.isCanceled()) { throw new CancelationException(); } - SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess, story); + SendMessageResponse response = socket.sendMessage(messages, sealedSenderAccess, story); return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); } catch (InvalidKeyException ike) { Log.w(TAG, ike); - unidentifiedAccess = Optional.empty(); + if (sealedSenderAccess != null) { + sealedSenderAccess = sealedSenderAccess.switchToFallback(); + } } catch (AuthorizationFailedException afe) { - if (unidentifiedAccess.isPresent()) { + if (sealedSenderAccess != null) { Log.w(TAG, "Got an AuthorizationFailedException when trying to send using sealed sender. Falling back."); - unidentifiedAccess = Optional.empty(); + sealedSenderAccess = sealedSenderAccess.switchToFallback(); } else { Log.w(TAG, "Got an AuthorizationFailedException without using sealed sender!", afe); throw afe; @@ -2038,7 +2035,7 @@ public class SignalServiceMessageSender { * @throws IOException - Unknown failure or a failure not representable by an unsuccessful {@code SendMessageResult}. */ private List sendMessage(List recipients, - List> unidentifiedAccess, + List sealedSenderAccesses, long timestamp, EnvelopeContent content, boolean online, @@ -2052,15 +2049,16 @@ public class SignalServiceMessageSender { Log.d(TAG, "[" + timestamp + "] Sending to " + recipients.size() + " recipients."); enforceMaxContentSize(content); - long startTime = System.currentTimeMillis(); - List> singleResults = new LinkedList<>(); - Iterator recipientIterator = recipients.iterator(); - Iterator> unidentifiedAccessIterator = unidentifiedAccess.iterator(); + long startTime = System.currentTimeMillis(); + List> singleResults = new LinkedList<>(); + Iterator recipientIterator = recipients.iterator(); + Iterator sealedSenderAccessIterator = sealedSenderAccesses.iterator(); while (recipientIterator.hasNext()) { - SignalServiceAddress recipient = recipientIterator.next(); - Optional access = unidentifiedAccessIterator.next(); - singleResults.add(sendMessageRx(recipient, access, timestamp, content, online, cancelationSignal, sendEvents, urgent, story, 0).toObservable()); + SignalServiceAddress recipient = recipientIterator.next(); + SealedSenderAccess sealedSenderAccess = sealedSenderAccessIterator.next(); + + singleResults.add(sendMessageRx(recipient, sealedSenderAccess, timestamp, content, online, cancelationSignal, sendEvents, urgent, story, 0).toObservable()); } List results; @@ -2126,7 +2124,7 @@ public class SignalServiceMessageSender { * errors via {@code onError} */ private Single sendMessageRx(SignalServiceAddress recipient, - final Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, long timestamp, EnvelopeContent content, boolean online, @@ -2140,7 +2138,7 @@ public class SignalServiceMessageSender { enforceMaxContentSize(content); Single messagesSingle = Single.fromCallable(() -> { - OutgoingPushMessageList messages = getEncryptedMessages(recipient, unidentifiedAccess, timestamp, content, online, urgent, story); + OutgoingPushMessageList messages = getEncryptedMessages(recipient, sealedSenderAccess, timestamp, content, online, urgent, story); if (retryCount == 0 && sendEvents != null) { sendEvents.onMessageEncrypted(); @@ -2161,7 +2159,7 @@ public class SignalServiceMessageSender { return Single.error(new CancelationException()); } - return messagingService.send(messages, unidentifiedAccess, story) + return messagingService.send(messages, sealedSenderAccess, story) .map(r -> new kotlin.Pair<>(messages, r)); }) .observeOn(scheduler) @@ -2196,14 +2194,14 @@ public class SignalServiceMessageSender { // Non-technical failures shouldn't be retried with socket return Single.error(throwable); } else if (throwable instanceof WebSocketUnavailableException) { - Log.i(TAG, "[sendMessage][" + timestamp + "] " + (unidentifiedAccess.isPresent() ? "Unidentified " : "") + "pipe unavailable, falling back... (" + throwable.getClass().getSimpleName() + ": " + throwable.getMessage() + ")"); + Log.i(TAG, "[sendMessage][" + timestamp + "] " + (sealedSenderAccess != null ? "Unidentified " : "") + "pipe unavailable, falling back... (" + throwable.getClass().getSimpleName() + ": " + throwable.getMessage() + ")"); } else if (throwable instanceof IOException) { Throwable cause = throwable.getCause() != null ? throwable.getCause() : throwable; - Log.w(TAG, "[sendMessage][" + timestamp + "] " + (unidentifiedAccess.isPresent() ? "Unidentified " : "") + "pipe failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")"); + Log.w(TAG, "[sendMessage][" + timestamp + "] " + (sealedSenderAccess != null ? "Unidentified " : "") + "pipe failed, falling back... (" + cause.getClass().getSimpleName() + ": " + cause.getMessage() + ")"); } return Single.fromCallable(() -> { - SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess, story); + SendMessageResponse response = socket.sendMessage(messages, sealedSenderAccess, story); return SendMessageResult.success( recipient, messages.getDevices(), @@ -2229,7 +2227,7 @@ public class SignalServiceMessageSender { Log.w(TAG, t); return sendMessageRx( recipient, - Optional.empty(), + SealedSenderAccess.NONE, timestamp, content, online, @@ -2240,11 +2238,11 @@ public class SignalServiceMessageSender { retryCount + 1 ); } else if (t instanceof AuthorizationFailedException) { - if (unidentifiedAccess.isPresent()) { + if (sealedSenderAccess != null) { Log.w(TAG, "Got an AuthorizationFailedException when trying to send using sealed sender. Falling back."); return sendMessageRx( recipient, - Optional.empty(), + sealedSenderAccess.switchToFallback(), timestamp, content, online, @@ -2268,7 +2266,7 @@ public class SignalServiceMessageSender { }) .flatMap(unused -> sendMessageRx( recipient, - unidentifiedAccess, + sealedSenderAccess, timestamp, content, online, @@ -2288,7 +2286,7 @@ public class SignalServiceMessageSender { }) .flatMap(unused -> sendMessageRx( recipient, - unidentifiedAccess, + sealedSenderAccess, timestamp, content, online, @@ -2336,17 +2334,18 @@ public class SignalServiceMessageSender { * * This method will handle sending out SenderKeyDistributionMessages as necessary. */ - private List sendGroupMessage(DistributionId distributionId, + private List sendGroupMessage(DistributionId distributionId, List recipients, - List unidentifiedAccess, - long timestamp, - Content content, - ContentHint contentHint, - Optional groupId, - boolean online, - SenderKeyGroupEvents sendEvents, - boolean urgent, - boolean story) + List unidentifiedAccess, + @Nullable GroupSendEndorsements groupSendEndorsements, + long timestamp, + Content content, + ContentHint contentHint, + Optional groupId, + boolean online, + SenderKeyGroupEvents sendEvents, + boolean urgent, + boolean story) throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException, InvalidRegistrationIdException { if (recipients.isEmpty()) { @@ -2364,34 +2363,36 @@ public class SignalServiceMessageSender { accessBySid.put(addressIterator.next().getServiceId(), accessIterator.next()); } + SealedSenderAccess sealedSenderAccess = SealedSenderAccess.forGroupSend(groupSendEndorsements, unidentifiedAccess, story); + for (int i = 0; i < RETRY_COUNT; i++) { GroupTargetInfo targetInfo = buildGroupTargetInfo(recipients); final GroupTargetInfo targetInfoSnapshot = targetInfo; - Set sharedWith = aciStore.getSenderKeySharedWith(distributionId); - List needsSenderKey = targetInfo.destinations.stream() - .filter(a -> !sharedWith.contains(a) || targetInfoSnapshot.sessions.get(a) == null) - .map(a -> ServiceId.parseOrThrow(a.getName())) - .distinct() - .map(SignalServiceAddress::new) - .collect(Collectors.toList()); - if (needsSenderKey.size() > 0) { - Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Need to send the distribution message to " + needsSenderKey.size() + " addresses."); - SenderKeyDistributionMessage message = getOrCreateNewGroupSession(distributionId); - List> access = needsSenderKey.stream() - .map(r -> { - UnidentifiedAccess targetAccess = accessBySid.get(r.getServiceId()); - return Optional.of(new UnidentifiedAccessPair(targetAccess, targetAccess)); - }) - .collect(Collectors.toList()); + Set sharedWith = aciStore.getSenderKeySharedWith(distributionId); + List needsSenderKeyTargets = targetInfo.destinations.stream() + .filter(a -> !sharedWith.contains(a) || targetInfoSnapshot.sessions.get(a) == null) + .map(a -> ServiceId.parseOrThrow(a.getName())) + .distinct() + .map(SignalServiceAddress::new) + .collect(Collectors.toList()); + if (needsSenderKeyTargets.size() > 0) { + Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Need to send the distribution message to " + needsSenderKeyTargets.size() + " addresses."); + SenderKeyDistributionMessage senderKeyDistributionMessage = getOrCreateNewGroupSession(distributionId); + List needsSenderKeyAccesses = needsSenderKeyTargets.stream() + .map(r -> accessBySid.get(r.getServiceId())) + .collect(Collectors.toList()); + + List needsSenderKeyGroupSendTokens = groupSendEndorsements != null ? groupSendEndorsements.forIndividuals(needsSenderKeyTargets) : null; + List needsSenderKeySealedSenderAccesses = SealedSenderAccess.forFanOutGroupSend(needsSenderKeyGroupSendTokens, sealedSenderAccess.getSenderCertificate(), needsSenderKeyAccesses); List results = sendSenderKeyDistributionMessage(distributionId, - needsSenderKey, - access, - message, + needsSenderKeyTargets, + needsSenderKeySealedSenderAccesses, + senderKeyDistributionMessage, groupId, urgent, - story && !groupId.isPresent()); // We don't want to flag SKDM's as stories for group stories, since we reuse distributionIds for normal group messages + story && groupId.isEmpty()); // We don't want to flag SKDM's as stories for group stories, since we reuse distributionIds for normal group messages List successes = results.stream() .filter(SendMessageResult::isSuccess) @@ -2403,7 +2404,7 @@ public class SignalServiceMessageSender { aciStore.markSenderKeySharedWith(distributionId, successAddresses); - Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKey.size() + " recipients."); + Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKeyTargets.size() + " recipients."); int failureCount = results.size() - successes.size(); if (failureCount > 0) { @@ -2434,26 +2435,20 @@ public class SignalServiceMessageSender { sendEvents.onSenderKeyShared(); - SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, aciStore, sessionLock, null); - SenderCertificate senderCertificate = unidentifiedAccess.get(0).getUnidentifiedCertificate(); + SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, aciStore, sessionLock, null); byte[] ciphertext; try { - ciphertext = cipher.encryptForGroup(distributionId, targetInfo.destinations, targetInfo.sessions, senderCertificate, content.encode(), contentHint, groupId); + ciphertext = cipher.encryptForGroup(distributionId, targetInfo.destinations, targetInfo.sessions, sealedSenderAccess.getSenderCertificate(), content.encode(), contentHint, groupId); } catch (org.signal.libsignal.protocol.UntrustedIdentityException e) { throw new UntrustedIdentityException("Untrusted during group encrypt", e.getName(), e.getUntrustedIdentity()); } sendEvents.onMessageEncrypted(); - byte[] joinedUnidentifiedAccess = new byte[16]; - for (UnidentifiedAccess access : unidentifiedAccess) { - joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.getUnidentifiedAccessKey()); - } - try { try { - SendGroupMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent, story).blockingGet()).getResultOrThrow(); + SendGroupMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.sendToGroup(ciphertext, sealedSenderAccess, timestamp, online, urgent, story).blockingGet()).getResultOrThrow(); return transformGroupResponseToMessageResults(targetInfo.devices, response, content); } catch (InvalidUnidentifiedAccessHeaderException | NotFoundException | GroupMismatchedDevicesException | GroupStaleDevicesException e) { // Non-technical failures shouldn't be retried with socket @@ -2464,7 +2459,7 @@ public class SignalServiceMessageSender { Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); } - SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online, urgent, story); + SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, sealedSenderAccess, timestamp, online, urgent, story); return transformGroupResponseToMessageResults(targetInfo.devices, response, content); } catch (GroupMismatchedDevicesException e) { Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling mismatched devices. (" + e.getMessage() + ")"); @@ -2478,6 +2473,13 @@ public class SignalServiceMessageSender { SignalServiceAddress address = new SignalServiceAddress(ServiceId.parseOrThrow(stale.getUuid()), Optional.empty()); handleStaleDevices(address, stale.getDevices()); } + } catch (InvalidUnidentifiedAccessHeaderException e) { + sealedSenderAccess = sealedSenderAccess.switchToFallback(); + if (sealedSenderAccess != null) { + Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Handling invalid group send endorsements. (" + e.getMessage() + ")"); + } else { + throw e; + } } Log.w(TAG, "[sendGroupMessage][" + timestamp + "] Attempt failed (i = " + i + ")"); @@ -2637,7 +2639,7 @@ public class SignalServiceMessageSender { } private OutgoingPushMessageList getEncryptedMessages(SignalServiceAddress recipient, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, long timestamp, EnvelopeContent plaintext, boolean online, @@ -2659,7 +2661,7 @@ public class SignalServiceMessageSender { for (int deviceId : deviceIds) { if (deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID || aciStore.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) { - messages.add(getEncryptedMessage(recipient, unidentifiedAccess, deviceId, plaintext, story)); + messages.add(getEncryptedMessage(recipient, sealedSenderAccess, deviceId, plaintext, story)); } } @@ -2668,7 +2670,7 @@ public class SignalServiceMessageSender { // Visible for testing only public OutgoingPushMessage getEncryptedMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, int deviceId, EnvelopeContent plaintext, boolean story) @@ -2679,7 +2681,7 @@ public class SignalServiceMessageSender { if (!aciStore.containsSession(signalProtocolAddress)) { try { - List preKeys = getPreKeys(recipient, unidentifiedAccess, deviceId, story); + List preKeys = getPreKeys(recipient, sealedSenderAccess, deviceId, story); for (PreKeyBundle preKey : preKeys) { Log.d(TAG, "Initializing prekey session for " + signalProtocolAddress); @@ -2702,24 +2704,24 @@ public class SignalServiceMessageSender { } try { - return cipher.encrypt(signalProtocolAddress, unidentifiedAccess, plaintext); + return cipher.encrypt(signalProtocolAddress, sealedSenderAccess, plaintext); } catch (org.signal.libsignal.protocol.UntrustedIdentityException e) { throw new UntrustedIdentityException("Untrusted on send", recipient.getIdentifier(), e.getUntrustedIdentity()); } } - private List getPreKeys(SignalServiceAddress recipient, Optional unidentifiedAccess, int deviceId, boolean story) throws IOException { + private List getPreKeys(SignalServiceAddress recipient, @Nullable SealedSenderAccess sealedSenderAccess, int deviceId, boolean story) throws IOException { try { // If it's only unrestricted because it's a story send, then we know it'll fail - if (story && unidentifiedAccess.isPresent() && unidentifiedAccess.get().isUnrestrictedForStory()) { - unidentifiedAccess = Optional.empty(); + if (story && SealedSenderAccess.isUnrestrictedForStory(sealedSenderAccess)) { + sealedSenderAccess = null; } - return socket.getPreKeys(recipient, unidentifiedAccess, deviceId); + return socket.getPreKeys(recipient, sealedSenderAccess, deviceId); } catch (NonSuccessfulResponseCodeException e) { if (e.getCode() == 401 && story) { Log.d(TAG, "Got 401 when fetching prekey for story. Trying without UD."); - return socket.getPreKeys(recipient, Optional.empty(), deviceId); + return socket.getPreKeys(recipient, null, deviceId); } else { throw e; } @@ -2782,25 +2784,6 @@ public class SignalServiceMessageSender { return addresses; } - private Optional getTargetUnidentifiedAccess(Optional unidentifiedAccess) { - if (unidentifiedAccess.isPresent()) { - return unidentifiedAccess.get().getTargetUnidentifiedAccess(); - } - - return Optional.empty(); - } - - private List> getTargetUnidentifiedAccess(List> unidentifiedAccess) { - List> results = new LinkedList<>(); - - for (Optional item : unidentifiedAccess) { - if (item.isPresent()) results.add(item.get().getTargetUnidentifiedAccess()); - else results.add(Optional.empty()); - } - - return results; - } - private EnvelopeContent enforceMaxContentSize(EnvelopeContent content) { int size = content.size(); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalWebSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalWebSocket.java index d326dfe99f..a114672775 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalWebSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalWebSocket.java @@ -1,7 +1,7 @@ package org.whispersystems.signalservice.api; import org.signal.libsignal.protocol.logging.Log; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.messages.EnvelopeResponse; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketFactory; @@ -11,7 +11,6 @@ import org.whispersystems.signalservice.internal.websocket.WebSocketConnection; import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage; import org.whispersystems.signalservice.internal.websocket.WebSocketResponseMessage; import org.whispersystems.signalservice.internal.websocket.WebsocketResponse; -import org.signal.core.util.Base64; import java.io.IOException; import java.util.ArrayList; @@ -19,6 +18,8 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.TimeoutException; +import javax.annotation.Nullable; + import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; @@ -198,10 +199,11 @@ public final class SignalWebSocket { } } - public Single request(WebSocketRequestMessage requestMessage, Optional unidentifiedAccess) { - if (unidentifiedAccess.isPresent()) { + public Single request(WebSocketRequestMessage requestMessage, @Nullable SealedSenderAccess sealedSenderAccess) { + if (sealedSenderAccess != null) { List headers = new ArrayList<>(requestMessage.headers); - headers.add("Unidentified-Access-Key:" + Base64.encodeWithPadding(unidentifiedAccess.get().getUnidentifiedAccessKey())); + headers.add(sealedSenderAccess.getHeader()); + WebSocketRequestMessage message = requestMessage.newBuilder() .headers(headers) .build(); @@ -209,7 +211,7 @@ public final class SignalWebSocket { return getUnidentifiedWebSocket().sendRequest(message) .flatMap(r -> { if (r.getStatus() == 401) { - return request(requestMessage); + return request(requestMessage, sealedSenderAccess.switchToFallback()); } return Single.just(r); }); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SealedSenderAccess.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SealedSenderAccess.kt new file mode 100644 index 0000000000..cfb765a0f9 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SealedSenderAccess.kt @@ -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, + 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?, senderCertificate: SenderCertificate?, unidentifiedAccesses: List): List { + 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, 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 + } + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java index f88449b34a..704ba1070f 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java @@ -60,6 +60,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import javax.annotation.Nullable; + /** * This is used to encrypt + decrypt received envelopes. */ @@ -110,17 +112,17 @@ public class SignalServiceCipher { } public OutgoingPushMessage encrypt(SignalProtocolAddress destination, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, EnvelopeContent content) throws UntrustedIdentityException, InvalidKeyException { try { SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination)); - if (unidentifiedAccess.isPresent()) { + if (sealedSenderAccess != null) { SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getServiceId().getRawUuid(), localAddress.getNumber() .orElse(null), localDeviceId)); - return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, unidentifiedAccess.get().getUnidentifiedCertificate()); + return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, sealedSenderAccess.getSenderCertificate()); } else { return content.processUnsealedSender(sessionCipher, destination); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccess.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccess.java index 5f4091800d..25856107ec 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccess.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccess.java @@ -65,7 +65,6 @@ public class UnidentifiedAccess { } } - private static byte[] createEmptyByteArray(int length) { return new byte[length]; } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccessPair.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccessPair.java deleted file mode 100644 index 9d0425c516..0000000000 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccessPair.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.whispersystems.signalservice.api.crypto; - - -import java.util.Optional; - -public class UnidentifiedAccessPair { - - private final Optional targetUnidentifiedAccess; - private final Optional selfUnidentifiedAccess; - - public UnidentifiedAccessPair(UnidentifiedAccess targetUnidentifiedAccess, UnidentifiedAccess selfUnidentifiedAccess) { - this.targetUnidentifiedAccess = Optional.of(targetUnidentifiedAccess); - this.selfUnidentifiedAccess = Optional.of(selfUnidentifiedAccess); - } - - public Optional getTargetUnidentifiedAccess() { - return targetUnidentifiedAccess; - } - - public Optional getSelfUnidentifiedAccess() { - return selfUnidentifiedAccess; - } -} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupResponse.kt new file mode 100644 index 0000000000..d751618574 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupResponse.kt @@ -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? +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupHistoryPage.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupHistoryPage.kt index 0a07259d77..fae1591c62 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupHistoryPage.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupHistoryPage.kt @@ -1,11 +1,12 @@ package org.whispersystems.signalservice.api.groupsv2 +import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse import org.whispersystems.signalservice.internal.push.PushServiceSocket.GroupHistory /** * Wraps result of group history fetch with it's associated paging data. */ -data class GroupHistoryPage(val changeLogs: List, val pagingData: PagingData) { +data class GroupHistoryPage(val changeLogs: List, val groupSendEndorsementsResponse: GroupSendEndorsementsResponse?, val pagingData: PagingData) { data class PagingData(val hasMorePages: Boolean, val nextPageRevision: Int) { companion object { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupSendEndorsements.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupSendEndorsements.kt new file mode 100644 index 0000000000..a8f5abbee0 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupSendEndorsements.kt @@ -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, + 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): List { + return addresses + .map { a -> endorsements[a.serviceId] } + .map { e -> e?.toFullToken(groupSecretParams, expiration) } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java index 58f1c54c70..0ce4797ccb 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java @@ -9,13 +9,16 @@ import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations; import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse; import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; +import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse; import org.signal.storageservice.protos.groups.AvatarUploadAttributes; import org.signal.storageservice.protos.groups.Group; import org.signal.storageservice.protos.groups.GroupAttributeBlob; import org.signal.storageservice.protos.groups.GroupChange; +import org.signal.storageservice.protos.groups.GroupChangeResponse; import org.signal.storageservice.protos.groups.GroupChanges; import org.signal.storageservice.protos.groups.GroupExternalCredential; import org.signal.storageservice.protos.groups.GroupJoinInfo; +import org.signal.storageservice.protos.groups.GroupResponse; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; @@ -31,6 +34,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import javax.annotation.Nonnull; @@ -73,9 +77,9 @@ public class GroupsV2Api { return new GroupsV2AuthorizationString(groupSecretParams, authCredentialPresentation); } - public void putNewGroup(GroupsV2Operations.NewGroup newGroup, - GroupsV2AuthorizationString authorization) - throws IOException + public DecryptedGroupResponse putNewGroup(GroupsV2Operations.NewGroup newGroup, + GroupsV2AuthorizationString authorization) + throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException { Group group = newGroup.getNewGroupMessage(); @@ -83,34 +87,38 @@ public class GroupsV2Api { String cdnKey = uploadAvatar(newGroup.getAvatar().get(), newGroup.getGroupSecretParams(), authorization); group = group.newBuilder() - .avatar(cdnKey) - .build(); + .avatar(cdnKey) + .build(); } - socket.putNewGroupsV2Group(group, authorization); + GroupResponse response = socket.putNewGroupsV2Group(group, authorization); + + return groupsOperations.forGroup(newGroup.getGroupSecretParams()) + .decryptGroup(Objects.requireNonNull(response.group), response.groupSendEndorsementsResponse.toByteArray()); } - public NetworkResult getGroupAsResult(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) { + public NetworkResult getGroupAsResult(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) { return NetworkResult.fromFetch(() -> getGroup(groupSecretParams, authorization)); } - public DecryptedGroup getGroup(GroupSecretParams groupSecretParams, - GroupsV2AuthorizationString authorization) - throws IOException, InvalidGroupStateException, VerificationFailedException + public DecryptedGroupResponse getGroup(GroupSecretParams groupSecretParams, + GroupsV2AuthorizationString authorization) + throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException { - Group group = socket.getGroupsV2Group(authorization); + GroupResponse response = socket.getGroupsV2Group(authorization); return groupsOperations.forGroup(groupSecretParams) - .decryptGroup(group); + .decryptGroup(Objects.requireNonNull(response.group), response.groupSendEndorsementsResponse.toByteArray()); } public GroupHistoryPage getGroupHistoryPage(GroupSecretParams groupSecretParams, int fromRevision, GroupsV2AuthorizationString authorization, - boolean includeFirstState) - throws IOException, InvalidGroupStateException, VerificationFailedException + boolean includeFirstState, + long sendEndorsementsExpirationMs) + throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException { - PushServiceSocket.GroupHistory group = socket.getGroupsV2GroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState); + PushServiceSocket.GroupHistory group = socket.getGroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState, sendEndorsementsExpirationMs); List result = new ArrayList<>(group.getGroupChanges().groupChanges.size()); GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams); @@ -121,7 +129,10 @@ public class GroupsV2Api { result.add(new DecryptedGroupChangeLog(decryptedGroup, decryptedChange)); } - return new GroupHistoryPage(result, GroupHistoryPage.PagingData.forGroupHistory(group)); + byte[] groupSendEndorsementsResponseBytes = group.getGroupChanges().groupSendEndorsementsResponse.toByteArray(); + GroupSendEndorsementsResponse groupSendEndorsementsResponse = groupSendEndorsementsResponseBytes.length > 0 ? new GroupSendEndorsementsResponse(groupSendEndorsementsResponseBytes) : null; + + return new GroupHistoryPage(result, groupSendEndorsementsResponse, GroupHistoryPage.PagingData.forGroupHistory(group)); } public NetworkResult getGroupJoinedAt(@Nonnull GroupsV2AuthorizationString authorization) { @@ -162,9 +173,9 @@ public class GroupsV2Api { return form.key; } - public GroupChange patchGroup(GroupChange.Actions groupChange, - GroupsV2AuthorizationString authorization, - Optional groupLinkPassword) + public GroupChangeResponse patchGroup(GroupChange.Actions groupChange, + GroupsV2AuthorizationString authorization, + Optional groupLinkPassword) throws IOException { return socket.patchGroupsV2Group(groupChange, authorization.toString(), groupLinkPassword); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java index d63b3ded56..28a1196aa0 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -10,6 +10,7 @@ import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.ProfileKeyCiphertext; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; +import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; @@ -53,6 +54,9 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import okio.ByteString; /** @@ -154,7 +158,7 @@ public final class GroupsV2Operations { private final GroupSecretParams groupSecretParams; private final ClientZkGroupCipher clientZkGroupCipher; - private GroupOperations(GroupSecretParams groupSecretParams) { + public GroupOperations(GroupSecretParams groupSecretParams) { this.groupSecretParams = groupSecretParams; this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams); } @@ -425,6 +429,15 @@ public final class GroupsV2Operations { return new PendingMember.Builder().member(member); } + public @Nonnull DecryptedGroupResponse decryptGroup(@Nonnull Group group, @Nonnull byte[] groupSendEndorsementsBytes) + throws VerificationFailedException, InvalidGroupStateException, InvalidInputException + { + DecryptedGroup decryptedGroup = decryptGroup(group); + GroupSendEndorsementsResponse groupSendEndorsementsResponse = groupSendEndorsementsBytes.length > 0 ? new GroupSendEndorsementsResponse(groupSendEndorsementsBytes) : null; + + return new DecryptedGroupResponse(decryptedGroup, groupSendEndorsementsResponse); + } + public DecryptedGroup decryptGroup(Group group) throws VerificationFailedException, InvalidGroupStateException { @@ -1019,6 +1032,46 @@ public final class GroupsV2Operations { return ids; } + public @Nullable ReceivedGroupSendEndorsements receiveGroupSendEndorsements(@Nonnull ACI selfAci, + @Nonnull DecryptedGroup decryptedGroup, + @Nullable ByteString groupSendEndorsementsResponse) + { + if (groupSendEndorsementsResponse != null && groupSendEndorsementsResponse.size() > 0) { + try { + return receiveGroupSendEndorsements(selfAci, decryptedGroup, new GroupSendEndorsementsResponse(groupSendEndorsementsResponse.toByteArray())); + } catch (InvalidInputException e) { + Log.w(TAG, "Unable to parse send endorsements response", e); + } + } + + return null; + } + + public @Nullable ReceivedGroupSendEndorsements receiveGroupSendEndorsements(@Nonnull ACI selfAci, + @Nonnull DecryptedGroup decryptedGroup, + @Nullable GroupSendEndorsementsResponse groupSendEndorsementsResponse) + { + if (groupSendEndorsementsResponse == null) { + return null; + } + + List 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 { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ReceivedGroupSendEndorsements.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ReceivedGroupSendEndorsements.kt new file mode 100644 index 0000000000..86e8252912 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ReceivedGroupSendEndorsements.kt @@ -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 +) { + constructor( + expiration: Instant, + members: List, + receivedEndorsements: GroupSendEndorsementsResponse.ReceivedEndorsements + ) : this( + expirationMs = expiration.toEpochMilli(), + endorsements = members.zip(receivedEndorsements.endorsements).toMap() + ) +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/MessagingService.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/MessagingService.java index 72712a88dd..2f4ca3d6cd 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/MessagingService.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/MessagingService.java @@ -1,7 +1,7 @@ package org.whispersystems.signalservice.api.services; import org.whispersystems.signalservice.api.SignalWebSocket; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -19,13 +19,14 @@ import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper; import org.whispersystems.signalservice.internal.websocket.ResponseMapper; import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage; -import org.signal.core.util.Base64; import java.security.SecureRandom; import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.Optional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import io.reactivex.rxjava3.core.Single; import okio.ByteString; @@ -42,7 +43,9 @@ public class MessagingService { this.signalWebSocket = signalWebSocket; } - public Single> send(OutgoingPushMessageList list, Optional unidentifiedAccess, boolean story) { + public Single> send(OutgoingPushMessageList list, + @Nullable SealedSenderAccess sealedSenderAccess, + boolean story) { List headers = new LinkedList() {{ add("content-type:application/json"); }}; @@ -66,15 +69,15 @@ public class MessagingService { .withCustomError(404, (status, body, getHeader) -> new UnregisteredUserException(list.getDestination(), new NotFoundException("not found"))) .build(); - return signalWebSocket.request(requestMessage, unidentifiedAccess) + return signalWebSocket.request(requestMessage, sealedSenderAccess) .map(responseMapper::map) .onErrorReturn(ServiceResponse::forUnknownError); } - public Single> sendToGroup(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent, boolean story) { + public Single> sendToGroup(byte[] body, @Nonnull SealedSenderAccess sealedSenderAccess, long timestamp, boolean online, boolean urgent, boolean story) { List headers = new LinkedList() {{ 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); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java index 55cdcaced2..64e18d595f 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java @@ -12,6 +12,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext; import org.signal.libsignal.zkgroup.profiles.ProfileKeyVersion; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalWebSocket; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; @@ -44,6 +45,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Single; @@ -72,7 +74,7 @@ public final class ProfileService { public Single> getProfile(@Nonnull SignalServiceAddress address, @Nonnull Optional profileKey, - @Nonnull Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, @Nonnull SignalServiceProfile.RequestType requestType, @Nonnull Locale locale) { @@ -116,9 +118,9 @@ public final class ProfileService { .withResponseMapper(new ProfileResponseMapper(requestType, requestContext)) .build(); - return signalWebSocket.request(requestMessage, unidentifiedAccess) + return signalWebSocket.request(requestMessage, sealedSenderAccess) .map(responseMapper::map) - .onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, unidentifiedAccess, requestType, locale)) + .onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, sealedSenderAccess, requestType, locale)) .onErrorReturn(ServiceResponse::forUnknownError); } @@ -139,19 +141,19 @@ public final class ProfileService { ResponseMapper responseMapper = DefaultResponseMapper.getDefault(IdentityCheckResponse.class); - return signalWebSocket.request(builder.build(), Optional.empty()) + return signalWebSocket.request(builder.build(), SealedSenderAccess.NONE) .map(responseMapper::map) - .onErrorResumeNext(t -> performIdentityCheckRestFallback(request, Optional.empty(), responseMapper)) + .onErrorResumeNext(t -> performIdentityCheckRestFallback(request, responseMapper)) .onErrorReturn(ServiceResponse::forUnknownError); } private Single> getProfileRestFallback(@Nonnull SignalServiceAddress address, @Nonnull Optional profileKey, - @Nonnull Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, @Nonnull SignalServiceProfile.RequestType requestType, @Nonnull Locale locale) { - return Single.fromFuture(receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType, locale), 10, TimeUnit.SECONDS) + return Single.fromFuture(receiver.retrieveProfile(address, profileKey, sealedSenderAccess, requestType, locale), 10, TimeUnit.SECONDS) .onErrorResumeNext(t -> { Throwable error; if (t instanceof ExecutionException && t.getCause() != null) { @@ -161,7 +163,7 @@ public final class ProfileService { } if (error instanceof AuthorizationFailedException) { - return Single.fromFuture(receiver.retrieveProfile(address, profileKey, Optional.empty(), requestType, locale), 10, TimeUnit.SECONDS); + return Single.fromFuture(receiver.retrieveProfile(address, profileKey, null, requestType, locale), 10, TimeUnit.SECONDS); } else { return Single.error(t); } @@ -170,9 +172,8 @@ public final class ProfileService { } private @NonNull Single> performIdentityCheckRestFallback(@Nonnull IdentityCheckRequest request, - @Nonnull Optional unidentifiedAccess, @Nonnull ResponseMapper responseMapper) { - return receiver.performIdentityCheck(request, unidentifiedAccess, responseMapper) + return receiver.performIdentityCheck(request, responseMapper) .onErrorResumeNext(t -> { Throwable error; if (t instanceof ExecutionException && t.getCause() != null) { @@ -182,7 +183,7 @@ public final class ProfileService { } if (error instanceof AuthorizationFailedException) { - return receiver.performIdentityCheck(request, Optional.empty(), responseMapper); + return receiver.performIdentityCheck(request, responseMapper); } else { return Single.error(t); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index e83538fd55..241282338c 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -39,9 +39,11 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse; import org.signal.storageservice.protos.groups.AvatarUploadAttributes; import org.signal.storageservice.protos.groups.Group; import org.signal.storageservice.protos.groups.GroupChange; +import org.signal.storageservice.protos.groups.GroupChangeResponse; import org.signal.storageservice.protos.groups.GroupChanges; import org.signal.storageservice.protos.groups.GroupExternalCredential; import org.signal.storageservice.protos.groups.GroupJoinInfo; +import org.signal.storageservice.protos.groups.GroupResponse; import org.signal.storageservice.protos.groups.Member; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; @@ -60,7 +62,7 @@ import org.whispersystems.signalservice.api.archive.BatchArchiveMediaRequest; import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse; import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest; import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.groupsv2.CredentialResponse; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; @@ -273,13 +275,13 @@ public class PushServiceSocket { private static final String STICKER_PATH = "stickers/%s/full/%d"; private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&zkcCredential=true"; - private static final String GROUPSV2_GROUP = "/v1/groups/"; - private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s"; - private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false"; - private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form"; - private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s"; - private static final String GROUPSV2_TOKEN = "/v1/groups/token"; - private static final String GROUPSV2_JOINED_AT = "/v1/groups/joined_at_version"; + private static final String GROUPSV2_GROUP = "/v2/groups/"; + private static final String GROUPSV2_GROUP_PASSWORD = "/v2/groups/?inviteLinkPassword=%s"; + private static final String GROUPSV2_GROUP_CHANGES = "/v2/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false"; + private static final String GROUPSV2_AVATAR_REQUEST = "/v2/groups/avatar/form"; + private static final String GROUPSV2_GROUP_JOIN = "/v2/groups/join/%s"; + private static final String GROUPSV2_TOKEN = "/v2/groups/token"; + private static final String GROUPSV2_JOINED_AT = "/v2/groups/joined_at_version"; private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions"; @@ -376,7 +378,7 @@ public class PushServiceSocket { public RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc) throws IOException { final String jsonBody = JsonUtil.toJson(new VerificationSessionMetadataRequestBody(credentialsProvider.getE164(), pushToken, mcc, mnc)); - try (Response response = makeServiceRequest(VERIFICATION_SESSION_PATH, "POST", jsonRequestBody(jsonBody), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty(), false)) { + try (Response response = makeServiceRequest(VERIFICATION_SESSION_PATH, "POST", jsonRequestBody(jsonBody), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) { return parseSessionMetadataResponse(response); } } @@ -384,7 +386,7 @@ public class PushServiceSocket { public RegistrationSessionMetadataResponse getSessionStatus(String sessionId) throws IOException { String path = VERIFICATION_SESSION_PATH + "/" + sessionId; - try (Response response = makeServiceRequest(path, "GET", jsonRequestBody(null), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty(), false)) { + try (Response response = makeServiceRequest(path, "GET", jsonRequestBody(null), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) { return parseSessionMetadataResponse(response); } } @@ -393,7 +395,7 @@ public class PushServiceSocket { String path = VERIFICATION_SESSION_PATH + "/" + sessionId; final UpdateVerificationSessionRequestBody requestBody = new UpdateVerificationSessionRequestBody(captchaToken, pushToken, pushChallengeToken, mcc, mnc); - try (Response response = makeServiceRequest(path, "PATCH", jsonRequestBody(JsonUtil.toJson(requestBody)), NO_HEADERS, new PatchRegistrationSessionResponseHandler(), Optional.empty(), false)) { + try (Response response = makeServiceRequest(path, "PATCH", jsonRequestBody(JsonUtil.toJson(requestBody)), NO_HEADERS, new PatchRegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) { return parseSessionMetadataResponse(response); } } @@ -414,7 +416,7 @@ public class PushServiceSocket { body.put("client", androidSmsRetriever ? "android-2021-03" : "android"); - try (Response response = makeServiceRequest(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, new RegistrationCodeRequestResponseHandler(), Optional.empty(), false)) { + try (Response response = makeServiceRequest(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, new RegistrationCodeRequestResponseHandler(), SealedSenderAccess.NONE, false)) { return parseSessionMetadataResponse(response); } } @@ -423,7 +425,7 @@ public class PushServiceSocket { String path = String.format(VERIFICATION_CODE_PATH, sessionId); Map body = new HashMap<>(); body.put("code", verificationCode); - try (Response response = makeServiceRequest(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, new RegistrationCodeSubmissionResponseHandler(), Optional.empty(), false)) { + try (Response response = makeServiceRequest(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, new RegistrationCodeSubmissionResponseHandler(), SealedSenderAccess.NONE, false)) { return parseSessionMetadataResponse(response); } } @@ -470,7 +472,7 @@ public class PushServiceSocket { skipDeviceTransfer, true); - String response = makeServiceRequest(path, "POST", JsonUtil.toJson(body), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty()); + String response = makeServiceRequest(path, "POST", JsonUtil.toJson(body), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE); return JsonUtil.fromJson(response, VerifyAccountResponse.class); } @@ -512,14 +514,14 @@ public class PushServiceSocket { long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime)); long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7); - String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null, NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty()); + String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE); return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class); } public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException { String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(request)); - makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty()); + makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE); } public void setArchivePublicKey(ECPublicKey publicKey, ArchiveCredentialPresentation credentialPresentation) throws IOException { @@ -706,7 +708,7 @@ public class PushServiceSocket { return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); } - public SendGroupMessageResponse sendGroupMessage(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent, boolean story) + public SendGroupMessageResponse sendGroupMessage(byte[] body, @Nonnull SealedSenderAccess sealedSenderAccess, long timestamp, boolean online, boolean urgent, boolean story) throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException { ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random); @@ -716,7 +718,7 @@ public class PushServiceSocket { Request.Builder requestBuilder = new Request.Builder(); requestBuilder.url(String.format("%s%s", connectionHolder.getUrl(), path)); requestBuilder.put(RequestBody.create(MediaType.get("application/vnd.signal-messenger.mrm"), body)); - requestBuilder.addHeader("Unidentified-Access-Key", Base64.encodeWithPadding(joinedUnidentifiedAccess)); + requestBuilder.addHeader(sealedSenderAccess.getHeaderName(), sealedSenderAccess.getHeaderValue()); if (signalAgent != null) { requestBuilder.addHeader("X-Signal-Agent", signalAgent); @@ -762,14 +764,14 @@ public class PushServiceSocket { } } - public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional unidentifiedAccess, boolean story) + public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, @Nullable SealedSenderAccess sealedSenderAccess, boolean story) throws IOException { try { - String responseText = makeServiceRequest(String.format("/v1/messages/%s?story=%s", bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, NO_HANDLER, unidentifiedAccess); + String responseText = makeServiceRequest(String.format("/v1/messages/%s?story=%s", bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, NO_HANDLER, sealedSenderAccess); SendMessageResponse response = JsonUtil.fromJson(responseText, SendMessageResponse.class); - response.setSentUnidentfied(unidentifiedAccess.isPresent()); + response.setSentUnidentfied(sealedSenderAccess != null); return response; } catch (NotFoundException nfe) { @@ -780,7 +782,7 @@ public class PushServiceSocket { public SignalServiceMessagesResult getMessages(boolean allowStories) throws IOException { Map headers = Collections.singletonMap("X-Signal-Receive-Stories", allowStories ? "true" : "false"); - try (Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, headers, NO_HANDLER, Optional.empty(), false)) { + try (Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, headers, NO_HANDLER, SealedSenderAccess.NONE, false)) { validateServiceResponse(response); List envelopes = readBodyJson(response.body(), SignalServiceEnvelopeEntityList.class).getMessages(); @@ -870,18 +872,18 @@ public class PushServiceSocket { * for all devices. If it is not a primary, it will only contain the prekeys for that specific device. */ public List getPreKeys(SignalServiceAddress destination, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, int deviceId) throws IOException { - return getPreKeysBySpecifier(destination, unidentifiedAccess, deviceId == 1 ? "*" : String.valueOf(deviceId)); + return getPreKeysBySpecifier(destination, sealedSenderAccess, deviceId == 1 ? "*" : String.valueOf(deviceId)); } /** * Retrieves a prekey for a specific device. */ public PreKeyBundle getPreKey(SignalServiceAddress destination, int deviceId) throws IOException { - List bundles = getPreKeysBySpecifier(destination, Optional.empty(), String.valueOf(deviceId)); + List bundles = getPreKeysBySpecifier(destination, null, String.valueOf(deviceId)); if (bundles.size() > 0) { return bundles.get(0); @@ -891,7 +893,7 @@ public class PushServiceSocket { } private List getPreKeysBySpecifier(SignalServiceAddress destination, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, String deviceSpecifier) throws IOException { @@ -900,7 +902,7 @@ public class PushServiceSocket { Log.d(TAG, "Fetching prekeys for " + destination.getIdentifier() + "." + deviceSpecifier + ", i.e. GET " + path); - String responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, NO_HANDLER, unidentifiedAccess); + String responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, NO_HANDLER, sealedSenderAccess); PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class); List bundles = new LinkedList<>(); @@ -958,7 +960,7 @@ public class PushServiceSocket { if (responseCode == 409) { throw new NonSuccessfulResponseCodeException(409); } - }, Optional.empty()); + }, null); } public void retrieveBackup(int cdnNumber, Map headers, String cdnPath, File destination, long maxSizeBytes, ProgressListener listener) @@ -1012,8 +1014,8 @@ public class PushServiceSocket { return output.toByteArray(); } - public ListenableFuture retrieveProfile(SignalServiceAddress target, Optional unidentifiedAccess, Locale locale) { - ListenableFuture response = submitServiceRequest(String.format(PROFILE_PATH, target.getIdentifier()), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess); + public ListenableFuture retrieveProfile(SignalServiceAddress target, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) { + ListenableFuture response = submitServiceRequest(String.format(PROFILE_PATH, target.getIdentifier()), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess); return FutureTransformers.map(response, body -> { try { @@ -1025,7 +1027,7 @@ public class PushServiceSocket { }); } - public ListenableFuture retrieveVersionedProfileAndCredential(ACI target, ProfileKey profileKey, Optional unidentifiedAccess, Locale locale) { + public ListenableFuture retrieveVersionedProfileAndCredential(ACI target, ProfileKey profileKey, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) { ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci()); ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target.getLibSignalAci(), profileKey); ProfileKeyCredentialRequest request = requestContext.getRequest(); @@ -1035,7 +1037,7 @@ public class PushServiceSocket { String subPath = String.format("%s/%s/%s?credentialType=expiringProfileKey", target, version, credentialRequest); - ListenableFuture response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess); + ListenableFuture response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess); return FutureTransformers.map(response, body -> formatProfileAndCredentialBody(requestContext, body)); } @@ -1061,12 +1063,12 @@ public class PushServiceSocket { } } - public ListenableFuture retrieveVersionedProfile(ACI target, ProfileKey profileKey, Optional unidentifiedAccess, Locale locale) { + public ListenableFuture retrieveVersionedProfile(ACI target, ProfileKey profileKey, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) { ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci()); String version = profileKeyIdentifier.serialize(); String subPath = String.format("%s/%s", target, version); - ListenableFuture response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess); + ListenableFuture response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess); return FutureTransformers.map(response, body -> { try { @@ -1102,7 +1104,7 @@ public class PushServiceSocket { requestBody, NO_HEADERS, PaymentsRegionException::responseCodeHandler, - Optional.empty()); + SealedSenderAccess.NONE); if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) { try { @@ -1126,13 +1128,12 @@ public class PushServiceSocket { } public Single> performIdentityCheck(@Nonnull IdentityCheckRequest request, - @Nonnull Optional unidentifiedAccess, @Nonnull ResponseMapper responseMapper) { Single> requestSingle = Single.fromCallable(() -> { - try (Response response = getServiceConnection(PROFILE_BATCH_CHECK_PATH, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), unidentifiedAccess, false)) { + try (Response response = getServiceConnection(PROFILE_BATCH_CHECK_PATH, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), SealedSenderAccess.NONE, false)) { String body = response.body() != null ? readBodyString(response.body()): ""; - return responseMapper.map(response.code(), body, response::header, unidentifiedAccess.isPresent()); + return responseMapper.map(response.code(), body, response::header, false); } }); @@ -1146,7 +1147,7 @@ public class PushServiceSocket { @Nonnull ResponseMapper responseMapper) { Single> requestSingle = Single.fromCallable(() -> { - try (Response response = getServiceConnection(BACKUP_AUTH_CHECK_V2, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) { + try (Response response = getServiceConnection(BACKUP_AUTH_CHECK_V2, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), SealedSenderAccess.NONE, false)) { String body = response.body() != null ? readBodyString(response.body()): ""; return responseMapper.map(response.code(), body, response::header, false); } @@ -1159,12 +1160,12 @@ public class PushServiceSocket { } public BackupV2AuthCheckResponse checkSvr2AuthCredentials(@Nullable String number, @Nonnull List passwords) throws IOException { - String response = makeServiceRequest(BACKUP_AUTH_CHECK_V2, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty()); + String response = makeServiceRequest(BACKUP_AUTH_CHECK_V2, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE); return JsonUtil.fromJson(response, BackupV2AuthCheckResponse.class); } public BackupV3AuthCheckResponse checkSvr3AuthCredentials(@Nullable String number, @Nonnull List passwords) throws IOException { - String response = makeServiceRequest(BACKUP_AUTH_CHECK_V3, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty()); + String response = makeServiceRequest(BACKUP_AUTH_CHECK_V3, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE); return JsonUtil.fromJson(response, BackupV3AuthCheckResponse.class); } @@ -1218,7 +1219,7 @@ public class PushServiceSocket { case 422: throw new UsernameMalformedException(); case 409: throw new UsernameTakenException(); } - }, Optional.empty()); + }, SealedSenderAccess.NONE); return JsonUtil.fromJsonResponse(responseString, ReserveUsernameResponse.class); } @@ -1249,7 +1250,7 @@ public class PushServiceSocket { case 410: throw new UsernameTakenException(); } - }, Optional.empty()); + }, SealedSenderAccess.NONE); return JsonUtil.fromJson(response, ConfirmUsernameResponse.class).getUsernameLinkHandle(); } catch (BaseUsernameException e) { @@ -1303,7 +1304,7 @@ public class PushServiceSocket { if (responseCode == 428) { throw new CaptchaRejectedException(); } - }, Optional.empty()); + }, SealedSenderAccess.NONE); } @@ -2139,7 +2140,7 @@ public class PushServiceSocket { private String makeServiceRequestWithoutAuthentication(String urlFragment, String method, String jsonBody, Map headers, ResponseCodeHandler responseCodeHandler) throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException { - try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, Optional.empty(), true)) { + try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, SealedSenderAccess.NONE, true)) { return readBodyString(response); } } @@ -2147,13 +2148,13 @@ public class PushServiceSocket { private String makeServiceRequest(String urlFragment, String method, String jsonBody) throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException { - return makeServiceRequest(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER, Optional.empty()); + return makeServiceRequest(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER, SealedSenderAccess.NONE); } - private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map headers, ResponseCodeHandler responseCodeHandler, Optional unidentifiedAccessKey) + private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map headers, ResponseCodeHandler responseCodeHandler, @Nullable SealedSenderAccess sealedSenderAccess) throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException { - try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, unidentifiedAccessKey, false)) { + try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, sealedSenderAccess, false)) { return readBodyString(response); } } @@ -2172,10 +2173,10 @@ public class PushServiceSocket { String method, String jsonBody, Map headers, - Optional unidentifiedAccessKey) + @Nullable SealedSenderAccess sealedSenderAccess) { - OkHttpClient okHttpClient = buildOkHttpClient(unidentifiedAccessKey.isPresent()); - Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, unidentifiedAccessKey, false)); + OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null); + Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, sealedSenderAccess, false)); synchronized (connections) { connections.add(call); @@ -2208,13 +2209,13 @@ public class PushServiceSocket { RequestBody body, Map headers, ResponseCodeHandler responseCodeHandler, - Optional unidentifiedAccessKey, + @Nullable SealedSenderAccess sealedSenderAccess, boolean doNotAddAuthenticationOrUnidentifiedAccessKey) throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException { Response response = null; try { - response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey, doNotAddAuthenticationOrUnidentifiedAccessKey); + response = getServiceConnection(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey); responseCodeHandler.handle(response.code(), response.body()); return validateServiceResponse(response); } catch (Exception e) { @@ -2288,13 +2289,13 @@ public class PushServiceSocket { String method, RequestBody body, Map headers, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, boolean doNotAddAuthenticationOrUnidentifiedAccessKey) throws PushNetworkException { try { - OkHttpClient okHttpClient = buildOkHttpClient(unidentifiedAccess.isPresent()); - Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, body, headers, unidentifiedAccess, doNotAddAuthenticationOrUnidentifiedAccessKey)); + OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null); + Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey)); synchronized (connections) { connections.add(call); @@ -2327,14 +2328,11 @@ public class PushServiceSocket { String method, RequestBody body, Map headers, - Optional unidentifiedAccess, + @Nullable SealedSenderAccess sealedSenderAccess, boolean doNotAddAuthenticationOrUnidentifiedAccessKey) { ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random); -// Log.d(TAG, "Push service URL: " + connectionHolder.getUrl()); -// Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), urlFragment)); - Request.Builder request = new Request.Builder(); request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment)); request.method(method, body); @@ -2344,8 +2342,8 @@ public class PushServiceSocket { } if (!headers.containsKey("Authorization") && !doNotAddAuthenticationOrUnidentifiedAccessKey) { - if (unidentifiedAccess.isPresent()) { - request.addHeader("Unidentified-Access-Key", Base64.encodeWithPadding(unidentifiedAccess.get().getUnidentifiedAccessKey())); + if (sealedSenderAccess != null) { + request.addHeader(sealedSenderAccess.getHeaderName(), sealedSenderAccess.getHeaderValue()); } else if (credentialsProvider.getPassword() != null) { request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider)); } @@ -2364,6 +2362,12 @@ public class PushServiceSocket { private Response makeStorageRequest(String authorization, String path, String method, RequestBody body, ResponseCodeHandler responseCodeHandler) throws PushNetworkException, NonSuccessfulResponseCodeException + { + return makeStorageRequest(authorization, path, method, body, NO_HEADERS, responseCodeHandler); + } + + private Response makeStorageRequest(String authorization, String path, String method, RequestBody body, Map headers, ResponseCodeHandler responseCodeHandler) + throws PushNetworkException, NonSuccessfulResponseCodeException { ConnectionHolder connectionHolder = getRandom(storageClients, random); OkHttpClient okHttpClient = connectionHolder.getClient() @@ -2372,8 +2376,6 @@ public class PushServiceSocket { .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) .build(); -// Log.d(TAG, "Opening URL: " + connectionHolder.getUrl()); - Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path); request.method(method, body); @@ -2385,6 +2387,10 @@ public class PushServiceSocket { request.addHeader("Authorization", authorization); } + for (Map.Entry entry : headers.entrySet()) { + request.addHeader(entry.getKey(), entry.getValue()); + } + Call call = okHttpClient.newCall(request.build()); synchronized (connections) { @@ -2784,7 +2790,7 @@ public class PushServiceSocket { null, NO_HEADERS, NO_HANDLER, - Optional.empty()); + SealedSenderAccess.NONE); return JsonUtil.fromJson(response, CredentialResponse.class); } @@ -2816,8 +2822,8 @@ public class PushServiceSocket { } }; - public void putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization) - throws NonSuccessfulResponseCodeException, PushNetworkException + public GroupResponse putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization) + throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException { try (Response response = makeStorageRequest(authorization.toString(), GROUPSV2_GROUP, @@ -2825,11 +2831,11 @@ public class PushServiceSocket { protobufRequestBody(group), GROUPS_V2_PUT_RESPONSE_HANDLER)) { - return; + return GroupResponse.ADAPTER.decode(readBodyBytes(response)); } } - public Group getGroupsV2Group(GroupsV2AuthorizationString authorization) + public GroupResponse getGroupsV2Group(GroupsV2AuthorizationString authorization) throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException { try (Response response = makeStorageRequest(authorization.toString(), @@ -2838,7 +2844,7 @@ public class PushServiceSocket { null, GROUPS_V2_GET_CURRENT_HANDLER)) { - return Group.ADAPTER.decode(readBodyBytes(response)); + return GroupResponse.ADAPTER.decode(readBodyBytes(response)); } } @@ -2855,7 +2861,7 @@ public class PushServiceSocket { } } - public GroupChange patchGroupsV2Group(GroupChange.Actions groupChange, String authorization, Optional groupLinkPassword) + public GroupChangeResponse patchGroupsV2Group(GroupChange.Actions groupChange, String authorization, Optional groupLinkPassword) throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException { String path; @@ -2872,17 +2878,21 @@ public class PushServiceSocket { protobufRequestBody(groupChange), GROUPS_V2_PATCH_RESPONSE_HANDLER)) { - return GroupChange.ADAPTER.decode(readBodyBytes(response)); + return GroupChangeResponse.ADAPTER.decode(readBodyBytes(response)); } } - public GroupHistory getGroupsV2GroupHistory(int fromVersion, GroupsV2AuthorizationString authorization, int highestKnownEpoch, boolean includeFirstState) + public GroupHistory getGroupHistory(int fromVersion, GroupsV2AuthorizationString authorization, int highestKnownEpoch, boolean includeFirstState, long sendEndorsementsExpirationMs) throws IOException { + Map headers = new HashMap<>(); + headers.put("Cached-Send-Endorsements", Long.toString(TimeUnit.MILLISECONDS.toSeconds(sendEndorsementsExpirationMs))); + try (Response response = makeStorageRequest(authorization.toString(), String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion, highestKnownEpoch, includeFirstState), "GET", null, + headers, GROUPS_V2_GET_LOGS_HANDLER)) { @@ -2890,12 +2900,7 @@ public class PushServiceSocket { throw new PushNetworkException("No body!"); } - GroupChanges groupChanges; - try (InputStream input = response.body().byteStream()) { - groupChanges = GroupChanges.ADAPTER.decode(input); - } catch (IOException e) { - throw new PushNetworkException(e); - } + GroupChanges groupChanges = GroupChanges.ADAPTER.decode(readBodyBytes(response)); if (response.code() == 206) { String contentRangeHeader = response.header("Content-Range"); diff --git a/libsignal-service/src/main/protowire/Groups.proto b/libsignal-service/src/main/protowire/Groups.proto index 3f0b7300bf..63970c7741 100644 --- a/libsignal-service/src/main/protowire/Groups.proto +++ b/libsignal-service/src/main/protowire/Groups.proto @@ -213,13 +213,24 @@ message GroupChange { uint32 changeEpoch = 3; } +message GroupResponse { + Group group = 1; + bytes groupSendEndorsementsResponse = 2; +} + message GroupChanges { message GroupChangeState { GroupChange groupChange = 1; Group groupState = 2; } - repeated GroupChangeState groupChanges = 1; + repeated GroupChangeState groupChanges = 1; + bytes groupSendEndorsementsResponse = 2; +} + +message GroupChangeResponse { + GroupChange groupChange = 1; + bytes groupSendEndorsementsResponse = 2; } message GroupAttributeBlob { diff --git a/microbenchmark/src/androidTest/java/org/signal/util/SignalClient.kt b/microbenchmark/src/androidTest/java/org/signal/util/SignalClient.kt index 646117afef..ce827bf5dd 100644 --- a/microbenchmark/src/androidTest/java/org/signal/util/SignalClient.kt +++ b/microbenchmark/src/androidTest/java/org/signal/util/SignalClient.kt @@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountDataStore import org.whispersystems.signalservice.api.SignalSessionLock import org.whispersystems.signalservice.api.crypto.ContentHint import org.whispersystems.signalservice.api.crypto.EnvelopeContent +import org.whispersystems.signalservice.api.crypto.SealedSenderAccess import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder import org.whispersystems.signalservice.api.crypto.SignalServiceCipher import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess @@ -93,7 +94,7 @@ class SignalClient { val outgoingPushMessage: OutgoingPushMessage = cipher.encrypt( SignalProtocolAddress(to.aci.toString(), 1), - Optional.empty(), + SealedSenderAccess.NONE, EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty()) ) @@ -124,7 +125,7 @@ class SignalClient { val outgoingPushMessage: OutgoingPushMessage = cipher.encrypt( 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()) )