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 10f27aa89a..9fba4b66ef 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 @@ -36,10 +36,8 @@ import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences -import java.io.IOException import java.util.* import java.util.concurrent.ConcurrentHashMap -import kotlin.jvm.Throws object ClosedGroupsProtocolV2 { const val groupSizeLimit = 100 @@ -443,13 +441,11 @@ object ClosedGroupsProtocolV2 { val name = group.title val members = group.members.map { it.serialize() } val admins = group.admins.map { it.serialize() } - // Users that are part of this add update val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } // newMembers to save is old members plus members included in this update val newMembers = members + updateMembers groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) @@ -488,7 +484,7 @@ object ClosedGroupsProtocolV2 { val admins = group.admins.map { it.serialize() } val name = closedGroupUpdate.name groupDB.updateTitle(groupID, name) - + // Notify the user if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) @@ -515,10 +511,10 @@ object ClosedGroupsProtocolV2 { if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { return } - // If admin leaves the group is disbanded + // If the admin leaves the group is disbanded val didAdminLeave = admins.contains(senderPublicKey) val updatedMemberList = members - senderPublicKey - val userLeft = userPublicKey == senderPublicKey + val userLeft = (userPublicKey == senderPublicKey) // if the admin left, we left, or we are the only remaining member: remove the group if (didAdminLeave || userLeft) { @@ -539,60 +535,6 @@ object ClosedGroupsProtocolV2 { } } - private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - // Prepare - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - // Unwrap the message - val name = closedGroupUpdate.name - val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null || !group.isActive) { - Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") - return - } - val oldMembers = group.members.map { it.serialize() } - // Check common group update logic - if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { - return - } - // Check that the admin wasn't removed unless the group was destroyed entirely - if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) { - Log.d("Loki", "Ignoring invalid closed group update message.") - return - } - // Remove the group from the user's set of public keys to poll for if the current user was removed - val wasCurrentUserRemoved = !members.contains(userPublicKey) - if (wasCurrentUserRemoved) { - disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) - } - // Generate and distribute a new encryption key pair if needed - val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet()) - val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey) - if (wasAnyUserRemoved && isCurrentUserAdmin) { - generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members) - } - // Update the group - groupDB.updateTitle(groupID, name) - 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 wasSenderRemoved = !members.contains(senderPublicKey) - val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE - val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE - val admins = group.admins.map { it.toString() } - if (userPublicKey == senderPublicKey) { - val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp) - } else { - insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp) - } - } - private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) { apiDB.removeClosedGroupPublicKey(groupPublicKey) // Remove the key pairs @@ -606,9 +548,10 @@ object ClosedGroupsProtocolV2 { private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, - senderPublicKey: String): Boolean { + senderPublicKey: String): Boolean { val oldMembers = group.members.map { it.serialize() } // Check that the message isn't from before the group was created + // TODO: We should check that formationTimestamp is the sent timestamp of the closed group update that created the group if (group.formationTimestamp > sentTimestamp) { Log.d("Loki", "Ignoring closed group update from before thread was created.") return false @@ -628,12 +571,12 @@ object ClosedGroupsProtocolV2 { val userKeyPair = apiDB.getUserX25519KeyPair() // Unwrap the message val groupDB = DatabaseFactory.getGroupDatabase(context) - val correctGroupPublicKey = when { + val groupPublicKeyToUse = when { groupPublicKey.isNotEmpty() -> groupPublicKey !closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toByteArray().toHexString() else -> "" } - val groupID = GroupUtil.doubleEncodeGroupID(correctGroupPublicKey) + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKeyToUse) val group = groupDB.getGroup(groupID).orNull() if (group == null) { Log.d("Loki", "Ignoring closed group encryption key pair message for nonexistent group.") @@ -651,7 +594,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, correctGroupPublicKey) + apiDB.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKeyToUse) Log.d("Loki", "Received a new closed group encryption key pair") } @@ -672,7 +615,7 @@ object ClosedGroupsProtocolV2 { private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, members: Collection, admins: Collection, threadID: Long, - sentTime: Long) { + sentTimestamp: Long) { val userPublicKey = TextSecurePreferences.getLocalNumber(context) val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val groupContextBuilder = GroupContext.newBuilder() @@ -681,11 +624,11 @@ object ClosedGroupsProtocolV2 { .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTime, 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) - if (mmsSmsDB.getMessageFor(sentTime,userPublicKey) != null) return - val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTime) + if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return + val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTimestamp) mmsDB.markAsSent(infoMessageID, true) } @@ -696,7 +639,7 @@ object ClosedGroupsProtocolV2 { } else { var groupPublicKey: String? = null try { - groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString() + groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString() // TODO: The toHexString() here might be unnecessary } catch (exception: Exception) { // Do nothing } @@ -707,4 +650,60 @@ object ClosedGroupsProtocolV2 { } } } + + // region Deprecated + private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + // Unwrap the message + val name = closedGroupUpdate.name + val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = groupDB.getGroup(groupID).orNull() + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") + return + } + val oldMembers = group.members.map { it.serialize() } + // Check common group update logic + if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { + return + } + // Check that the admin wasn't removed unless the group was destroyed entirely + if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) { + Log.d("Loki", "Ignoring invalid closed group update message.") + return + } + // Remove the group from the user's set of public keys to poll for if the current user was removed + val wasCurrentUserRemoved = !members.contains(userPublicKey) + if (wasCurrentUserRemoved) { + disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) + } + // Generate and distribute a new encryption key pair if needed + val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet()) + val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey) + if (wasAnyUserRemoved && isCurrentUserAdmin) { + generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members) + } + // Update the group + groupDB.updateTitle(groupID, name) + 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 wasSenderRemoved = !members.contains(senderPublicKey) + val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE + val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE + val admins = group.admins.map { it.toString() } + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp) + } else { + insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp) + } + } + // endregion } \ No newline at end of file