From 3437ac63bb8834060e28b586bbc87b396273b9c7 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 24 Feb 2026 14:10:21 -0500 Subject: [PATCH] Fix group recipient being created without a group record. --- .../securesms/database/RecipientTable.kt | 16 ++++++------ .../messages/DataMessageProcessor.kt | 10 +++++--- .../messages/IncomingMessageObserver.kt | 25 +++++++++++++++++-- .../securesms/messages/MessageDecryptor.kt | 18 +++++++------ .../messages/SyncMessageProcessor.kt | 2 +- 5 files changed, 49 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index d4bad6a276..ae323a8f45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -1005,14 +1005,16 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da val groupId = GroupId.v2(masterKey) val values = getValuesForStorageGroupV2(insert, true) - writableDatabase.insertOrThrow(TABLE_NAME, null, values) + val createdId = writableDatabase.withinTransaction { + writableDatabase.insertOrThrow(TABLE_NAME, null, values) - Log.i(TAG, "Creating restore placeholder for $groupId") - val createdId = groups.create( - groupMasterKey = masterKey, - groupState = DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION), - groupSendEndorsements = null - ) + Log.i(TAG, "Creating restore placeholder for $groupId") + groups.create( + groupMasterKey = masterKey, + groupState = DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION), + groupSendEndorsements = null + ) + } if (createdId == null) { Log.w(TAG, "Unable to create restore placeholder for $groupId, group already exists") diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt index 4d002e4997..465e8636ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt @@ -182,7 +182,7 @@ object DataMessageProcessor { message.giftBadge != null -> insertResult = handleGiftMessage(context, envelope, metadata, message, senderRecipient, threadRecipient.id, receivedTime) message.isMediaMessage -> insertResult = handleMediaMessage(context, envelope, metadata, message, senderRecipient, threadRecipient, groupId, receivedTime, localMetrics, batchCache) message.body != null -> insertResult = handleTextMessage(context, envelope, metadata, message, senderRecipient, threadRecipient, groupId, receivedTime, localMetrics, batchCache) - message.groupCallUpdate != null -> handleGroupCallUpdateMessage(envelope, message, senderRecipient.id, groupId) + message.groupCallUpdate != null -> handleGroupCallUpdateMessage(envelope, senderRecipient.id, groupId) message.pollCreate != null -> insertResult = handlePollCreate(context, envelope, metadata, message, senderRecipient, threadRecipient, groupId, receivedTime) message.pollTerminate != null -> insertResult = handlePollTerminate(context, envelope, metadata, message, senderRecipient, earlyMessageCacheEntry, threadRecipient, groupId, receivedTime) message.pollVote != null -> messageId = handlePollVote(context, envelope, message, senderRecipient, earlyMessageCacheEntry) @@ -1048,19 +1048,21 @@ object DataMessageProcessor { fun handleGroupCallUpdateMessage( envelope: Envelope, - message: DataMessage, senderRecipientId: RecipientId, groupId: GroupId.V2? ) { log(envelope.timestamp!!, "Group call update message.") - val groupCallUpdate: DataMessage.GroupCallUpdate = message.groupCallUpdate!! - if (groupId == null) { warn(envelope.timestamp!!, "Invalid group for group call update message") return } + if (!SignalDatabase.groups.groupExists(groupId)) { + warn(envelope.timestamp!!, "Received group call update message for unknown groupId: $groupId") + return + } + val groupRecipientId = SignalDatabase.recipients.getOrInsertFromPossiblyMigratedGroupId(groupId) GroupCallPeekJob.enqueue( diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt index 9fda11d5c2..81c3043b31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt @@ -13,17 +13,21 @@ import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.models.ServiceId import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log +import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.crypto.ReentrantSessionLock import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.groups.GroupsV2ProcessingLock +import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor +import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil.startWhenCapable import org.thoughtcrime.securesms.jobs.PushProcessMessageErrorJob import org.thoughtcrime.securesms.jobs.PushProcessMessageJob +import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob import org.thoughtcrime.securesms.jobs.UnableToStartException import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.isDecisionPending @@ -321,11 +325,28 @@ class IncomingMessageObserver( } is MessageDecryptor.Result.Error -> { return result.followUpOperations + FollowUpOperation { - PushProcessMessageErrorJob( + val jobs = mutableListOf() + + if (result.errorMetadata.groupMasterKey != null) { + val groupId = result.errorMetadata.groupId!! + if (!SignalDatabase.groups.getGroup(groupId).isPresent) { + Log.w(TAG, "Decryption error in group, but group not found. Creating placeholder for groupId: $groupId") + SignalDatabase.groups.create( + groupMasterKey = result.errorMetadata.groupMasterKey!!, + groupState = DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION), + groupSendEndorsements = null + ) + jobs += RequestGroupV2InfoJob(groupId) + } + } + + jobs += PushProcessMessageErrorJob( result.toMessageState(), result.errorMetadata.toExceptionMetadata(), result.envelope.timestamp!! - ).asChain() + ) + + AppDependencies.jobManager.startChain(jobs) } } is MessageDecryptor.Result.Ignore -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt index 4adaa2a388..f379339281 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt @@ -16,9 +16,9 @@ import org.signal.core.models.ServiceId.PNI import org.signal.core.util.LRUCache import org.signal.core.util.PendingIntentFlags import org.signal.core.util.UuidUtil -import org.signal.core.util.isAbsent import org.signal.core.util.logging.Log import org.signal.core.util.logging.logW +import org.signal.core.util.orNull import org.signal.core.util.roundedString import org.signal.libsignal.metadata.InvalidMetadataMessageException import org.signal.libsignal.metadata.InvalidMetadataVersionException @@ -373,7 +373,7 @@ object MessageDecryptor { val groupId: GroupId? = protocolException.parseGroupId(envelope) val threadId: Long? = if (groupId != null) { - if (SignalDatabase.groups.getGroup(groupId).isAbsent()) { + if (!SignalDatabase.groups.groupExists(groupId)) { Log.w(TAG, "${logPrefix(envelope, senderServiceId)} No group found for $groupId! Not inserting a retry receipt.") return@FollowUpOperation null } @@ -560,20 +560,20 @@ object MessageDecryptor { return ErrorMetadata( sender = this.sender, senderDevice = this.senderDevice, - groupId = if (this.groupId.isPresent) GroupId.v2(GroupMasterKey(this.groupId.get())) else null + groupMasterKey = this.groupId.map(::GroupMasterKey).orNull() ) } private fun SignalServiceCipherResult.toErrorMetadata(): ErrorMetadata { - val groupId = if (this.content.dataMessage.hasGroupContext) { - GroupId.v2(GroupMasterKey(this.content.dataMessage!!.groupV2!!.masterKey!!.toByteArray())) + val groupMasterKey = if (this.content.dataMessage.hasGroupContext) { + GroupMasterKey(this.content.dataMessage!!.groupV2!!.masterKey!!.toByteArray()) } else { null } return ErrorMetadata( sender = this.metadata.sourceServiceId.toString(), senderDevice = this.metadata.sourceDeviceId, - groupId = groupId + groupMasterKey = groupMasterKey ) } @@ -641,8 +641,10 @@ object MessageDecryptor { data class ErrorMetadata( val sender: String, val senderDevice: Int, - val groupId: GroupId? - ) + val groupMasterKey: GroupMasterKey? + ) { + val groupId: GroupId.V2? by lazy { groupMasterKey?.let { GroupId.v2(it) } } + } data class DecryptionErrorCount( var count: Int, diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index a8a2c32226..34718fef83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -237,7 +237,7 @@ object SyncMessageProcessor { handleSynchronizeSentGv2Update(context, envelope, sent) threadId = SignalDatabase.threads.getOrCreateThreadIdFor(getSyncMessageDestination(sent)) } - dataMessage.groupCallUpdate != null -> DataMessageProcessor.handleGroupCallUpdateMessage(envelope, dataMessage, senderRecipient.id, groupId) + dataMessage.groupCallUpdate != null -> DataMessageProcessor.handleGroupCallUpdateMessage(envelope, senderRecipient.id, groupId) dataMessage.isEmptyGroupV2Message -> warn(envelope.timestamp!!, "Empty GV2 message! Doing nothing.") dataMessage.isExpirationUpdate -> threadId = handleSynchronizeSentExpirationUpdate(sent) dataMessage.storyContext != null -> threadId = handleSynchronizeSentStoryReply(sent, envelope.timestamp!!)