Merge pull request #309 from loki-project/shared-sender-keys

Remaining Shared Sender Keys Bits & Pieces Part 1/2
This commit is contained in:
Niels Andriesse 2020-09-02 12:06:03 +10:00 committed by GitHub
commit 8e6920f37e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 168 additions and 149 deletions

View File

@ -3,7 +3,8 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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">
<LinearLayout <LinearLayout
android:id="@+id/mainContentContainer" android:id="@+id/mainContentContainer"
@ -59,4 +60,22 @@
</LinearLayout> </LinearLayout>
<RelativeLayout
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A4000000"
android:visibility="gone"
android:alpha="0">
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Large.ThreeBounce"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_centerInParent="true"
app:SpinKit_Color="@color/text" />
</RelativeLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -15,6 +15,7 @@ import android.widget.Toast
import kotlinx.android.synthetic.main.activity_create_closed_group.* import kotlinx.android.synthetic.main.activity_create_closed_group.*
import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.database.Address 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.database.ThreadDatabase
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol 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.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
@ -118,13 +121,19 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
if (selectedMembers.count() < 1) { if (selectedMembers.count() < 1) {
return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() 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() 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 userPublicKey = TextSecurePreferences.getLocalNumber(this)
val groupID = ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )) loader.fadeIn()
val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) 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() { private fun createLegacyClosedGroup() {

View File

@ -228,14 +228,13 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
val maxGroupMembers = if (isSSKBasedClosedGroup) ClosedGroupsProtocol.groupSizeLimit else Companion.legacyGroupSizeLimit 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 // 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() return Toast.makeText(this, R.string.activity_edit_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
} }
if (isSSKBasedClosedGroup) { if (isSSKBasedClosedGroup) {
ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
name, admins.map { it.address.serialize() })
} else { } else {
GroupManager.updateGroup(this, groupID, members, null, name, admins) GroupManager.updateGroup(this, groupID, members, null, name, admins)
} }

View File

@ -63,8 +63,8 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) :
override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set<ClosedGroupSenderKey> { override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set<ClosedGroupSenderKey> {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" val query = "${Companion.closedGroupPublicKey} = ?"
return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey )) { cursor ->
val chainKey = cursor.getString(Companion.chainKey) val chainKey = cursor.getString(Companion.chainKey)
val keyIndex = cursor.getInt(Companion.keyIndex) val keyIndex = cursor.getInt(Companion.keyIndex)
val senderPublicKey = cursor.getString(Companion.senderPublicKey) val senderPublicKey = cursor.getString(Companion.senderPublicKey)

View File

@ -1,9 +1,10 @@
package org.thoughtcrime.securesms.loki.protocol package org.thoughtcrime.securesms.loki.protocol
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.util.Log import android.util.Log
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -35,55 +36,78 @@ object ClosedGroupsProtocol {
val isSharedSenderKeysEnabled = false val isSharedSenderKeysEnabled = false
val groupSizeLimit = 10 val groupSizeLimit = 10
public fun createClosedGroup(context: Context, name: String, members: Collection<String>): String { public fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
// Prepare val deferred = deferred<String, Exception>()
val userPublicKey = TextSecurePreferences.getLocalNumber(context) Thread {
// Generate a key pair for the group // Prepare
val groupKeyPair = Curve.generateKeyPair() val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix // Generate a key pair for the group
val membersAsData = members.map { Hex.fromStringCondensed(it) } val groupKeyPair = Curve.generateKeyPair()
// Create ratchets for all members val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
val senderKeys: List<ClosedGroupSenderKey> = members.map { publicKey -> val membersAsData = members.map { Hex.fromStringCondensed(it) }
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) // Create ratchets for all members
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) val senderKeys: List<ClosedGroupSenderKey> = members.map { publicKey ->
} val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
// Create the group ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
val groupID = doubleEncodeGroupID(groupPublicKey) }
val admins = setOf( userPublicKey ) // Create the group
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }), val groupID = doubleEncodeGroupID(groupPublicKey)
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) })) val admins = setOf( userPublicKey )
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
// Establish sessions if needed null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) }))
establishSessionsWithMembersIfNeeded(context, members) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
// Send a closed group update message to all members using established channels // Establish sessions if needed
val adminsAsData = admins.map { Hex.fromStringCondensed(it) } establishSessionsWithMembersIfNeeded(context, members)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), // Send a closed group update message to all members using established channels
senderKeys, membersAsData, adminsAsData) val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
for (member in members) { val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(),
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) senderKeys, membersAsData, adminsAsData)
ApplicationContext.getInstance(context).jobManager.add(job) for (member in members) {
} if (member == userPublicKey) { continue }
// TODO: Wait for the messages to finish sending val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
// Add the group to the user's set of public keys to poll for job.setContext(context)
DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) job.onRun() // Run the job immediately
// Notify the user }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) // Add the group to the user's set of public keys to poll for
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) 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
return groupID return deferred.promise
} }
public fun addMembers(context: Context, newMembers: Collection<String>, groupPublicKey: String) { @JvmStatic
// Prepare 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 leave nonexistent closed group.")
return
}
val name = group.title
val oldMembers = group.members.map { it.serialize() }.toSet()
val newMembers = oldMembers.minus(userPublicKey)
update(context, groupPublicKey, newMembers, name)
}
public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val sskDatabase = DatabaseFactory.getSSKDatabase(context) val sskDatabase = DatabaseFactory.getSSKDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull() val group = groupDB.getGroup(groupID).orNull()
if (group == null) { if (group == null) {
Log.d("Loki", "Can't add users to nonexistent closed group.") Log.d("Loki", "Can't update nonexistent closed group.")
return 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 admins = group.admins.map { it.serialize() }
val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
@ -91,114 +115,74 @@ object ClosedGroupsProtocol {
Log.d("Loki", "Couldn't get private key for closed group.") Log.d("Loki", "Couldn't get private key for closed group.")
return return
} }
// Add the members to the member list val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
val members = group.members.map { it.serialize() }.toMutableSet() val removedMembers = oldMembers.minus(members)
members.addAll(newMembers) val isUserLeaving = removedMembers.contains(userPublicKey)
val membersAsData = members.map { Hex.fromStringCondensed(it) } if (wasAnyUserRemoved) {
// Generate ratchets for the new members if (isUserLeaving && removedMembers.count() != 1) {
val senderKeys: List<ClosedGroupSenderKey> = newMembers.map { publicKey -> Log.d("Loki", "Can't remove self and others simultaneously.")
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) return
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) }
} // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually)
// 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),
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, name, setOf(), membersAsData, adminsAsData)
senderKeys, membersAsData, adminsAsData) val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) job.setContext(context)
ApplicationContext.getInstance(context).jobManager.add(job) job.onRun() // Run the job immediately
// Establish sessions if needed // Delete all ratchets (it's important that this happens * after * sending out the update)
establishSessionsWithMembersIfNeeded(context, newMembers) sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
// Send closed group update messages to the new members using established channels // 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
val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys // send it out to all members (minus the removed ones) using established channels.
for (member in members) { if (isUserLeaving) {
@Suppress("NAME_SHADOWING") sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupDB.setActive(groupID, false)
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
@Suppress("NAME_SHADOWING") } else {
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) // Establish sessions if needed
ApplicationContext.getInstance(context).jobManager.add(job) establishSessionsWithMembersIfNeeded(context, members)
} // Send out the user's new ratchet to all members (minus the removed ones) using established channels
// Update the group val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
// Notify the user for (member in members) {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) if (member == userPublicKey) { continue }
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) @Suppress("NAME_SHADOWING")
} val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
@Suppress("NAME_SHADOWING")
@JvmStatic val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
public fun leave(context: Context, groupPublicKey: String) { ApplicationContext.getInstance(context).jobManager.add(job)
val userPublicKey = TextSecurePreferences.getLocalNumber(context) }
removeMembers(context, setOf( userPublicKey ), groupPublicKey) }
}
public fun removeMembers(context: Context, membersToRemove: Collection<String>, groupPublicKey: 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.")
return
}
val name = group.title
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)
} else { } else {
// Generate ratchets for any new members
val newMembers = members.minus(oldMembers)
val newSenderKeys: List<ClosedGroupSenderKey> = 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,
newSenderKeys, membersAsData, adminsAsData)
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
// Establish sessions if needed // Establish sessions if needed
establishSessionsWithMembersIfNeeded(context, members) establishSessionsWithMembersIfNeeded(context, newMembers)
// Send out the user's new ratchet to all members (minus the removed ones) using established channels // Send closed group update messages to the new members using established channels
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + newSenderKeys
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) for (member in newMembers) {
for (member in members) {
@Suppress("NAME_SHADOWING") @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") @Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job) ApplicationContext.getInstance(context).jobManager.add(job)
} }
} }
// Update the group // Update the group
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) groupDB.updateTitle(groupID, name)
// Notify the user if (!isUserLeaving) {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
}
public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String, admins: Collection<String>) {
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.updateMembers(groupID, members.map { Address.fromSerialized(it) })
// Notify the user // Notify the user
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
@ -206,6 +190,7 @@ object ClosedGroupsProtocol {
@JvmStatic @JvmStatic
public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) { 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 // Establish session if needed
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
// Send the request // Send the request
@ -317,11 +302,13 @@ object ClosedGroupsProtocol {
if (wasCurrentUserRemoved) { if (wasCurrentUserRemoved) {
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
groupDB.setActive(groupID, false) groupDB.setActive(groupID, false)
groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
} else { } else {
establishSessionsWithMembersIfNeeded(context, members) establishSessionsWithMembersIfNeeded(context, members)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
for (member in members) { for (member in members) {
if (member == userPublicKey) { continue }
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job) ApplicationContext.getInstance(context).jobManager.add(job)
@ -330,7 +317,10 @@ object ClosedGroupsProtocol {
} }
// Update the group // Update the group
groupDB.updateTitle(groupID, name) 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 // Notify the user
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
@ -354,6 +344,7 @@ object ClosedGroupsProtocol {
return return
} }
// Respond to the request // Respond to the request
Log.d("Loki", "Responding to sender key request from: $senderPublicKey.")
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
@ -389,6 +380,7 @@ object ClosedGroupsProtocol {
return return
} }
// Store the sender key // Store the sender key
Log.d("Loki", "Received a sender key from: $senderPublicKey.")
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet) sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet)
} }