Add basic CDSv2 database writes and unit tests.

This commit is contained in:
Greyson Parrelli
2022-05-16 12:15:11 -04:00
committed by Cody Henthorne
parent 307be5c75e
commit dda5ce4809
8 changed files with 324 additions and 27 deletions

View File

@@ -74,7 +74,7 @@ object ContactDiscovery {
context = context,
descriptor = "refresh-all",
refresh = {
if (FeatureFlags.usePnpCds()) {
if (FeatureFlags.phoneNumberPrivacy()) {
ContactDiscoveryRefreshV2.refreshAll(context)
} else {
ContactDiscoveryRefreshV1.refreshAll(context)
@@ -95,7 +95,7 @@ object ContactDiscovery {
context = context,
descriptor = "refresh-multiple",
refresh = {
if (FeatureFlags.usePnpCds()) {
if (FeatureFlags.phoneNumberPrivacy()) {
ContactDiscoveryRefreshV2.refresh(context, recipients)
} else {
ContactDiscoveryRefreshV1.refresh(context, recipients)
@@ -114,7 +114,7 @@ object ContactDiscovery {
context = context,
descriptor = "refresh-single",
refresh = {
if (FeatureFlags.usePnpCds()) {
if (FeatureFlags.phoneNumberPrivacy()) {
ContactDiscoveryRefreshV2.refresh(context, listOf(recipient))
} else {
ContactDiscoveryRefreshV1.refresh(context, listOf(recipient))

View File

@@ -121,8 +121,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
const val TABLE_NAME = "recipient"
const val ID = "_id"
private const val SERVICE_ID = "uuid"
private const val PNI_COLUMN = "pni"
const val SERVICE_ID = "uuid"
const val PNI_COLUMN = "pni"
private const val USERNAME = "username"
const val PHONE = "phone"
const val EMAIL = "email"
@@ -403,6 +403,14 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return getByColumn(SERVICE_ID, serviceId.toString())
}
/**
* Will return a recipient matching the PNI, but only in the explicit [PNI_COLUMN]. This should only be checked in conjunction with [getByServiceId] as a way
* to avoid creating a recipient we already merged.
*/
fun getByPni(pni: PNI): Optional<RecipientId> {
return getByColumn(PNI_COLUMN, pni.toString())
}
fun getByUsername(username: String): Optional<RecipientId> {
return getByColumn(USERNAME, username)
}
@@ -2131,7 +2139,11 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
/**
* A dumb implementation of processing CDSv2 results. Suitable only for testing and not for actual use.
* Processes CDSv2 results, merging recipients as necessary.
*
* Important: This is under active development and is not suitable for actual use.
*
* @return A set of [RecipientId]s that were updated/inserted.
*/
fun bulkProcessCdsV2Result(mapping: Map<String, CdsV2Result>): Set<RecipientId> {
val ids: MutableSet<RecipientId> = mutableSetOf()
@@ -2140,7 +2152,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
db.beginTransaction()
try {
for ((e164, result) in mapping) {
ids += getAndPossiblyMerge(result.bestServiceId(), e164, true)
ids += processCdsV2Result(e164, result.pni, result.aci)
}
db.setTransactionSuccessful()
@@ -2151,6 +2163,47 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return ids
}
@VisibleForTesting
fun processCdsV2Result(e164: String, pni: PNI, aci: ACI?): RecipientId {
val byE164: RecipientId? = getByE164(e164).orElse(null)
val byPni: RecipientId? = getByServiceId(pni).orElse(null)
val byPniOnly: RecipientId? = getByPni(pni).orElse(null)
val byAci: RecipientId? = aci?.let { getByServiceId(it).orElse(null) }
val commonId: RecipientId? = listOf(byE164, byPni, byPniOnly, byAci).commonId()
val allRequiredDbFields: List<RecipientId?> = if (aci != null) listOf(byE164, byAci, byPniOnly) else listOf(byE164, byPni, byPniOnly)
val allRequiredDbFieldPopulated: Boolean = allRequiredDbFields.all { it != null }
// All ID's agree and the database is up-to-date
if (commonId != null && allRequiredDbFieldPopulated) {
return commonId
}
// All ID's agree but we need to update the database
if (commonId != null && !allRequiredDbFieldPopulated) {
writableDatabase
.update(TABLE_NAME)
.values(
PHONE to e164,
SERVICE_ID to (aci ?: pni).toString(),
PNI_COLUMN to pni.toString(),
REGISTERED to RegisteredState.REGISTERED.id,
STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey())
)
.where("$ID = ?", commonId)
.run()
return commonId
}
// Nothing matches
if (byE164 == null && byPni == null && byAci == null) {
val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForCdsInsert(e164, pni, aci))
return RecipientId.from(id)
}
throw NotImplementedError("Handle cases where IDs map to different individuals")
}
fun getUninvitedRecipientsForInsights(): List<RecipientId> {
val results: MutableList<RecipientId> = LinkedList()
val args = arrayOf((System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)).toString())
@@ -2865,6 +2918,18 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return values
}
private fun buildContentValuesForCdsInsert(e164: String, pni: PNI, aci: ACI?): ContentValues {
val serviceId: ServiceId = aci ?: pni
return ContentValues().apply {
put(PHONE, e164)
put(SERVICE_ID, serviceId.toString())
put(PNI_COLUMN, pni.toString())
put(REGISTERED, RegisteredState.REGISTERED.id)
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
put(AVATAR_COLOR, AvatarColor.random().serialize())
}
}
private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues {
return ContentValues().apply {
val profileName = ProfileName.fromParts(contact.givenName.orElse(null), contact.familyName.orElse(null))
@@ -2940,6 +3005,22 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
}
/**
* @return The common id if all non-null ids are equal, or null if all are null or at least one non-null pair doesn't match.
*/
private fun Collection<RecipientId?>.commonId(): RecipientId? {
val nonNull = this.filterNotNull()
if (nonNull.isEmpty()) {
return null
}
return if (nonNull.all { it.equals(nonNull[0]) }) {
nonNull[0]
} else {
null
}
}
/**
* Should only be used for debugging! A very destructive action that clears all known serviceIds.
*/

View File

@@ -307,7 +307,7 @@ public class RetrieveProfileJob extends BaseJob {
recipientDatabase.markProfilesFetched(success, System.currentTimeMillis());
// 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()) {
if ((operationState.unregistered.size() > 0 || newlyRegistered.size() > 0) && !FeatureFlags.phoneNumberPrivacy()) {
Log.i(TAG, "Marking " + newlyRegistered.size() + " users as registered and " + operationState.unregistered.size() + " users as unregistered.");
recipientDatabase.bulkUpdatedRegisteredStatus(newlyRegistered, operationState.unregistered);
}

View File

@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
import org.thoughtcrime.securesms.messages.MessageDecryptionUtil.DecryptionResult;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
@@ -30,6 +31,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.Closeable;
import java.io.IOException;
@@ -81,6 +83,11 @@ public class IncomingMessageProcessor {
* one was created. Otherwise null.
*/
public @Nullable String processEnvelope(@NonNull SignalServiceEnvelope envelope) {
if (FeatureFlags.phoneNumberPrivacy() && envelope.hasSourceE164()) {
Log.w(TAG, "PNP enabled -- mimicking PNP by dropping the E164 from the envelope.");
envelope = envelope.withoutE164();
}
if (envelope.hasSourceUuid()) {
Recipient.externalHighTrustPush(context, envelope.getSourceAddress());
}

View File

@@ -81,7 +81,7 @@ public final class MessageDecryptionUtil {
ServiceId pni = SignalStore.account().requirePni();
ServiceId destination;
if (!FeatureFlags.usePnpCds()) {
if (!FeatureFlags.phoneNumberPrivacy()) {
destination = aci;
} else if (envelope.hasDestinationUuid()) {
destination = ServiceId.parseOrThrow(envelope.getDestinationUuid());

View File

@@ -60,7 +60,6 @@ public final class FeatureFlags {
private static final String GROUP_NAME_MAX_LENGTH = "global.groupsv2.maxNameLength";
private static final String INTERNAL_USER = "android.internalUser";
private static final String VERIFY_V2 = "android.verifyV2";
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
private static final String CLIENT_EXPIRATION = "android.clientExpiration";
public static final String DONATE_MEGAPHONE = "android.donate.2";
private static final String CUSTOM_VIDEO_MUXER = "android.customVideoMuxer";
@@ -94,7 +93,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";
private static final String PHONE_NUMBER_PRIVACY = "android.pnp";
private static final String USE_FCM_FOREGROUND_SERVICE = "android.useFcmForegroundService.3";
private static final String STORIES_AUTO_DOWNLOAD_MAXIMUM = "android.stories.autoDownloadMaximum";
private static final String GIFT_BADGES = "android.giftBadges.2";
@@ -152,8 +151,7 @@ public final class FeatureFlags {
@VisibleForTesting
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(
PHONE_NUMBER_PRIVACY_VERSION,
PNP_CDS
PHONE_NUMBER_PRIVACY
);
/**
@@ -327,11 +325,11 @@ public final class FeatureFlags {
}
/**
* Whether the user can choose phone number privacy settings, and;
* Whether to fetch and store the secondary certificate
* Whether phone number privacy is enabled.
* IMPORTANT: This is under active development. Enabling this *will* break your contacts in terrible, irreversible ways.
*/
public static boolean phoneNumberPrivacy() {
return getVersionFlag(PHONE_NUMBER_PRIVACY_VERSION) == VersionFlag.ON;
return getBoolean(PHONE_NUMBER_PRIVACY, false) && Environment.IS_STAGING;
}
/** Whether to use the custom streaming muxer or built in android muxer. */
@@ -493,15 +491,6 @@ 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);
}
public static boolean useFcmForegroundService() {
return getBoolean(USE_FCM_FOREGROUND_SERVICE, false);
}