From 61498037f3efee10e578b185d8fd153f62c01db9 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 24 Aug 2022 18:16:42 -0400 Subject: [PATCH] Add support for PniSignatureMessages. --- .../RecipientDatabaseTest_processPnpTuple.kt | 7 - ...DatabaseTest_processPnpTupleToChangeSet.kt | 5 - .../changenumber/ChangeNumberRepository.kt | 2 +- .../PendingPniSignatureMessageDatabase.kt | 107 +++++++++ .../securesms/database/RecipientDatabase.kt | 215 +++++++++--------- .../securesms/database/SignalDatabase.kt | 8 + .../helpers/SignalDatabaseMigrations.kt | 8 +- .../migration/PniSignaturesMigration.kt | 27 +++ .../database/model/RecipientRecord.kt | 4 +- .../jobs/PaymentNotificationSendJob.java | 6 +- .../securesms/jobs/PushDecryptMessageJob.java | 45 +++- .../securesms/jobs/PushMediaSendJob.java | 8 +- .../securesms/jobs/PushTextSendJob.java | 7 +- .../jobs/SendDeliveryReceiptJob.java | 3 +- .../securesms/jobs/SendReadReceiptJob.java | 3 +- .../securesms/jobs/SendViewedReceiptJob.java | 3 +- .../securesms/messages/GroupSendUtil.java | 24 +- .../messages/MessageContentProcessor.java | 3 + .../messages/MessageDecryptionUtil.java | 12 + .../securesms/recipients/Recipient.java | 21 +- .../recipients/RecipientDetails.java | 3 + .../database/RecipientDatabaseTestUtils.kt | 3 +- .../core/util/SQLiteDatabaseExtensions.kt | 9 + .../api/SignalServiceMessageSender.java | 90 +++++--- .../api/messages/SignalServiceContent.java | 126 ++++++++-- .../api/messages/SignalServiceEnvelope.java | 5 +- .../SignalServicePniSignatureMessage.java | 29 +++ .../signalservice/api/push/PNI.java | 5 + .../src/main/proto/SignalService.proto | 24 +- 29 files changed, 602 insertions(+), 210 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/PendingPniSignatureMessageDatabase.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/PniSignaturesMigration.kt create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServicePniSignatureMessage.java diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTuple.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTuple.kt index c50f051a7c..83c0fd177c 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTuple.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTuple.kt @@ -60,13 +60,6 @@ class RecipientDatabaseTest_processPnpTuple { } } - @Test(expected = IllegalStateException::class) - fun noMatch_pniOnly() { - test { - process(null, PNI_A, null) - } - } - @Test(expected = IllegalStateException::class) fun noMatch_noData() { test { diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTupleToChangeSet.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTupleToChangeSet.kt index eab6aca1f7..cdd6cea579 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTupleToChangeSet.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_processPnpTupleToChangeSet.kt @@ -67,11 +67,6 @@ class RecipientDatabaseTest_processPnpTupleToChangeSet { ) } - @Test(expected = IllegalStateException::class) - fun noMatch_pniOnly() { - db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false) - } - @Test(expected = IllegalStateException::class) fun noMatch_noData() { db.processPnpTupleToChangeSet(null, null, null, pniVerified = false) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt index 8bddec65a5..6df870349b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt @@ -110,7 +110,6 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan if (MessageDigest.isEqual(oldStorageId, newStorageId)) { Log.w(TAG, "Self storage id was not rotated, attempting to rotate again") SignalDatabase.recipients.rotateStorageId(Recipient.self().id) - Recipient.self().live().refresh() StorageSyncHelper.scheduleSyncForDataChange() val secondAttemptStorageId: ByteArray? = Recipient.self().storageServiceId if (MessageDigest.isEqual(oldStorageId, secondAttemptStorageId)) { @@ -119,6 +118,7 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan } SignalDatabase.recipients.setPni(Recipient.self().id, pni) + ApplicationDependencies.getRecipientCache().clear() SignalStore.account().setE164(e164) SignalStore.account().setPni(pni) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PendingPniSignatureMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/PendingPniSignatureMessageDatabase.kt new file mode 100644 index 0000000000..9b99faced8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/PendingPniSignatureMessageDatabase.kt @@ -0,0 +1,107 @@ +package org.thoughtcrime.securesms.database + +import android.content.Context +import androidx.core.content.contentValuesOf +import org.signal.core.util.delete +import org.signal.core.util.exists +import org.signal.core.util.logging.Log +import org.signal.core.util.update +import org.signal.core.util.withinTransaction +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.FeatureFlags +import org.whispersystems.signalservice.api.messages.SendMessageResult + +/** + * Contains records of messages that have been sent with PniSignatures on them. + * When we receive delivery receipts for these messages, we remove entries from the table and can clear + * the `needsPniSignature` flag on the recipient when all are delivered. + */ +class PendingPniSignatureMessageDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) { + + companion object { + private val TAG = Log.tag(PendingPniSignatureMessageDatabase::class.java) + + const val TABLE_NAME = "pending_pni_signature_message" + + private const val ID = "_id" + private const val RECIPIENT_ID = "recipient_id" + private const val SENT_TIMESTAMP = "sent_timestamp" + private const val DEVICE_ID = "device_id" + + const val CREATE_TABLE = """ + CREATE TABLE $TABLE_NAME ( + $ID INTEGER PRIMARY KEY, + $RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE, + $SENT_TIMESTAMP INTEGER NOT NULL, + $DEVICE_ID INTEGER NOT NULL + ) + """ + + val CREATE_INDEXES = arrayOf( + "CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON $TABLE_NAME ($RECIPIENT_ID, $SENT_TIMESTAMP, $DEVICE_ID)" + ) + } + + fun insertIfNecessary(recipientId: RecipientId, sentTimestamp: Long, result: SendMessageResult) { + if (!FeatureFlags.phoneNumberPrivacy()) return + + if (!result.isSuccess) { + return + } + + writableDatabase.withinTransaction { db -> + for (deviceId in result.success.devices) { + val values = contentValuesOf( + RECIPIENT_ID to recipientId.serialize(), + SENT_TIMESTAMP to sentTimestamp, + DEVICE_ID to deviceId + ) + + db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE) + } + } + } + + fun acknowledgeReceipts(recipientId: RecipientId, sentTimestamps: Collection, deviceId: Int) { + if (!FeatureFlags.phoneNumberPrivacy()) return + + writableDatabase.withinTransaction { db -> + val count = db + .delete(TABLE_NAME) + .where("$RECIPIENT_ID = ? AND $SENT_TIMESTAMP IN (?) AND $DEVICE_ID = ?", recipientId, sentTimestamps.joinToString(separator = ","), deviceId) + .run() + + if (count <= 0) { + return@withinTransaction + } + + val stillPending: Boolean = db.exists(TABLE_NAME, "$RECIPIENT_ID = ? AND $SENT_TIMESTAMP = ?", recipientId, sentTimestamps) + + if (!stillPending) { + Log.i(TAG, "All devices for ($recipientId, $sentTimestamps) have acked the PNI signature message. Clearing flag and removing any other pending receipts.") + SignalDatabase.recipients.clearNeedsPniSignature(recipientId) + + db + .delete(TABLE_NAME) + .where("$RECIPIENT_ID = ?", recipientId) + .run() + } + } + } + + /** + * Deletes all record of pending PNI verification messages. Should only be called after the user changes their number. + */ + fun deleteAll() { + if (!FeatureFlags.phoneNumberPrivacy()) return + writableDatabase.delete(TABLE_NAME).run() + } + + fun remapRecipient(oldId: RecipientId, newId: RecipientId) { + writableDatabase + .update(TABLE_NAME) + .values(RECIPIENT_ID to newId.serialize()) + .where("$RECIPIENT_ID = ?", oldId) + .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 7c93ea2bbc..ef8f0079b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -14,7 +14,7 @@ import net.zetetic.database.sqlcipher.SQLiteConstraintException import org.signal.core.util.Bitmask import org.signal.core.util.CursorUtil import org.signal.core.util.SqlUtil -import org.signal.core.util.delete +import org.signal.core.util.exists import org.signal.core.util.logging.Log import org.signal.core.util.optionalBlob import org.signal.core.util.optionalBoolean @@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog import org.thoughtcrime.securesms.database.SignalDatabase.Companion.notificationProfiles +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.pendingPniSignatureMessages import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions import org.thoughtcrime.securesms.database.SignalDatabase.Companion.runPostSuccessfulTransaction import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions @@ -180,6 +181,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : private const val SORT_NAME = "sort_name" private const val IDENTITY_STATUS = "identity_status" private const val IDENTITY_KEY = "identity_key" + private const val NEEDS_PNI_SIGNATURE = "needs_pni_signature" @JvmField val CREATE_TABLE = @@ -237,7 +239,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : $CUSTOM_CHAT_COLORS_ID INTEGER DEFAULT 0, $BADGES BLOB DEFAULT NULL, $PNI_COLUMN TEXT DEFAULT NULL, - $DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL + $DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL, + $NEEDS_PNI_SIGNATURE INTEGER DEFAULT 0 ) """.trimIndent() @@ -297,7 +300,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : CHAT_COLORS, CUSTOM_CHAT_COLORS_ID, BADGES, - DISTRIBUTION_LIST_ID + DISTRIBUTION_LIST_ID, + NEEDS_PNI_SIGNATURE ) private val ID_PROJECTION = arrayOf(ID) @@ -418,9 +422,13 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return getByColumn(USERNAME, username) } + fun isAssociated(serviceId: ServiceId, pni: PNI): Boolean { + return readableDatabase.exists(TABLE_NAME, "$SERVICE_ID = ? AND $PNI_COLUMN = ?", serviceId.toString(), pni.toString()) + } + @JvmOverloads fun getAndPossiblyMerge(serviceId: ServiceId?, e164: String?, changeSelf: Boolean = false): RecipientId { - return if (FeatureFlags.recipientMergeV2()) { + return if (FeatureFlags.recipientMergeV2() || FeatureFlags.phoneNumberPrivacy()) { getAndPossiblyMergePnp(serviceId, e164, changeSelf) } else { getAndPossiblyMergeLegacy(serviceId, e164, changeSelf) @@ -562,7 +570,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } if (result.operations.isNotEmpty()) { - Log.i(TAG, "[getAndPossiblyMergePnp] BreadCrumbs: ${result.breadCrumbs}, Operations: ${result.operations}") + Log.i(TAG, "[getAndPossiblyMergePnp] ($serviceId, $pni, $e164) BreadCrumbs: ${result.breadCrumbs}, Operations: ${result.operations}") } db.setTransactionSuccessful() @@ -2038,6 +2046,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } + /** + * Does *not* handle clearing the recipient cache. It is assumed the caller handles this. + */ fun updateSelfPhone(e164: String) { val db = writableDatabase @@ -2052,6 +2063,13 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : throw AssertionError("[updateSelfPhone] Self recipient id changed when updating phone. old: $id new: $newId") } + db + .update(TABLE_NAME) + .values(NEEDS_PNI_SIGNATURE to 0) + .run() + + SignalDatabase.pendingPniSignatureMessages.deleteAll() + db.setTransactionSuccessful() } finally { db.endTransaction() @@ -2303,7 +2321,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } - val finalId: RecipientId = writePnpChangeSetToDisk(changeSet, pnpEnabled) + val finalId: RecipientId = writePnpChangeSetToDisk(changeSet, pnpEnabled, pni) return ProcessPnpTupleResult( finalId = finalId, @@ -2316,7 +2334,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } @VisibleForTesting - fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, pnpEnabled: Boolean): RecipientId { + fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, pnpEnabled: Boolean, inputPni: PNI?): RecipientId { for (operation in changeSet.operations) { @Exhaustive when (operation) { @@ -2378,29 +2396,13 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : val secondary = getRecord(operation.secondaryId) if (primary.serviceId != null && !primary.sidIsPni() && secondary.e164 != null) { - merge(operation.primaryId, operation.secondaryId) + merge(operation.primaryId, operation.secondaryId, inputPni) } else { if (!pnpEnabled) { throw AssertionError("This type of merge is not supported in production!") } - Log.w(TAG, "WARNING: Performing an unfinished PNP merge! This operation currently only has a basic implementation only suitable for basic testing!") - - writableDatabase - .delete(TABLE_NAME) - .where("$ID = ?", operation.secondaryId) - .run() - - writableDatabase - .update(TABLE_NAME) - .values( - PHONE to (primary.e164 ?: secondary.e164), - PNI_COLUMN to (primary.pni ?: secondary.pni)?.toString(), - SERVICE_ID to (primary.serviceId ?: secondary.serviceId)?.toString(), - REGISTERED to RegisteredState.REGISTERED.id - ) - .where("$ID = ?", operation.primaryId) - .run() + merge(operation.primaryId, operation.secondaryId, inputPni) } } is PnpOperation.SessionSwitchoverInsert -> { @@ -2435,7 +2437,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : @VisibleForTesting fun processPnpTupleToChangeSet(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false): PnpChangeSet { check(e164 != null || pni != null || aci != null) { "Must provide at least one field!" } - check(pni == null || e164 != null) { "If a PNI is provided, you must also provide an E164!" } val breadCrumbs: MutableList = mutableListOf() @@ -3238,6 +3239,25 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } + /** + * Indicates that the recipient knows our PNI, and therefore needs to be sent PNI signature messages until we know that they have our PNI-ACI association. + */ + fun markNeedsPniSignature(recipientId: RecipientId) { + if (update(recipientId, contentValuesOf(NEEDS_PNI_SIGNATURE to 1))) { + Log.i(TAG, "Marked $recipientId as needing a PNI signature message.") + Recipient.live(recipientId).refresh() + } + } + + /** + * Indicates that we successfully told all of this recipient's devices our PNI-ACI association, and therefore no longer needs us to send it to them. + */ + fun clearNeedsPniSignature(recipientId: RecipientId) { + if (update(recipientId, contentValuesOf(NEEDS_PNI_SIGNATURE to 0))) { + Recipient.live(recipientId).refresh() + } + } + fun setHasGroupsInCommon(recipientIds: List) { if (recipientIds.isEmpty()) { return @@ -3401,26 +3421,28 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : * Merges one ACI recipient with an E164 recipient. It is assumed that the E164 recipient does * *not* have an ACI. */ - private fun merge(byAci: RecipientId, byE164: RecipientId): RecipientId { + private fun merge(primaryId: RecipientId, secondaryId: RecipientId, newPni: PNI? = null): RecipientId { ensureInTransaction() val db = writableDatabase - val aciRecord = getRecord(byAci) - val e164Record = getRecord(byE164) + val primaryRecord = getRecord(primaryId) + val secondaryRecord = getRecord(secondaryId) - // Identities - ApplicationDependencies.getProtocolStore().aci().identities().delete(e164Record.e164!!) + // Clean up any E164-based identities (legacy stuff) + if (secondaryRecord.e164 != null) { + ApplicationDependencies.getProtocolStore().aci().identities().delete(secondaryRecord.e164) + } // Group Receipts val groupReceiptValues = ContentValues() - groupReceiptValues.put(GroupReceiptDatabase.RECIPIENT_ID, byAci.serialize()) - db.update(GroupReceiptDatabase.TABLE_NAME, groupReceiptValues, GroupReceiptDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)) + groupReceiptValues.put(GroupReceiptDatabase.RECIPIENT_ID, primaryId.serialize()) + db.update(GroupReceiptDatabase.TABLE_NAME, groupReceiptValues, GroupReceiptDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(secondaryId)) // Groups val groupDatabase = groups - for (group in groupDatabase.getGroupsContainingMember(byE164, false, true)) { + for (group in groupDatabase.getGroupsContainingMember(secondaryId, false, true)) { val newMembers = LinkedHashSet(group.members).apply { - remove(byE164) - add(byAci) + remove(secondaryId) + add(primaryId) } val groupValues = ContentValues().apply { @@ -3429,18 +3451,18 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : db.update(GroupDatabase.TABLE_NAME, groupValues, GroupDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(group.recipientId)) if (group.isV2Group) { - groupDatabase.removeUnmigratedV1Members(group.id.requireV2(), listOf(byE164)) + groupDatabase.removeUnmigratedV1Members(group.id.requireV2(), listOf(secondaryId)) } } // Threads - val threadMerge = threads.merge(byAci, byE164) + val threadMerge = threads.merge(primaryId, secondaryId) // SMS Messages val smsValues = ContentValues().apply { - put(SmsDatabase.RECIPIENT_ID, byAci.serialize()) + put(SmsDatabase.RECIPIENT_ID, primaryId.serialize()) } - db.update(SmsDatabase.TABLE_NAME, smsValues, SmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)) + db.update(SmsDatabase.TABLE_NAME, smsValues, SmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(secondaryId)) if (threadMerge.neededMerge) { val values = ContentValues().apply { @@ -3451,9 +3473,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : // MMS Messages val mmsValues = ContentValues().apply { - put(MmsDatabase.RECIPIENT_ID, byAci.serialize()) + put(MmsDatabase.RECIPIENT_ID, primaryId.serialize()) } - db.update(MmsDatabase.TABLE_NAME, mmsValues, MmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)) + db.update(MmsDatabase.TABLE_NAME, mmsValues, MmsDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(secondaryId)) if (threadMerge.neededMerge) { val values = ContentValues() @@ -3461,35 +3483,14 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : db.update(MmsDatabase.TABLE_NAME, values, MmsDatabase.THREAD_ID + " = ?", SqlUtil.buildArgs(threadMerge.previousThreadId)) } - // Sessions - val localAci: ACI = SignalStore.account().requireAci() - val sessionDatabase = sessions - val hasE164Session = sessionDatabase.getAllFor(localAci, e164Record.e164).isNotEmpty() - val hasAciSession = sessionDatabase.getAllFor(localAci, aciRecord.serviceId.toString()).isNotEmpty() - - if (hasE164Session && hasAciSession) { - Log.w(TAG, "Had a session for both users. Deleting the E164.", true) - sessionDatabase.deleteAllFor(localAci, e164Record.e164) - } else if (hasE164Session && !hasAciSession) { - Log.w(TAG, "Had a session for E164, but not ACI. Re-assigning to the ACI.", true) - val values = ContentValues().apply { - put(SessionDatabase.ADDRESS, aciRecord.serviceId.toString()) - } - db.update(SessionDatabase.TABLE_NAME, values, "${SessionDatabase.ACCOUNT_ID} = ? AND ${SessionDatabase.ADDRESS} = ?", SqlUtil.buildArgs(localAci, e164Record.e164)) - } else if (!hasE164Session && hasAciSession) { - Log.w(TAG, "Had a session for ACI, but not E164. No action necessary.", true) - } else { - Log.w(TAG, "Had no sessions. No action necessary.", true) - } - // MSL - messageLog.remapRecipient(byE164, byAci) + messageLog.remapRecipient(secondaryId, primaryId) // Mentions val mentionRecipientValues = ContentValues().apply { - put(MentionDatabase.RECIPIENT_ID, byAci.serialize()) + put(MentionDatabase.RECIPIENT_ID, primaryId.serialize()) } - db.update(MentionDatabase.TABLE_NAME, mentionRecipientValues, MentionDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)) + db.update(MentionDatabase.TABLE_NAME, mentionRecipientValues, MentionDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(secondaryId)) if (threadMerge.neededMerge) { val mentionThreadValues = ContentValues().apply { @@ -3501,59 +3502,62 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : threads.update(threadMerge.threadId, false, false) // Reactions - reactions.remapRecipient(byE164, byAci) + reactions.remapRecipient(secondaryId, primaryId) // Notification Profiles - notificationProfiles.remapRecipient(byE164, byAci) + notificationProfiles.remapRecipient(secondaryId, primaryId) // DistributionLists - distributionLists.remapRecipient(byE164, byAci) + distributionLists.remapRecipient(secondaryId, primaryId) // Story Sends - storySends.remapRecipient(byE164, byAci) + storySends.remapRecipient(secondaryId, primaryId) + + // PendingPniSignatureMessage + pendingPniSignatureMessages.remapRecipient(secondaryId, primaryId) // Recipient - Log.w(TAG, "Deleting recipient $byE164", true) - db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164)) - RemappedRecords.getInstance().addRecipient(byE164, byAci) + Log.w(TAG, "Deleting recipient $secondaryId", true) + db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(secondaryId)) + RemappedRecords.getInstance().addRecipient(secondaryId, primaryId) - // TODO [pnp] We should pass in the PNI involved in the merge and prefer that over either of the ones in the records val uuidValues = contentValuesOf( - PHONE to e164Record.e164, - PNI_COLUMN to (e164Record.pni ?: aciRecord.pni)?.toString(), - BLOCKED to (e164Record.isBlocked || aciRecord.isBlocked), - MESSAGE_RINGTONE to Optional.ofNullable(aciRecord.messageRingtone).or(Optional.ofNullable(e164Record.messageRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null), - MESSAGE_VIBRATE to if (aciRecord.messageVibrateState != VibrateState.DEFAULT) aciRecord.messageVibrateState.id else e164Record.messageVibrateState.id, - CALL_RINGTONE to Optional.ofNullable(aciRecord.callRingtone).or(Optional.ofNullable(e164Record.callRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null), - CALL_VIBRATE to if (aciRecord.callVibrateState != VibrateState.DEFAULT) aciRecord.callVibrateState.id else e164Record.callVibrateState.id, - NOTIFICATION_CHANNEL to (aciRecord.notificationChannel ?: e164Record.notificationChannel), - MUTE_UNTIL to if (aciRecord.muteUntil > 0) aciRecord.muteUntil else e164Record.muteUntil, - CHAT_COLORS to Optional.ofNullable(aciRecord.chatColors).or(Optional.ofNullable(e164Record.chatColors)).map { colors: ChatColors? -> colors!!.serialize().toByteArray() }.orElse(null), - AVATAR_COLOR to aciRecord.avatarColor.serialize(), - CUSTOM_CHAT_COLORS_ID to Optional.ofNullable(aciRecord.chatColors).or(Optional.ofNullable(e164Record.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null), - SEEN_INVITE_REMINDER to e164Record.insightsBannerTier.id, - DEFAULT_SUBSCRIPTION_ID to e164Record.getDefaultSubscriptionId().orElse(-1), - MESSAGE_EXPIRATION_TIME to if (aciRecord.expireMessages > 0) aciRecord.expireMessages else e164Record.expireMessages, + PHONE to (secondaryRecord.e164 ?: primaryRecord.e164), + SERVICE_ID to (primaryRecord.serviceId ?: secondaryRecord.serviceId)?.toString(), + PNI_COLUMN to (newPni ?: secondaryRecord.pni ?: primaryRecord.pni)?.toString(), + BLOCKED to (secondaryRecord.isBlocked || primaryRecord.isBlocked), + MESSAGE_RINGTONE to Optional.ofNullable(primaryRecord.messageRingtone).or(Optional.ofNullable(secondaryRecord.messageRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null), + MESSAGE_VIBRATE to if (primaryRecord.messageVibrateState != VibrateState.DEFAULT) primaryRecord.messageVibrateState.id else secondaryRecord.messageVibrateState.id, + CALL_RINGTONE to Optional.ofNullable(primaryRecord.callRingtone).or(Optional.ofNullable(secondaryRecord.callRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null), + CALL_VIBRATE to if (primaryRecord.callVibrateState != VibrateState.DEFAULT) primaryRecord.callVibrateState.id else secondaryRecord.callVibrateState.id, + NOTIFICATION_CHANNEL to (primaryRecord.notificationChannel ?: secondaryRecord.notificationChannel), + MUTE_UNTIL to if (primaryRecord.muteUntil > 0) primaryRecord.muteUntil else secondaryRecord.muteUntil, + CHAT_COLORS to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.serialize().toByteArray() }.orElse(null), + AVATAR_COLOR to primaryRecord.avatarColor.serialize(), + CUSTOM_CHAT_COLORS_ID to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null), + SEEN_INVITE_REMINDER to secondaryRecord.insightsBannerTier.id, + DEFAULT_SUBSCRIPTION_ID to secondaryRecord.getDefaultSubscriptionId().orElse(-1), + MESSAGE_EXPIRATION_TIME to if (primaryRecord.expireMessages > 0) primaryRecord.expireMessages else secondaryRecord.expireMessages, REGISTERED to RegisteredState.REGISTERED.id, - SYSTEM_GIVEN_NAME to e164Record.systemProfileName.givenName, - SYSTEM_FAMILY_NAME to e164Record.systemProfileName.familyName, - SYSTEM_JOINED_NAME to e164Record.systemProfileName.toString(), - SYSTEM_PHOTO_URI to e164Record.systemContactPhotoUri, - SYSTEM_PHONE_LABEL to e164Record.systemPhoneLabel, - SYSTEM_CONTACT_URI to e164Record.systemContactUri, - PROFILE_SHARING to (aciRecord.profileSharing || e164Record.profileSharing), - CAPABILITIES to max(aciRecord.rawCapabilities, e164Record.rawCapabilities), - MENTION_SETTING to if (aciRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) aciRecord.mentionSetting.id else e164Record.mentionSetting.id + SYSTEM_GIVEN_NAME to secondaryRecord.systemProfileName.givenName, + SYSTEM_FAMILY_NAME to secondaryRecord.systemProfileName.familyName, + SYSTEM_JOINED_NAME to secondaryRecord.systemProfileName.toString(), + SYSTEM_PHOTO_URI to secondaryRecord.systemContactPhotoUri, + SYSTEM_PHONE_LABEL to secondaryRecord.systemPhoneLabel, + SYSTEM_CONTACT_URI to secondaryRecord.systemContactUri, + PROFILE_SHARING to (primaryRecord.profileSharing || secondaryRecord.profileSharing), + CAPABILITIES to max(primaryRecord.rawCapabilities, secondaryRecord.rawCapabilities), + MENTION_SETTING to if (primaryRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) primaryRecord.mentionSetting.id else secondaryRecord.mentionSetting.id ) - if (aciRecord.profileKey != null) { - updateProfileValuesForMerge(uuidValues, aciRecord) - } else if (e164Record.profileKey != null) { - updateProfileValuesForMerge(uuidValues, e164Record) + if (primaryRecord.profileKey != null) { + updateProfileValuesForMerge(uuidValues, primaryRecord) + } else if (secondaryRecord.profileKey != null) { + updateProfileValuesForMerge(uuidValues, secondaryRecord) } - db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byAci)) - return byAci + db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(primaryId)) + return primaryId } private fun ensureInTransaction() { @@ -3834,7 +3838,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : syncExtras = getSyncExtras(cursor), extras = getExtras(cursor), hasGroupsInCommon = cursor.requireBoolean(GROUPS_IN_COMMON), - badges = parseBadgeList(cursor.requireBlob(BADGES)) + badges = parseBadgeList(cursor.requireBlob(BADGES)), + needsPniSignature = cursor.requireBoolean(NEEDS_PNI_SIGNATURE) ) } 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 51c9802759..a7b5b79558 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt @@ -72,6 +72,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data val storySendsDatabase: StorySendsDatabase = StorySendsDatabase(context, this) val cdsDatabase: CdsDatabase = CdsDatabase(context, this) val remoteMegaphoneDatabase: RemoteMegaphoneDatabase = RemoteMegaphoneDatabase(context, this) + val pendingPniSignatureMessageDatabase: PendingPniSignatureMessageDatabase = PendingPniSignatureMessageDatabase(context, this) override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) { db.setForeignKeyConstraintsEnabled(true) @@ -107,6 +108,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data db.execSQL(StorySendsDatabase.CREATE_TABLE) db.execSQL(CdsDatabase.CREATE_TABLE) db.execSQL(RemoteMegaphoneDatabase.CREATE_TABLE) + db.execSQL(PendingPniSignatureMessageDatabase.CREATE_TABLE) executeStatements(db, SearchDatabase.CREATE_TABLE) executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE) executeStatements(db, MessageSendLogDatabase.CREATE_TABLE) @@ -131,6 +133,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data executeStatements(db, DonationReceiptDatabase.CREATE_INDEXS) db.execSQL(StorySendsDatabase.CREATE_INDEX) executeStatements(db, DistributionListDatabase.CREATE_INDEXES) + executeStatements(db, PendingPniSignatureMessageDatabase.CREATE_INDEXES) executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS) executeStatements(db, ReactionDatabase.CREATE_TRIGGERS) @@ -502,5 +505,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data @get:JvmName("remoteMegaphones") val remoteMegaphones: RemoteMegaphoneDatabase get() = instance!!.remoteMegaphoneDatabase + + @get:JvmStatic + @get:JvmName("pendingPniSignatureMessages") + val pendingPniSignatureMessages: PendingPniSignatureMessageDatabase + get() = instance!!.pendingPniSignatureMessageDatabase } } 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 fef051702a..8f45e52b44 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 @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet import org.thoughtcrime.securesms.database.KeyValueDatabase import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.helpers.migration.MyStoryMigration +import org.thoughtcrime.securesms.database.helpers.migration.PniSignaturesMigration import org.thoughtcrime.securesms.database.helpers.migration.UrgentMslFlagMigration import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList import org.thoughtcrime.securesms.groups.GroupId @@ -208,8 +209,9 @@ object SignalDatabaseMigrations { private const val MY_STORY_MIGRATION = 151 private const val STORY_GROUP_TYPES = 152 private const val MY_STORY_MIGRATION_2 = 153 + private const val PNI_SIGNATURES = 154 - const val DATABASE_VERSION = 153 + const val DATABASE_VERSION = 154 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -2690,6 +2692,10 @@ object SignalDatabaseMigrations { if (oldVersion < MY_STORY_MIGRATION_2) { MyStoryMigration.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < PNI_SIGNATURES) { + PniSignaturesMigration.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/PniSignaturesMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/PniSignaturesMigration.kt new file mode 100644 index 0000000000..e71fd99f3a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/PniSignaturesMigration.kt @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * Introduces the tables and fields required to keep track of whether we need to send a PNI signature message and if the ones we've sent out have been received. + */ +object PniSignaturesMigration : SignalDatabaseMigration { + + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE recipient ADD COLUMN needs_pni_signature") + + db.execSQL( + """ + CREATE TABLE pending_pni_signature_message ( + _id INTEGER PRIMARY KEY, + recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, + sent_timestamp INTEGER NOT NULL, + device_id INTEGER NOT NULL + ) + """.trimIndent() + ) + + db.execSQL("CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON pending_pni_signature_message (recipient_id, sent_timestamp, device_id)") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index c07a4239b9..8346ea86da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -82,7 +82,9 @@ data class RecipientRecord( val extras: Recipient.Extras?, @get:JvmName("hasGroupsInCommon") val hasGroupsInCommon: Boolean, - val badges: List + val badges: List, + @get:JvmName("needsPniSignature") + val needsPniSignature: Boolean ) { fun getDefaultSubscriptionId(): Optional { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java index 5a97dc046a..9b4c412f3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java @@ -110,7 +110,11 @@ public final class PaymentNotificationSendJob extends BaseJob { .withPayment(new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote()))) .build(); - SendMessageResult sendMessageResult = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.DEFAULT, dataMessage, IndividualSendEvents.EMPTY, false); + SendMessageResult sendMessageResult = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.DEFAULT, dataMessage, IndividualSendEvents.EMPTY, false, recipient.needsPniSignature()); + + if (recipient.needsPniSignature()) { + SignalDatabase.pendingPniSignatureMessages().insertIfNecessary(recipientId, dataMessage.getTimestamp(), sendMessageResult); + } if (sendMessageResult.getIdentityFailure() != null) { Log.w(TAG, "Identity failure for " + recipient.getId()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index b5f66cf0d5..351b85b159 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -8,10 +8,13 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import org.signal.core.util.logging.Log; +import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage; import org.thoughtcrime.securesms.MainActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore; +import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -24,6 +27,8 @@ import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.LinkedList; @@ -100,6 +105,10 @@ public final class PushDecryptMessageJob extends BaseJob { handleSenderKeyDistributionMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getSenderKeyDistributionMessage().get()); } + if (result.getContent().getPniSignatureMessage().isPresent()) { + handlePniSignatureMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getPniSignatureMessage().get()); + } + jobs.add(new PushProcessMessageJob(result.getContent(), smsMessageId, envelope.getTimestamp())); } else if (result.getException() != null && result.getState() != MessageState.NOOP) { jobs.add(new PushProcessMessageJob(result.getState(), result.getException(), smsMessageId, envelope.getTimestamp())); @@ -122,11 +131,45 @@ public final class PushDecryptMessageJob extends BaseJob { } private void handleSenderKeyDistributionMessage(@NonNull SignalServiceAddress address, int deviceId, @NonNull SenderKeyDistributionMessage message) { - Log.i(TAG, "Processing SenderKeyDistributionMessage."); + Log.i(TAG, "Processing SenderKeyDistributionMessage from " + address.getServiceId() + "." + deviceId); SignalServiceMessageSender sender = ApplicationDependencies.getSignalServiceMessageSender(); sender.processSenderKeyDistributionMessage(new SignalProtocolAddress(address.getIdentifier(), deviceId), message); } + private void handlePniSignatureMessage(@NonNull SignalServiceAddress address, int deviceId, @NonNull SignalServicePniSignatureMessage pniSignatureMessage) { + Log.i(TAG, "Processing PniSignatureMessage from " + address.getServiceId() + "." + deviceId); + + PNI pni = pniSignatureMessage.getPni(); + + if (SignalDatabase.recipients().isAssociated(address.getServiceId(), pni)) { + Log.i(TAG, "[handlePniSignatureMessage] ACI (" + address.getServiceId() + ") and PNI (" + pni + ") are already associated."); + return; + } + + SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities(); + SignalProtocolAddress aciAddress = new SignalProtocolAddress(address.getIdentifier(), deviceId); + SignalProtocolAddress pniAddress = new SignalProtocolAddress(pni.toString(), deviceId); + IdentityKey aciIdentity = identityStore.getIdentity(aciAddress); + IdentityKey pniIdentity = identityStore.getIdentity(pniAddress); + + if (aciIdentity == null) { + Log.w(TAG, "[validatePniSignature] No identity found for ACI address " + aciAddress); + return; + } + + if (pniIdentity == null) { + Log.w(TAG, "[validatePniSignature] No identity found for PNI address " + pniAddress); + return; + } + + if (pniIdentity.verifyAlternateIdentity(aciIdentity, pniSignatureMessage.getSignature())) { + Log.i(TAG, "[validatePniSignature] PNI signature is valid. Associating ACI (" + address.getServiceId() + ") with PNI (" + pni + ")"); + SignalDatabase.recipients().getAndPossiblyMergePnpVerified(address.getServiceId(), pni, address.getNumber().orElse(null)); + } else { + Log.w(TAG, "[validatePniSignature] Invalid PNI signature! Cannot associate ACI (" + address.getServiceId() + ") with PNI (" + pni + ")"); + } + } + private boolean needsMigration() { return TextSecurePreferences.getNeedsSqlCipherMigration(context); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 016e6bffb0..dbc64c3370 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -257,8 +257,14 @@ public class PushMediaSendJob extends PushSendJob { SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId, true), false); return syncAccess.isPresent(); } else { - SendMessageResult result = messageSender.sendDataMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage, IndividualSendEvents.EMPTY, message.isUrgent()); + SendMessageResult result = messageSender.sendDataMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage, IndividualSendEvents.EMPTY, message.isUrgent(), messageRecipient.needsPniSignature()); + SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId, true), message.isUrgent()); + + if (messageRecipient.needsPniSignature()) { + SignalDatabase.pendingPniSignatureMessages().insertIfNecessary(messageRecipient.getId(), message.getSentTimeMillis(), result); + } + return result.getSuccess().isUnidentified(); } } catch (UnregisteredUserException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 58ee5c4de2..cb0c5e2998 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -198,9 +198,14 @@ public class PushTextSendJob extends PushSendJob { return syncAccess.isPresent(); } else { SignalLocalMetrics.IndividualMessageSend.onDeliveryStarted(messageId); - SendMessageResult result = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.RESENDABLE, textSecureMessage, new MetricEventListener(messageId), true); + SendMessageResult result = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.RESENDABLE, textSecureMessage, new MetricEventListener(messageId), true, messageRecipient.needsPniSignature()); SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getDateSent(), result, ContentHint.RESENDABLE, new MessageId(messageId, false), true); + + if (messageRecipient.needsPniSignature()) { + SignalDatabase.pendingPniSignatureMessages().insertIfNecessary(messageRecipient.getId(), message.getDateSent(), result); + } + return result.getSuccess().isUnidentified(); } } catch (UnregisteredUserException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java index 86fe7fe74b..bdc9cf6b7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java @@ -119,7 +119,8 @@ public class SendDeliveryReceiptJob extends BaseJob { SendMessageResult result = messageSender.sendReceipt(remoteAddress, UnidentifiedAccessUtil.getAccessFor(context, recipient), - receiptMessage); + receiptMessage, + recipient.needsPniSignature()); if (messageId != null) { SignalDatabase.messageLog().insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, messageId, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java index 039a65fbfb..deeca34ab7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java @@ -183,7 +183,8 @@ public class SendReadReceiptJob extends BaseJob { SendMessageResult result = messageSender.sendReceipt(remoteAddress, UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), - receiptMessage); + receiptMessage, + recipient.needsPniSignature()); if (Util.hasItems(messageIds)) { SignalDatabase.messageLog().insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, messageIds, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java index 6c5375de44..d1bfd0d9d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java @@ -179,7 +179,8 @@ public class SendViewedReceiptJob extends BaseJob { SendMessageResult result = messageSender.sendReceipt(remoteAddress, UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), - receiptMessage); + receiptMessage, + recipient.needsPniSignature()); if (Util.hasItems(messageIds)) { SignalDatabase.messageLog().insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, messageIds, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java index a072475445..5b083501ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java @@ -77,6 +77,8 @@ public final class GroupSendUtil { * {@link SendMessageResult}s just like we're used to. * * Messages sent this way, if failed to be decrypted by the receiving party, can be requested to be resent. + * Note that the ContentHint may not be {@link ContentHint#RESENDABLE} -- it just means that we have an actual record of the message + * and we could resend it if asked. * * @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group. * @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false. @@ -348,7 +350,7 @@ public final class GroupSendUtil { final AtomicLong entryId = new AtomicLong(-1); final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog(); - List results = sendOperation.sendLegacy(messageSender, targets, access, recipientUpdate, result -> { + List results = sendOperation.sendLegacy(messageSender, targets, legacyTargets, access, recipientUpdate, result -> { if (!includeInMessageLog) { return; } @@ -416,6 +418,7 @@ public final class GroupSendUtil { @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, + @NonNull List targetRecipients, @NonNull List> access, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @@ -471,14 +474,26 @@ public final class GroupSendUtil { @Override public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, + @NonNull List targetRecipients, @NonNull List> access, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @Nullable CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException { - LegacyGroupEvents listener = relatedMessageId != null ? new LegacyMetricEventListener(relatedMessageId.getId()) : LegacyGroupEvents.EMPTY; - return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message, listener, partialListener, cancelationSignal, urgent); + if (targets.size() == 1 && relatedMessageId == null) { + Recipient targetRecipient = targetRecipients.get(0); + SendMessageResult result = messageSender.sendDataMessage(targets.get(0), access.get(0), contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, targetRecipient.needsPniSignature()); + + if (targetRecipient.needsPniSignature()) { + SignalDatabase.pendingPniSignatureMessages().insertIfNecessary(targetRecipients.get(0).getId(), getSentTimestamp(), result); + } + + return Collections.singletonList(result); + } else { + LegacyGroupEvents listener = relatedMessageId != null ? new LegacyMetricEventListener(relatedMessageId.getId()) : LegacyGroupEvents.EMPTY; + return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message, listener, partialListener, cancelationSignal, urgent); + } } @Override @@ -534,6 +549,7 @@ public final class GroupSendUtil { @Override public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, + @NonNull List targetRecipients, @NonNull List> access, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @@ -592,6 +608,7 @@ public final class GroupSendUtil { @Override public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, + @NonNull List targetRecipients, @NonNull List> access, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, @@ -662,6 +679,7 @@ public final class GroupSendUtil { @Override public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, @NonNull List targets, + @NonNull List targetRecipients, @NonNull List> access, boolean isRecipientUpdate, @Nullable PartialSendCompleteListener partialListener, diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 05ebe2ae54..eaa04f4f3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -383,6 +383,8 @@ public final class MessageContentProcessor { handleRetryReceipt(content, content.getDecryptionErrorMessage().get(), senderRecipient); } else if (content.getSenderKeyDistributionMessage().isPresent()) { // Already handled, here in order to prevent unrecognized message log + } else if (content.getPniSignatureMessage().isPresent()) { + // Already handled, here in order to prevent unrecognized message log } else { warn(String.valueOf(content.getTimestamp()), "Got unrecognized message!"); } @@ -2559,6 +2561,7 @@ public final class MessageContentProcessor { PushProcessEarlyMessagesJob.enqueue(); } + SignalDatabase.pendingPniSignatureMessages().acknowledgeReceipts(senderRecipient.getId(), message.getTimestamps(), content.getSenderDevice()); SignalDatabase.messageLog().deleteEntriesForRecipient(message.getTimestamps(), senderRecipient.getId(), content.getSenderDevice()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java index f58dbb63b3..c39223f5ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java @@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationIds; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.FeatureFlags; import org.whispersystems.signalservice.api.InvalidMessageStructureException; import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; @@ -54,6 +55,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -89,6 +91,16 @@ public final class MessageDecryptionUtil { destination = aci; } + if (destination.equals(pni)) { + if (envelope.hasSourceUuid()) { + RecipientId sender = RecipientId.from(envelope.getSourceAddress()); + SignalDatabase.recipients().markNeedsPniSignature(sender); + } else { + Log.w(TAG, "[" + envelope.getTimestamp() + "] Got a sealed sender message to our PNI? Invalid message, ignoring."); + return DecryptionResult.forNoop(Collections.emptyList()); + } + } + if (!destination.equals(aci) && !destination.equals(pni)) { Log.w(TAG, "Destination of " + destination + " does not match our ACI (" + aci + ") or PNI (" + pni + ")! Defaulting to ACI."); destination = aci; diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index fb23561447..863858e1e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -138,6 +138,7 @@ public class Recipient { private final boolean hasGroupsInCommon; private final List badges; private final boolean isReleaseNotesRecipient; + private final boolean needsPniSignature; /** * Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be @@ -215,20 +216,6 @@ public class Recipient { return externalPush(signalServiceAddress.getServiceId(), signalServiceAddress.getNumber().orElse(null)); } - /** - * Returns a fully-populated {@link Recipient} based off of a {@link SignalServiceAddress}, - * creating one in the database if necessary. We special-case GV1 members because we want to - * prioritize E164 addresses and not use the UUIDs if possible. - */ - @WorkerThread - public static @NonNull Recipient externalGV1Member(@NonNull SignalServiceAddress address) { - if (address.getNumber().isPresent()) { - return externalPush(null, address.getNumber().get()); - } else { - return externalPush(address.getServiceId()); - } - } - /** * Returns a fully-populated {@link Recipient} based off of a ServiceId, creating one * in the database if necessary. @@ -452,6 +439,7 @@ public class Recipient { this.hasGroupsInCommon = false; this.badges = Collections.emptyList(); this.isReleaseNotesRecipient = false; + this.needsPniSignature = false; } public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) { @@ -510,6 +498,7 @@ public class Recipient { this.hasGroupsInCommon = details.hasGroupsInCommon; this.badges = details.badges; this.isReleaseNotesRecipient = details.isReleaseChannel; + this.needsPniSignature = details.needsPniSignature; } public @NonNull RecipientId getId() { @@ -1221,6 +1210,10 @@ public class Recipient { return isReleaseNotesRecipient || isSelf; } + public boolean needsPniSignature() { + return FeatureFlags.phoneNumberPrivacy() && needsPniSignature; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index 81697b5700..25bf12ed6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -88,6 +88,7 @@ public class RecipientDetails { final boolean hasGroupsInCommon; final List badges; final boolean isReleaseChannel; + final boolean needsPniSignature; public RecipientDetails(@Nullable String groupName, @Nullable String systemContactName, @@ -153,6 +154,7 @@ public class RecipientDetails { this.hasGroupsInCommon = record.hasGroupsInCommon(); this.badges = record.getBadges(); this.isReleaseChannel = isReleaseChannel; + this.needsPniSignature = record.needsPniSignature(); } private RecipientDetails() { @@ -210,6 +212,7 @@ public class RecipientDetails { this.hasGroupsInCommon = false; this.badges = Collections.emptyList(); this.isReleaseChannel = false; + this.needsPniSignature = false; } public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientRecord settings) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 2e97eba2a9..88b56bbf4d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -144,7 +144,8 @@ object RecipientDatabaseTestUtils { syncExtras, extras, hasGroupsInCommon, - badges + badges, + false ), participants, isReleaseChannel 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 e5912d20ca..a92fd55984 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 @@ -34,6 +34,15 @@ fun SupportSQLiteDatabase.getTableRowCount(table: String): Int { } } +/** + * Checks if a row exists that matches the query. + */ +fun SupportSQLiteDatabase.exists(table: String, query: String, vararg args: Any): Boolean { + return this.query("SELECT EXISTS(SELECT 1 FROM $table WHERE $query)", SqlUtil.buildArgs(*args)).use { cursor -> + cursor.moveToFirst() && cursor.getInt(0) == 1 + } +} + /** * Begins a SELECT statement with a helpful builder pattern. */ diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 73faa0b439..b83de60c74 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api; import com.google.protobuf.ByteString; import org.signal.libsignal.metadata.certificate.SenderCertificate; +import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidRegistrationIdException; import org.signal.libsignal.protocol.NoSessionException; @@ -63,6 +64,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.DistributionId; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; @@ -81,6 +83,7 @@ import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.Uint64RangeException; import org.whispersystems.signalservice.api.util.Uint64Util; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; @@ -96,6 +99,7 @@ import org.whispersystems.signalservice.internal.push.PushAttachmentData; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse; import org.whispersystems.signalservice.internal.push.SendMessageResponse; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content; @@ -155,11 +159,13 @@ public class SignalServiceMessageSender { private static final int RETRY_COUNT = 4; private final PushServiceSocket socket; - private final SignalServiceAccountDataStore store; + private final SignalServiceAccountDataStore aciStore; private final SignalSessionLock sessionLock; private final SignalServiceAddress localAddress; private final int localDeviceId; + private final PNI localPni; private final Optional eventListener; + private final IdentityKeyPair localPniIdentity; private final AttachmentService attachmentService; private final MessagingService messagingService; @@ -180,15 +186,17 @@ public class SignalServiceMessageSender { boolean automaticNetworkRetry) { this.socket = new PushServiceSocket(urls, credentialsProvider, signalAgent, clientZkProfileOperations, automaticNetworkRetry); - this.store = store.aci(); + this.aciStore = store.aci(); this.sessionLock = sessionLock; this.localAddress = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164()); this.localDeviceId = credentialsProvider.getDeviceId(); + this.localPni = credentialsProvider.getPni(); this.attachmentService = new AttachmentService(signalWebSocket); this.messagingService = new MessagingService(signalWebSocket); this.eventListener = eventListener; this.executor = executor != null ? executor : Executors.newSingleThreadExecutor(); this.maxEnvelopeSize = maxEnvelopeSize; + this.localPniIdentity = store.pni().getIdentityKeyPair(); } /** @@ -199,10 +207,18 @@ public class SignalServiceMessageSender { */ public SendMessageResult sendReceipt(SignalServiceAddress recipient, Optional unidentifiedAccess, - SignalServiceReceiptMessage message) + SignalServiceReceiptMessage message, + boolean includePniSignature) throws IOException, UntrustedIdentityException { - Content content = createReceiptContent(message); + Content content = createReceiptContent(message); + + if (includePniSignature) { + content = content.toBuilder() + .setPniSignatureMessage(createPniSignatureMessage()) + .build(); + } + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); return sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null, false); @@ -264,7 +280,7 @@ public class SignalServiceMessageSender { EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.empty()); List sendMessageResults = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null, false); - if (store.isMultiDevice()) { + if (aciStore.isMultiDevice()) { SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, isRecipientUpdate, manifest); sendSyncMessage(syncMessage, Optional.empty()); } @@ -288,7 +304,7 @@ public class SignalServiceMessageSender { Content content = createStoryContent(message); List sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.IMPLICIT, groupId, false, SenderKeyGroupEvents.EMPTY, false); - if (store.isMultiDevice()) { + if (aciStore.isMultiDevice()) { SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, isRecipientUpdate, manifest); sendSyncMessage(syncMessage, Optional.empty()); } @@ -363,13 +379,22 @@ public class SignalServiceMessageSender { ContentHint contentHint, SignalServiceDataMessage message, IndividualSendEvents sendEvents, - boolean urgent) + boolean urgent, + boolean includePniSignature) throws UntrustedIdentityException, IOException { Log.d(TAG, "[" + message.getTimestamp() + "] Sending a data message."); - Content content = createMessageContent(message); - EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); + Content content = createMessageContent(message); + + if (includePniSignature) { + Log.d(TAG, "[" + message.getTimestamp() + "] Including PNI signature."); + content = content.toBuilder() + .setPniSignatureMessage(createPniSignatureMessage()) + .build(); + } + + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); sendEvents.onMessageEncrypted(); @@ -396,7 +421,7 @@ public class SignalServiceMessageSender { */ public SenderKeyDistributionMessage getOrCreateNewGroupSession(DistributionId distributionId) { SignalProtocolAddress self = new SignalProtocolAddress(localAddress.getIdentifier(), localDeviceId); - return new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).create(self, distributionId.asUuid()); + return new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(aciStore)).create(self, distributionId.asUuid()); } /** @@ -423,7 +448,7 @@ public class SignalServiceMessageSender { * Processes an inbound {@link SenderKeyDistributionMessage}. */ public void processSenderKeyDistributionMessage(SignalProtocolAddress sender, SenderKeyDistributionMessage senderKeyDistributionMessage) { - new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).process(sender, senderKeyDistributionMessage); + new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(aciStore)).process(sender, senderKeyDistributionMessage); } /** @@ -465,7 +490,7 @@ public class SignalServiceMessageSender { sendEvents.onMessageSent(); - if (store.isMultiDevice()) { + if (aciStore.isMultiDevice()) { Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate, Collections.emptySet()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); @@ -511,7 +536,7 @@ public class SignalServiceMessageSender { } } - if (needsSyncInResults || store.isMultiDevice()) { + if (needsSyncInResults || aciStore.isMultiDevice()) { Optional recipient = Optional.empty(); if (!message.getGroupContext().isPresent() && recipients.size() == 1) { recipient = Optional.of(recipients.get(0)); @@ -771,6 +796,15 @@ public class SignalServiceMessageSender { return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, false); } + private SignalServiceProtos.PniSignatureMessage createPniSignatureMessage() { + byte[] signature = localPniIdentity.signAlternateIdentity(aciStore.getIdentityKeyPair().getPublicKey()); + + return SignalServiceProtos.PniSignatureMessage.newBuilder() + .setPni(UuidUtil.toByteString(localPni.uuid())) + .setSignature(ByteString.copyFrom(signature)) + .build(); + } + private Content createTypingContent(SignalServiceTypingMessage message) { Content.Builder container = Content.newBuilder(); TypingMessage.Builder builder = TypingMessage.newBuilder(); @@ -1755,7 +1789,7 @@ public class SignalServiceMessageSender { if (!unidentifiedAccess.isPresent()) { try { SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, Optional.empty()).blockingGet()).getResultOrThrow(); - return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); + return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); } catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) { // Non-technical failures shouldn't be retried with socket throw e; @@ -1768,7 +1802,7 @@ public class SignalServiceMessageSender { } else if (unidentifiedAccess.isPresent()) { try { SendMessageResponse response = new MessagingService.SendResponseProcessor<>(messagingService.send(messages, unidentifiedAccess).blockingGet()).getResultOrThrow(); - return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); + return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); } catch (InvalidUnidentifiedAccessHeaderException | UnregisteredUserException | MismatchedDevicesException | StaleDevicesException e) { // Non-technical failures shouldn't be retried with socket throw e; @@ -1789,7 +1823,7 @@ public class SignalServiceMessageSender { SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess); - return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || store.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); + return SendMessageResult.success(recipient, messages.getDevices(), response.sentUnidentified(), response.getNeedsSync() || aciStore.isMultiDevice(), System.currentTimeMillis() - startTime, content.getContent()); } catch (InvalidKeyException ike) { Log.w(TAG, ike); @@ -1849,7 +1883,7 @@ public class SignalServiceMessageSender { for (int i = 0; i < RETRY_COUNT; i++) { GroupTargetInfo targetInfo = buildGroupTargetInfo(recipients); - Set sharedWith = store.getSenderKeySharedWith(distributionId); + Set sharedWith = aciStore.getSenderKeySharedWith(distributionId); List needsSenderKey = targetInfo.destinations.stream() .filter(a -> !sharedWith.contains(a)) .map(a -> ServiceId.parseOrThrow(a.getName())) @@ -1876,7 +1910,7 @@ public class SignalServiceMessageSender { Set successSids = successes.stream().map(a -> a.getServiceId().toString()).collect(Collectors.toSet()); Set successAddresses = targetInfo.destinations.stream().filter(a -> successSids.contains(a.getName())).collect(Collectors.toSet()); - store.markSenderKeySharedWith(distributionId, successAddresses); + aciStore.markSenderKeySharedWith(distributionId, successAddresses); Log.i(TAG, "[sendGroupMessage][" + timestamp + "] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKey.size() + " recipients."); @@ -1909,7 +1943,7 @@ public class SignalServiceMessageSender { sendEvents.onSenderKeyShared(); - SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, store, sessionLock, null); + SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, aciStore, sessionLock, null); SenderCertificate senderCertificate = unidentifiedAccess.get(0).getUnidentifiedCertificate(); byte[] ciphertext; @@ -1963,7 +1997,7 @@ public class SignalServiceMessageSender { private GroupTargetInfo buildGroupTargetInfo(List recipients) { List addressNames = recipients.stream().map(SignalServiceAddress::getIdentifier).collect(Collectors.toList()); - Set destinations = store.getAllAddressesWithActiveSessions(addressNames); + Set destinations = aciStore.getAllAddressesWithActiveSessions(addressNames); Map> devicesByAddressName = new HashMap<>(); destinations.addAll(recipients.stream() @@ -2009,7 +2043,7 @@ public class SignalServiceMessageSender { List success = recipients.keySet() .stream() .filter(r -> !unregistered.contains(r.getServiceId())) - .map(a -> SendMessageResult.success(a, recipients.get(a), true, store.isMultiDevice(), -1, Optional.of(content))) + .map(a -> SendMessageResult.success(a, recipients.get(a), true, aciStore.isMultiDevice(), -1, Optional.of(content))) .collect(Collectors.toList()); List results = new ArrayList<>(success.size() + failures.size()); @@ -2109,7 +2143,7 @@ public class SignalServiceMessageSender { { List messages = new LinkedList<>(); - List subDevices = store.getSubDeviceSessions(recipient.getIdentifier()); + List subDevices = aciStore.getSubDeviceSessions(recipient.getIdentifier()); List deviceIds = new ArrayList<>(subDevices.size() + 1); deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID); @@ -2120,7 +2154,7 @@ public class SignalServiceMessageSender { } for (int deviceId : deviceIds) { - if (deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID || store.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) { + if (deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID || aciStore.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) { messages.add(getEncryptedMessage(socket, recipient, unidentifiedAccess, deviceId, plaintext)); } } @@ -2136,16 +2170,16 @@ public class SignalServiceMessageSender { throws IOException, InvalidKeyException, UntrustedIdentityException { SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId); - SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, store, sessionLock, null); + SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, aciStore, sessionLock, null); - if (!store.containsSession(signalProtocolAddress)) { + if (!aciStore.containsSession(signalProtocolAddress)) { try { List preKeys = socket.getPreKeys(recipient, unidentifiedAccess, deviceId); for (PreKeyBundle preKey : preKeys) { try { SignalProtocolAddress preKeyAddress = new SignalProtocolAddress(recipient.getIdentifier(), preKey.getDeviceId()); - SignalSessionBuilder sessionBuilder = new SignalSessionBuilder(sessionLock, new SessionBuilder(store, preKeyAddress)); + SignalSessionBuilder sessionBuilder = new SignalSessionBuilder(sessionLock, new SessionBuilder(aciStore, preKeyAddress)); sessionBuilder.process(preKey); } catch (org.signal.libsignal.protocol.UntrustedIdentityException e) { throw new UntrustedIdentityException("Untrusted identity key!", recipient.getIdentifier(), preKey.getIdentityKey()); @@ -2179,7 +2213,7 @@ public class SignalServiceMessageSender { PreKeyBundle preKey = socket.getPreKey(recipient, missingDeviceId); try { - SignalSessionBuilder sessionBuilder = new SignalSessionBuilder(sessionLock, new SessionBuilder(store, new SignalProtocolAddress(recipient.getIdentifier(), missingDeviceId))); + SignalSessionBuilder sessionBuilder = new SignalSessionBuilder(sessionLock, new SessionBuilder(aciStore, new SignalProtocolAddress(recipient.getIdentifier(), missingDeviceId))); sessionBuilder.process(preKey); } catch (org.signal.libsignal.protocol.UntrustedIdentityException e) { throw new UntrustedIdentityException("Untrusted identity key!", recipient.getIdentifier(), preKey.getIdentityKey()); @@ -2199,7 +2233,7 @@ public class SignalServiceMessageSender { List addressesToClear = convertToProtocolAddresses(recipient, devices); for (SignalProtocolAddress address : addressesToClear) { - store.archiveSession(address); + aciStore.archiveSession(address); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 215143cf3d..0cd024379a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -46,6 +46,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.payments.Money; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.StorageKey; @@ -82,17 +83,19 @@ public final class SignalServiceContent { private final Optional groupId; private final String destinationUuid; - private final Optional message; - private final Optional synchronizeMessage; - private final Optional callMessage; - private final Optional readMessage; - private final Optional typingMessage; - private final Optional senderKeyDistributionMessage; - private final Optional decryptionErrorMessage; - private final Optional storyMessage; + private final Optional message; + private final Optional synchronizeMessage; + private final Optional callMessage; + private final Optional readMessage; + private final Optional typingMessage; + private final Optional senderKeyDistributionMessage; + private final Optional decryptionErrorMessage; + private final Optional storyMessage; + private final Optional pniSignatureMessage; private SignalServiceContent(SignalServiceDataMessage message, Optional senderKeyDistributionMessage, + Optional pniSignatureMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -123,10 +126,12 @@ public final class SignalServiceContent { this.senderKeyDistributionMessage = senderKeyDistributionMessage; this.decryptionErrorMessage = Optional.empty(); this.storyMessage = Optional.empty(); + this.pniSignatureMessage = pniSignatureMessage; } private SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, Optional senderKeyDistributionMessage, + Optional pniSignatureMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -157,10 +162,12 @@ public final class SignalServiceContent { this.senderKeyDistributionMessage = senderKeyDistributionMessage; this.decryptionErrorMessage = Optional.empty(); this.storyMessage = Optional.empty(); + this.pniSignatureMessage = pniSignatureMessage; } private SignalServiceContent(SignalServiceCallMessage callMessage, Optional senderKeyDistributionMessage, + Optional pniSignatureMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -191,10 +198,12 @@ public final class SignalServiceContent { this.senderKeyDistributionMessage = senderKeyDistributionMessage; this.decryptionErrorMessage = Optional.empty(); this.storyMessage = Optional.empty(); + this.pniSignatureMessage = pniSignatureMessage; } private SignalServiceContent(SignalServiceReceiptMessage receiptMessage, Optional senderKeyDistributionMessage, + Optional pniSignatureMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -225,10 +234,12 @@ public final class SignalServiceContent { this.senderKeyDistributionMessage = senderKeyDistributionMessage; this.decryptionErrorMessage = Optional.empty(); this.storyMessage = Optional.empty(); + this.pniSignatureMessage = pniSignatureMessage; } private SignalServiceContent(DecryptionErrorMessage errorMessage, Optional senderKeyDistributionMessage, + Optional pniSignatureMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -259,10 +270,12 @@ public final class SignalServiceContent { this.senderKeyDistributionMessage = senderKeyDistributionMessage; this.decryptionErrorMessage = Optional.of(errorMessage); this.storyMessage = Optional.empty(); + this.pniSignatureMessage = pniSignatureMessage; } private SignalServiceContent(SignalServiceTypingMessage typingMessage, Optional senderKeyDistributionMessage, + Optional pniSignatureMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -293,9 +306,11 @@ public final class SignalServiceContent { this.senderKeyDistributionMessage = senderKeyDistributionMessage; this.decryptionErrorMessage = Optional.empty(); this.storyMessage = Optional.empty(); + this.pniSignatureMessage = pniSignatureMessage; } private SignalServiceContent(SenderKeyDistributionMessage senderKeyDistributionMessage, + Optional pniSignatureMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -326,9 +341,11 @@ public final class SignalServiceContent { this.senderKeyDistributionMessage = Optional.of(senderKeyDistributionMessage); this.decryptionErrorMessage = Optional.empty(); this.storyMessage = Optional.empty(); + this.pniSignatureMessage = pniSignatureMessage; } - private SignalServiceContent(SignalServiceStoryMessage storyMessage, + private SignalServiceContent(SignalServicePniSignatureMessage pniSignatureMessage, + Optional senderKeyDistributionMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -356,9 +373,46 @@ public final class SignalServiceContent { this.callMessage = Optional.empty(); this.readMessage = Optional.empty(); this.typingMessage = Optional.empty(); - this.senderKeyDistributionMessage = Optional.empty(); + this.senderKeyDistributionMessage = senderKeyDistributionMessage; + this.decryptionErrorMessage = Optional.empty(); + this.storyMessage = Optional.empty(); + this.pniSignatureMessage = Optional.of(pniSignatureMessage); + } + + private SignalServiceContent(SignalServiceStoryMessage storyMessage, + Optional senderKeyDistributionMessage, + Optional pniSignatureMessage, + SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt, + String serverUuid, + Optional groupId, + String destinationUuid, + SignalServiceContentProto serializedState) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; + this.serverUuid = serverUuid; + this.groupId = groupId; + this.destinationUuid = destinationUuid; + this.serializedState = serializedState; + + this.message = Optional.empty(); + this.synchronizeMessage = Optional.empty(); + this.callMessage = Optional.empty(); + this.readMessage = Optional.empty(); + this.typingMessage = Optional.empty(); + this.senderKeyDistributionMessage = senderKeyDistributionMessage; this.decryptionErrorMessage = Optional.empty(); this.storyMessage = Optional.of(storyMessage); + this.pniSignatureMessage = pniSignatureMessage; } public Optional getDataMessage() { @@ -393,6 +447,10 @@ public final class SignalServiceContent { return decryptionErrorMessage; } + public Optional getPniSignatureMessage() { + return pniSignatureMessage; + } + public SignalServiceAddress getSender() { return sender; } @@ -456,20 +514,7 @@ public final class SignalServiceContent { SignalServiceAddress localAddress = SignalServiceAddressProtobufSerializer.fromProtobuf(serviceContentProto.getLocalAddress()); if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.LEGACYDATAMESSAGE) { - SignalServiceProtos.DataMessage message = serviceContentProto.getLegacyDataMessage(); - - return new SignalServiceContent(createSignalServiceMessage(metadata, message), - Optional.empty(), - metadata.getSender(), - metadata.getSenderDevice(), - metadata.getTimestamp(), - metadata.getServerReceivedTimestamp(), - metadata.getServerDeliveredTimestamp(), - metadata.isNeedsReceipt(), - metadata.getServerGuid(), - metadata.getGroupId(), - metadata.getDestinationUuid(), - serviceContentProto); + throw new InvalidMessageStructureException("Legacy message!"); } else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) { SignalServiceProtos.Content message = serviceContentProto.getContent(); Optional senderKeyDistributionMessage = Optional.empty(); @@ -482,9 +527,21 @@ public final class SignalServiceContent { } } + Optional pniSignatureMessage = Optional.empty(); + + if (message.hasPniSignatureMessage()) { + PNI pni = PNI.parseOrNull(message.getPniSignatureMessage().getPni().toByteArray()); + if (pni != null) { + pniSignatureMessage = Optional.of(new SignalServicePniSignatureMessage(pni, message.getPniSignatureMessage().getSignature().toByteArray())); + } else { + Log.w(TAG, "Invalid PNI on PNI signature message! Ignoring."); + } + } + if (message.hasDataMessage()) { return new SignalServiceContent(createSignalServiceMessage(metadata, message.getDataMessage()), senderKeyDistributionMessage, + pniSignatureMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -498,6 +555,7 @@ public final class SignalServiceContent { } else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) { return new SignalServiceContent(createSynchronizeMessage(metadata, message.getSyncMessage()), senderKeyDistributionMessage, + pniSignatureMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -511,6 +569,7 @@ public final class SignalServiceContent { } else if (message.hasCallMessage()) { return new SignalServiceContent(createCallMessage(message.getCallMessage()), senderKeyDistributionMessage, + pniSignatureMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -524,6 +583,7 @@ public final class SignalServiceContent { } else if (message.hasReceiptMessage()) { return new SignalServiceContent(createReceiptMessage(metadata, message.getReceiptMessage()), senderKeyDistributionMessage, + pniSignatureMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -537,6 +597,7 @@ public final class SignalServiceContent { } else if (message.hasTypingMessage()) { return new SignalServiceContent(createTypingMessage(metadata, message.getTypingMessage()), senderKeyDistributionMessage, + pniSignatureMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -550,6 +611,7 @@ public final class SignalServiceContent { } else if (message.hasDecryptionErrorMessage()) { return new SignalServiceContent(createDecryptionErrorMessage(metadata, message.getDecryptionErrorMessage()), senderKeyDistributionMessage, + pniSignatureMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -562,6 +624,21 @@ public final class SignalServiceContent { serviceContentProto); } else if (message.hasStoryMessage()) { return new SignalServiceContent(createStoryMessage(message.getStoryMessage()), + senderKeyDistributionMessage, + pniSignatureMessage, + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), + false, + metadata.getServerGuid(), + metadata.getGroupId(), + metadata.getDestinationUuid(), + serviceContentProto); + } else if (pniSignatureMessage.isPresent()) { + return new SignalServiceContent(pniSignatureMessage.get(), + senderKeyDistributionMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -575,6 +652,7 @@ public final class SignalServiceContent { } else if (senderKeyDistributionMessage.isPresent()) { // IMPORTANT: This block should always be last, since you can pair SKDM's with other content return new SignalServiceContent(senderKeyDistributionMessage.get(), + pniSignatureMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java index 47b0a5f428..39b9caf6a2 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java @@ -9,10 +9,8 @@ package org.whispersystems.signalservice.api.messages; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; -import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope; @@ -21,7 +19,6 @@ import org.whispersystems.util.Base64; import java.io.IOException; import java.util.Optional; -import java.util.UUID; /** * This class represents an encrypted Signal Service envelope. @@ -162,7 +159,7 @@ public class SignalServiceEnvelope { * @return The envelope's sender as a SignalServiceAddress. */ public SignalServiceAddress getSourceAddress() { - return new SignalServiceAddress(ACI.parseOrNull(envelope.getSourceUuid())); + return new SignalServiceAddress(ServiceId.parseOrNull(envelope.getSourceUuid())); } /** diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServicePniSignatureMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServicePniSignatureMessage.java new file mode 100644 index 0000000000..c53fd13d92 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServicePniSignatureMessage.java @@ -0,0 +1,29 @@ +package org.whispersystems.signalservice.api.messages; + + +import org.whispersystems.signalservice.api.push.PNI; + +/** + * When someone sends a message to your PNI, you need to attach one of these PNI signature messages, + * proving that you own the PNI identity. + * + * The signature is generated by signing your ACI public key with your PNI identity. + */ +public class SignalServicePniSignatureMessage { + + private final PNI pni; + private final byte[] signature; + + public SignalServicePniSignatureMessage(PNI pni, byte[] signature) { + this.pni = pni; + this.signature = signature; + } + + public PNI getPni() { + return pni; + } + + public byte[] getSignature() { + return signature; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java index bb565789c5..a524005909 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java @@ -23,6 +23,11 @@ public final class PNI extends ServiceId { return from(UUID.fromString(raw)); } + public static PNI parseOrNull(byte[] raw) { + UUID uuid = UuidUtil.parseOrNull(raw); + return uuid != null ? from(uuid) : null; + } + private PNI(UUID uuid) { super(uuid); } diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index d967a3bf77..8c0c680184 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -39,15 +39,16 @@ message Envelope { } message Content { - optional DataMessage dataMessage = 1; - optional SyncMessage syncMessage = 2; - optional CallMessage callMessage = 3; - optional NullMessage nullMessage = 4; - optional ReceiptMessage receiptMessage = 5; - optional TypingMessage typingMessage = 6; - optional bytes senderKeyDistributionMessage = 7; - optional bytes decryptionErrorMessage = 8; - optional StoryMessage storyMessage = 9; + optional DataMessage dataMessage = 1; + optional SyncMessage syncMessage = 2; + optional CallMessage callMessage = 3; + optional NullMessage nullMessage = 4; + optional ReceiptMessage receiptMessage = 5; + optional TypingMessage typingMessage = 6; + optional bytes senderKeyDistributionMessage = 7; + optional bytes decryptionErrorMessage = 8; + optional StoryMessage storyMessage = 9; + optional PniSignatureMessage pniSignatureMessage = 10; } message CallMessage { @@ -711,3 +712,8 @@ message DecryptionErrorMessage { optional uint64 timestamp = 2; optional uint32 deviceId = 3; } + +message PniSignatureMessage { + optional bytes pni = 1; + optional bytes signature = 2; +} \ No newline at end of file