Add support for PniSignatureMessages.

This commit is contained in:
Greyson Parrelli 2022-08-24 18:16:42 -04:00 committed by GitHub
parent 1e499fd12f
commit 61498037f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 602 additions and 210 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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<Long>, 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()
}
}

View file

@ -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<String> = 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<RecipientId?>) {
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)
)
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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)")
}
}

View file

@ -82,7 +82,9 @@ data class RecipientRecord(
val extras: Recipient.Extras?,
@get:JvmName("hasGroupsInCommon")
val hasGroupsInCommon: Boolean,
val badges: List<Badge>
val badges: List<Badge>,
@get:JvmName("needsPniSignature")
val needsPniSignature: Boolean
) {
fun getDefaultSubscriptionId(): Optional<Int> {

View file

@ -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());

View file

@ -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);
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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 <em>may not</em> be {@link ContentHint#RESENDABLE} -- it just means that we have an actual record of the message
* and we <em>could</em> 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<SendMessageResult> results = sendOperation.sendLegacy(messageSender, targets, access, recipientUpdate, result -> {
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, targets, legacyTargets, access, recipientUpdate, result -> {
if (!includeInMessageLog) {
return;
}
@ -416,6 +418,7 @@ public final class GroupSendUtil {
@NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,
@ -471,14 +474,26 @@ public final class GroupSendUtil {
@Override
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> 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<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,
@ -592,6 +608,7 @@ public final class GroupSendUtil {
@Override
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,
@ -662,6 +679,7 @@ public final class GroupSendUtil {
@Override
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Recipient> targetRecipients,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,

View file

@ -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());
}

View file

@ -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;

View file

@ -138,6 +138,7 @@ public class Recipient {
private final boolean hasGroupsInCommon;
private final List<Badge> 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;

View file

@ -88,6 +88,7 @@ public class RecipientDetails {
final boolean hasGroupsInCommon;
final List<Badge> 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) {

View file

@ -144,7 +144,8 @@ object RecipientDatabaseTestUtils {
syncExtras,
extras,
hasGroupsInCommon,
badges
badges,
false
),
participants,
isReleaseChannel

View file

@ -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.
*/

View file

@ -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> 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<UnidentifiedAccessPair> 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<SendMessageResult> 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<SendMessageResult> 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<SignalServiceAddress> 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<SignalProtocolAddress> sharedWith = store.getSenderKeySharedWith(distributionId);
Set<SignalProtocolAddress> sharedWith = aciStore.getSenderKeySharedWith(distributionId);
List<SignalServiceAddress> needsSenderKey = targetInfo.destinations.stream()
.filter(a -> !sharedWith.contains(a))
.map(a -> ServiceId.parseOrThrow(a.getName()))
@ -1876,7 +1910,7 @@ public class SignalServiceMessageSender {
Set<String> successSids = successes.stream().map(a -> a.getServiceId().toString()).collect(Collectors.toSet());
Set<SignalProtocolAddress> 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<SignalServiceAddress> recipients) {
List<String> addressNames = recipients.stream().map(SignalServiceAddress::getIdentifier).collect(Collectors.toList());
Set<SignalProtocolAddress> destinations = store.getAllAddressesWithActiveSessions(addressNames);
Set<SignalProtocolAddress> destinations = aciStore.getAllAddressesWithActiveSessions(addressNames);
Map<String, List<Integer>> devicesByAddressName = new HashMap<>();
destinations.addAll(recipients.stream()
@ -2009,7 +2043,7 @@ public class SignalServiceMessageSender {
List<SendMessageResult> 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<SendMessageResult> results = new ArrayList<>(success.size() + failures.size());
@ -2109,7 +2143,7 @@ public class SignalServiceMessageSender {
{
List<OutgoingPushMessage> messages = new LinkedList<>();
List<Integer> subDevices = store.getSubDeviceSessions(recipient.getIdentifier());
List<Integer> subDevices = aciStore.getSubDeviceSessions(recipient.getIdentifier());
List<Integer> 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<PreKeyBundle> 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<SignalProtocolAddress> addressesToClear = convertToProtocolAddresses(recipient, devices);
for (SignalProtocolAddress address : addressesToClear) {
store.archiveSession(address);
aciStore.archiveSession(address);
}
}

View file

@ -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<byte[]> groupId;
private final String destinationUuid;
private final Optional<SignalServiceDataMessage> message;
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
private final Optional<SignalServiceCallMessage> callMessage;
private final Optional<SignalServiceReceiptMessage> readMessage;
private final Optional<SignalServiceTypingMessage> typingMessage;
private final Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage;
private final Optional<DecryptionErrorMessage> decryptionErrorMessage;
private final Optional<SignalServiceStoryMessage> storyMessage;
private final Optional<SignalServiceDataMessage> message;
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
private final Optional<SignalServiceCallMessage> callMessage;
private final Optional<SignalServiceReceiptMessage> readMessage;
private final Optional<SignalServiceTypingMessage> typingMessage;
private final Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage;
private final Optional<DecryptionErrorMessage> decryptionErrorMessage;
private final Optional<SignalServiceStoryMessage> storyMessage;
private final Optional<SignalServicePniSignatureMessage> pniSignatureMessage;
private SignalServiceContent(SignalServiceDataMessage message,
Optional<SenderKeyDistributionMessage> senderKeyDistributionMessage,
Optional<SignalServicePniSignatureMessage> 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> senderKeyDistributionMessage,
Optional<SignalServicePniSignatureMessage> 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> senderKeyDistributionMessage,
Optional<SignalServicePniSignatureMessage> 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> senderKeyDistributionMessage,
Optional<SignalServicePniSignatureMessage> 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> senderKeyDistributionMessage,
Optional<SignalServicePniSignatureMessage> 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> senderKeyDistributionMessage,
Optional<SignalServicePniSignatureMessage> 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<SignalServicePniSignatureMessage> 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> 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> senderKeyDistributionMessage,
Optional<SignalServicePniSignatureMessage> pniSignatureMessage,
SignalServiceAddress sender,
int senderDevice,
long timestamp,
long serverReceivedTimestamp,
long serverDeliveredTimestamp,
boolean needsReceipt,
String serverUuid,
Optional<byte[]> 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<SignalServiceDataMessage> getDataMessage() {
@ -393,6 +447,10 @@ public final class SignalServiceContent {
return decryptionErrorMessage;
}
public Optional<SignalServicePniSignatureMessage> 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> senderKeyDistributionMessage = Optional.empty();
@ -482,9 +527,21 @@ public final class SignalServiceContent {
}
}
Optional<SignalServicePniSignatureMessage> 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(),

View file

@ -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()));
}
/**

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;
}