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/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt index 6b4060aa17..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 @@ -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(Hex.fromStringCondensed(destination)) + } } 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 @@ -227,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 90d18dd0d8..9ab9e80486 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 @@ -5,7 +5,6 @@ import android.util.Log 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 @@ -129,232 +128,118 @@ 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) } - 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) + @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) } - return deferred.promise } - fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection) { + @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) + } + + private fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection) { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context) val apiDB = DatabaseFactory.getLokiAPIDatabase(context) @@ -383,7 +268,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, 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()) @@ -392,7 +277,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(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers, targetUser), System.currentTimeMillis()) if (force) { job.setContext(context) job.onRun() // Run the job immediately @@ -546,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) }) @@ -573,7 +458,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, groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false) + } } } } @@ -628,7 +515,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) @@ -735,7 +623,12 @@ object ClosedGroupsProtocolV2 { val userKeyPair = apiDB.getUserX25519KeyPair() // Unwrap the message val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = doubleEncodeGroupID(groupPublicKey) + val correctGroupPublicKey = when { + groupPublicKey.isNotEmpty() -> groupPublicKey + !closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toByteArray().toHexString() + else -> "" + } + 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.") @@ -753,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") }