Fix group recipient being created without a group record.

This commit is contained in:
Cody Henthorne
2026-02-24 14:10:21 -05:00
parent d798a35c38
commit 3437ac63bb
5 changed files with 49 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

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