Fix identity key update failure when profile key verification failed.

This commit is contained in:
Cody Henthorne
2025-08-01 13:31:29 -04:00
parent a60f3a26fb
commit e6e869e074
13 changed files with 79 additions and 32 deletions

View File

@@ -1241,7 +1241,7 @@ object BackupRepository {
recipientSet
}
RetrieveProfileJob.enqueue(recipientIds)
RetrieveProfileJob.enqueue(recipientIds, skipDebounce = false)
AppDependencies.jobManager.add(CreateReleaseChannelJob.create())

View File

@@ -255,7 +255,7 @@ class ConversationViewModel(
.conversationRecipient
.filter { it.isRegistered }
.take(1)
.subscribeBy { RetrieveProfileJob.enqueue(it.id) }
.subscribeBy { RetrieveProfileJob.enqueue(it.id, skipDebounce = false) }
.addTo(disposables)
disposables += recipientRepository

View File

@@ -238,7 +238,7 @@ public class SignalBaseIdentityKeyStore {
}
if (!identityKey.equals(identityRecord.getIdentityKey())) {
Log.w(TAG, "Identity keys don't match... service: " + identityKey.hashCode() + " database: " + identityRecord.getIdentityKey().hashCode());
Log.w(TAG, "Identity keys don't match... service: ***" + (identityKey.hashCode() % 100) + " database: ***" + (identityRecord.getIdentityKey().hashCode() % 100));
return false;
}

View File

@@ -508,7 +508,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
db.runPostSuccessfulTransaction {
if (result.affectedIds.isNotEmpty()) {
result.affectedIds.forEach { AppDependencies.databaseObserver.notifyRecipientChanged(it) }
RetrieveProfileJob.enqueue(result.affectedIds)
RetrieveProfileJob.enqueue(result.affectedIds, skipDebounce = true)
}
if (result.oldIds.isNotEmpty()) {
@@ -1653,6 +1653,35 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return false
}
/**
* Clears the profile key.
*
* It clears out the profile key credential and resets the unidentified access mode.
*/
fun clearProfileKeyData(id: RecipientId) {
val selection = "$ID = ?"
val args = arrayOf(id.serialize())
val valuesToCompare = contentValuesOf(
PROFILE_KEY to null
)
val valuesToSet = contentValuesOf(
PROFILE_KEY to null,
EXPIRING_PROFILE_KEY_CREDENTIAL to null,
SEALED_SENDER_MODE to SealedSenderAccessMode.UNKNOWN.mode,
LAST_PROFILE_FETCH to 0
)
val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare)
if (update(updateQuery, valuesToSet)) {
rotateStorageId(id)
AppDependencies.databaseObserver.notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
/**
* Sets the profile key iff currently null.
*

View File

@@ -221,7 +221,7 @@ public class IndividualSendJob extends PushSendJob {
}
database.addMismatchedIdentity(messageId, recipient.getId(), uie.getIdentityKey());
database.markAsSentFailed(messageId);
RetrieveProfileJob.enqueue(recipient.getId());
RetrieveProfileJob.enqueue(recipient.getId(), true);
} catch (ProofRequiredException e) {
ProofRequiredExceptionHandler.Result result = ProofRequiredExceptionHandler.handle(context, e, SignalDatabase.threads().getRecipientForThreadId(threadId), threadId, messageId);
if (result.isRetry()) {

View File

@@ -509,7 +509,7 @@ public final class PushGroupSendJob extends PushSendJob {
.map(mismatch -> mismatch.getRecipientId())
.collect(Collectors.toSet());
RetrieveProfileJob.enqueue(mismatchRecipientIds);
RetrieveProfileJob.enqueue(mismatchRecipientIds, true);
} else if (!networkFailures.isEmpty()) {
long retryAfter = results.stream()
.filter(r -> r.getRateLimitFailure() != null)

View File

@@ -53,9 +53,9 @@ import kotlin.time.Duration.Companion.minutes
/**
* Retrieves a users profile and sets the appropriate local fields.
*/
class RetrieveProfileJob private constructor(parameters: Parameters, private val recipientIds: MutableSet<RecipientId>) : BaseJob(parameters) {
constructor(recipientIds: Set<RecipientId>) : this(
Parameters.Builder()
class RetrieveProfileJob private constructor(parameters: Parameters, private val recipientIds: MutableSet<RecipientId>, private val skipDebounce: Boolean) : BaseJob(parameters) {
private constructor(recipientIds: Set<RecipientId>, skipDebounce: Boolean) : this(
parameters = Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.apply {
if (recipientIds.size < 5) {
@@ -65,12 +65,14 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
}
.setMaxAttempts(3)
.build(),
recipientIds.toMutableSet()
recipientIds = recipientIds.toMutableSet(),
skipDebounce = skipDebounce
)
override fun serialize(): ByteArray? {
return JsonJobData.Builder()
.putStringListAsArray(KEY_RECIPIENTS, recipientIds.map { it.serialize() })
.putBoolean(KEY_SKIP_DEBOUNCE, skipDebounce)
.serialize()
}
@@ -99,7 +101,7 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
val currentTime = System.currentTimeMillis()
val debounceThreshold = currentTime - PROFILE_FETCH_DEBOUNCE_TIME_MS
val recipientsToFetch = recipients.filter { recipient ->
recipient.hasServiceId && recipient.lastProfileFetchTime < debounceThreshold
recipient.hasServiceId && (skipDebounce || recipient.lastProfileFetchTime < debounceThreshold)
}
if (recipientsToFetch.isEmpty()) {
@@ -138,7 +140,7 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
Log.d(TAG, "Fetched ${localRecords.size} existing records.")
stopwatch.split("disk-fetch")
val successIds: Set<RecipientId> = recipientIds - response.retryableFailures
val successIds: Set<RecipientId> = response.successes.map { it.id }.toSet()
val newlyRegisteredIds: Set<RecipientId> = response.successes
.mapNotNull { recipientsById[it.id] }
.filterNot { it.isRegistered }
@@ -189,6 +191,14 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
}
stopwatch.split("registered-update")
if (response.verificationFailures.isNotEmpty()) {
Log.i(TAG, "Removing profile keys for ${response.verificationFailures.size} users due to verification errors")
for (recipientId in response.verificationFailures) {
SignalDatabase.recipients.clearProfileKeyData(recipientId)
}
}
stopwatch.split("verification-update")
for (idProfilePair in response.successes) {
setIdentityKey(recipientsById[idProfilePair.id]!!, idProfilePair.profileWithCredential.profile.identityKey)
}
@@ -523,8 +533,9 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
override fun create(parameters: Parameters, serializedData: ByteArray?): RetrieveProfileJob {
val data = JsonJobData.deserialize(serializedData)
val recipientIds: MutableSet<RecipientId> = data.getStringArray(KEY_RECIPIENTS).map { RecipientId.from(it) }.toMutableSet()
val skipDebounce: Boolean = data.getBooleanOrDefault(KEY_SKIP_DEBOUNCE, false)
return RetrieveProfileJob(parameters, recipientIds)
return RetrieveProfileJob(parameters, recipientIds, skipDebounce)
}
}
@@ -532,6 +543,7 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
const val KEY = "RetrieveProfileJob"
private val TAG = Log.tag(RetrieveProfileJob::class.java)
private const val KEY_RECIPIENTS = "recipients"
private const val KEY_SKIP_DEBOUNCE = "skip_debounce"
private const val DEDUPE_KEY_RETRIEVE_AVATAR = KEY + "_RETRIEVE_PROFILE_AVATAR"
private const val QUEUE_PREFIX = "RetrieveProfileJob_"
@@ -546,8 +558,8 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
*/
@JvmStatic
@WorkerThread
fun enqueue(recipientId: RecipientId) {
forRecipients(setOf(recipientId)).firstOrNull()?.let { job ->
fun enqueue(recipientId: RecipientId, skipDebounce: Boolean) {
forRecipients(setOf(recipientId), skipDebounce).firstOrNull()?.let { job ->
AppDependencies.jobManager.add(job)
}
}
@@ -558,9 +570,9 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
*/
@JvmStatic
@WorkerThread
fun enqueue(recipientIds: Set<RecipientId>) {
fun enqueue(recipientIds: Set<RecipientId>, skipDebounce: Boolean) {
val jobManager = AppDependencies.jobManager
for (job in forRecipients(recipientIds)) {
for (job in forRecipients(recipientIds, skipDebounce)) {
jobManager.add(job)
}
}
@@ -572,7 +584,7 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
*/
@JvmStatic
@WorkerThread
fun forRecipients(recipientIds: Set<RecipientId>): List<Job> {
fun forRecipients(recipientIds: Set<RecipientId>, skipDebounce: Boolean = false): List<Job> {
val combined: MutableSet<RecipientId> = HashSet(recipientIds.size)
var includeSelf = false
@@ -589,8 +601,8 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
if (includeSelf) {
add(RefreshOwnProfileJob())
}
if (combined.size > 0) {
add(RetrieveProfileJob(combined))
if (combined.isNotEmpty()) {
add(RetrieveProfileJob(combined, skipDebounce))
}
}
}
@@ -622,7 +634,7 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
if (ids.isNotEmpty()) {
Log.i(TAG, "Optimistically refreshing ${ids.size} eligible recipient(s).")
enqueue(ids.toSet())
enqueue(ids.toSet(), false)
} else {
Log.i(TAG, "No recipients to refresh.")
}

View File

@@ -256,7 +256,7 @@ object DataMessageProcessor {
if (SignalDatabase.recipients.setProfileKey(senderRecipient.id, messageProfileKey)) {
log(timestamp, "Profile key on message from " + senderRecipient.id + " didn't match our local store. It has been updated.")
SignalDatabase.runPostSuccessfulTransaction {
RetrieveProfileJob.enqueue(senderRecipient.id)
RetrieveProfileJob.enqueue(senderRecipient.id, skipDebounce = true)
}
}
} else {

View File

@@ -15,7 +15,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
@@ -69,7 +69,7 @@ class EditProfileViewModel extends ViewModel {
SignalExecutors.BOUNDED.execute(() -> {
onRecipientChanged(Recipient.self().fresh());
RetrieveProfileJob.enqueue(Recipient.self().getId());
AppDependencies.getJobManager().add(new RefreshOwnProfileJob());
});
Recipient.self().live().observeForever(observer);

View File

@@ -803,7 +803,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
callMessage);
} catch (UntrustedIdentityException e) {
Log.i(TAG, "onSendCallMessage onFailure: ", e);
RetrieveProfileJob.enqueue(recipient.getId());
RetrieveProfileJob.enqueue(recipient.getId(), true);
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), UNTRUSTED_IDENTITY));
} catch (ProofRequiredException e) {
Log.i(TAG, "onSendCallMessage onFailure: ", e);
@@ -856,7 +856,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
if (Util.hasItems(identifyFailureRecipientIds)) {
process((s, p) -> p.handleGroupMessageSentError(s, identifyFailureRecipientIds, UNTRUSTED_IDENTITY));
RetrieveProfileJob.enqueue(identifyFailureRecipientIds);
RetrieveProfileJob.enqueue(identifyFailureRecipientIds, true);
}
} catch (UntrustedIdentityException | IOException | InvalidInputException e) {
Log.w(TAG, "onSendCallMessageToGroup failed", e);
@@ -1207,7 +1207,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
callMessage);
process((s, p) -> p.handleMessageSentSuccess(s, remotePeer.getCallId()));
} catch (UntrustedIdentityException e) {
RetrieveProfileJob.enqueue(remotePeer.getId());
RetrieveProfileJob.enqueue(remotePeer.getId(), true);
processSendMessageFailureWithChangeDetection(remotePeer,
(s, p) -> p.handleMessageSentError(s,
remotePeer.getCallId(),

View File

@@ -174,7 +174,7 @@ class ContactRecordProcessor(
if (localAci != null && mergedIdentityKey != null && remote.proto.identityKey.isNotEmpty() && !mergedIdentityKey.contentEquals(remote.proto.identityKey.toByteArray())) {
Log.w(TAG, "The local and remote identity keys do not match for " + localAci + ". Enqueueing a profile fetch.")
enqueue(trustedPush(localAci, localPni, local.proto.e164).id)
enqueue(trustedPush(localAci, localPni, local.proto.e164).id, true)
}
val mergedPni: PNI?

View File

@@ -150,7 +150,7 @@ class StoryGroupReplyFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
SignalExecutors.BOUNDED.execute {
RetrieveProfileJob.enqueue(groupRecipientId)
RetrieveProfileJob.enqueue(groupRecipientId, skipDebounce = false)
}
recyclerView = view.findViewById(R.id.recycler)