From c8f1c862fb64690256ca63033df52b26f43e7d79 Mon Sep 17 00:00:00 2001 From: jubb Date: Thu, 18 Feb 2021 11:56:45 +1100 Subject: [PATCH 1/8] refactor: turn inner wrapped group edit calls into synchronous calls to remove unnecessary nesting --- .../activities/EditClosedGroupActivity.kt | 13 +- .../loki/protocol/ClosedGroupsProtocolV2.kt | 235 ++++++++---------- 2 files changed, 107 insertions(+), 141 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt index 80fd0281ab..aff070fdf6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt @@ -280,18 +280,15 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey!!) } else { task { - val name = - if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name) - else Promise.of(Unit) - name.get() + if (hasNameChanged) { + ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity, groupPublicKey!!, name) + } members.filterNot { it in originalMembers }.let { adds -> if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() }) - else Promise.of(Unit) - }.get() + } originalMembers.filterNot { it in members }.let { removes -> if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() }) - else Promise.of(Unit) - }.get() + } } } promise.successUi { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index fff03d6b74..0f09ca80e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log +import androidx.annotation.WorkerThread import com.google.protobuf.ByteString import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred @@ -129,147 +130,115 @@ object ClosedGroupsProtocolV2 { } @JvmStatic - fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List): Promise { - return task { - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Can't leave nonexistent closed group.") - return@task Error.NoThread - } - val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd - // Save the new group members - groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) - val membersAsData = updatedMembers.map { Hex.fromStringCondensed(it) } - val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) } - val admins = group.admins.map { it.serialize() } - val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val sentTime = System.currentTimeMillis() - val encryptionKeyPair = pendingKeyPair.getOrElse(groupPublicKey) { - Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)) - }.orNull() - if (encryptionKeyPair == null) { - Log.d("Loki", "Couldn't get encryption key pair for closed group.") - return@task Error.NoKeyPair - } - val name = group.title - // Send the update to the group - val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) - // Send closed group update messages to any new members individually - for (member in membersToAdd) { - @Suppress("NAME_SHADOWING") - val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) - @Suppress("NAME_SHADOWING") - val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime) - ApplicationContext.getInstance(context).jobManager.add(newMemberJob) - } - } - } - - @JvmStatic - fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List): Promise { - return task { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Can't leave nonexistent closed group.") - return@task Error.NoThread - } - val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove - // Save the new group members - groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) - val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) } - val admins = group.admins.map { it.serialize() } - val sentTime = System.currentTimeMillis() - val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) - if (encryptionKeyPair == null) { - Log.d("Loki", "Couldn't get encryption key pair for closed group.") - return@task Error.NoKeyPair - } - if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) { - Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") - return@task Error.InvalidUpdate - } - val name = group.title - // Send the update to the group - val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) - val isCurrentUserAdmin = admins.contains(userPublicKey) - if (isCurrentUserAdmin) { - generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers) - } - return@task Unit - } - } - - @JvmStatic - fun explicitNameChange(context: Context, groupPublicKey: String, newName: String): Promise { - val deferred = deferred() - ThreadUtils.queue { - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - val members = group.members.map { it.serialize() }.toSet() - val admins = group.admins.map { it.serialize() } - val sentTime = System.currentTimeMillis() - if (group == null) { - Log.d("Loki", "Can't leave nonexistent closed group.") - return@queue deferred.reject(Error.NoThread) - } - // Send the update to the group - val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime) - // Update the group - groupDB.updateTitle(groupID, newName) - deferred.resolve(Unit) - } - return deferred.promise - } - - @JvmStatic - fun leave(context: Context, groupPublicKey: String): Promise { - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List) { + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Can't leave nonexistent closed group.") - return Promise.ofFail(Error.NoThread) + throw Error.NoThread + } + val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd + val membersAsData = updatedMembers.map { Hex.fromStringCondensed(it) } + val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) } + val admins = group.admins.map { it.serialize() } + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + val sentTime = System.currentTimeMillis() + val encryptionKeyPair = pendingKeyPair.getOrElse(groupPublicKey) { + Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)) + }.orNull() + if (encryptionKeyPair == null) { + Log.d("Loki", "Couldn't get encryption key pair for closed group.") + throw Error.NoKeyPair } val name = group.title - val oldMembers = group.members.map { it.serialize() }.toSet() - val newMembers: Set - val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey) - if (!isCurrentUserAdmin) { - newMembers = oldMembers.minus(userPublicKey) - } else { - newMembers = setOf() // If the admin leaves the group is destroyed + // Send the update to the group + val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) + job.setContext(context) + job.onRun() // Run the job immediately + // Save the new group members + groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) + // Send closed group update messages to any new members individually + for (member in membersToAdd) { + @Suppress("NAME_SHADOWING") + val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) + @Suppress("NAME_SHADOWING") + val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime) + ApplicationContext.getInstance(context).jobManager.add(newMemberJob) } - return update(context, groupPublicKey, newMembers, name) + } + + @JvmStatic + fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Can't leave nonexistent closed group.") + throw Error.NoThread + } + val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove + val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) } + val admins = group.admins.map { it.serialize() } + val sentTime = System.currentTimeMillis() + val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + if (encryptionKeyPair == null) { + Log.d("Loki", "Couldn't get encryption key pair for closed group.") + throw Error.NoKeyPair + } + if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) { + Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") + throw Error.InvalidUpdate + } + val name = group.title + // Send the update to the group + val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) + job.setContext(context) + job.onRun() // Run the job immediately + // Save the new group members + groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) + val isCurrentUserAdmin = admins.contains(userPublicKey) + if (isCurrentUserAdmin) { + generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers) + } + } + + @JvmStatic + fun explicitNameChange(context: Context, groupPublicKey: String, newName: String) { + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + val members = group.members.map { it.serialize() }.toSet() + val admins = group.admins.map { it.serialize() } + val sentTime = System.currentTimeMillis() + if (group == null) { + Log.d("Loki", "Can't leave nonexistent closed group.") + throw Error.NoThread + } + // Send the update to the group + val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime) + job.setContext(context) + job.onRun() // Run the job immediately + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime) + // Update the group + groupDB.updateTitle(groupID, newName) } fun update(context: Context, groupPublicKey: String, members: Collection, name: String): Promise { From 8df2a8af0138f8b64cae626f97473b60827e53a1 Mon Sep 17 00:00:00 2001 From: jubb Date: Fri, 19 Feb 2021 12:04:19 +1100 Subject: [PATCH 2/8] fix: send new kp to each user individually vs target group --- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index fff03d6b74..4c842d389c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -383,7 +383,7 @@ object ClosedGroupsProtocolV2 { pendingKeyPair[groupPublicKey] = Optional.absent() } - private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection, force: Boolean = true) { + private fun sendEncryptionKeyPair(context: Context, target: String, newKeyPair: ECKeyPair, targetMembers: Collection, force: Boolean = true) { val proto = SignalServiceProtos.KeyPair.newBuilder() proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize()) @@ -392,7 +392,7 @@ object ClosedGroupsProtocolV2 { val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey) ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext) } - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis()) + val job = ClosedGroupUpdateMessageSendJobV2(target, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis()) if (force) { job.setContext(context) job.onRun() // Run the job immediately @@ -573,7 +573,9 @@ object ClosedGroupsProtocolV2 { if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") } else { - sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, newMembers, false) + for (user in updateMembers) { + sendEncryptionKeyPair(context, user, encryptionKeyPair, newMembers, false) + } } } } From 9df35ede1478414ac126816307a98c8a7dd07d54 Mon Sep 17 00:00:00 2001 From: jubb Date: Fri, 19 Feb 2021 17:33:23 +1100 Subject: [PATCH 3/8] feat: add sending group's public key with the target user 1 on 1 message for enc key pair --- .../ClosedGroupUpdateMessageSendJobV2.kt | 23 ++++- .../loki/protocol/ClosedGroupsProtocolV2.kt | 98 ++----------------- 2 files changed, 25 insertions(+), 96 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt index 6b4060aa17..75b1420b0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt @@ -27,7 +27,10 @@ import org.session.libsignal.utilities.Hex import java.util.* import java.util.concurrent.TimeUnit -class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind, private val sentTime: Long) : BaseJob(parameters) { +class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, + private val destination: String, + private val kind: Kind, + private val sentTime: Long) : BaseJob(parameters) { sealed class Kind { class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection, val admins: Collection) : Kind() @@ -36,7 +39,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete class RemoveMembers(val members: Collection) : Kind() class AddMembers(val members: Collection) : Kind() class NameChange(val name: String) : Kind() - class EncryptionKeyPair(val wrappers: Collection) : Kind() // The new encryption key pair encrypted for each member individually + class EncryptionKeyPair(val wrappers: Collection, val targetUser: String?) : Kind() // The new encryption key pair encrypted for each member individually } companion object { @@ -116,6 +119,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete builder.putString("kind", "EncryptionKeyPair") val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) } builder.putString("wrappers", wrappers) + builder.putString("targetUser", kind.targetUser) } } return builder.build() @@ -146,7 +150,8 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete } "EncryptionKeyPair" -> { val wrappers: Collection = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) } - kind = Kind.EncryptionKeyPair(wrappers) + val targetUser = data.getString("targetUser") + kind = Kind.EncryptionKeyPair(wrappers, targetUser) } "RemoveMembers" -> { val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } @@ -170,6 +175,11 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete } public override fun onRun() { + val sendDestination = if (kind is Kind.EncryptionKeyPair && kind.targetUser != null) { + kind.targetUser + } else { + destination + } val contentMessage = SignalServiceProtos.Content.newBuilder() val dataMessage = SignalServiceProtos.DataMessage.newBuilder() val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdateV2.newBuilder() @@ -193,6 +203,9 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete is Kind.EncryptionKeyPair -> { closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() }) + if (kind.targetUser != null) { + closedGroupUpdate.publicKey = ByteString.copyFrom(kind.targetUser.toByteArray()) + } } Kind.Leave -> { closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT @@ -214,8 +227,8 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete contentMessage.dataMessage = dataMessage.build() val serializedContentMessage = contentMessage.build().toByteArray() val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() - val address = SignalServiceAddress(destination) - val recipient = recipient(context, destination) + val address = SignalServiceAddress(sendDestination) + val recipient = recipient(context, sendDestination) val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) val ttl = when (kind) { is Kind.EncryptionKeyPair -> 4 * 24 * 60 * 60 * 1000 diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index ab7c7fb307..6738bbfcee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -2,11 +2,9 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log -import androidx.annotation.WorkerThread import com.google.protobuf.ByteString import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred -import nl.komponents.kovenant.task import org.session.libsignal.libsignal.ecc.Curve import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey @@ -241,89 +239,7 @@ object ClosedGroupsProtocolV2 { groupDB.updateTitle(groupID, newName) } - fun update(context: Context, groupPublicKey: String, members: Collection, name: String): Promise { - val deferred = deferred() - ThreadUtils.queue { - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Can't update nonexistent closed group.") - return@queue deferred.reject(Error.NoThread) - } - val sentTime = System.currentTimeMillis() - val oldMembers = group.members.map { it.serialize() }.toSet() - val newMembers = members.minus(oldMembers) - val membersAsData = members.map { Hex.fromStringCondensed(it) } - val admins = group.admins.map { it.serialize() } - val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) - if (encryptionKeyPair == null) { - Log.d("Loki", "Couldn't get encryption key pair for closed group.") - return@queue deferred.reject(Error.NoKeyPair) - } - val removedMembers = oldMembers.minus(members) - if (removedMembers.contains(admins.first()) && members.isNotEmpty()) { - Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") - return@queue deferred.reject(Error.InvalidUpdate) - } - val isUserLeaving = removedMembers.contains(userPublicKey) - if (isUserLeaving && members.isNotEmpty()) { - if (removedMembers.count() != 1 || newMembers.isNotEmpty()) { - Log.d("Loki", "Can't remove self and add or remove others simultaneously.") - return@queue deferred.reject(Error.InvalidUpdate) - } - } - // Send the update to the group - @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData) - @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately - if (isUserLeaving) { - // Remove the group private key and unsubscribe from PNs - apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) - apiDB.removeClosedGroupPublicKey(groupPublicKey) - // Mark the group as inactive - groupDB.setActive(groupID, false) - groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) - // Notify the PN server - LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) - } else { - // Generate and distribute a new encryption key pair if needed - val wasAnyUserRemoved = removedMembers.isNotEmpty() - val isCurrentUserAdmin = admins.contains(userPublicKey) - if (wasAnyUserRemoved && isCurrentUserAdmin) { - generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members.minus(newMembers)) - } - // Send closed group update messages to any new members individually - for (member in newMembers) { - @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) - @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime) - ApplicationContext.getInstance(context).jobManager.add(job) - } - } - // Update the group - groupDB.updateTitle(groupID, name) - if (!isUserLeaving) { - // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) - } - // Notify the user - val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID, sentTime) - deferred.resolve(Unit) - } - return deferred.promise - } - - fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection) { + private fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection) { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context) val apiDB = DatabaseFactory.getLokiAPIDatabase(context) @@ -352,7 +268,7 @@ object ClosedGroupsProtocolV2 { pendingKeyPair[groupPublicKey] = Optional.absent() } - private fun sendEncryptionKeyPair(context: Context, target: String, newKeyPair: ECKeyPair, targetMembers: Collection, force: Boolean = true) { + private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection, targetUser: String? = null, force: Boolean = true) { val proto = SignalServiceProtos.KeyPair.newBuilder() proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize()) @@ -361,7 +277,7 @@ object ClosedGroupsProtocolV2 { val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey) ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext) } - val job = ClosedGroupUpdateMessageSendJobV2(target, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis()) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers, targetUser), System.currentTimeMillis()) if (force) { job.setContext(context) job.onRun() // Run the job immediately @@ -515,17 +431,17 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } + // Check common group update logic if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { return } val name = group.title - // Check common group update logic val members = group.members.map { it.serialize() } val admins = group.admins.map { it.serialize() } - // Users that are part of this remove update + // Users that are part of this add update val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } - // newMembers to save is old members minus removed members + // newMembers to save is old members plus members included in this update val newMembers = members + updateMembers groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) @@ -543,7 +459,7 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Couldn't get encryption key pair for closed group.") } else { for (user in updateMembers) { - sendEncryptionKeyPair(context, user, encryptionKeyPair, newMembers, false) + sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, newMembers, targetUser = user, force = false) } } } From 7e86343efe7e7642f589072c283b3a6e1a7df13c Mon Sep 17 00:00:00 2001 From: jubb Date: Mon, 22 Feb 2021 10:18:24 +1100 Subject: [PATCH 4/8] fix: remove the insert outgoing for local leave after network call, use groupPublicKey if envelope.source is empty in handleEncPair --- .../protocol/ClosedGroupUpdateMessageSendJobV2.kt | 4 ++-- .../loki/protocol/ClosedGroupsProtocolV2.kt | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt index 75b1420b0e..fe63e63dd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt @@ -204,7 +204,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() }) if (kind.targetUser != null) { - closedGroupUpdate.publicKey = ByteString.copyFrom(kind.targetUser.toByteArray()) + closedGroupUpdate.publicKey = ByteString.copyFrom(destination.toByteArray()) } } Kind.Leave -> { @@ -240,7 +240,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete sentTime, serializedContentMessage, false, ttl, false, true, false, false, Optional.absent()) } catch (e: Exception) { - Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") + Log.d("Loki", "Failed to send closed group update message to: $sendDestination due to error: $e.") } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index 6738bbfcee..e6998a97c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -103,9 +103,6 @@ object ClosedGroupsProtocolV2 { val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() - val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey - val admins = group.admins.map { it.serialize() } - val name = group.title val sentTime = System.currentTimeMillis() if (group == null) { Log.d("Loki", "Can't leave nonexistent closed group.") @@ -117,9 +114,6 @@ object ClosedGroupsProtocolV2 { job.setContext(context) job.onRun() // Run the job immediately // Notify the user - val infoType = GroupContext.Type.QUIT - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) // Remove the group private key and unsubscribe from PNs disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) deferred.resolve(Unit) @@ -622,7 +616,11 @@ object ClosedGroupsProtocolV2 { val userKeyPair = apiDB.getUserX25519KeyPair() // Unwrap the message val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = doubleEncodeGroupID(groupPublicKey) + val groupID = if (groupPublicKey.isEmpty() && !closedGroupUpdate.publicKey.isEmpty) { + doubleEncodeGroupID(closedGroupUpdate.publicKey.toStringUtf8()) + } else { + doubleEncodeGroupID(groupPublicKey) + } val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group encryption key pair message for nonexistent group.") From c740963fe24c47fb89a2b8d65cf215f2fe409564 Mon Sep 17 00:00:00 2001 From: jubb Date: Mon, 22 Feb 2021 10:34:21 +1100 Subject: [PATCH 5/8] fix: use a when to make logic more readable --- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index e6998a97c5..c67719cfa7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -616,10 +616,12 @@ object ClosedGroupsProtocolV2 { val userKeyPair = apiDB.getUserX25519KeyPair() // Unwrap the message val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = if (groupPublicKey.isEmpty() && !closedGroupUpdate.publicKey.isEmpty) { - doubleEncodeGroupID(closedGroupUpdate.publicKey.toStringUtf8()) - } else { - doubleEncodeGroupID(groupPublicKey) + val groupID = when { + groupPublicKey.isNotEmpty() -> groupPublicKey + !closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toStringUtf8() + else -> "" + }.let { + doubleEncodeGroupID(it) } val group = groupDB.getGroup(groupID).orNull() if (group == null) { From 766266d54dfa97a4b418e2cba4f698eb67a85990 Mon Sep 17 00:00:00 2001 From: jubb Date: Mon, 22 Feb 2021 10:40:18 +1100 Subject: [PATCH 6/8] fix: handle group of size 1 being destroyed locally for admin --- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index c67719cfa7..ee10b3926d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -509,7 +509,8 @@ object ClosedGroupsProtocolV2 { val updatedMemberList = members - senderPublicKey val userLeft = userPublicKey == senderPublicKey - if (didAdminLeave || userLeft) { + // if the admin left, we left, or we are the only remaining member: remove the group + if (didAdminLeave || userLeft || updatedMemberList.size == 1) { disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) } else { val isCurrentUserAdmin = admins.contains(userPublicKey) From 7f95f0f2d6695385a4d12e40c11b1cb063a41964 Mon Sep 17 00:00:00 2001 From: jubb Date: Mon, 22 Feb 2021 11:45:52 +1100 Subject: [PATCH 7/8] fix: only one wrapper and proper encoding now --- .../loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt | 2 +- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt index fe63e63dd7..99197ecf5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt @@ -204,7 +204,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() }) if (kind.targetUser != null) { - closedGroupUpdate.publicKey = ByteString.copyFrom(destination.toByteArray()) + closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(destination)) } } Kind.Leave -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index ee10b3926d..5c24ad7c9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -103,6 +103,9 @@ object ClosedGroupsProtocolV2 { val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() + val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey + val admins = group.admins.map { it.serialize() } + val name = group.title val sentTime = System.currentTimeMillis() if (group == null) { Log.d("Loki", "Can't leave nonexistent closed group.") @@ -114,6 +117,9 @@ object ClosedGroupsProtocolV2 { job.setContext(context) job.onRun() // Run the job immediately // Notify the user + val infoType = GroupContext.Type.QUIT + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) // Remove the group private key and unsubscribe from PNs disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) deferred.resolve(Unit) @@ -453,7 +459,7 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Couldn't get encryption key pair for closed group.") } else { for (user in updateMembers) { - sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, newMembers, targetUser = user, force = false) + sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false) } } } @@ -619,7 +625,7 @@ object ClosedGroupsProtocolV2 { val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = when { groupPublicKey.isNotEmpty() -> groupPublicKey - !closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toStringUtf8() + !closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toByteArray().toHexString() else -> "" }.let { doubleEncodeGroupID(it) From 2d93d83610fb9a6f12bb8093bd0e346479aeee0f Mon Sep 17 00:00:00 2001 From: jubb Date: Mon, 22 Feb 2021 12:05:00 +1100 Subject: [PATCH 8/8] fix: store group public key as corrected public key --- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index 5c24ad7c9b..72103a3df6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -623,13 +623,12 @@ object ClosedGroupsProtocolV2 { val userKeyPair = apiDB.getUserX25519KeyPair() // Unwrap the message val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = when { + val correctGroupPublicKey = when { groupPublicKey.isNotEmpty() -> groupPublicKey !closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toByteArray().toHexString() else -> "" - }.let { - doubleEncodeGroupID(it) } + val groupID = doubleEncodeGroupID(correctGroupPublicKey) val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group encryption key pair message for nonexistent group.") @@ -647,7 +646,7 @@ object ClosedGroupsProtocolV2 { val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) // Store it - apiDB.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey) + apiDB.addClosedGroupEncryptionKeyPair(keyPair, correctGroupPublicKey) Log.d("Loki", "Received a new closed group encryption key pair") }