|
|
|
@ -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<String>, admins: Collection<String>, 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
|
|
|
|
|
}
|