Clean up GSE usage and remove combined-UAK fallback for multi-recipient send.

This commit is contained in:
Cody Henthorne
2025-07-24 14:34:00 -04:00
committed by Michelle Tang
parent 99fb70c20c
commit 7499bd77b4
6 changed files with 72 additions and 126 deletions

View File

@@ -62,7 +62,6 @@ import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.RetryPendingSendsJob;
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
@@ -76,13 +75,13 @@ import org.thoughtcrime.securesms.jobs.RefreshSvrCredentialsJob;
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.RetryPendingSendsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
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;
@@ -227,7 +226,6 @@ public class ApplicationContext extends Application implements AppForegroundObse
.addPostRender(GroupRingCleanupJob::enqueue)
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
.addPostRender(() -> ActiveCallManager.clearNotifications(this))
.addPostRender(() -> GroupSendEndorsementInternalNotifier.init())
.addPostRender(RestoreOptimizedMediaJob::enqueueIfNecessary)
.addPostRender(RetryPendingSendsJob::enqueueForAll)
.execute();

View File

@@ -14,12 +14,10 @@ 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
@@ -27,7 +25,7 @@ import kotlin.time.Duration.Companion.minutes
/**
* Internal user only notifier when "bad" things happen with group send endorsement sends.
*/
object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListener {
object GroupSendEndorsementInternalNotifier {
private const val TAG = "GSENotifier"
@@ -36,27 +34,14 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene
private var lastMissingNotify: Duration = 0.milliseconds
private var lastFallbackNotify: Duration = 0.milliseconds
@JvmStatic
fun init() {
if (RemoteConfig.internalUser) {
SealedSenderAccess.fallbackListener = this
fun maybePostGroupSendFallbackError(context: Context) {
if (!RemoteConfig.internalUser) {
return
}
}
override fun onAccessToTokenFallback() {
Log.w(TAG, "Fallback from access key to token", Throwable())
postFallbackError(AppDependencies.application)
}
Log.internal().w(TAG, "Group send with GSE failed, GSE was likely out of date or incorrect", Throwable())
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++
@@ -65,8 +50,8 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene
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")
.setContentTitle("[Internal-only] Group send failure (GSE)")
.setContentText("Please tap to get a debug log")
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable()))
.build()
@@ -77,7 +62,13 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene
}
@JvmStatic
fun postMissingGroupSendEndorsement(context: Context) {
fun maybePostMissingGroupSendEndorsement(context: Context) {
if (!RemoteConfig.internalUser) {
return
}
Log.internal().w(TAG, "GSE missing for recipient", Throwable())
val now = System.currentTimeMillis().milliseconds
if (lastMissingNotify + 5.minutes > now) {
return
@@ -85,8 +76,8 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene
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")
.setContentTitle("[Internal-only] Missing recipient (GSE)")
.setContentText("Please tap to get a debug log")
.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable()))
.build()
@@ -94,23 +85,4 @@ object GroupSendEndorsementInternalNotifier : SealedSenderAccess.FallbackListene
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

@@ -14,8 +14,8 @@ 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.SealedSenderAccessUtil;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.database.MessageSendLogTables;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListId;
@@ -34,7 +34,6 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
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;
@@ -161,11 +160,11 @@ public final class GroupSendUtil {
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
* {@link SendMessageResult}s just like we're used to.
*
* @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group.
* @param groupId The groupId of the group you're sending to
*/
@WorkerThread
public static List<SendMessageResult> sendCallMessage(@NonNull Context context,
@Nullable GroupId.V2 groupId,
@NonNull GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
@NonNull SignalServiceCallMessage message)
throws IOException, UntrustedIdentityException
@@ -292,9 +291,11 @@ public final class GroupSendUtil {
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);
Log.w(TAG, "Unable to update group send endorsements, falling back to legacy", e);
useGroupSendEndorsements = false;
groupSendEndorsementRecords = new GroupSendEndorsementRecords(Collections.emptyMap());
GroupSendEndorsementInternalNotifier.maybePostGroupSendFallbackError(context);
} else {
Log.w(TAG, "Unable to update group send endorsements, using what we have", e);
}
@@ -308,49 +309,38 @@ public final class GroupSendUtil {
List<Recipient> senderKeyTargets = new LinkedList<>();
List<Recipient> legacyTargets = new LinkedList<>();
for (Recipient recipient : registeredTargets) {
Optional<UnidentifiedAccess> access = recipients.getAccessPair(recipient.getId());
boolean validMembership = groupId == null || (groupRecord.isPresent() && groupRecord.get().getMembers().contains(recipient.getId()));
if (useGroupSendEndorsements) {
// Determine recipients that can be sent to via sender key vs must use legacy fan-out
if (distributionId == null) {
Log.i(TAG, "No DistributionId. Using legacy.");
legacyTargets.addAll(registeredTargets);
} else if (isStorySend) {
Log.i(TAG, "Sending a story. Using sender key for all " + allTargets.size() + " recipients.");
senderKeyTargets.addAll(registeredTargets);
} else if (!useGroupSendEndorsements) {
Log.i(TAG, "No group send endorsements, using legacy for all " + allTargets.size() + " recipients.");
legacyTargets.addAll(registeredTargets);
} else {
for (Recipient recipient : registeredTargets) {
boolean validMembership = groupRecord.get().getMembers().contains(recipient.getId());
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);
}
GroupSendEndorsementInternalNotifier.maybePostMissingGroupSendEndorsement(context);
}
}
} else {
// Use sender key
if (recipient.getHasServiceId() &&
access.isPresent() &&
validMembership)
{
senderKeyTargets.add(recipient);
} else {
legacyTargets.add(recipient);
}
}
}
if (distributionId == null) {
Log.i(TAG, "No DistributionId. Using legacy.");
legacyTargets.addAll(senderKeyTargets);
senderKeyTargets.clear();
} else if (isStorySend) {
Log.i(TAG, "Sending a story. Using sender key for all " + allTargets.size() + " recipients.");
senderKeyTargets.clear();
senderKeyTargets.addAll(registeredTargets);
legacyTargets.clear();
} else if (SignalStore.internal().getRemoveSenderKeyMinimum()) {
// Enforce minimum number of sender key destinations
if (SignalStore.internal().getRemoveSenderKeyMinimum()) {
Log.i(TAG, "Sender key minimum removed. Using for " + senderKeyTargets.size() + " recipients.");
} else if (senderKeyTargets.size() < 2) {
Log.i(TAG, "Too few sender-key-capable users (" + senderKeyTargets.size() + "). Doing all legacy sends.");
} else if (senderKeyTargets.size() < 2 && !isStorySend) {
Log.i(TAG, "Too few sender-key-capable users (" + senderKeyTargets.size() + ") for non-story send. Doing all legacy sends.");
legacyTargets.addAll(senderKeyTargets);
senderKeyTargets.clear();
} else {
@@ -431,8 +421,8 @@ public final class GroupSendUtil {
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);
if (useGroupSendEndorsements) {
GroupSendEndorsementInternalNotifier.maybePostGroupSendFallbackError(context);
}
} catch (NoSessionException e) {
Log.w(TAG, "No session. Falling back to legacy sends.", e);
@@ -712,6 +702,8 @@ public final class GroupSendUtil {
@Nullable PartialSendBatchCompleteListener partialListener)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
{
Preconditions.checkNotNull(groupSendEndorsements, "GSEs must be non-null for non-story sender key send.");
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());
@@ -780,6 +772,8 @@ public final class GroupSendUtil {
@Nullable PartialSendBatchCompleteListener partialSendListener)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
{
Preconditions.checkNotNull(groupSendEndorsements, "GSEs must be non-null for non-story sender key send.");
return messageSender.sendCallMessage(distributionId, targets, access, groupSendEndorsements, message, partialSendListener);
}