From 2eba9a8d72b88632818f6c80685eb048ad2d1799 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 31 Aug 2022 15:38:22 -0400 Subject: [PATCH] Add support for doing normal CDS queries on CDSv2. --- .../contacts/sync/ContactDiscovery.kt | 16 +- .../sync/ContactDiscoveryRefreshV1.java | 6 +- .../sync/ContactDiscoveryRefreshV2.kt | 272 ++++++++++-------- .../contacts/sync/FuzzyPhoneNumberHelper.java | 14 +- .../securesms/database/RecipientDatabase.kt | 79 +++-- .../securesms/util/FeatureFlags.java | 14 +- .../sync/FuzzyPhoneNumberHelperTest.java | 14 +- .../api/SignalServiceAccountManager.java | 3 +- .../api/services/CdsiV2Service.java | 8 +- libsignal/service/src/main/proto/CDSI.proto | 4 + 10 files changed, 274 insertions(+), 156 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt index bf79691f4..a47734f3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt @@ -80,7 +80,9 @@ object ContactDiscovery { descriptor = "refresh-all", refresh = { if (FeatureFlags.phoneNumberPrivacy()) { - ContactDiscoveryRefreshV2.refreshAll(context) + ContactDiscoveryRefreshV2.refreshAll(context, useCompat = false) + } else if (FeatureFlags.cdsV2Compat()) { + ContactDiscoveryRefreshV2.refreshAll(context, useCompat = true) } else if (FeatureFlags.cdsV2LoadTesting()) { loadTestRefreshAll(context) } else { @@ -103,7 +105,9 @@ object ContactDiscovery { descriptor = "refresh-multiple", refresh = { if (FeatureFlags.phoneNumberPrivacy()) { - ContactDiscoveryRefreshV2.refresh(context, recipients) + ContactDiscoveryRefreshV2.refresh(context, recipients, useCompat = false) + } else if (FeatureFlags.cdsV2Compat()) { + ContactDiscoveryRefreshV2.refresh(context, recipients, useCompat = true) } else if (FeatureFlags.cdsV2LoadTesting()) { loadTestRefresh(context, recipients) } else { @@ -124,7 +128,9 @@ object ContactDiscovery { descriptor = "refresh-single", refresh = { if (FeatureFlags.phoneNumberPrivacy()) { - ContactDiscoveryRefreshV2.refresh(context, listOf(recipient)) + ContactDiscoveryRefreshV2.refresh(context, listOf(recipient), useCompat = false) + } else if (FeatureFlags.cdsV2Compat()) { + ContactDiscoveryRefreshV2.refresh(context, listOf(recipient), useCompat = true) } else if (FeatureFlags.cdsV2LoadTesting()) { loadTestRefresh(context, listOf(recipient)) } else { @@ -381,14 +387,14 @@ object ContactDiscovery { private fun loadTestRefreshAll(context: Context): RefreshResult { return loadTestOperation( { ContactDiscoveryRefreshV1.refreshAll(context) }, - { ContactDiscoveryRefreshV2.refreshAll(context, ignoreResults = true) } + { ContactDiscoveryRefreshV2.refreshAll(context, useCompat = false, ignoreResults = true) } ) } private fun loadTestRefresh(context: Context, recipients: List): RefreshResult { return loadTestOperation( { ContactDiscoveryRefreshV1.refresh(context, recipients) }, - { ContactDiscoveryRefreshV2.refresh(context, recipients, ignoreResults = true) } + { ContactDiscoveryRefreshV2.refresh(context, recipients, useCompat = false, ignoreResults = true) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java index 9e0ae7d5a..49ffbadca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java @@ -122,7 +122,7 @@ class ContactDiscoveryRefreshV1 { if (result.getNumberRewrites().size() > 0) { Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers."); - recipientDatabase.updatePhoneNumbers(result.getNumberRewrites()); + recipientDatabase.rewritePhoneNumbers(result.getNumberRewrites()); } Map aciMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers()); @@ -250,8 +250,8 @@ class ContactDiscoveryRefreshV1 { KeyStore iasKeyStore = getIasKeyStore(context); try { - Map results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE); - FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult); + Map results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE); + FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult); return new ContactIntersection(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers); } catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException | InvalidKeyException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt index b0eccee7f..ca62d450b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt @@ -4,20 +4,25 @@ import android.content.Context import androidx.annotation.WorkerThread import org.signal.contacts.SystemContactsRepository import org.signal.core.util.Stopwatch +import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log -import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.thoughtcrime.securesms.BuildConfig -import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.InputResult +import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.OutputResult +import org.thoughtcrime.securesms.database.RecipientDatabase.CdsV2Result import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobs.RetrieveProfileJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId -import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.push.ACI import org.whispersystems.signalservice.api.services.CdsiV2Service import java.io.IOException import java.util.Optional +import java.util.concurrent.Callable +import java.util.concurrent.Future /** * Performs the CDS refresh using the V2 interface (either CDSH or CDSI) that returns both PNIs and ACIs. @@ -39,149 +44,190 @@ object ContactDiscoveryRefreshV2 { @WorkerThread @Synchronized @JvmStatic - fun refreshAll(context: Context, ignoreResults: Boolean = false): ContactDiscovery.RefreshResult { - val stopwatch = Stopwatch("refresh-all") - - val previousE164s: Set = if (SignalStore.misc().cdsToken != null) { - SignalDatabase.cds.getAllE164s() - } else { - Log.w(TAG, "No token set! Cannot provide previousE164s.") - emptySet() - } - stopwatch.split("previous") - + fun refreshAll(context: Context, useCompat: Boolean, ignoreResults: Boolean = false): ContactDiscovery.RefreshResult { val recipientE164s: Set = SignalDatabase.recipients.getAllE164s().sanitize() - val newRecipientE164s: Set = recipientE164s - previousE164s - stopwatch.split("recipient") - val systemE164s: Set = SystemContactsRepository.getAllDisplayNumbers(context).toE164s(context).sanitize() - val newSystemE164s: Set = systemE164s - previousE164s - stopwatch.split("system") - val newE164s: Set = newRecipientE164s + newSystemE164s - - if (newE164s.isEmpty() && previousE164s.isEmpty()) { - return ContactDiscovery.RefreshResult(emptySet(), emptyMap()) - } - - val tokenToUse: ByteArray? = if (previousE164s.isNotEmpty()) { - SignalStore.misc().cdsToken - } else { - if (SignalStore.misc().cdsToken != null) { - Log.w(TAG, "We have a token, but our previousE164 list is empty! We cannot provide a token.") - } - null - } - - val response: CdsiV2Service.Response = makeRequest( - previousE164s = previousE164s, - newE164s = newE164s, - serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(), - token = tokenToUse, + return refreshInternal( + recipientE164s = recipientE164s, + systemE164s = systemE164s, + inputPreviousE164s = SignalDatabase.cds.getAllE164s(), saveToken = true, - tag = "refresh-all" + useCompat = useCompat, + ignoreResults = ignoreResults ) - stopwatch.split("network") - - SignalDatabase.cds.updateAfterCdsQuery(newE164s, recipientE164s + systemE164s) - stopwatch.split("cds-db") - - var registeredIds: Set = emptySet() - - if (ignoreResults) { - Log.w(TAG, "[refresh-all] Ignoring CDSv2 results.") - } else { - registeredIds = SignalDatabase.recipients.bulkProcessCdsV2Result( - response.results - .mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) } - ) - stopwatch.split("recipient-db") - - SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList()) - stopwatch.split("update-registered") - } - - stopwatch.stop(TAG) - Log.d(TAG, "[refresh-all] Used ${response.quotaUsedDebugOnly} units of our quota.") - - return ContactDiscovery.RefreshResult(registeredIds, emptyMap()) } @Throws(IOException::class) @WorkerThread @Synchronized @JvmStatic - fun refresh(context: Context, inputRecipients: List, ignoreResults: Boolean = false): ContactDiscovery.RefreshResult { - val stopwatch = Stopwatch("refresh-some") - - val recipients = inputRecipients.map { it.resolve() } - stopwatch.split("resolve") - - val inputIds: Set = recipients.map { it.id }.toSet() + fun refresh(context: Context, inputRecipients: List, useCompat: Boolean, ignoreResults: Boolean = false): ContactDiscovery.RefreshResult { + val recipients: List = inputRecipients.map { it.resolve() } val inputE164s: Set = recipients.mapNotNull { it.e164.orElse(null) }.toSet() - if (inputE164s.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) { + return if (inputE164s.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) { Log.i(TAG, "List of specific recipients to refresh is too large! (Size: ${recipients.size}). Doing a full refresh instead.") - val fullResult: ContactDiscovery.RefreshResult = refreshAll(context, ignoreResults) - return ContactDiscovery.RefreshResult( + val fullResult: ContactDiscovery.RefreshResult = refreshAll(context, ignoreResults) + val inputIds: Set = recipients.map { it.id }.toSet() + + ContactDiscovery.RefreshResult( registeredIds = fullResult.registeredIds.intersect(inputIds), rewrites = fullResult.rewrites.filterKeys { inputE164s.contains(it) } ) - } - - if (inputE164s.isEmpty()) { - Log.w(TAG, "No numbers to refresh!") - return ContactDiscovery.RefreshResult(emptySet(), emptyMap()) } else { - Log.i(TAG, "Doing a one-off request for ${inputE164s.size} recipients.") - } - - val response: CdsiV2Service.Response = makeRequest( - previousE164s = emptySet(), - newE164s = inputE164s, - serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(), - token = null, - saveToken = false, - tag = "refresh-some" - ) - stopwatch.split("network") - - var registeredIds: Set = emptySet() - - if (ignoreResults) { - Log.w(TAG, "[refresh-some] Ignoring CDSv2 results.") - } else { - registeredIds = SignalDatabase.recipients.bulkProcessCdsV2Result( - response.results - .mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) } + refreshInternal( + recipientE164s = inputE164s, + systemE164s = inputE164s, + inputPreviousE164s = emptySet(), + saveToken = false, + useCompat = useCompat, + ignoreResults = ignoreResults ) - stopwatch.split("recipient-db") - - SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList()) - stopwatch.split("update-registered") } - - Log.d(TAG, "[refresh-some] Used ${response.quotaUsedDebugOnly} units of our quota.") - stopwatch.stop(TAG) - - return ContactDiscovery.RefreshResult(registeredIds, emptyMap()) } @Throws(IOException::class) - private fun makeRequest(previousE164s: Set, newE164s: Set, serviceIds: Map, token: ByteArray?, saveToken: Boolean, tag: String): CdsiV2Service.Response { - return ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdsi( + private fun refreshInternal( + recipientE164s: Set, + systemE164s: Set, + inputPreviousE164s: Set, + saveToken: Boolean, + useCompat: Boolean, + ignoreResults: Boolean + ): ContactDiscovery.RefreshResult { + val stopwatch = Stopwatch("refreshInternal-${if (useCompat) "compat" else "v2"}") + + val previousE164s: Set = if (SignalStore.misc().cdsToken != null) inputPreviousE164s else emptySet() + + val allE164s: Set = recipientE164s + systemE164s + val newRawE164s: Set = allE164s - previousE164s + val fuzzyInput: InputResult = FuzzyPhoneNumberHelper.generateInput(newRawE164s, recipientE164s) + val newE164s: Set = fuzzyInput.numbers + + if (newE164s.isEmpty() && previousE164s.isEmpty()) { + Log.w(TAG, "[refreshInternal] No data to send! Ignoring.") + return ContactDiscovery.RefreshResult(emptySet(), emptyMap()) + } + + val token: ByteArray? = if (previousE164s.isNotEmpty()) SignalStore.misc().cdsToken else null + + stopwatch.split("preamble") + + val response: CdsiV2Service.Response = ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdsi( previousE164s, newE164s, - serviceIds, + SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(), + useCompat, Optional.ofNullable(token), BuildConfig.CDSI_MRENCLAVE ) { tokenToSave -> if (saveToken) { SignalStore.misc().cdsToken = tokenToSave - Log.d(TAG, "[$tag] Token saved!") + Log.d(TAG, "Token saved!") + } else { + Log.d(TAG, "Ignoring token.") } } + Log.d(TAG, "[refreshInternal] Used ${response.quotaUsedDebugOnly} quota.") + stopwatch.split("network") + + SignalDatabase.cds.updateAfterCdsQuery(newE164s, allE164s + newE164s) + stopwatch.split("cds-db") + + val registeredIds: MutableSet = mutableSetOf() + val rewrites: MutableMap = mutableMapOf() + + if (ignoreResults) { + Log.w(TAG, "[refreshInternal] Ignoring CDSv2 results.") + } else { + if (useCompat) { + val transformed: Map = response.results.mapValues { entry -> entry.value.aci.orElse(null) } + val fuzzyOutput: OutputResult = FuzzyPhoneNumberHelper.generateOutput(transformed, fuzzyInput) + + if (transformed.values.any { it == null }) { + throw IOException("Unexpected null ACI!") + } + + SignalDatabase.recipients.rewritePhoneNumbers(fuzzyOutput.rewrites) + stopwatch.split("rewrite-e164") + + val aciMap: Map = SignalDatabase.recipients.bulkProcessCdsResult(fuzzyOutput.numbers) + + registeredIds += aciMap.keys + rewrites += fuzzyOutput.rewrites + stopwatch.split("process-result") + + val existingIds: Set = SignalDatabase.recipients.getAllPossiblyRegisteredByE164(recipientE164s + rewrites.values) + val inactiveIds: Set = (existingIds - registeredIds).removeRegisteredButUnlisted() + + SignalDatabase.recipients.bulkUpdatedRegisteredStatus(aciMap, inactiveIds) + stopwatch.split("update-registered") + } else { + val transformed: Map = response.results.mapValues { entry -> CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) } + val fuzzyOutput: OutputResult = FuzzyPhoneNumberHelper.generateOutput(transformed, fuzzyInput) + + SignalDatabase.recipients.rewritePhoneNumbers(fuzzyOutput.rewrites) + stopwatch.split("rewrite-e164") + + val existingIds: Set = SignalDatabase.recipients.getAllPossiblyRegisteredByE164(recipientE164s + rewrites.values) + val inactiveIds: Set = (existingIds - registeredIds).removeRegisteredButUnlisted() + + registeredIds += SignalDatabase.recipients.bulkProcessCdsV2Result(fuzzyOutput.numbers) + rewrites += fuzzyOutput.rewrites + stopwatch.split("process-result") + + SignalDatabase.recipients.bulkUpdatedRegisteredStatusV2(registeredIds, inactiveIds) + stopwatch.split("update-registered") + } + } + + stopwatch.stop(TAG) + + return ContactDiscovery.RefreshResult(registeredIds, rewrites) + } + + private fun hasCommunicatedWith(recipient: Recipient): Boolean { + val localAci = SignalStore.account().requireAci() + return SignalDatabase.threads.hasThread(recipient.id) || (recipient.hasServiceId() && SignalDatabase.sessions.hasSessionFor(localAci, recipient.requireServiceId().toString())) + } + + @WorkerThread + private fun Set.removeRegisteredButUnlisted(): Set { + val futures: List>> = Recipient.resolvedList(this) + .filter { hasCommunicatedWith(it) } + .map { + SignalExecutors.UNBOUNDED.submit( + Callable { + try { + it.id to ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(it.requireServiceId()) + } catch (e: IOException) { + it.id to null + } + } + ) + } + + val registeredIds: MutableSet = mutableSetOf() + val retryIds: MutableSet = mutableSetOf() + + for (future in futures) { + val (id, registered) = future.get() + if (registered == null) { + retryIds += id + registeredIds += id + } else if (registered) { + registeredIds += id + } + } + + if (retryIds.isNotEmpty()) { + Log.w(TAG, "Failed to determine registered status of ${retryIds.size} recipients. Assuming registered, but enqueuing profile jobs to check later.") + RetrieveProfileJob.enqueue(retryIds) + } + + return this - registeredIds } private fun Set.toE164s(context: Context): Set { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelper.java index 175ba1c13..80e7c9beb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelper.java @@ -51,8 +51,8 @@ class FuzzyPhoneNumberHelper { * these results and our initial input set, we can decide if we need to rewrite which number we * have stored locally. */ - static @NonNull OutputResult generateOutput(@NonNull Map registeredNumbers, @NonNull InputResult inputResult) { - Map allNumbers = new HashMap<>(registeredNumbers); + static @NonNull OutputResult generateOutput(@NonNull Map registeredNumbers, @NonNull InputResult inputResult) { + Map allNumbers = new HashMap<>(registeredNumbers); Map rewrites = new HashMap<>(); for (Map.Entry entry : inputResult.getMapOfOriginalToVariant().entrySet()) { @@ -76,7 +76,7 @@ class FuzzyPhoneNumberHelper { } } - return new OutputResult(allNumbers, rewrites); + return new OutputResult<>(allNumbers, rewrites); } private interface FuzzyMatcher { @@ -170,16 +170,16 @@ class FuzzyPhoneNumberHelper { } } - public static class OutputResult { - private final Map numbers; + public static class OutputResult { + private final Map numbers; private final Map rewrites; - private OutputResult(@NonNull Map numbers, @NonNull Map rewrites) { + private OutputResult(@NonNull Map numbers, @NonNull Map rewrites) { this.numbers = numbers; this.rewrites = rewrites; } - public @NonNull Map getNumbers() { + public @NonNull Map getNumbers() { return numbers; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index f825a518e..5308d09ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -1141,22 +1141,22 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : Recipient.self().live().refresh() } - fun updatePhoneNumbers(mapping: Map) { + /** + * Takes a mapping of old->new phone numbers and updates the table to match. + * Intended to be used to handle changing number formats. + */ + fun rewritePhoneNumbers(mapping: Map) { if (mapping.isEmpty()) return - val db = writableDatabase - db.beginTransaction() - try { - val query = "$PHONE = ?" - for ((key, value) in mapping) { - val values = ContentValues().apply { - put(PHONE, value) - } - db.updateWithOnConflict(TABLE_NAME, values, query, arrayOf(key), SQLiteDatabase.CONFLICT_IGNORE) + Log.i(TAG, "Rewriting ${mapping.size} phone numbers.") + + writableDatabase.withinTransaction { + for ((originalE164, updatedE164) in mapping) { + writableDatabase.update(TABLE_NAME) + .values(PHONE to updatedE164) + .where("$PHONE = ?", originalE164) + .run(SQLiteDatabase.CONFLICT_IGNORE) } - db.setTransactionSuccessful() - } finally { - db.endTransaction() } } @@ -2130,6 +2130,27 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return results } + /** + * Gives you all of the recipientIds of possibly-registered users (i.e. REGISTERED or UNKNOWN) that can be found by the set of + * provided E164s. + */ + fun getAllPossiblyRegisteredByE164(e164s: Set): Set { + val results: MutableSet = mutableSetOf() + val queries: List = SqlUtil.buildCollectionQuery(PHONE, e164s) + + for (query in queries) { + readableDatabase.query(TABLE_NAME, arrayOf(ID, REGISTERED), query.where, query.whereArgs, null, null, null).use { cursor -> + while (cursor.moveToNext()) { + if (RegisteredState.fromId(cursor.requireInt(REGISTERED)) != RegisteredState.NOT_REGISTERED) { + results += RecipientId.from(cursor.requireLong(ID)) + } + } + } + } + + return results + } + fun setPni(id: RecipientId, pni: PNI) { writableDatabase .update(TABLE_NAME) @@ -2295,6 +2316,32 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return ids } + fun bulkUpdatedRegisteredStatusV2(registered: Set, unregistered: Collection) { + writableDatabase.withinTransaction { + val registeredValues = contentValuesOf( + REGISTERED to RegisteredState.REGISTERED.id + ) + + for (id in registered) { + if (update(id, registeredValues)) { + setStorageIdIfNotSet(id) + ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) + } + } + + val unregisteredValues = contentValuesOf( + REGISTERED to RegisteredState.NOT_REGISTERED.id, + STORAGE_SERVICE_ID to null + ) + + for (id in unregistered) { + if (update(id, unregisteredValues)) { + ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) + } + } + } + } + /** * Takes a tuple of (e164, pni, aci) and incorporates it into our database. * It is assumed that we are in a transaction. @@ -2302,7 +2349,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : * @return The [RecipientId] of the resulting recipient. */ @VisibleForTesting - fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false, pnpEnabled: Boolean = FeatureFlags.phoneNumberPrivacy()): ProcessPnpTupleResult { + fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false): ProcessPnpTupleResult { val changeSet: PnpChangeSet = processPnpTupleToChangeSet(e164, pni, aci, pniVerified, changeSelf) val affectedIds: MutableSet = mutableSetOf() @@ -2328,7 +2375,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } - val finalId: RecipientId = writePnpChangeSetToDisk(changeSet, pnpEnabled, pni) + val finalId: RecipientId = writePnpChangeSetToDisk(changeSet, pni) return ProcessPnpTupleResult( finalId = finalId, @@ -2341,7 +2388,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } @VisibleForTesting - fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, pnpEnabled: Boolean, inputPni: PNI?): RecipientId { + fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, inputPni: PNI?): RecipientId { for (operation in changeSet.operations) { @Exhaustive when (operation) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 385c9a68d..8fb5a1e99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -103,6 +103,7 @@ public final class FeatureFlags { private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2"; private static final String CDS_V2_LOAD_TEST = "android.cdsV2LoadTest"; private static final String SMS_EXPORTER = "android.sms.exporter"; + private static final String CDS_V2_COMPAT = "android.cdsV2Compat"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -158,7 +159,8 @@ public final class FeatureFlags { CAMERAX_MODEL_BLOCKLIST, RECIPIENT_MERGE_V2, CDS_V2_LOAD_TEST, - SMS_EXPORTER + SMS_EXPORTER, + CDS_V2_COMPAT ); @VisibleForTesting @@ -222,7 +224,8 @@ public final class FeatureFlags { TELECOM_MODEL_BLOCKLIST, CAMERAX_MODEL_BLOCKLIST, RECIPIENT_MERGE_V2, - CDS_V2_LOAD_TEST + CDS_V2_LOAD_TEST, + CDS_V2_COMPAT ); /** @@ -567,6 +570,13 @@ public final class FeatureFlags { return getBoolean(SMS_EXPORTER, false); } + /** + * Whether or not we should use CDSv2 with the compat flag on as our primary CDS. + */ + public static boolean cdsV2Compat() { + return getBoolean(CDS_V2_COMPAT, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelperTest.java b/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelperTest.java index b667b57fc..11bedc77e 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelperTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/FuzzyPhoneNumberHelperTest.java @@ -99,7 +99,7 @@ public class FuzzyPhoneNumberHelperTest { @Test public void generateOutput_noMxNumbers() { - OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(US_A, ACI_A, US_B, ACI_B), new InputResult(setOf(US_A, US_B), Collections.emptyMap())); + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(US_A, ACI_A, US_B, ACI_B), new InputResult(setOf(US_A, US_B), Collections.emptyMap())); assertEquals(2, result.getNumbers().size()); assertEquals(ACI_A, result.getNumbers().get(US_A)); @@ -109,7 +109,7 @@ public class FuzzyPhoneNumberHelperTest { @Test public void generateOutput_bothMatch_no1To1() { - OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A, ACI_A, MX_A_1, ACI_B), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A, ACI_A, MX_A_1, ACI_B), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); assertEquals(1, result.getNumbers().size()); assertEquals(ACI_A, result.getNumbers().get(MX_A)); @@ -118,7 +118,7 @@ public class FuzzyPhoneNumberHelperTest { @Test public void generateOutput_bothMatch_1toNo1() { - OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A, ACI_A, MX_A_1, ACI_B), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A, ACI_A, MX_A_1, ACI_B), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); assertEquals(1, result.getNumbers().size()); assertEquals(ACI_A, result.getNumbers().get(MX_A)); @@ -127,7 +127,7 @@ public class FuzzyPhoneNumberHelperTest { @Test public void generateOutput_no1Match_no1To1() { - OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A, ACI_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A, ACI_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); assertEquals(1, result.getNumbers().size()); assertEquals(ACI_A, result.getNumbers().get(MX_A)); @@ -136,7 +136,7 @@ public class FuzzyPhoneNumberHelperTest { @Test public void generateOutput_no1Match_1ToNo1() { - OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A, ACI_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A, ACI_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); assertEquals(1, result.getNumbers().size()); assertEquals(ACI_A, result.getNumbers().get(MX_A)); @@ -145,7 +145,7 @@ public class FuzzyPhoneNumberHelperTest { @Test public void generateOutput_1Match_1ToNo1() { - OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A_1, ACI_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A_1, ACI_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A))); assertEquals(1, result.getNumbers().size()); assertEquals(ACI_A, result.getNumbers().get(MX_A_1)); @@ -154,7 +154,7 @@ public class FuzzyPhoneNumberHelperTest { @Test public void generateOutput_1Match_no1To1() { - OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A_1, ACI_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); + OutputResult result = FuzzyPhoneNumberHelper.generateOutput(mapOf(MX_A_1, ACI_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1))); assertEquals(1, result.getNumbers().size()); assertEquals(ACI_A, result.getNumbers().get(MX_A_1)); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index ea0f0b0f6..4fd3c7877 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -521,6 +521,7 @@ public class SignalServiceAccountManager { public CdsiV2Service.Response getRegisteredUsersWithCdsi(Set previousE164s, Set newE164s, Map serviceIds, + boolean requireAcis, Optional token, String mrEnclave, Consumer tokenSaver) @@ -528,7 +529,7 @@ public class SignalServiceAccountManager { { CdsiAuthResponse auth = pushServiceSocket.getCdsiAuth(); CdsiV2Service service = new CdsiV2Service(configuration, mrEnclave); - CdsiV2Service.Request request = new CdsiV2Service.Request(previousE164s, newE164s, serviceIds, token); + CdsiV2Service.Request request = new CdsiV2Service.Request(previousE164s, newE164s, serviceIds, requireAcis, token); Single> single = service.getRegisteredUsers(auth.getUsername(), auth.getPassword(), request, tokenSaver); ServiceResponse serviceResponse; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java index 8b70d252f..9b6e2d670 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java @@ -101,7 +101,8 @@ public final class CdsiV2Service { .setPrevE164S(toByteString(previousE164s)) .setNewE164S(toByteString(newE164s)) .setDiscardE164S(toByteString(removedE164s)) - .setAciUakPairs(toByteString(request.serviceIds)); + .setAciUakPairs(toByteString(request.serviceIds)) + .setReturnAcisWithoutUaks(request.requireAcis); if (request.token != null) { builder.setToken(ByteString.copyFrom(request.token)); @@ -154,9 +155,11 @@ public final class CdsiV2Service { final Map serviceIds; + final boolean requireAcis; + final byte[] token; - public Request(Set previousE164s, Set newE164s, Map serviceIds, Optional token) { + public Request(Set previousE164s, Set newE164s, Map serviceIds, boolean requireAcis, Optional token) { if (previousE164s.size() > 0 && !token.isPresent()) { throw new IllegalArgumentException("You must have a token if you have previousE164s!"); } @@ -165,6 +168,7 @@ public final class CdsiV2Service { this.newE164s = newE164s; this.removedE164s = Collections.emptySet(); this.serviceIds = serviceIds; + this.requireAcis = requireAcis; this.token = token.orElse(null); } diff --git a/libsignal/service/src/main/proto/CDSI.proto b/libsignal/service/src/main/proto/CDSI.proto index d5ed445a4..bd1b2ce4f 100644 --- a/libsignal/service/src/main/proto/CDSI.proto +++ b/libsignal/service/src/main/proto/CDSI.proto @@ -28,6 +28,10 @@ message ClientRequest { // After receiving a new token from the server, send back a message just // containing a token_ack. bool token_ack = 7; + + // Request that, if the server allows, both ACI and PNI be returned even + // if the aci_uak_pairs don't match. + bool return_acis_without_uaks = 8; } message ClientResponse {