From 52f50e3252cd98a44ee868b7d0b40e4923314436 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 31 Aug 2020 13:53:31 +1000 Subject: [PATCH 1/7] Fix SSK based closed group size inconsistency --- .../securesms/loki/activities/CreateClosedGroupActivity.kt | 2 +- .../securesms/loki/activities/EditClosedGroupActivity.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index 306173446e..16b2304238 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -118,7 +118,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM if (selectedMembers.count() < 1) { return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() } - if (selectedMembers.count() > ClosedGroupsProtocol.groupSizeLimit) { // Minus one because we're going to include self later + if (selectedMembers.count() >= ClosedGroupsProtocol.groupSizeLimit) { // Minus one because we're going to include self later return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() } val userPublicKey = TextSecurePreferences.getLocalNumber(this) diff --git a/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt index cf66fff5ea..2326562a48 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt @@ -228,7 +228,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { } val maxGroupMembers = if (isSSKBasedClosedGroup) ClosedGroupsProtocol.groupSizeLimit else Companion.legacyGroupSizeLimit - if (members.size > maxGroupMembers) { + if (members.size >= maxGroupMembers) { // TODO: Update copy for SSK based closed groups return Toast.makeText(this, R.string.activity_edit_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() } From 5957afa1479eb739b5ba4ff342695b57b8e8b286 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 31 Aug 2020 14:54:03 +1000 Subject: [PATCH 2/7] Debug SSK based closed group editing --- .../activities/EditClosedGroupActivity.kt | 3 +- .../loki/protocol/ClosedGroupsProtocol.kt | 167 ++++++++---------- 2 files changed, 71 insertions(+), 99 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt index 2326562a48..455587c8af 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt @@ -234,8 +234,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { } if (isSSKBasedClosedGroup) { - ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, - name, admins.map { it.address.serialize() }) + ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name) } else { GroupManager.updateGroup(this, groupID, members, null, name, admins) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 3df6187a7b..1c347de7ab 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -73,131 +73,104 @@ object ClosedGroupsProtocol { return groupID } - public fun addMembers(context: Context, newMembers: Collection, groupPublicKey: String) { - // Prepare - val sskDatabase = DatabaseFactory.getSSKDatabase(context) + @JvmStatic + public fun leave(context: Context, groupPublicKey: String) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() if (group == null) { - Log.d("Loki", "Can't add users to nonexistent closed group.") + Log.d("Loki", "Can't leave nonexistent closed group.") return } val name = group.title - val admins = group.admins.map { it.serialize() } - val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) - if (groupPrivateKey == null) { - Log.d("Loki", "Couldn't get private key for closed group.") - return - } - // Add the members to the member list - val members = group.members.map { it.serialize() }.toMutableSet() - members.addAll(newMembers) - val membersAsData = members.map { Hex.fromStringCondensed(it) } - // Generate ratchets for the new members - val senderKeys: List = newMembers.map { publicKey -> - val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) - ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) - } - // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, - senderKeys, membersAsData, adminsAsData) - val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) - // Establish sessions if needed - establishSessionsWithMembersIfNeeded(context, newMembers) - // Send closed group update messages to the new members using established channels - val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys - for (member in members) { - @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, - Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) - @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) - } - // Update the group - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) - // Notify the user - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + val oldMembers = group.members.map { it.serialize() }.toSet() + val newMembers = oldMembers.minus(userPublicKey) + update(context, groupPublicKey, newMembers, name) } - @JvmStatic - public fun leave(context: Context, groupPublicKey: String) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - removeMembers(context, setOf( userPublicKey ), groupPublicKey) - } - - public fun removeMembers(context: Context, membersToRemove: Collection, groupPublicKey: String) { + public fun update(context: Context, groupPublicKey: String, members: Collection, name: String) { val userPublicKey = TextSecurePreferences.getLocalNumber(context) val sskDatabase = DatabaseFactory.getSSKDatabase(context) - val isUserLeaving = membersToRemove.contains(userPublicKey) - if (isUserLeaving && membersToRemove.count() != 1) { - Log.d("Loki", "Can't remove self and others simultaneously.") - return - } val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() if (group == null) { - Log.d("Loki", "Can't add users to nonexistent closed group.") + Log.d("Loki", "Can't update nonexistent closed group.") return } - val name = group.title + val oldMembers = group.members.map { it.serialize() }.toSet() + val membersAsData = members.map { Hex.fromStringCondensed(it) } val admins = group.admins.map { it.serialize() } val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - // Remove the members from the member list - val members = group.members.map { it.serialize() }.toSet().minus(membersToRemove) - val membersAsData = members.map { Hex.fromStringCondensed(it) } - // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), - name, setOf(), membersAsData, adminsAsData) - val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) - job.setContext(context) - job.onRun() // Run the job immediately - // Delete all ratchets (it's important that this happens after sending out the update) - sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) - // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and - // send it out to all members (minus the removed ones) using established channels. - if (isUserLeaving) { - sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) - groupDB.setActive(groupID, false) + val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) + if (groupPrivateKey == null) { + Log.d("Loki", "Couldn't get private key for closed group.") + return + } + val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() + if (wasAnyUserRemoved) { + val removedMembers = oldMembers.minus(members) + val isUserLeaving = removedMembers.contains(userPublicKey) + if (isUserLeaving && removedMembers.count() != 1) { + Log.d("Loki", "Can't remove self and others simultaneously.") + return + } + // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), + name, setOf(), membersAsData, adminsAsData) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + job.setContext(context) + job.onRun() // Run the job immediately + // Delete all ratchets (it's important that this happens * after * sending out the update) + sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) + // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and + // send it out to all members (minus the removed ones) using established channels. + if (isUserLeaving) { + sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) + groupDB.setActive(groupID, false) + } else { + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + // Send out the user's new ratchet to all members (minus the removed ones) using established channels + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + for (member in members) { + if (member == userPublicKey) { continue } + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + } } else { + // Generate ratchets for any new members + val newMembers = members.minus(oldMembers) + val senderKeys: List = newMembers.map { publicKey -> + val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) + } + // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, + senderKeys, membersAsData, adminsAsData) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) // Establish sessions if needed - establishSessionsWithMembersIfNeeded(context, members) - // Send out the user's new ratchet to all members (minus the removed ones) using established channels - val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) - val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) - for (member in members) { + establishSessionsWithMembersIfNeeded(context, newMembers) + // Send closed group update messages to the new members using established channels + val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys + for (member in newMembers) { @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, + Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) @Suppress("NAME_SHADOWING") val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) } } // Update the group - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) - // Notify the user - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID) - } - - public fun update(context: Context, groupPublicKey: String, members: Collection, name: String, admins: Collection) { - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = doubleEncodeGroupID(groupPublicKey) - if (groupDB.getGroup(groupID).orNull() == null) { - Log.d("Loki", "Can't update nonexistent closed group.") - return - } - // Send the update to the group - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), - name, setOf(), members.map { Hex.fromStringCondensed(it) }, admins.map { Hex.fromStringCondensed(it) }) - val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) - // Update the group + groupDB.updateTitle(groupID, name) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) // Notify the user val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) From e7b2e3a223ce642f50eb03f33bbd4d2d12058d0b Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 31 Aug 2020 16:03:46 +1000 Subject: [PATCH 3/7] Debug --- .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 1c347de7ab..8e92e7f312 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context -import android.graphics.Bitmap import android.util.Log import com.google.protobuf.ByteString import org.thoughtcrime.securesms.ApplicationContext @@ -60,6 +59,7 @@ object ClosedGroupsProtocol { val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), senderKeys, membersAsData, adminsAsData) for (member in members) { + if (member == userPublicKey) { continue } val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) } @@ -147,19 +147,19 @@ object ClosedGroupsProtocol { } else { // Generate ratchets for any new members val newMembers = members.minus(oldMembers) - val senderKeys: List = newMembers.map { publicKey -> + val newSenderKeys: List = newMembers.map { publicKey -> val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) } // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, - senderKeys, membersAsData, adminsAsData) + newSenderKeys, membersAsData, adminsAsData) val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) // Establish sessions if needed establishSessionsWithMembersIfNeeded(context, newMembers) // Send closed group update messages to the new members using established channels - val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys + val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + newSenderKeys for (member in newMembers) { @Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, @@ -295,6 +295,7 @@ object ClosedGroupsProtocol { val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) for (member in members) { + if (member == userPublicKey) { continue } val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) ApplicationContext.getInstance(context).jobManager.add(job) From 968177c012c707456f1193206d7563800bac8bbd Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 1 Sep 2020 10:38:16 +1000 Subject: [PATCH 4/7] Add logs --- .../securesms/loki/protocol/ClosedGroupsProtocol.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 8e92e7f312..050b62774a 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -179,6 +179,7 @@ object ClosedGroupsProtocol { @JvmStatic public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) { + Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.") // Establish session if needed ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) // Send the request @@ -328,6 +329,7 @@ object ClosedGroupsProtocol { return } // Respond to the request + Log.d("Loki", "Responding to sender key request from: $senderPublicKey.") ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) @@ -363,6 +365,7 @@ object ClosedGroupsProtocol { return } // Store the sender key + Log.d("Loki", "Received a sender key from: $senderPublicKey.") val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet) } From 146df2c5efd08ec0acc334078cc4f5d036d94cd3 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 2 Sep 2020 10:22:19 +1000 Subject: [PATCH 5/7] Fix incorrect database query --- .../securesms/loki/database/SharedSenderKeysDatabase.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index 23afeeaca0..15b415e8e0 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -63,8 +63,8 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set { val database = databaseHelper.readableDatabase - val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" - return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> + val query = "${Companion.closedGroupPublicKey} = ?" + return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey )) { cursor -> val chainKey = cursor.getString(Companion.chainKey) val keyIndex = cursor.getInt(Companion.keyIndex) val senderPublicKey = cursor.getString(Companion.senderPublicKey) From 2732b9594061d6c71f8b4dc68c7c614d6bbc97e6 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 2 Sep 2020 11:47:54 +1000 Subject: [PATCH 6/7] Show a loader while the group is being created --- res/layout/activity_create_closed_group.xml | 21 ++++- .../activities/CreateClosedGroupActivity.kt | 15 +++- .../loki/protocol/ClosedGroupsProtocol.kt | 79 ++++++++++--------- 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/res/layout/activity_create_closed_group.xml b/res/layout/activity_create_closed_group.xml index 52a3300865..59d9fe00e0 100644 --- a/res/layout/activity_create_closed_group.xml +++ b/res/layout/activity_create_closed_group.xml @@ -3,7 +3,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@drawable/default_session_background" > + xmlns:app="http://schemas.android.com/apk/res-auto" + android:background="@drawable/default_session_background"> + + + + + + \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index 16b2304238..30d59281d6 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -15,6 +15,7 @@ import android.widget.Toast import kotlinx.android.synthetic.main.activity_create_closed_group.* import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView import network.loki.messenger.R +import nl.komponents.kovenant.ui.successUi import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.database.Address @@ -22,6 +23,8 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol +import org.thoughtcrime.securesms.loki.utilities.fadeIn +import org.thoughtcrime.securesms.loki.utilities.fadeOut import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.TextSecurePreferences @@ -122,9 +125,15 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() } val userPublicKey = TextSecurePreferences.getLocalNumber(this) - val groupID = ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )) - val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) - openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) + loader.fadeIn() + ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> + loader.fadeOut() + val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) + if (!isFinishing) { + openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) + finish() + } + } } private fun createLegacyClosedGroup() { diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 050b62774a..363a1a7fdf 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log import com.google.protobuf.ByteString +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory @@ -34,43 +36,48 @@ object ClosedGroupsProtocol { val isSharedSenderKeysEnabled = false val groupSizeLimit = 10 - public fun createClosedGroup(context: Context, name: String, members: Collection): String { - // Prepare - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - // Generate a key pair for the group - val groupKeyPair = Curve.generateKeyPair() - val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix - val membersAsData = members.map { Hex.fromStringCondensed(it) } - // Create ratchets for all members - val senderKeys: List = members.map { publicKey -> - val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) - ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) - } - // Create the group - val groupID = doubleEncodeGroupID(groupPublicKey) - val admins = setOf( userPublicKey ) - DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), - null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) - // Establish sessions if needed - establishSessionsWithMembersIfNeeded(context, members) - // Send a closed group update message to all members using established channels - val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), - senderKeys, membersAsData, adminsAsData) - for (member in members) { - if (member == userPublicKey) { continue } - val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) - } - // TODO: Wait for the messages to finish sending - // Add the group to the user's set of public keys to poll for - DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) - // Notify the user - val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + public fun createClosedGroup(context: Context, name: String, members: Collection): Promise { + val deferred = deferred() + Thread { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + // Generate a key pair for the group + val groupKeyPair = Curve.generateKeyPair() + val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix + val membersAsData = members.map { Hex.fromStringCondensed(it) } + // Create ratchets for all members + val senderKeys: List = members.map { publicKey -> + val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) + } + // Create the group + val groupID = doubleEncodeGroupID(groupPublicKey) + val admins = setOf( userPublicKey ) + DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), + null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) + DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + // Send a closed group update message to all members using established channels + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), + senderKeys, membersAsData, adminsAsData) + for (member in members) { + if (member == userPublicKey) { continue } + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + job.setContext(context) + job.onRun() // Run the job immediately + } + // Add the group to the user's set of public keys to poll for + DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) + // Notify the user + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + // Fulfill the promise + deferred.resolve(groupID) + }.start() // Return - return groupID + return deferred.promise } @JvmStatic From 154181f518ca63ea5c2079eddcdb2bdd0273348c Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 2 Sep 2020 12:03:15 +1000 Subject: [PATCH 7/7] Fix group leaving --- .../loki/protocol/ClosedGroupsProtocol.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 363a1a7fdf..0841aaf2a6 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -116,9 +116,9 @@ object ClosedGroupsProtocol { return } val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() + val removedMembers = oldMembers.minus(members) + val isUserLeaving = removedMembers.contains(userPublicKey) if (wasAnyUserRemoved) { - val removedMembers = oldMembers.minus(members) - val isUserLeaving = removedMembers.contains(userPublicKey) if (isUserLeaving && removedMembers.count() != 1) { Log.d("Loki", "Can't remove self and others simultaneously.") return @@ -136,6 +136,7 @@ object ClosedGroupsProtocol { if (isUserLeaving) { sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) groupDB.setActive(groupID, false) + groupDB.remove(groupID, Address.fromSerialized(userPublicKey)) } else { // Establish sessions if needed establishSessionsWithMembersIfNeeded(context, members) @@ -178,7 +179,10 @@ object ClosedGroupsProtocol { } // Update the group groupDB.updateTitle(groupID, name) - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + 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 threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) @@ -298,6 +302,7 @@ object ClosedGroupsProtocol { if (wasCurrentUserRemoved) { sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) groupDB.setActive(groupID, false) + groupDB.remove(groupID, Address.fromSerialized(userPublicKey)) } else { establishSessionsWithMembersIfNeeded(context, members) val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) @@ -312,7 +317,10 @@ object ClosedGroupsProtocol { } // Update the group groupDB.updateTitle(groupID, name) - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + if (!wasCurrentUserRemoved) { + // 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 type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE