From d3096c56cb43c481c0043365e732691ecdf6768b Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 11 Apr 2022 19:59:17 -0400 Subject: [PATCH] Basic client usage of CDSHv2. This provides a basic (read: useful-for-development-yet-broken) client usage of CDSHv2. --- .../app/internal/InternalSettingsFragment.kt | 40 +++++ .../contacts/sync/ContactDiscovery.kt | 19 ++- .../sync/ContactDiscoveryRefreshV2.kt | 150 ++++++++++++++++++ .../securesms/crypto/ProfileKeyUtil.java | 23 +++ .../securesms/database/CdsDatabase.kt | 90 +++++++++++ .../securesms/database/RecipientDatabase.kt | 74 ++++++++- .../securesms/database/SignalDatabase.kt | 27 ++-- .../helpers/SignalDatabaseMigrations.kt | 16 +- .../securesms/jobs/RetrieveProfileJob.java | 4 +- .../keyvalue/MiscellaneousValues.java | 12 ++ .../securesms/util/FeatureFlags.java | 13 +- app/src/main/res/values/strings.xml | 5 + .../core/util/SQLiteDatabaseExtensions.kt | 1 + 13 files changed, 457 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/CdsDatabase.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index d21a275b1b..5f1fe94075 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -406,7 +406,28 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter dividerPref() + sectionHeaderPref(R.string.preferences__internal_cds) + + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_clear_history), + summary = DSLSettingsText.from(R.string.preferences__internal_clear_history_description), + onClick = { + clearCdsHistory() + } + ) + + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_clear_all_service_ids), + summary = DSLSettingsText.from(R.string.preferences__internal_clear_all_service_ids_description), + onClick = { + clearAllServiceIds() + } + ) + + dividerPref() + sectionHeaderPref(R.string.ConversationListTabs__stories) + switchPref( title = DSLSettingsText.from(R.string.preferences__internal_disable_stories), isChecked = state.disableStories, @@ -518,4 +539,23 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter private fun enqueueSubscriptionRedemption() { SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue() } + + private fun clearCdsHistory() { + SignalDatabase.cds.clearAll() + SignalStore.misc().cdsToken = null + Toast.makeText(context, "Cleared all CDS history.", Toast.LENGTH_SHORT).show() + } + + private fun clearAllServiceIds() { + MaterialAlertDialogBuilder(requireContext()) + .setMessage("Are you sure? Never do this on a non-test device.") + .setPositiveButton(android.R.string.ok) { _, _ -> + SignalDatabase.recipients.debugClearServiceIds() + Toast.makeText(context, "Cleared all service IDs.", Toast.LENGTH_SHORT).show() + } + .setNegativeButton(android.R.string.cancel) { d, _ -> + d.dismiss() + } + .show() + } } 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 11e40899de..511ea8bb73 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 @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.registration.RegistrationUtil import org.thoughtcrime.securesms.sms.IncomingJoinedMessage import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.Stopwatch import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util @@ -69,7 +70,11 @@ object ContactDiscovery { context = context, descriptor = "refresh-all", refresh = { - ContactDiscoveryRefreshV1.refreshAll(context) + if (FeatureFlags.usePnpCds()) { + ContactDiscoveryRefreshV2.refreshAll(context) + } else { + ContactDiscoveryRefreshV1.refreshAll(context) + } }, removeSystemContactLinksIfMissing = true, notifyOfNewUsers = notifyOfNewUsers @@ -86,7 +91,11 @@ object ContactDiscovery { context = context, descriptor = "refresh-multiple", refresh = { - ContactDiscoveryRefreshV1.refresh(context, recipients) + if (FeatureFlags.usePnpCds()) { + ContactDiscoveryRefreshV2.refresh(context, recipients) + } else { + ContactDiscoveryRefreshV1.refresh(context, recipients) + } }, removeSystemContactLinksIfMissing = false, notifyOfNewUsers = notifyOfNewUsers @@ -101,7 +110,11 @@ object ContactDiscovery { context = context, descriptor = "refresh-single", refresh = { - ContactDiscoveryRefreshV1.refresh(context, listOf(recipient)) + if (FeatureFlags.usePnpCds()) { + ContactDiscoveryRefreshV2.refresh(context, listOf(recipient)) + } else { + ContactDiscoveryRefreshV1.refresh(context, listOf(recipient)) + } }, removeSystemContactLinksIfMissing = false, notifyOfNewUsers = notifyOfNewUsers 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 new file mode 100644 index 0000000000..016387fc43 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt @@ -0,0 +1,150 @@ +package org.thoughtcrime.securesms.contacts.sync + +import android.content.Context +import androidx.annotation.WorkerThread +import org.signal.contacts.SystemContactsRepository +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.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +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.thoughtcrime.securesms.util.Stopwatch +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.services.CdshV2Service +import java.io.IOException +import java.lang.NumberFormatException +import java.util.Optional + +/** + * Performs the CDS refresh using the V2 interface (either CDSH or CDSI) that returns both PNIs and ACIs. + */ +object ContactDiscoveryRefreshV2 { + + private val TAG = Log.tag(ContactDiscoveryRefreshV2::class.java) + + /** + * The maximum number items we will allow in a 'one-off' request. + * One-off requests, while much faster, will always deduct the request size from our rate limit. + * So we need to be careful about making it too large. + * If a request size is over this limit, we will always fall back to a full sync. + */ + private const val MAXIMUM_ONE_OFF_REQUEST_SIZE = 3 + + @Throws(IOException::class) + @WorkerThread + @Synchronized + @JvmStatic + fun refreshAll(context: Context): ContactDiscovery.RefreshResult { + val stopwatch = Stopwatch("refresh-all") + + val previousE164s: Set = SignalDatabase.cds.getAllE164s() + stopwatch.split("previous") + + 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 + + val response: CdshV2Service.Response = makeRequest( + previousE164s = previousE164s, + newE164s = newE164s, + serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(), + ) + stopwatch.split("network") + + SignalStore.misc().cdsToken = response.token + SignalDatabase.cds.updateAfterCdsQuery(newE164s, recipientE164s + systemE164s) + stopwatch.split("cds-db") + + val registeredIds: Set = SignalDatabase.recipients.bulkProcessCdsV2Result( + response.results + .mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) } + ) + stopwatch.split("recipient-db") + + stopwatch.stop(TAG) + + return ContactDiscovery.RefreshResult(registeredIds, emptyMap()) + } + + @Throws(IOException::class) + @WorkerThread + @Synchronized + @JvmStatic + fun refresh(context: Context, inputRecipients: List): ContactDiscovery.RefreshResult { + val stopwatch = Stopwatch("refresh-some") + + val recipients = inputRecipients.map { it.resolve() } + stopwatch.split("resolve") + + val inputIds: Set = recipients.map { it.id }.toSet() + val inputE164s: Set = recipients.mapNotNull { it.e164.orElse(null) }.toSet() + + if (recipients.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) + + return ContactDiscovery.RefreshResult( + registeredIds = fullResult.registeredIds.intersect(inputIds), + rewrites = fullResult.rewrites.filterKeys { inputE164s.contains(it) } + ) + } + + Log.i(TAG, "Doing a one-off request for ${recipients.size} recipients.") + + val response: CdshV2Service.Response = makeRequest( + previousE164s = emptySet(), + newE164s = inputE164s, + serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs() + ) + stopwatch.split("network") + + val registeredIds: Set = SignalDatabase.recipients.bulkProcessCdsV2Result( + response.results + .mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) } + ) + stopwatch.split("recipient-db") + + stopwatch.stop(TAG) + + return ContactDiscovery.RefreshResult(registeredIds, emptyMap()) + } + + @Throws(IOException::class) + private fun makeRequest(previousE164s: Set, newE164s: Set, serviceIds: Map): CdshV2Service.Response { + return ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdshV2( + previousE164s, + newE164s, + serviceIds, + Optional.ofNullable(SignalStore.misc().cdsToken), + BuildConfig.CDSH_PUBLIC_KEY, + BuildConfig.CDSH_CODE_HASH + ) + } + + private fun Set.toE164s(context: Context): Set { + return this.map { PhoneNumberFormatter.get(context).format(it) }.toSet() + } + + private fun Set.sanitize(): Set { + return this + .filter { + try { + it.startsWith("+") && it.length > 1 && it[1] != '0' && it.toLong() > 0 + } catch (e: NumberFormatException) { + false + } + } + .toSet() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java index bb421de855..a740f2cc93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java @@ -8,8 +8,10 @@ import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Util; +import java.io.IOException; import java.util.Locale; import java.util.Optional; @@ -40,6 +42,27 @@ public final class ProfileKeyUtil { return null; } + public static @Nullable ProfileKey profileKeyOrNull(@Nullable String base64) { + if (base64 == null) { + return null; + } + + byte[] decoded; + try { + decoded = Base64.decode(base64); + } catch (IOException e) { + Log.w(TAG, "Failed to decode profile key."); + return null; + } + + try { + return new ProfileKey(decoded); + } catch (InvalidInputException e) { + Log.w(TAG, String.format(Locale.US, "Seen non-null profile key of wrong length %d", decoded.length), e); + return null; + } + } + public static @Nullable ProfileKeyCredential profileKeyCredentialOrNull(@Nullable byte[] profileKeyCredential) { if (profileKeyCredential != null) { try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CdsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CdsDatabase.kt new file mode 100644 index 0000000000..605957b927 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CdsDatabase.kt @@ -0,0 +1,90 @@ +package org.thoughtcrime.securesms.database + +import android.content.ContentValues +import android.content.Context +import androidx.core.content.contentValuesOf +import org.signal.core.util.SqlUtil +import org.signal.core.util.delete +import org.signal.core.util.logging.Log +import org.signal.core.util.requireNonNullString +import org.signal.core.util.select +import org.signal.core.util.update + +/** + * Keeps track of the numbers we've previously queried CDS for. + * + * This is important for rate-limiting: our rate-limiting strategy hinges on keeping + * an accurate history of numbers we've queried so that we're only "charged" for + * querying new numbers. + */ +class CdsDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) { + companion object { + private val TAG = Log.tag(CdsDatabase::class.java) + + const val TABLE_NAME = "cds" + + private const val ID = "_id" + const val E164 = "e164" + private const val LAST_SEEN_AT = "last_seen_at" + + const val CREATE_TABLE = """ + CREATE TABLE $TABLE_NAME ( + $ID INTEGER PRIMARY KEY, + $E164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE, + $LAST_SEEN_AT INTEGER DEFAULT 0 + ) + """ + } + + fun getAllE164s(): Set { + val e164s: MutableSet = mutableSetOf() + + readableDatabase + .select(E164) + .from(TABLE_NAME) + .run() + .use { cursor -> + while (cursor.moveToNext()) { + e164s += cursor.requireNonNullString(E164) + } + } + + return e164s + } + + /** + * @param newE164s The newly-added E164s that we hadn't previously queried for. + * @param seenE164s The E164s that were seen in either the system contacts or recipients table. + * This should be a superset of [newE164s] + * + */ + fun updateAfterCdsQuery(newE164s: Set, seenE164s: Set) { + val lastSeen = System.currentTimeMillis() + + writableDatabase.beginTransaction() + try { + val insertValues: List = newE164s.map { contentValuesOf(E164 to it) } + + SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(E164), insertValues) + .forEach { writableDatabase.execSQL(it.where, it.whereArgs) } + + for (e164 in seenE164s) { + writableDatabase + .update(TABLE_NAME) + .values(LAST_SEEN_AT to lastSeen) + .where("$E164 = ?", e164) + .run() + } + + writableDatabase.setTransactionSuccessful() + } finally { + writableDatabase.endTransaction() + } + } + + fun clearAll() { + writableDatabase + .delete(TABLE_NAME) + .run() + } +} 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 3b3519f61e..b7a0e343a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -25,6 +25,8 @@ import org.signal.core.util.requireInt import org.signal.core.util.requireLong import org.signal.core.util.requireNonNullString import org.signal.core.util.requireString +import org.signal.core.util.select +import org.signal.core.util.update import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.InvalidKeyException import org.signal.libsignal.zkgroup.InvalidInputException @@ -237,7 +239,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : val CREATE_INDEXS = arrayOf( "CREATE INDEX IF NOT EXISTS recipient_group_type_index ON $TABLE_NAME ($GROUP_TYPE);", - "CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON $TABLE_NAME ($PNI_COLUMN)" + "CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON $TABLE_NAME ($PNI_COLUMN)", + "CREATE INDEX IF NOT EXISTS recipient_service_id_profile_key ON $TABLE_NAME ($SERVICE_ID, $PROFILE_KEY) WHERE $SERVICE_ID NOT NULL AND $PROFILE_KEY NOT NULL" ) private val RECIPIENT_PROJECTION: Array = arrayOf( @@ -499,6 +502,28 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } + fun getAllServiceIdProfileKeyPairs(): Map { + val serviceIdToProfileKey: MutableMap = mutableMapOf() + + readableDatabase + .select(SERVICE_ID, PROFILE_KEY) + .from(TABLE_NAME) + .where("$SERVICE_ID NOT NULL AND $PROFILE_KEY NOT NULL") + .run() + .use { cursor -> + while (cursor.moveToNext()) { + val serviceId: ServiceId? = ServiceId.parseOrNull(cursor.requireString(SERVICE_ID)) + val profileKey: ProfileKey? = ProfileKeyUtil.profileKeyOrNull(cursor.requireString(PROFILE_KEY)) + + if (serviceId != null && profileKey != null) { + serviceIdToProfileKey[serviceId] = profileKey + } + } + } + + return serviceIdToProfileKey + } + private fun fetchRecipient(serviceId: ServiceId?, e164: String?, highTrust: Boolean, changeSelf: Boolean): RecipientFetch { val byE164 = e164?.let { getByE164(it) } ?: Optional.empty() val byAci = serviceId?.let { getByServiceId(it) } ?: Optional.empty() @@ -2087,6 +2112,27 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return aciMap } + /** + * A dumb implementation of processing CDSv2 results. Suitable only for testing and not for actual use. + */ + fun bulkProcessCdsV2Result(mapping: Map): Set { + val ids: MutableSet = mutableSetOf() + val db = writableDatabase + + db.beginTransaction() + try { + for ((e164, result) in mapping) { + ids += getAndPossiblyMerge(result.bestServiceId(), e164, true) + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + + return ids + } + fun getUninvitedRecipientsForInsights(): List { val results: MutableList = LinkedList() val args = arrayOf((System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)).toString()) @@ -2876,6 +2922,19 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } + /** + * Should only be used for debugging! A very destructive action that clears all known serviceIds. + */ + fun debugClearServiceIds() { + writableDatabase + .update(TABLE_NAME) + .values( + SERVICE_ID to null, + PNI_COLUMN to null + ) + .run() + } + fun getRecord(context: Context, cursor: Cursor): RecipientRecord { return getRecord(context, cursor, ID) } @@ -3431,4 +3490,17 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : val serviceId: ServiceId? = null, val e164: String? = null ) + + data class CdsV2Result( + val pni: PNI, + val aci: ACI? + ) { + fun bestServiceId(): ServiceId { + return if (aci != null) { + aci + } else { + pni + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt index 88fa2da816..25601f05a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt @@ -71,6 +71,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data val donationReceiptDatabase: DonationReceiptDatabase = DonationReceiptDatabase(context, this) val distributionListDatabase: DistributionListDatabase = DistributionListDatabase(context, this) val storySendsDatabase: StorySendsDatabase = StorySendsDatabase(context, this) + val cdsDatabase: CdsDatabase = CdsDatabase(context, this) override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) { db.enableWriteAheadLogging() @@ -105,6 +106,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data db.execSQL(ReactionDatabase.CREATE_TABLE) db.execSQL(DonationReceiptDatabase.CREATE_TABLE) db.execSQL(StorySendsDatabase.CREATE_TABLE) + db.execSQL(CdsDatabase.CREATE_TABLE) executeStatements(db, SearchDatabase.CREATE_TABLE) executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE) executeStatements(db, MessageSendLogDatabase.CREATE_TABLE) @@ -328,6 +330,11 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data val avatarPicker: AvatarPickerDatabase get() = instance!!.avatarPickerDatabase + @get:JvmStatic + @get:JvmName("cds") + val cds: CdsDatabase + get() = instance!!.cdsDatabase + @get:JvmStatic @get:JvmName("chatColors") val chatColors: ChatColorsDatabase @@ -338,6 +345,11 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data val distributionLists: DistributionListDatabase get() = instance!!.distributionListDatabase + @get:JvmStatic + @get:JvmName("donationReceipts") + val donationReceipts: DonationReceiptDatabase + get() = instance!!.donationReceiptDatabase + @get:JvmStatic @get:JvmName("drafts") val drafts: DraftDatabase @@ -474,19 +486,14 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data val stickers: StickerDatabase get() = instance!!.stickerDatabase - @get:JvmStatic - @get:JvmName("unknownStorageIds") - val unknownStorageIds: UnknownStorageIdDatabase - get() = instance!!.storageIdDatabase - - @get:JvmStatic - @get:JvmName("donationReceipts") - val donationReceipts: DonationReceiptDatabase - get() = instance!!.donationReceiptDatabase - @get:JvmStatic @get:JvmName("storySends") val storySends: StorySendsDatabase get() = instance!!.storySendsDatabase + + @get:JvmStatic + @get:JvmName("unknownStorageIds") + val unknownStorageIds: UnknownStorageIdDatabase + get() = instance!!.storageIdDatabase } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 8d11b014ef..79b2164980 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -193,8 +193,9 @@ object SignalDatabaseMigrations { private const val STORY_TYPE_AND_DISTRIBUTION = 137 private const val CLEAN_DELETED_DISTRIBUTION_LISTS = 138 private const val REMOVE_KNOWN_UNKNOWNS = 139 + private const val CDS_V2 = 140 - const val DATABASE_VERSION = 139 + const val DATABASE_VERSION = 140 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -2509,6 +2510,19 @@ object SignalDatabaseMigrations { val count: Int = db.delete("storage_key", "type <= ?", SqlUtil.buildArgs(4)) Log.i(TAG, "Cleaned up $count invalid unknown records.") } + + if (oldVersion < CDS_V2) { + db.execSQL("CREATE INDEX IF NOT EXISTS recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL") + db.execSQL( + """ + CREATE TABLE cds ( + _id INTEGER PRIMARY KEY, + e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE, + last_seen_at INTEGER DEFAULT 0 + ) + """ + ) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index d00ce5da7a..d6358a7cb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.Base64; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.ProfileUtil; import org.signal.core.util.SetUtil; @@ -306,7 +307,8 @@ public class RetrieveProfileJob extends BaseJob { }); recipientDatabase.markProfilesFetched(success, System.currentTimeMillis()); - if (operationState.unregistered.size() > 0 || newlyRegistered.size() > 0) { + // XXX The service hasn't implemented profiles for PNIs yet, so if using PNP CDS we don't want to mark users without profiles as unregistered. + if ((operationState.unregistered.size() > 0 || newlyRegistered.size() > 0) && !FeatureFlags.usePnpCds()) { Log.i(TAG, "Marking " + newlyRegistered.size() + " users as registered and " + operationState.unregistered.size() + " users as unregistered."); recipientDatabase.bulkUpdatedRegisteredStatus(newlyRegistered, operationState.unregistered); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index 39d6c5364e..a2370bb146 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.keyvalue; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.Collections; import java.util.List; @@ -19,6 +20,7 @@ public final class MiscellaneousValues extends SignalStoreValues { private static final String CENSORSHIP_LAST_CHECK_TIME = "misc.censorship.last_check_time"; private static final String CENSORSHIP_SERVICE_REACHABLE = "misc.censorship.service_reachable"; private static final String LAST_GV2_PROFILE_CHECK_TIME = "misc.last_gv2_profile_check_time"; + private static final String CDS_TOKEN = "misc.cds_token"; MiscellaneousValues(@NonNull KeyValueStore store) { super(store); @@ -137,4 +139,14 @@ public final class MiscellaneousValues extends SignalStoreValues { public void setLastGv2ProfileCheckTime(long value) { putLong(LAST_GV2_PROFILE_CHECK_TIME, value); } + + public @Nullable byte[] getCdsToken() { + return getBlob(CDS_TOKEN, null); + } + + public void setCdsToken(@Nullable byte[] token) { + getStore().beginWrite() + .putBlob(CDS_TOKEN, token) + .commit(); + } } 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 26e313c3a7..1912ceb124 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -95,6 +95,7 @@ public final class FeatureFlags { private static final String USE_HARDWARE_AEC_IF_OLD = "android.calling.useHardwareAecIfOlderThanApi29"; private static final String USE_AEC3 = "android.calling.useAec3"; private static final String PAYMENTS_COUNTRY_BLOCKLIST = "android.payments.blocklist"; + private static final String PNP_CDS = "android.pnp.cds"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -147,7 +148,8 @@ public final class FeatureFlags { @VisibleForTesting static final Set NOT_REMOTE_CAPABLE = SetUtil.newHashSet( - PHONE_NUMBER_PRIVACY_VERSION + PHONE_NUMBER_PRIVACY_VERSION, + PNP_CDS ); /** @@ -494,6 +496,15 @@ public final class FeatureFlags { return getBoolean(USE_AEC3, true); } + /** + * Whether or not to use the phone number privacy CDS flow. Only currently works in staging. + * + * Note: This feature is in very early stages of development and *will* break your contacts. + */ + public static boolean usePnpCds() { + return Environment.IS_STAGING && getBoolean(PNP_CDS, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0a2a43732..dd6c2b8a96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2709,6 +2709,11 @@ Set last version seen back 10 versions Add sample note Disable stories + CDS + Clear history + Clears all CDS history, meaning the next sync will consider all numbers to be new. + Clear all service IDs + Clears all known service IDs. Do not use on your personal device! diff --git a/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt b/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt index 1105b572fa..b8e5ae94dc 100644 --- a/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt +++ b/core-util/src/main/java/org/signal/core/util/SQLiteDatabaseExtensions.kt @@ -224,3 +224,4 @@ class DeleteBuilderPart2( return db.delete(tableName, where, whereArgs) } } +