Add Group Send Endorsements support.

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

View file

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

View file

@ -16,7 +16,7 @@ import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.ecc.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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<UnidentifiedAccessPair> 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<UnidentifiedAccessPair> 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<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> 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<RecipientId, Optional<UnidentifiedAccessPair>> getAccessMapFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean isForStory) {
List<Optional<UnidentifiedAccessPair>> accessList = getAccessFor(context, recipients, isForStory, true);
public static Map<RecipientId, Optional<UnidentifiedAccess>> getAccessMapFor(@NonNull List<Recipient> recipients, boolean isForStory) {
List<Optional<UnidentifiedAccess>> accessList = getAccessFor(recipients, isForStory, true);
Iterator<Recipient> recipientIterator = recipients.iterator();
Iterator<Optional<UnidentifiedAccessPair>> accessIterator = accessList.iterator();
Iterator<Recipient> recipientIterator = recipients.iterator();
Iterator<Optional<UnidentifiedAccess>> accessIterator = accessList.iterator();
Map<RecipientId, Optional<UnidentifiedAccessPair>> accessMap = new HashMap<>(recipients.size());
Map<RecipientId, Optional<UnidentifiedAccess>> accessMap = new HashMap<>(recipients.size());
while (recipientIterator.hasNext()) {
accessMap.put(recipientIterator.next().getId(), accessIterator.next());
@ -77,40 +81,22 @@ public class UnidentifiedAccessUtil {
}
@WorkerThread
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean log) {
return getAccessFor(context, recipients, false, log);
}
@WorkerThread
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean isForStory, boolean log) {
final byte[] ourUnidentifiedAccessKey;
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = UNRESTRICTED_KEY;
} else {
ourUnidentifiedAccessKey = ProfileKeyUtil.getSelfProfileKey().deriveAccessKey();
}
private static List<Optional<UnidentifiedAccess>> getAccessFor(@NonNull List<Recipient> recipients, boolean isForStory, boolean log) {
CertificateType certificateType = getUnidentifiedAccessCertificateType();
byte[] ourUnidentifiedAccessCertificate = SignalStore.certificate().getUnidentifiedAccessCertificate(certificateType);
List<Optional<UnidentifiedAccessPair>> access = recipients.parallelStream().map(recipient -> {
UnidentifiedAccessPair unidentifiedAccessPair = null;
List<Optional<UnidentifiedAccess>> 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<UnidentifiedAccessPair> 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) {

View file

@ -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<String> = 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<RecipientId>): Boolean {
return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null)
return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null, null)
}
@CheckReturnValue
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<RecipientId>,
avatar: SignalServiceAttachmentPointer?,
groupMasterKey: GroupMasterKey?,
groupState: DecryptedGroup?
groupState: DecryptedGroup?,
receivedGroupSendEndorsements: ReceivedGroupSendEndorsements?
): Boolean {
val membershipValues = mutableListOf<ContentValues>()
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<GroupRecord> = 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<RecipientId> = 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<RecipientId> = 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<RecipientId>) {
private fun performMembershipUpdate(database: SQLiteDatabase, groupId: GroupId, members: Collection<RecipientId>, 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<RecipientId, GroupSendEndorsement?> = receivedGroupSendEndorsements.toGroupSendEndorsementRecords().endorsements
writableDatabase.withinTransaction { db ->
db.update(MembershipTable.TABLE_NAME, contentValuesOf(MembershipTable.ENDORSEMENT to null), "${MembershipTable.GROUP_ID} = ?", arrayOf(groupId.serialize()))
for ((recipientId, endorsement) in endorsements) {
db.update(MembershipTable.TABLE_NAME)
.values(MembershipTable.ENDORSEMENT to endorsement?.serialize())
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, recipientId)
.run()
}
writableDatabase
.update(TABLE_NAME)
.values(GROUP_SEND_ENDORSEMENTS_EXPIRATION to receivedGroupSendEndorsements.expirationMs)
.where("$GROUP_ID = ?", groupId)
.run()
}
}
fun getGroupSendEndorsements(groupId: GroupId): GroupSendEndorsementRecords {
val allEndorsements: Map<RecipientId, GroupSendEndorsement?> = readableDatabase
.select(MembershipTable.RECIPIENT_ID, MembershipTable.ENDORSEMENT)
.from(MembershipTable.TABLE_NAME)
.where("${MembershipTable.GROUP_ID} = ?", groupId)
.run()
.readToMap { cursor ->
val recipientId = RecipientId.from(cursor.requireLong(MembershipTable.RECIPIENT_ID))
val endorsement = cursor.requireBlob(MembershipTable.ENDORSEMENT)?.let { endorsementBlob ->
try {
GroupSendEndorsement(endorsementBlob)
} catch (e: InvalidInputException) {
Log.w(TAG, "Unable to parse group send endorsement for $recipientId", e)
null
}
}
recipientId to endorsement
}
return GroupSendEndorsementRecords(allEndorsements)
}
fun getGroupSendFullToken(threadId: Long, recipientId: RecipientId): GroupSendFullToken? {
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
if (threadRecipient == null || !threadRecipient.isGroup) {
return null
}
return getGroupSendFullToken(threadRecipient.requireGroupId().requireV2(), recipientId)
}
fun getGroupSendFullToken(groupId: GroupId.V2, recipientId: RecipientId): GroupSendFullToken? {
val groupRecord = SignalDatabase.groups.getGroup(groupId).orElse(null) ?: return null
val endorsement = SignalDatabase.groups.getGroupSendEndorsement(groupId, recipientId) ?: return null
val groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupRecord.requireV2GroupProperties().groupMasterKey)
return endorsement.toFullToken(groupSecretParams, Instant.ofEpochMilli(groupRecord.groupSendEndorsementExpiration))
}
private fun getGroupSendEndorsement(groupId: GroupId, recipientId: RecipientId): GroupSendEndorsement? {
return readableDatabase
.select(MembershipTable.ENDORSEMENT)
.from(MembershipTable.TABLE_NAME)
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, recipientId)
.run()
.readToSingleObject { c ->
c.requireBlob(MembershipTable.ENDORSEMENT)?.let { endorsementBlob ->
try {
GroupSendEndorsement(endorsementBlob)
} catch (e: InvalidInputException) {
Log.w(TAG, "Unable to parse group send endorsement for $recipientId", e)
null
}
}
}
}
@WorkerThread
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<RecipientId>.toContentValues(groupId: GroupId): List<ContentValues> {
private fun Collection<RecipientId>.toContentValues(groupId: GroupId, groupSendEndorsementRecords: GroupSendEndorsementRecords?): List<ContentValues> {
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<ServiceId>): MutableList<RecipientId> {
return serviceIds
.map { serviceId ->

View file

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

View file

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

View file

@ -95,6 +95,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V234_ThumbnailResto
import org.thoughtcrime.securesms.database.helpers.migration.V235_AttachmentUuidColumn
import org.thoughtcrime.securesms.database.helpers.migration.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) {

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting
import org.thoughtcrime.securesms.database.RecipientTable.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,

View file

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

View file

@ -18,6 +18,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.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<RecipientId> members,
int disappearingMessageTimerSeconds)
private @NonNull DecryptedGroupResponse createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
@Nullable String name,
@Nullable byte[] avatar,
@NonNull Collection<RecipientId> 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;

View file

@ -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<DecryptedGroupChangeLog>
val serverHistory: List<DecryptedGroupChangeLog>,
val groupSendEndorsementsResponse: GroupSendEndorsementsResponse?
) {
constructor(previousGroupState: DecryptedGroup?, changedGroupState: DecryptedGroup?, change: DecryptedGroupChange?) : this(previousGroupState, listOf(DecryptedGroupChangeLog(changedGroupState, change)))
constructor(
previousGroupState: DecryptedGroup?,
changedGroupState: DecryptedGroup?,
change: DecryptedGroupChange?
) : this(previousGroupState, listOf(DecryptedGroupChangeLog(changedGroupState, change)), null)
val earliestRevisionNumber: Int
get() {

View file

@ -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<GroupStateDiff, GroupHistoryPage.PagingData> {
private fun getGroupChangeLogs(
localState: DecryptedGroup?,
logsNeededFromRevision: Int,
includeFirstState: Boolean,
sendEndorsementsExpirationMs: Long
): Pair<GroupStateDiff, GroupHistoryPage.PagingData> {
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
}

View file

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

View file

@ -18,7 +18,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.api.push.exceptions.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)

View file

@ -17,7 +17,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.api.push.exceptions.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 {

View file

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

View file

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

View file

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

View file

@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
import org.whispersystems.signalservice.api.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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRe
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage
import org.whispersystems.signalservice.api.messages.multidevice.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<SignalServiceStoryMessageRecipient> = 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")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<UnidentifiedAccessPair> access = UnidentifiedAccessUtil.getAccessFor(context, recipient);
Optional<byte[]> group = groupId.map(GroupId::getDecodedId);
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<byte[]> 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

View file

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

View file

@ -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<SignalServiceAddress> address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, targetRecipient));
SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId);
List<Optional<UnidentifiedAccessPair>> access = UnidentifiedAccessUtil.getAccessFor(context, Collections.singletonList(targetRecipient));
SignalServiceMessageSender messageSender = AppDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, targetRecipient));
SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId);
List<SealedSenderAccess> access = Collections.singletonList(SealedSenderAccessUtil.getSealedSenderAccessFor(targetRecipient, createGroupSendFullToken));
SendMessageResult result = messageSender.sendSenderKeyDistributionMessage(distributionId, address, access, message, Optional.ofNullable(groupId).map(GroupId::getDecodedId), false, false).get(0);

View file

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

View file

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

View file

@ -7,34 +7,44 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import 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<Recipient> unregisteredTargets = allTargets.stream().filter(Recipient::isUnregistered).collect(Collectors.toSet());
List<Recipient> registeredTargets = allTargets.stream().filter(r -> !unregisteredTargets.contains(r)).collect(Collectors.toList());
RecipientData recipients = new RecipientData(context, registeredTargets, isStorySend);
Optional<GroupRecord> groupRecord = groupId != null ? SignalDatabase.groups().getGroup(groupId) : Optional.empty();
RecipientData recipients = new RecipientData(context, registeredTargets, isStorySend);
Optional<GroupRecord> groupRecord = groupId != null ? SignalDatabase.groups().getGroup(groupId) : Optional.empty();
GroupSendEndorsementRecords groupSendEndorsementRecords = groupRecord.filter(GroupRecord::isV2Group).map(g -> SignalDatabase.groups().getGroupSendEndorsements(g.getId())).orElse(null);
long groupSendEndorsementExpiration = groupRecord.map(GroupRecord::getGroupSendEndorsementExpiration).orElse(0L);
SenderCertificate senderCertificate = SealedSenderAccessUtil.getSealedSenderCertificate();
boolean useGroupSendEndorsements = groupSendEndorsementRecords != null;
if (useGroupSendEndorsements && senderCertificate == null) {
Log.w(TAG, "Can't use group send endorsements without a sealed sender certificate, falling back to access key");
useGroupSendEndorsements = false;
} else if (useGroupSendEndorsements) {
boolean refreshGroupSendEndorsements = false;
if (groupSendEndorsementExpiration == 0) {
Log.i(TAG, "No group send endorsements expiration set, need to refresh");
refreshGroupSendEndorsements = true;
} else if (groupSendEndorsementExpiration - TimeUnit.HOURS.toMillis(2) < System.currentTimeMillis()) {
Log.i(TAG, "Group send endorsements are expired or expire imminently, refresh. Expires in " + (groupSendEndorsementExpiration - System.currentTimeMillis()) + "ms");
refreshGroupSendEndorsements = true;
} else if (groupSendEndorsementRecords.isMissingAnyEndorsements()) {
Log.i(TAG, "Missing group send endorsements for some members, refresh.");
refreshGroupSendEndorsements = true;
}
if (refreshGroupSendEndorsements) {
try {
GroupManager.updateGroupSendEndorsements(context, groupRecord.get().requireV2GroupProperties().getGroupMasterKey());
groupSendEndorsementExpiration = SignalDatabase.groups().getGroupSendEndorsementsExpiration(groupId);
groupSendEndorsementRecords = SignalDatabase.groups().getGroupSendEndorsements(groupId);
} catch (GroupChangeException | IOException e) {
if (groupSendEndorsementExpiration == 0) {
Log.w(TAG, "Unable to update group send endorsements, falling back to access key", e);
useGroupSendEndorsements = false;
groupSendEndorsementRecords = new GroupSendEndorsementRecords(Collections.emptyMap());
} else {
Log.w(TAG, "Unable to update group send endorsements, using what we have", e);
}
}
Log.d(TAG, "Refresh all group state because we needed to refresh gse");
AppDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId));
}
}
List<Recipient> senderKeyTargets = new LinkedList<>();
List<Recipient> legacyTargets = new LinkedList<>();
for (Recipient recipient : registeredTargets) {
Optional<UnidentifiedAccessPair> access = recipients.getAccessPair(recipient.getId());
boolean validMembership = true;
Optional<UnidentifiedAccess> 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<SendMessageResult> 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<SignalServiceAddress> targets = senderKeyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList());
List<UnidentifiedAccess> access = senderKeyTargets.stream().map(r -> recipients.requireAccess(r.getId())).collect(Collectors.toList());
List<SignalServiceAddress> targets = new ArrayList<>(senderKeyTargets.size());
List<UnidentifiedAccess> access = new ArrayList<>(senderKeyTargets.size());
Map<ServiceId.ACI, GroupSendEndorsement> senderKeyEndorsements = new HashMap<>(senderKeyTargets.size());
GroupSendEndorsements groupSendEndorsements = null;
for (Recipient recipient : senderKeyTargets) {
targets.add(recipients.getAddress(recipient.getId()));
if (useGroupSendEndorsements) {
senderKeyEndorsements.put(recipient.requireAci(), groupSendEndorsementRecords.getEndorsement(recipient.getId()));
access.add(recipients.getAccess(recipient.getId()));
} else {
access.add(recipients.requireAccess(recipient.getId()));
}
}
if (useGroupSendEndorsements) {
groupSendEndorsements = new GroupSendEndorsements(
groupSendEndorsementExpiration,
senderKeyEndorsements,
senderCertificate,
GroupSecretParams.deriveFromMasterKey(groupRecord.get().requireV2GroupProperties().getGroupMasterKey())
);
}
final MessageSendLogTables messageLogDatabase = SignalDatabase.messageLog();
final AtomicLong entryId = new AtomicLong(-1);
final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog();
List<SendMessageResult> results = sendOperation.sendWithSenderKey(messageSender, distributionId, targets, access, isRecipientUpdate, partialResults -> {
List<SendMessageResult> results = sendOperation.sendWithSenderKey(messageSender, distributionId, targets, access, groupSendEndorsements, isRecipientUpdate, partialResults -> {
if (!includeInMessageLog) {
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<SignalServiceAddress> targets = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList());
List<Optional<UnidentifiedAccessPair>> access = legacyTargets.stream().map(r -> recipients.getAccessPair(r.getId())).collect(Collectors.toList());
boolean recipientUpdate = isRecipientUpdate || allResults.size() > 0;
List<SignalServiceAddress> legacyTargetAddresses = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList());
List<UnidentifiedAccess> legacyTargetAccesses = legacyTargets.stream().map(r -> recipients.getAccess(r.getId())).collect(Collectors.toList());
List<GroupSendFullToken> groupSendTokens = null;
boolean recipientUpdate = isRecipientUpdate || allResults.isEmpty();
if (useGroupSendEndorsements) {
Instant expiration = Instant.ofEpochMilli(groupSendEndorsementExpiration);
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupRecord.get().requireV2GroupProperties().getGroupMasterKey());
groupSendTokens = new ArrayList<>(legacyTargetAddresses.size());
for (Recipient r : legacyTargets) {
GroupSendEndorsement endorsement = groupSendEndorsementRecords.getEndorsement(r.getId());
if (r.getHasAci() && endorsement != null) {
groupSendTokens.add(endorsement.toFullToken(groupSecretParams, expiration));
} else {
groupSendTokens.add(null);
}
}
}
final MessageSendLogTables messageLogDatabase = SignalDatabase.messageLog();
final AtomicLong entryId = new AtomicLong(-1);
final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog();
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, targets, legacyTargets, access, recipientUpdate, result -> {
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, legacyTargetAddresses, legacyTargets, SealedSenderAccess.forFanOutGroupSend(groupSendTokens, SealedSenderAccessUtil.getSealedSenderCertificate(), legacyTargetAccesses), recipientUpdate, result -> {
if (!includeInMessageLog) {
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<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> 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<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
@NonNull List<SealedSenderAccess> sealedSenderAccesses,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal)
@ -504,19 +613,20 @@ public final class GroupSendUtil {
@NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> 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<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
@NonNull List<SealedSenderAccess> 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<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> 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<SendMessageResult> 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<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
@NonNull List<SealedSenderAccess> 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<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> 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<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
@NonNull List<SealedSenderAccess> 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<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> 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<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
@NonNull List<SealedSenderAccess> 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<RecipientId, Optional<UnidentifiedAccessPair>> accessById;
private final Map<RecipientId, Optional<UnidentifiedAccess>> accessById;
private final Map<RecipientId, SignalServiceAddress> addressById;
private final RecipientAccessList accessList;
RecipientData(@NonNull Context context, @NonNull List<Recipient> recipients, boolean isForStory) throws IOException {
this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients, isForStory);
this.accessById = SealedSenderAccessUtil.getAccessMapFor(recipients, isForStory);
this.addressById = mapAddresses(context, recipients);
this.accessList = new RecipientAccessList(recipients);
}
@ -853,22 +968,22 @@ public final class GroupSendUtil {
return Objects.requireNonNull(addressById.get(id));
}
@NonNull Optional<UnidentifiedAccessPair> getAccessPair(@NonNull RecipientId id) {
@NonNull Optional<UnidentifiedAccess> getAccessPair(@NonNull RecipientId id) {
return Objects.requireNonNull(accessById.get(id));
}
@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<RecipientId> requireRecipientIds(@NonNull List<SignalServiceAddress> addresses) {
return addresses.stream().map(accessList::requireIdByAddress).collect(Collectors.toList());
}
private static @NonNull Map<RecipientId, SignalServiceAddress> mapAddresses(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException {
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);

View file

@ -32,7 +32,7 @@ import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.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()

View file

@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting
import org.thoughtcrime.securesms.database.RecipientTable.MissingRecipientException
import org.thoughtcrime.securesms.database.RecipientTable.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 &&

View file

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

View file

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

View file

@ -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<ProfileAndCredential> 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> unidentifiedAccess = allowUnidentifiedAccess ? getUnidentifiedAccess(context, recipient) : Optional.empty();
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
ProfileService profileService = AppDependencies.getProfileService();
SealedSenderAccess sealedSenderAccess = allowUnidentifiedAccess ? SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, false) : SealedSenderAccess.NONE;
Optional<ProfileKey> 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<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) {
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient, false);
if (unidentifiedAccess.isPresent()) {
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
}
return Optional.empty();
}
private static @NonNull SignalServiceAddress toSignalServiceAddress(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
if (recipient.getRegistered() == RecipientTable.RegisteredState.NOT_REGISTERED) {
if (recipient.getHasServiceId()) {

View file

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

View file

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

View file

@ -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<GroupId.V2>(), any()) } returns Unit
every { groupTable.update(any<GroupId.V2>(), any(), any()) } returns Unit
every { sendGroupUpdateHelper.sendGroupUpdate(masterKey, any(), any(), any()) } returns GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1)
every { 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<DecryptedGroup>()
verify { groupTable.update(groupId, capture(decryptedGroupArg)) }
verify { groupTable.update(groupId, capture(decryptedGroupArg), any()) }
then(decryptedGroupArg.captured)
}

View file

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

View file

@ -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<GroupMasterKey>(), any<DecryptedGroup>()) }
justRun { groupTable.update(any<GroupMasterKey>(), any<DecryptedGroup>(), any<ReceivedGroupSendEndorsements>()) }
}
if (data.expectTableCreate) {
every { groupTable.create(any<GroupMasterKey>(), any<DecryptedGroup>()) } returns groupId
every { groupTable.create(any<GroupMasterKey>(), any<DecryptedGroup>(), any<ReceivedGroupSendEndorsements>()) } returns groupId
}
if (data.expectTableUpdate || data.expectTableCreate) {
@ -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) }
}
}

View file

@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api;
import com.squareup.wire.FieldEncoding;
import org.signal.core.util.Base64;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException;
@ -26,6 +27,7 @@ import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.archive.ArchiveApi;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
@ -88,7 +90,6 @@ import org.whispersystems.signalservice.internal.storage.protos.WriteOperation;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper;
import org.signal.core.util.Base64;
import java.io.IOException;
import java.security.MessageDigest;
@ -757,7 +758,7 @@ public class SignalServiceAccountManager {
throws NonSuccessfulResponseCodeException, PushNetworkException
{
try {
ProfileAndCredential credential = this.pushServiceSocket.retrieveVersionedProfileAndCredential(serviceId, profileKey, Optional.empty(), locale).get(10, TimeUnit.SECONDS);
ProfileAndCredential credential = this.pushServiceSocket.retrieveVersionedProfileAndCredential(serviceId, profileKey, SealedSenderAccess.NONE, locale).get(10, TimeUnit.SECONDS);
return credential.getExpiringProfileKeyCredential();
} catch (InterruptedException | TimeoutException e) {
throw new PushNetworkException(e);

View file

@ -17,7 +17,7 @@ import org.whispersystems.signalservice.api.backup.BackupKey;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@ -98,7 +98,7 @@ public class SignalServiceMessageReceiver {
public ListenableFuture<ProfileAndCredential> retrieveProfile(SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
@Nullable SealedSenderAccess sealedSenderAccess,
SignalServiceProfile.RequestType requestType,
Locale locale)
{
@ -115,16 +115,16 @@ public class SignalServiceMessageReceiver {
}
if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) {
return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), unidentifiedAccess, locale);
return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), sealedSenderAccess, locale);
} else {
return FutureTransformers.map(socket.retrieveVersionedProfile(aci, profileKey.get(), unidentifiedAccess, locale), profile -> {
return FutureTransformers.map(socket.retrieveVersionedProfile(aci, profileKey.get(), sealedSenderAccess, locale), profile -> {
return new ProfileAndCredential(profile,
SignalServiceProfile.RequestType.PROFILE,
Optional.empty());
});
}
} else {
return FutureTransformers.map(socket.retrieveProfile(address, unidentifiedAccess, locale), profile -> {
return FutureTransformers.map(socket.retrieveProfile(address, sealedSenderAccess, locale), profile -> {
return new ProfileAndCredential(profile,
SignalServiceProfile.RequestType.PROFILE,
Optional.empty());
@ -146,8 +146,8 @@ public class SignalServiceMessageReceiver {
return new FileInputStream(destination);
}
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull Optional<UnidentifiedAccess> unidentifiedAccess, @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
return socket.performIdentityCheck(request, unidentifiedAccess, responseMapper);
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
return socket.performIdentityCheck(request, responseMapper);
}
/**

View file

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

View file

@ -1,7 +1,7 @@
package org.whispersystems.signalservice.api;
import org.signal.libsignal.protocol.logging.Log;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.messages.EnvelopeResponse;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
@ -11,7 +11,6 @@ import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage;
import org.whispersystems.signalservice.internal.websocket.WebSocketResponseMessage;
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse;
import org.signal.core.util.Base64;
import java.io.IOException;
import java.util.ArrayList;
@ -19,6 +18,8 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@ -198,10 +199,11 @@ public final class SignalWebSocket {
}
}
public Single<WebsocketResponse> request(WebSocketRequestMessage requestMessage, Optional<UnidentifiedAccess> unidentifiedAccess) {
if (unidentifiedAccess.isPresent()) {
public Single<WebsocketResponse> request(WebSocketRequestMessage requestMessage, @Nullable SealedSenderAccess sealedSenderAccess) {
if (sealedSenderAccess != null) {
List<String> headers = new ArrayList<>(requestMessage.headers);
headers.add("Unidentified-Access-Key:" + Base64.encodeWithPadding(unidentifiedAccess.get().getUnidentifiedAccessKey()));
headers.add(sealedSenderAccess.getHeader());
WebSocketRequestMessage message = requestMessage.newBuilder()
.headers(headers)
.build();
@ -209,7 +211,7 @@ public final class SignalWebSocket {
return getUnidentifiedWebSocket().sendRequest(message)
.flatMap(r -> {
if (r.getStatus() == 401) {
return request(requestMessage);
return request(requestMessage, sealedSenderAccess.switchToFallback());
}
return Single.just(r);
});

View file

@ -0,0 +1,198 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.crypto
import org.signal.core.util.Base64
import org.signal.libsignal.metadata.certificate.SenderCertificate
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken
import org.whispersystems.signalservice.api.groupsv2.GroupSendEndorsements
import org.whispersystems.util.ByteArrayUtil
/**
* Provides single interface for the various ways to send via sealed sender.
*/
sealed class SealedSenderAccess {
abstract val senderCertificate: SenderCertificate
abstract val headerName: String
abstract val headerValue: String
val header: String
get() = "$headerName:$headerValue"
abstract fun switchToFallback(): SealedSenderAccess?
/**
* For sending to an single recipient using group send endorsement/token first and then fallback to
* access key if available.
*/
class IndividualGroupSendTokenFirst(
private val groupSendToken: GroupSendFullToken,
override val senderCertificate: SenderCertificate,
val unidentifiedAccess: UnidentifiedAccess? = null
) : SealedSenderAccess() {
override val headerName: String = "Group-Send-Token"
override val headerValue: String by lazy { Base64.encodeWithPadding(groupSendToken.serialize()) }
override fun switchToFallback(): SealedSenderAccess? {
fallbackListener?.onTokenToAccessFallback(unidentifiedAccess != null)
return if (unidentifiedAccess != null) {
IndividualUnidentifiedAccessFirst(unidentifiedAccess)
} else {
null
}
}
}
/**
* For sending to an single recipient using access key first and then fallback to group send
* token if available. The token is created lazily via the provided [createGroupSendToken] function.
*/
class IndividualUnidentifiedAccessFirst(
val unidentifiedAccess: UnidentifiedAccess,
private val createGroupSendToken: CreateGroupSendToken? = null
) : SealedSenderAccess() {
override val senderCertificate: SenderCertificate
get() = unidentifiedAccess.unidentifiedCertificate
override val headerName: String = "Unidentified-Access-Key"
override val headerValue: String by lazy { Base64.encodeWithPadding(unidentifiedAccess.unidentifiedAccessKey) }
override fun switchToFallback(): SealedSenderAccess? {
val groupSendToken = createGroupSendToken?.create()
return if (groupSendToken != null) {
fallbackListener?.onAccessToTokenFallback()
IndividualGroupSendTokenFirst(groupSendToken, senderCertificate)
} else {
null
}
}
}
/**
* For sending to a "group" of recipients using group send endorsements/tokens.
*/
class GroupGroupSendToken(
private val groupSendEndorsements: GroupSendEndorsements
) : SealedSenderAccess() {
override val headerName: String = "Group-Send-Token"
override val headerValue: String by lazy { Base64.encodeWithPadding(groupSendEndorsements.serialize()) }
override val senderCertificate: SenderCertificate
get() = groupSendEndorsements.sealedSenderCertificate
override fun switchToFallback(): SealedSenderAccess? {
return null
}
}
/**
* For sending to a "group" of recipients using access keys.
*/
class GroupUnidentifiedAccess(
private val unidentifiedAccess: List<UnidentifiedAccess>,
override val senderCertificate: SenderCertificate = unidentifiedAccess.first().unidentifiedCertificate
) : SealedSenderAccess() {
override val headerName: String = "Unidentified-Access-Key"
override val headerValue: String by lazy {
var joinedUnidentifiedAccess = ByteArray(16)
for (access in unidentifiedAccess) {
joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.unidentifiedAccessKey)
}
Base64.encodeWithPadding(joinedUnidentifiedAccess)
}
override fun switchToFallback(): SealedSenderAccess? {
return null
}
}
/**
* Provide a lazy way to create a group send token.
*/
fun interface CreateGroupSendToken {
fun create(): GroupSendFullToken?
}
interface FallbackListener {
fun onAccessToTokenFallback()
fun onTokenToAccessFallback(hasAccessKeyFallback: Boolean)
}
companion object {
var fallbackListener: FallbackListener? = null
@JvmField
val NONE: SealedSenderAccess? = null
@JvmStatic
fun forIndividualWithGroupFallback(
unidentifiedAccess: UnidentifiedAccess?,
senderCertificate: SenderCertificate?,
createGroupSendToken: CreateGroupSendToken?
): SealedSenderAccess? {
if (unidentifiedAccess != null) {
return IndividualUnidentifiedAccessFirst(unidentifiedAccess, createGroupSendToken)
}
val groupSendToken = createGroupSendToken?.create()
if (groupSendToken != null && senderCertificate != null) {
return IndividualGroupSendTokenFirst(groupSendToken, senderCertificate)
}
return null
}
@JvmStatic
fun forIndividual(unidentifiedAccess: UnidentifiedAccess?): SealedSenderAccess? {
return unidentifiedAccess?.let { IndividualUnidentifiedAccessFirst(it) }
}
@JvmStatic
fun forFanOutGroupSend(groupSendTokens: List<GroupSendFullToken?>?, senderCertificate: SenderCertificate?, unidentifiedAccesses: List<UnidentifiedAccess?>): List<SealedSenderAccess?> {
if (groupSendTokens == null) {
return unidentifiedAccesses.map { a -> forIndividual(a) }
}
require(groupSendTokens.size == unidentifiedAccesses.size)
return groupSendTokens
.zip(unidentifiedAccesses)
.map { (token, unidentifiedAccess) ->
if (unidentifiedAccess != null) {
IndividualUnidentifiedAccessFirst(unidentifiedAccess) { token }
} else if (token != null && senderCertificate != null) {
IndividualGroupSendTokenFirst(token, senderCertificate)
} else {
null
}
}
}
@JvmStatic
fun forGroupSend(groupSendEndorsements: GroupSendEndorsements?, unidentifiedAccess: List<UnidentifiedAccess>, forStory: Boolean): SealedSenderAccess {
return if (groupSendEndorsements != null && !forStory) {
GroupGroupSendToken(groupSendEndorsements)
} else {
GroupUnidentifiedAccess(unidentifiedAccess)
}
}
@JvmStatic
fun isUnrestrictedForStory(sealedSenderAccess: SealedSenderAccess?): Boolean {
return when (sealedSenderAccess) {
is IndividualGroupSendTokenFirst -> sealedSenderAccess.unidentifiedAccess?.isUnrestrictedForStory ?: false
is IndividualUnidentifiedAccessFirst -> sealedSenderAccess.unidentifiedAccess.isUnrestrictedForStory
else -> false
}
}
}
}

View file

@ -60,6 +60,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
/**
* This is used to encrypt + decrypt received envelopes.
*/
@ -110,17 +112,17 @@ public class SignalServiceCipher {
}
public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess,
@Nullable SealedSenderAccess sealedSenderAccess,
EnvelopeContent content)
throws UntrustedIdentityException, InvalidKeyException
{
try {
SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination));
if (unidentifiedAccess.isPresent()) {
if (sealedSenderAccess != null) {
SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getServiceId().getRawUuid(), localAddress.getNumber()
.orElse(null), localDeviceId));
return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, unidentifiedAccess.get().getUnidentifiedCertificate());
return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, sealedSenderAccess.getSenderCertificate());
} else {
return content.processUnsealedSender(sessionCipher, destination);
}

View file

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

View file

@ -1,23 +0,0 @@
package org.whispersystems.signalservice.api.crypto;
import java.util.Optional;
public class UnidentifiedAccessPair {
private final Optional<UnidentifiedAccess> targetUnidentifiedAccess;
private final Optional<UnidentifiedAccess> selfUnidentifiedAccess;
public UnidentifiedAccessPair(UnidentifiedAccess targetUnidentifiedAccess, UnidentifiedAccess selfUnidentifiedAccess) {
this.targetUnidentifiedAccess = Optional.of(targetUnidentifiedAccess);
this.selfUnidentifiedAccess = Optional.of(selfUnidentifiedAccess);
}
public Optional<UnidentifiedAccess> getTargetUnidentifiedAccess() {
return targetUnidentifiedAccess;
}
public Optional<UnidentifiedAccess> getSelfUnidentifiedAccess() {
return selfUnidentifiedAccess;
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.groupsv2
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
import org.signal.storageservice.protos.groups.local.DecryptedGroup
/**
* Decrypted response from server operations that includes our global group state and
* our specific-to-us group send endorsements.
*/
class DecryptedGroupResponse(
val group: DecryptedGroup,
val groupSendEndorsementsResponse: GroupSendEndorsementsResponse?
)

View file

@ -1,11 +1,12 @@
package org.whispersystems.signalservice.api.groupsv2
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
import org.whispersystems.signalservice.internal.push.PushServiceSocket.GroupHistory
/**
* Wraps result of group history fetch with it's associated paging data.
*/
data class GroupHistoryPage(val changeLogs: List<DecryptedGroupChangeLog>, val pagingData: PagingData) {
data class GroupHistoryPage(val changeLogs: List<DecryptedGroupChangeLog>, val groupSendEndorsementsResponse: GroupSendEndorsementsResponse?, val pagingData: PagingData) {
data class PagingData(val hasMorePages: Boolean, val nextPageRevision: Int) {
companion object {

View file

@ -0,0 +1,38 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.groupsv2
import org.signal.libsignal.metadata.certificate.SenderCertificate
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.time.Instant
/**
* Helper container for all data needed to send with group send endorsements.
*/
data class GroupSendEndorsements(
val expirationMs: Long,
val endorsements: Map<ServiceId.ACI, GroupSendEndorsement>,
val sealedSenderCertificate: SenderCertificate,
val groupSecretParams: GroupSecretParams
) {
private val expiration: Instant by lazy { Instant.ofEpochMilli(expirationMs) }
private val combinedEndorsement: GroupSendEndorsement by lazy { GroupSendEndorsement.combine(endorsements.values) }
fun serialize(): ByteArray {
return combinedEndorsement.toFullToken(groupSecretParams, expiration).serialize()
}
fun forIndividuals(addresses: List<SignalServiceAddress>): List<GroupSendFullToken?> {
return addresses
.map { a -> endorsements[a.serviceId] }
.map { e -> e?.toFullToken(groupSecretParams, expiration) }
}
}

View file

@ -9,13 +9,16 @@ import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChangeResponse;
import org.signal.storageservice.protos.groups.GroupChanges;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.storageservice.protos.groups.GroupResponse;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
@ -31,6 +34,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nonnull;
@ -73,9 +77,9 @@ public class GroupsV2Api {
return new GroupsV2AuthorizationString(groupSecretParams, authCredentialPresentation);
}
public void putNewGroup(GroupsV2Operations.NewGroup newGroup,
GroupsV2AuthorizationString authorization)
throws IOException
public DecryptedGroupResponse putNewGroup(GroupsV2Operations.NewGroup newGroup,
GroupsV2AuthorizationString authorization)
throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException
{
Group group = newGroup.getNewGroupMessage();
@ -83,34 +87,38 @@ public class GroupsV2Api {
String cdnKey = uploadAvatar(newGroup.getAvatar().get(), newGroup.getGroupSecretParams(), authorization);
group = group.newBuilder()
.avatar(cdnKey)
.build();
.avatar(cdnKey)
.build();
}
socket.putNewGroupsV2Group(group, authorization);
GroupResponse response = socket.putNewGroupsV2Group(group, authorization);
return groupsOperations.forGroup(newGroup.getGroupSecretParams())
.decryptGroup(Objects.requireNonNull(response.group), response.groupSendEndorsementsResponse.toByteArray());
}
public NetworkResult<DecryptedGroup> getGroupAsResult(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) {
public NetworkResult<DecryptedGroupResponse> getGroupAsResult(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) {
return NetworkResult.fromFetch(() -> getGroup(groupSecretParams, authorization));
}
public DecryptedGroup getGroup(GroupSecretParams groupSecretParams,
GroupsV2AuthorizationString authorization)
throws IOException, InvalidGroupStateException, VerificationFailedException
public DecryptedGroupResponse getGroup(GroupSecretParams groupSecretParams,
GroupsV2AuthorizationString authorization)
throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException
{
Group group = socket.getGroupsV2Group(authorization);
GroupResponse response = socket.getGroupsV2Group(authorization);
return groupsOperations.forGroup(groupSecretParams)
.decryptGroup(group);
.decryptGroup(Objects.requireNonNull(response.group), response.groupSendEndorsementsResponse.toByteArray());
}
public GroupHistoryPage getGroupHistoryPage(GroupSecretParams groupSecretParams,
int fromRevision,
GroupsV2AuthorizationString authorization,
boolean includeFirstState)
throws IOException, InvalidGroupStateException, VerificationFailedException
boolean includeFirstState,
long sendEndorsementsExpirationMs)
throws IOException, InvalidGroupStateException, VerificationFailedException, InvalidInputException
{
PushServiceSocket.GroupHistory group = socket.getGroupsV2GroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState);
PushServiceSocket.GroupHistory group = socket.getGroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState, sendEndorsementsExpirationMs);
List<DecryptedGroupChangeLog> result = new ArrayList<>(group.getGroupChanges().groupChanges.size());
GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);
@ -121,7 +129,10 @@ public class GroupsV2Api {
result.add(new DecryptedGroupChangeLog(decryptedGroup, decryptedChange));
}
return new GroupHistoryPage(result, GroupHistoryPage.PagingData.forGroupHistory(group));
byte[] groupSendEndorsementsResponseBytes = group.getGroupChanges().groupSendEndorsementsResponse.toByteArray();
GroupSendEndorsementsResponse groupSendEndorsementsResponse = groupSendEndorsementsResponseBytes.length > 0 ? new GroupSendEndorsementsResponse(groupSendEndorsementsResponseBytes) : null;
return new GroupHistoryPage(result, groupSendEndorsementsResponse, GroupHistoryPage.PagingData.forGroupHistory(group));
}
public NetworkResult<Integer> getGroupJoinedAt(@Nonnull GroupsV2AuthorizationString authorization) {
@ -162,9 +173,9 @@ public class GroupsV2Api {
return form.key;
}
public GroupChange patchGroup(GroupChange.Actions groupChange,
GroupsV2AuthorizationString authorization,
Optional<byte[]> groupLinkPassword)
public GroupChangeResponse patchGroup(GroupChange.Actions groupChange,
GroupsV2AuthorizationString authorization,
Optional<byte[]> groupLinkPassword)
throws IOException
{
return socket.patchGroupsV2Group(groupChange, authorization.toString(), groupLinkPassword);

View file

@ -10,6 +10,7 @@ import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.ProfileKeyCiphertext;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
@ -53,6 +54,9 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import okio.ByteString;
/**
@ -154,7 +158,7 @@ public final class GroupsV2Operations {
private final GroupSecretParams groupSecretParams;
private final ClientZkGroupCipher clientZkGroupCipher;
private GroupOperations(GroupSecretParams groupSecretParams) {
public GroupOperations(GroupSecretParams groupSecretParams) {
this.groupSecretParams = groupSecretParams;
this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
}
@ -425,6 +429,15 @@ public final class GroupsV2Operations {
return new PendingMember.Builder().member(member);
}
public @Nonnull DecryptedGroupResponse decryptGroup(@Nonnull Group group, @Nonnull byte[] groupSendEndorsementsBytes)
throws VerificationFailedException, InvalidGroupStateException, InvalidInputException
{
DecryptedGroup decryptedGroup = decryptGroup(group);
GroupSendEndorsementsResponse groupSendEndorsementsResponse = groupSendEndorsementsBytes.length > 0 ? new GroupSendEndorsementsResponse(groupSendEndorsementsBytes) : null;
return new DecryptedGroupResponse(decryptedGroup, groupSendEndorsementsResponse);
}
public DecryptedGroup decryptGroup(Group group)
throws VerificationFailedException, InvalidGroupStateException
{
@ -1019,6 +1032,46 @@ public final class GroupsV2Operations {
return ids;
}
public @Nullable ReceivedGroupSendEndorsements receiveGroupSendEndorsements(@Nonnull ACI selfAci,
@Nonnull DecryptedGroup decryptedGroup,
@Nullable ByteString groupSendEndorsementsResponse)
{
if (groupSendEndorsementsResponse != null && groupSendEndorsementsResponse.size() > 0) {
try {
return receiveGroupSendEndorsements(selfAci, decryptedGroup, new GroupSendEndorsementsResponse(groupSendEndorsementsResponse.toByteArray()));
} catch (InvalidInputException e) {
Log.w(TAG, "Unable to parse send endorsements response", e);
}
}
return null;
}
public @Nullable ReceivedGroupSendEndorsements receiveGroupSendEndorsements(@Nonnull ACI selfAci,
@Nonnull DecryptedGroup decryptedGroup,
@Nullable GroupSendEndorsementsResponse groupSendEndorsementsResponse)
{
if (groupSendEndorsementsResponse == null) {
return null;
}
List<ACI> members = decryptedGroup.members.stream().map(m -> ACI.parseOrThrow(m.aciBytes)).collect(Collectors.toList());
GroupSendEndorsementsResponse.ReceivedEndorsements endorsements = null;
try {
endorsements = groupSendEndorsementsResponse.receive(
members.stream().map(ACI::getLibSignalAci).collect(Collectors.toList()),
selfAci.getLibSignalAci(),
groupSecretParams,
serverPublicParams
);
} catch (VerificationFailedException e) {
Log.w(TAG, "Unable to receive send endorsements for group", e);
}
return endorsements != null ? new ReceivedGroupSendEndorsements(groupSendEndorsementsResponse.getExpiration(), members, endorsements)
: null;
}
}
public static class NewGroup {

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.groupsv2
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
import org.whispersystems.signalservice.api.push.ServiceId
import java.time.Instant
/**
* Group send endorsement data received from the server.
*/
data class ReceivedGroupSendEndorsements(
val expirationMs: Long,
val endorsements: Map<ServiceId.ACI, GroupSendEndorsement>
) {
constructor(
expiration: Instant,
members: List<ServiceId.ACI>,
receivedEndorsements: GroupSendEndorsementsResponse.ReceivedEndorsements
) : this(
expirationMs = expiration.toEpochMilli(),
endorsements = members.zip(receivedEndorsements.endorsements).toMap()
)
}

View file

@ -1,7 +1,7 @@
package org.whispersystems.signalservice.api.services;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.internal.ServiceResponse;
@ -19,13 +19,14 @@ import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper;
import org.whispersystems.signalservice.internal.websocket.ResponseMapper;
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage;
import org.signal.core.util.Base64;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import io.reactivex.rxjava3.core.Single;
import okio.ByteString;
@ -42,7 +43,9 @@ public class MessagingService {
this.signalWebSocket = signalWebSocket;
}
public Single<ServiceResponse<SendMessageResponse>> send(OutgoingPushMessageList list, Optional<UnidentifiedAccess> unidentifiedAccess, boolean story) {
public Single<ServiceResponse<SendMessageResponse>> send(OutgoingPushMessageList list,
@Nullable SealedSenderAccess sealedSenderAccess,
boolean story) {
List<String> headers = new LinkedList<String>() {{
add("content-type:application/json");
}};
@ -66,15 +69,15 @@ public class MessagingService {
.withCustomError(404, (status, body, getHeader) -> new UnregisteredUserException(list.getDestination(), new NotFoundException("not found")))
.build();
return signalWebSocket.request(requestMessage, unidentifiedAccess)
return signalWebSocket.request(requestMessage, sealedSenderAccess)
.map(responseMapper::map)
.onErrorReturn(ServiceResponse::forUnknownError);
}
public Single<ServiceResponse<SendGroupMessageResponse>> sendToGroup(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent, boolean story) {
public Single<ServiceResponse<SendGroupMessageResponse>> sendToGroup(byte[] body, @Nonnull SealedSenderAccess sealedSenderAccess, long timestamp, boolean online, boolean urgent, boolean story) {
List<String> headers = new LinkedList<String>() {{
add("content-type:application/vnd.signal-messenger.mrm");
add("Unidentified-Access-Key:" + Base64.encodeWithPadding(joinedUnidentifiedAccess));
add(sealedSenderAccess.getHeader());
}};
String path = String.format(Locale.US, "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s&story=%s", timestamp, online, urgent, story);

View file

@ -12,6 +12,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyVersion;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
@ -44,6 +45,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Single;
@ -72,7 +74,7 @@ public final class ProfileService {
public Single<ServiceResponse<ProfileAndCredential>> getProfile(@Nonnull SignalServiceAddress address,
@Nonnull Optional<ProfileKey> profileKey,
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
@Nullable SealedSenderAccess sealedSenderAccess,
@Nonnull SignalServiceProfile.RequestType requestType,
@Nonnull Locale locale)
{
@ -116,9 +118,9 @@ public final class ProfileService {
.withResponseMapper(new ProfileResponseMapper(requestType, requestContext))
.build();
return signalWebSocket.request(requestMessage, unidentifiedAccess)
return signalWebSocket.request(requestMessage, sealedSenderAccess)
.map(responseMapper::map)
.onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, unidentifiedAccess, requestType, locale))
.onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, sealedSenderAccess, requestType, locale))
.onErrorReturn(ServiceResponse::forUnknownError);
}
@ -139,19 +141,19 @@ public final class ProfileService {
ResponseMapper<IdentityCheckResponse> responseMapper = DefaultResponseMapper.getDefault(IdentityCheckResponse.class);
return signalWebSocket.request(builder.build(), Optional.empty())
return signalWebSocket.request(builder.build(), SealedSenderAccess.NONE)
.map(responseMapper::map)
.onErrorResumeNext(t -> performIdentityCheckRestFallback(request, Optional.empty(), responseMapper))
.onErrorResumeNext(t -> performIdentityCheckRestFallback(request, responseMapper))
.onErrorReturn(ServiceResponse::forUnknownError);
}
private Single<ServiceResponse<ProfileAndCredential>> getProfileRestFallback(@Nonnull SignalServiceAddress address,
@Nonnull Optional<ProfileKey> profileKey,
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
@Nullable SealedSenderAccess sealedSenderAccess,
@Nonnull SignalServiceProfile.RequestType requestType,
@Nonnull Locale locale)
{
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType, locale), 10, TimeUnit.SECONDS)
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, sealedSenderAccess, requestType, locale), 10, TimeUnit.SECONDS)
.onErrorResumeNext(t -> {
Throwable error;
if (t instanceof ExecutionException && t.getCause() != null) {
@ -161,7 +163,7 @@ public final class ProfileService {
}
if (error instanceof AuthorizationFailedException) {
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, Optional.empty(), requestType, locale), 10, TimeUnit.SECONDS);
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, null, requestType, locale), 10, TimeUnit.SECONDS);
} else {
return Single.error(t);
}
@ -170,9 +172,8 @@ public final class ProfileService {
}
private @NonNull Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheckRestFallback(@Nonnull IdentityCheckRequest request,
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
@Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
return receiver.performIdentityCheck(request, unidentifiedAccess, responseMapper)
return receiver.performIdentityCheck(request, responseMapper)
.onErrorResumeNext(t -> {
Throwable error;
if (t instanceof ExecutionException && t.getCause() != null) {
@ -182,7 +183,7 @@ public final class ProfileService {
}
if (error instanceof AuthorizationFailedException) {
return receiver.performIdentityCheck(request, Optional.empty(), responseMapper);
return receiver.performIdentityCheck(request, responseMapper);
} else {
return Single.error(t);
}

View file

@ -39,9 +39,11 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChangeResponse;
import org.signal.storageservice.protos.groups.GroupChanges;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.storageservice.protos.groups.GroupResponse;
import org.signal.storageservice.protos.groups.Member;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
@ -60,7 +62,7 @@ import org.whispersystems.signalservice.api.archive.BatchArchiveMediaRequest;
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse;
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest;
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
@ -273,13 +275,13 @@ public class PushServiceSocket {
private static final String STICKER_PATH = "stickers/%s/full/%d";
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&zkcCredential=true";
private static final String GROUPSV2_GROUP = "/v1/groups/";
private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s";
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false";
private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form";
private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s";
private static final String GROUPSV2_TOKEN = "/v1/groups/token";
private static final String GROUPSV2_JOINED_AT = "/v1/groups/joined_at_version";
private static final String GROUPSV2_GROUP = "/v2/groups/";
private static final String GROUPSV2_GROUP_PASSWORD = "/v2/groups/?inviteLinkPassword=%s";
private static final String GROUPSV2_GROUP_CHANGES = "/v2/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false";
private static final String GROUPSV2_AVATAR_REQUEST = "/v2/groups/avatar/form";
private static final String GROUPSV2_GROUP_JOIN = "/v2/groups/join/%s";
private static final String GROUPSV2_TOKEN = "/v2/groups/token";
private static final String GROUPSV2_JOINED_AT = "/v2/groups/joined_at_version";
private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions";
@ -376,7 +378,7 @@ public class PushServiceSocket {
public RegistrationSessionMetadataResponse createVerificationSession(@Nullable String pushToken, @Nullable String mcc, @Nullable String mnc) throws IOException {
final String jsonBody = JsonUtil.toJson(new VerificationSessionMetadataRequestBody(credentialsProvider.getE164(), pushToken, mcc, mnc));
try (Response response = makeServiceRequest(VERIFICATION_SESSION_PATH, "POST", jsonRequestBody(jsonBody), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty(), false)) {
try (Response response = makeServiceRequest(VERIFICATION_SESSION_PATH, "POST", jsonRequestBody(jsonBody), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) {
return parseSessionMetadataResponse(response);
}
}
@ -384,7 +386,7 @@ public class PushServiceSocket {
public RegistrationSessionMetadataResponse getSessionStatus(String sessionId) throws IOException {
String path = VERIFICATION_SESSION_PATH + "/" + sessionId;
try (Response response = makeServiceRequest(path, "GET", jsonRequestBody(null), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty(), false)) {
try (Response response = makeServiceRequest(path, "GET", jsonRequestBody(null), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) {
return parseSessionMetadataResponse(response);
}
}
@ -393,7 +395,7 @@ public class PushServiceSocket {
String path = VERIFICATION_SESSION_PATH + "/" + sessionId;
final UpdateVerificationSessionRequestBody requestBody = new UpdateVerificationSessionRequestBody(captchaToken, pushToken, pushChallengeToken, mcc, mnc);
try (Response response = makeServiceRequest(path, "PATCH", jsonRequestBody(JsonUtil.toJson(requestBody)), NO_HEADERS, new PatchRegistrationSessionResponseHandler(), Optional.empty(), false)) {
try (Response response = makeServiceRequest(path, "PATCH", jsonRequestBody(JsonUtil.toJson(requestBody)), NO_HEADERS, new PatchRegistrationSessionResponseHandler(), SealedSenderAccess.NONE, false)) {
return parseSessionMetadataResponse(response);
}
}
@ -414,7 +416,7 @@ public class PushServiceSocket {
body.put("client", androidSmsRetriever ? "android-2021-03" : "android");
try (Response response = makeServiceRequest(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, new RegistrationCodeRequestResponseHandler(), Optional.empty(), false)) {
try (Response response = makeServiceRequest(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, new RegistrationCodeRequestResponseHandler(), SealedSenderAccess.NONE, false)) {
return parseSessionMetadataResponse(response);
}
}
@ -423,7 +425,7 @@ public class PushServiceSocket {
String path = String.format(VERIFICATION_CODE_PATH, sessionId);
Map<String, String> body = new HashMap<>();
body.put("code", verificationCode);
try (Response response = makeServiceRequest(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, new RegistrationCodeSubmissionResponseHandler(), Optional.empty(), false)) {
try (Response response = makeServiceRequest(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, new RegistrationCodeSubmissionResponseHandler(), SealedSenderAccess.NONE, false)) {
return parseSessionMetadataResponse(response);
}
}
@ -470,7 +472,7 @@ public class PushServiceSocket {
skipDeviceTransfer,
true);
String response = makeServiceRequest(path, "POST", JsonUtil.toJson(body), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty());
String response = makeServiceRequest(path, "POST", JsonUtil.toJson(body), NO_HEADERS, new RegistrationSessionResponseHandler(), SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, VerifyAccountResponse.class);
}
@ -512,14 +514,14 @@ public class PushServiceSocket {
long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime));
long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7);
String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null, NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class);
}
public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException {
String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(request));
makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
}
public void setArchivePublicKey(ECPublicKey publicKey, ArchiveCredentialPresentation credentialPresentation) throws IOException {
@ -706,7 +708,7 @@ public class PushServiceSocket {
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
}
public SendGroupMessageResponse sendGroupMessage(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online, boolean urgent, boolean story)
public SendGroupMessageResponse sendGroupMessage(byte[] body, @Nonnull SealedSenderAccess sealedSenderAccess, long timestamp, boolean online, boolean urgent, boolean story)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random);
@ -716,7 +718,7 @@ public class PushServiceSocket {
Request.Builder requestBuilder = new Request.Builder();
requestBuilder.url(String.format("%s%s", connectionHolder.getUrl(), path));
requestBuilder.put(RequestBody.create(MediaType.get("application/vnd.signal-messenger.mrm"), body));
requestBuilder.addHeader("Unidentified-Access-Key", Base64.encodeWithPadding(joinedUnidentifiedAccess));
requestBuilder.addHeader(sealedSenderAccess.getHeaderName(), sealedSenderAccess.getHeaderValue());
if (signalAgent != null) {
requestBuilder.addHeader("X-Signal-Agent", signalAgent);
@ -762,14 +764,14 @@ public class PushServiceSocket {
}
}
public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional<UnidentifiedAccess> unidentifiedAccess, boolean story)
public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, @Nullable SealedSenderAccess sealedSenderAccess, boolean story)
throws IOException
{
try {
String responseText = makeServiceRequest(String.format("/v1/messages/%s?story=%s", bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, NO_HANDLER, unidentifiedAccess);
String responseText = makeServiceRequest(String.format("/v1/messages/%s?story=%s", bundle.getDestination(), story ? "true" : "false"), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, NO_HANDLER, sealedSenderAccess);
SendMessageResponse response = JsonUtil.fromJson(responseText, SendMessageResponse.class);
response.setSentUnidentfied(unidentifiedAccess.isPresent());
response.setSentUnidentfied(sealedSenderAccess != null);
return response;
} catch (NotFoundException nfe) {
@ -780,7 +782,7 @@ public class PushServiceSocket {
public SignalServiceMessagesResult getMessages(boolean allowStories) throws IOException {
Map<String, String> headers = Collections.singletonMap("X-Signal-Receive-Stories", allowStories ? "true" : "false");
try (Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, headers, NO_HANDLER, Optional.empty(), false)) {
try (Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, headers, NO_HANDLER, SealedSenderAccess.NONE, false)) {
validateServiceResponse(response);
List<SignalServiceEnvelopeEntity> envelopes = readBodyJson(response.body(), SignalServiceEnvelopeEntityList.class).getMessages();
@ -870,18 +872,18 @@ public class PushServiceSocket {
* for all devices. If it is not a primary, it will only contain the prekeys for that specific device.
*/
public List<PreKeyBundle> getPreKeys(SignalServiceAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess,
@Nullable SealedSenderAccess sealedSenderAccess,
int deviceId)
throws IOException
{
return getPreKeysBySpecifier(destination, unidentifiedAccess, deviceId == 1 ? "*" : String.valueOf(deviceId));
return getPreKeysBySpecifier(destination, sealedSenderAccess, deviceId == 1 ? "*" : String.valueOf(deviceId));
}
/**
* Retrieves a prekey for a specific device.
*/
public PreKeyBundle getPreKey(SignalServiceAddress destination, int deviceId) throws IOException {
List<PreKeyBundle> bundles = getPreKeysBySpecifier(destination, Optional.empty(), String.valueOf(deviceId));
List<PreKeyBundle> bundles = getPreKeysBySpecifier(destination, null, String.valueOf(deviceId));
if (bundles.size() > 0) {
return bundles.get(0);
@ -891,7 +893,7 @@ public class PushServiceSocket {
}
private List<PreKeyBundle> getPreKeysBySpecifier(SignalServiceAddress destination,
Optional<UnidentifiedAccess> unidentifiedAccess,
@Nullable SealedSenderAccess sealedSenderAccess,
String deviceSpecifier)
throws IOException
{
@ -900,7 +902,7 @@ public class PushServiceSocket {
Log.d(TAG, "Fetching prekeys for " + destination.getIdentifier() + "." + deviceSpecifier + ", i.e. GET " + path);
String responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, NO_HANDLER, unidentifiedAccess);
String responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, NO_HANDLER, sealedSenderAccess);
PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class);
List<PreKeyBundle> bundles = new LinkedList<>();
@ -958,7 +960,7 @@ public class PushServiceSocket {
if (responseCode == 409) {
throw new NonSuccessfulResponseCodeException(409);
}
}, Optional.empty());
}, null);
}
public void retrieveBackup(int cdnNumber, Map<String, String> headers, String cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
@ -1012,8 +1014,8 @@ public class PushServiceSocket {
return output.toByteArray();
}
public ListenableFuture<SignalServiceProfile> retrieveProfile(SignalServiceAddress target, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) {
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, target.getIdentifier()), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess);
public ListenableFuture<SignalServiceProfile> retrieveProfile(SignalServiceAddress target, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, target.getIdentifier()), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
return FutureTransformers.map(response, body -> {
try {
@ -1025,7 +1027,7 @@ public class PushServiceSocket {
});
}
public ListenableFuture<ProfileAndCredential> retrieveVersionedProfileAndCredential(ACI target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) {
public ListenableFuture<ProfileAndCredential> retrieveVersionedProfileAndCredential(ACI target, ProfileKey profileKey, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci());
ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target.getLibSignalAci(), profileKey);
ProfileKeyCredentialRequest request = requestContext.getRequest();
@ -1035,7 +1037,7 @@ public class PushServiceSocket {
String subPath = String.format("%s/%s/%s?credentialType=expiringProfileKey", target, version, credentialRequest);
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess);
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
return FutureTransformers.map(response, body -> formatProfileAndCredentialBody(requestContext, body));
}
@ -1061,12 +1063,12 @@ public class PushServiceSocket {
}
}
public ListenableFuture<SignalServiceProfile> retrieveVersionedProfile(ACI target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) {
public ListenableFuture<SignalServiceProfile> retrieveVersionedProfile(ACI target, ProfileKey profileKey, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci());
String version = profileKeyIdentifier.serialize();
String subPath = String.format("%s/%s", target, version);
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess);
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
return FutureTransformers.map(response, body -> {
try {
@ -1102,7 +1104,7 @@ public class PushServiceSocket {
requestBody,
NO_HEADERS,
PaymentsRegionException::responseCodeHandler,
Optional.empty());
SealedSenderAccess.NONE);
if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) {
try {
@ -1126,13 +1128,12 @@ public class PushServiceSocket {
}
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request,
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
@Nonnull ResponseMapper<IdentityCheckResponse> responseMapper)
{
Single<ServiceResponse<IdentityCheckResponse>> requestSingle = Single.fromCallable(() -> {
try (Response response = getServiceConnection(PROFILE_BATCH_CHECK_PATH, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), unidentifiedAccess, false)) {
try (Response response = getServiceConnection(PROFILE_BATCH_CHECK_PATH, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), SealedSenderAccess.NONE, false)) {
String body = response.body() != null ? readBodyString(response.body()): "";
return responseMapper.map(response.code(), body, response::header, unidentifiedAccess.isPresent());
return responseMapper.map(response.code(), body, response::header, false);
}
});
@ -1146,7 +1147,7 @@ public class PushServiceSocket {
@Nonnull ResponseMapper<BackupV2AuthCheckResponse> responseMapper)
{
Single<ServiceResponse<BackupV2AuthCheckResponse>> requestSingle = Single.fromCallable(() -> {
try (Response response = getServiceConnection(BACKUP_AUTH_CHECK_V2, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) {
try (Response response = getServiceConnection(BACKUP_AUTH_CHECK_V2, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), SealedSenderAccess.NONE, false)) {
String body = response.body() != null ? readBodyString(response.body()): "";
return responseMapper.map(response.code(), body, response::header, false);
}
@ -1159,12 +1160,12 @@ public class PushServiceSocket {
}
public BackupV2AuthCheckResponse checkSvr2AuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V2, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V2, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, BackupV2AuthCheckResponse.class);
}
public BackupV3AuthCheckResponse checkSvr3AuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V3, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V3, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, BackupV3AuthCheckResponse.class);
}
@ -1218,7 +1219,7 @@ public class PushServiceSocket {
case 422: throw new UsernameMalformedException();
case 409: throw new UsernameTakenException();
}
}, Optional.empty());
}, SealedSenderAccess.NONE);
return JsonUtil.fromJsonResponse(responseString, ReserveUsernameResponse.class);
}
@ -1249,7 +1250,7 @@ public class PushServiceSocket {
case 410:
throw new UsernameTakenException();
}
}, Optional.empty());
}, SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, ConfirmUsernameResponse.class).getUsernameLinkHandle();
} catch (BaseUsernameException e) {
@ -1303,7 +1304,7 @@ public class PushServiceSocket {
if (responseCode == 428) {
throw new CaptchaRejectedException();
}
}, Optional.empty());
}, SealedSenderAccess.NONE);
}
@ -2139,7 +2140,7 @@ public class PushServiceSocket {
private String makeServiceRequestWithoutAuthentication(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, Optional.empty(), true)) {
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, SealedSenderAccess.NONE, true)) {
return readBodyString(response);
}
}
@ -2147,13 +2148,13 @@ public class PushServiceSocket {
private String makeServiceRequest(String urlFragment, String method, String jsonBody)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
return makeServiceRequest(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER, Optional.empty());
return makeServiceRequest(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER, SealedSenderAccess.NONE);
}
private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler, Optional<UnidentifiedAccess> unidentifiedAccessKey)
private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler, @Nullable SealedSenderAccess sealedSenderAccess)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, unidentifiedAccessKey, false)) {
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, sealedSenderAccess, false)) {
return readBodyString(response);
}
}
@ -2172,10 +2173,10 @@ public class PushServiceSocket {
String method,
String jsonBody,
Map<String, String> headers,
Optional<UnidentifiedAccess> unidentifiedAccessKey)
@Nullable SealedSenderAccess sealedSenderAccess)
{
OkHttpClient okHttpClient = buildOkHttpClient(unidentifiedAccessKey.isPresent());
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, unidentifiedAccessKey, false));
OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null);
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, sealedSenderAccess, false));
synchronized (connections) {
connections.add(call);
@ -2208,13 +2209,13 @@ public class PushServiceSocket {
RequestBody body,
Map<String, String> headers,
ResponseCodeHandler responseCodeHandler,
Optional<UnidentifiedAccess> unidentifiedAccessKey,
@Nullable SealedSenderAccess sealedSenderAccess,
boolean doNotAddAuthenticationOrUnidentifiedAccessKey)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
Response response = null;
try {
response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey, doNotAddAuthenticationOrUnidentifiedAccessKey);
response = getServiceConnection(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey);
responseCodeHandler.handle(response.code(), response.body());
return validateServiceResponse(response);
} catch (Exception e) {
@ -2288,13 +2289,13 @@ public class PushServiceSocket {
String method,
RequestBody body,
Map<String, String> headers,
Optional<UnidentifiedAccess> unidentifiedAccess,
@Nullable SealedSenderAccess sealedSenderAccess,
boolean doNotAddAuthenticationOrUnidentifiedAccessKey)
throws PushNetworkException
{
try {
OkHttpClient okHttpClient = buildOkHttpClient(unidentifiedAccess.isPresent());
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, body, headers, unidentifiedAccess, doNotAddAuthenticationOrUnidentifiedAccessKey));
OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null);
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey));
synchronized (connections) {
connections.add(call);
@ -2327,14 +2328,11 @@ public class PushServiceSocket {
String method,
RequestBody body,
Map<String, String> headers,
Optional<UnidentifiedAccess> unidentifiedAccess,
@Nullable SealedSenderAccess sealedSenderAccess,
boolean doNotAddAuthenticationOrUnidentifiedAccessKey) {
ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random);
// Log.d(TAG, "Push service URL: " + connectionHolder.getUrl());
// Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), urlFragment));
Request.Builder request = new Request.Builder();
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
request.method(method, body);
@ -2344,8 +2342,8 @@ public class PushServiceSocket {
}
if (!headers.containsKey("Authorization") && !doNotAddAuthenticationOrUnidentifiedAccessKey) {
if (unidentifiedAccess.isPresent()) {
request.addHeader("Unidentified-Access-Key", Base64.encodeWithPadding(unidentifiedAccess.get().getUnidentifiedAccessKey()));
if (sealedSenderAccess != null) {
request.addHeader(sealedSenderAccess.getHeaderName(), sealedSenderAccess.getHeaderValue());
} else if (credentialsProvider.getPassword() != null) {
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
}
@ -2364,6 +2362,12 @@ public class PushServiceSocket {
private Response makeStorageRequest(String authorization, String path, String method, RequestBody body, ResponseCodeHandler responseCodeHandler)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
return makeStorageRequest(authorization, path, method, body, NO_HEADERS, responseCodeHandler);
}
private Response makeStorageRequest(String authorization, String path, String method, RequestBody body, Map<String, String> headers, ResponseCodeHandler responseCodeHandler)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(storageClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient()
@ -2372,8 +2376,6 @@ public class PushServiceSocket {
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
// Log.d(TAG, "Opening URL: " + connectionHolder.getUrl());
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
request.method(method, body);
@ -2385,6 +2387,10 @@ public class PushServiceSocket {
request.addHeader("Authorization", authorization);
}
for (Map.Entry<String, String> entry : headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
Call call = okHttpClient.newCall(request.build());
synchronized (connections) {
@ -2784,7 +2790,7 @@ public class PushServiceSocket {
null,
NO_HEADERS,
NO_HANDLER,
Optional.empty());
SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, CredentialResponse.class);
}
@ -2816,8 +2822,8 @@ public class PushServiceSocket {
}
};
public void putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization)
throws NonSuccessfulResponseCodeException, PushNetworkException
public GroupResponse putNewGroupsV2Group(Group group, GroupsV2AuthorizationString authorization)
throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
{
try (Response response = makeStorageRequest(authorization.toString(),
GROUPSV2_GROUP,
@ -2825,11 +2831,11 @@ public class PushServiceSocket {
protobufRequestBody(group),
GROUPS_V2_PUT_RESPONSE_HANDLER))
{
return;
return GroupResponse.ADAPTER.decode(readBodyBytes(response));
}
}
public Group getGroupsV2Group(GroupsV2AuthorizationString authorization)
public GroupResponse getGroupsV2Group(GroupsV2AuthorizationString authorization)
throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
{
try (Response response = makeStorageRequest(authorization.toString(),
@ -2838,7 +2844,7 @@ public class PushServiceSocket {
null,
GROUPS_V2_GET_CURRENT_HANDLER))
{
return Group.ADAPTER.decode(readBodyBytes(response));
return GroupResponse.ADAPTER.decode(readBodyBytes(response));
}
}
@ -2855,7 +2861,7 @@ public class PushServiceSocket {
}
}
public GroupChange patchGroupsV2Group(GroupChange.Actions groupChange, String authorization, Optional<byte[]> groupLinkPassword)
public GroupChangeResponse patchGroupsV2Group(GroupChange.Actions groupChange, String authorization, Optional<byte[]> groupLinkPassword)
throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
{
String path;
@ -2872,17 +2878,21 @@ public class PushServiceSocket {
protobufRequestBody(groupChange),
GROUPS_V2_PATCH_RESPONSE_HANDLER))
{
return GroupChange.ADAPTER.decode(readBodyBytes(response));
return GroupChangeResponse.ADAPTER.decode(readBodyBytes(response));
}
}
public GroupHistory getGroupsV2GroupHistory(int fromVersion, GroupsV2AuthorizationString authorization, int highestKnownEpoch, boolean includeFirstState)
public GroupHistory getGroupHistory(int fromVersion, GroupsV2AuthorizationString authorization, int highestKnownEpoch, boolean includeFirstState, long sendEndorsementsExpirationMs)
throws IOException
{
Map<String, String> headers = new HashMap<>();
headers.put("Cached-Send-Endorsements", Long.toString(TimeUnit.MILLISECONDS.toSeconds(sendEndorsementsExpirationMs)));
try (Response response = makeStorageRequest(authorization.toString(),
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion, highestKnownEpoch, includeFirstState),
"GET",
null,
headers,
GROUPS_V2_GET_LOGS_HANDLER))
{
@ -2890,12 +2900,7 @@ public class PushServiceSocket {
throw new PushNetworkException("No body!");
}
GroupChanges groupChanges;
try (InputStream input = response.body().byteStream()) {
groupChanges = GroupChanges.ADAPTER.decode(input);
} catch (IOException e) {
throw new PushNetworkException(e);
}
GroupChanges groupChanges = GroupChanges.ADAPTER.decode(readBodyBytes(response));
if (response.code() == 206) {
String contentRangeHeader = response.header("Content-Range");

View file

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

View file

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