clean & fix expiration timer setting issue

This commit is contained in:
Ryan ZHAO
2021-03-12 11:27:51 +11:00
parent 0e049469aa
commit 60f51af295
9 changed files with 10 additions and 1447 deletions

View File

@@ -268,7 +268,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
"SendReadReceiptJob",
"TypingSendJob",
"AttachmentUploadJob",
"RequestGroupInfoJob");
"RequestGroupInfoJob",
"ClosedGroupUpdateMessageSendJobV2");
}
db.setTransactionSuccessful();

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.dependencies;
import android.content.Context;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -30,30 +29,12 @@ public class SignalCommunicationModule {
private final Context context;
private SignalServiceMessageSender messageSender;
private SignalServiceMessageReceiver messageReceiver;
public SignalCommunicationModule(Context context) {
this.context = context;
}
@Provides
public synchronized SignalServiceMessageSender provideSignalMessageSender() {
if (this.messageSender == null) {
this.messageSender = new SignalServiceMessageSender(new SignalProtocolStoreImpl(context),
TextSecurePreferences.getLocalNumber(context),
DatabaseFactory.getLokiAPIDatabase(context),
DatabaseFactory.getLokiThreadDatabase(context),
DatabaseFactory.getLokiMessageDatabase(context),
new SessionProtocolImpl(context),
DatabaseFactory.getLokiUserDatabase(context),
DatabaseFactory.getGroupDatabase(context),
((ApplicationContext)context.getApplicationContext()).broadcaster);
}
return this.messageSender;
}
@Provides
synchronized SignalServiceMessageReceiver provideSignalMessageReceiver() {
if (this.messageReceiver == null) {

View File

@@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJobV2;
import java.util.ArrayList;
import java.util.Arrays;
@@ -32,7 +31,6 @@ public final class JobManagerFactories {
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
put(ClosedGroupUpdateMessageSendJobV2.KEY, new ClosedGroupUpdateMessageSendJobV2.Factory());
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory());
put(PushDecryptJob.KEY, new PushDecryptJob.Factory());

View File

@@ -67,7 +67,6 @@ import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SignalServiceContent;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
@@ -84,8 +83,6 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;
import network.loki.messenger.R;
public class PushDecryptJob extends BaseJob implements InjectableType {
@@ -102,8 +99,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private MessageNotifier messageNotifier;
@Inject SignalServiceMessageSender messageSender;
public PushDecryptJob(Context context) {
this(context, -1);
}
@@ -458,10 +453,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
String syncTarget = message.getSyncTarget().orNull();
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
handleExpirationUpdate(content, message, Optional.absent());
}
Long threadId = null;
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {

View File

@@ -1,253 +0,0 @@
package org.thoughtcrime.securesms.loki.protocol
import com.google.protobuf.ByteString
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.session.libsession.messaging.jobs.Data
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.loki.utilities.TTLUtilities
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BaseJob
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Hex
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) {
sealed class Kind {
class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
class Update(val name: String, val members: Collection<ByteArray>) : Kind()
object Leave : Kind()
class RemoveMembers(val members: Collection<ByteArray>) : Kind()
class AddMembers(val members: Collection<ByteArray>) : Kind()
class NameChange(val name: String) : Kind()
class EncryptionKeyPair(val wrappers: Collection<KeyPairWrapper>, val targetUser: String?) : Kind() // The new encryption key pair encrypted for each member individually
}
companion object {
const val KEY = "ClosedGroupUpdateMessageSendJobV2"
}
@Serializable
data class KeyPairWrapper(val publicKey: String, val encryptedKeyPair: ByteArray) {
companion object {
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair.toByteArray())
}
}
fun toProto(): DataMessage.ClosedGroupControlMessage.KeyPairWrapper {
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
result.encryptedKeyPair = ByteString.copyFrom(encryptedKeyPair)
return result.build()
}
}
constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(20)
.build(),
destination,
kind,
sentTime)
override fun getFactoryKey(): String { return KEY }
override fun serialize(): Data {
val builder = Data.Builder()
builder.putString("destination", destination)
builder.putLong("sentTime", sentTime)
when (kind) {
is Kind.New -> {
builder.putString("kind", "New")
builder.putByteArray("publicKey", kind.publicKey)
builder.putString("name", kind.name)
builder.putByteArray("encryptionKeyPairPublicKey", kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
builder.putByteArray("encryptionKeyPairPrivateKey", kind.encryptionKeyPair.privateKey.serialize())
val members = kind.members.joinToString(" - ") { it.toHexString() }
builder.putString("members", members)
val admins = kind.admins.joinToString(" - ") { it.toHexString() }
builder.putString("admins", admins)
}
is Kind.Update -> {
builder.putString("kind", "Update")
builder.putString("name", kind.name)
val members = kind.members.joinToString(" - ") { it.toHexString() }
builder.putString("members", members)
}
is Kind.RemoveMembers -> {
builder.putString("kind", "RemoveMembers")
val members = kind.members.joinToString(" - ") { it.toHexString() }
builder.putString("members", members)
}
Kind.Leave -> {
builder.putString("kind", "Leave")
}
is Kind.AddMembers -> {
builder.putString("kind", "AddMembers")
val members = kind.members.joinToString(" - ") { it.toHexString() }
builder.putString("members", members)
}
is Kind.NameChange -> {
builder.putString("kind", "NameChange")
builder.putString("name", kind.name)
}
is Kind.EncryptionKeyPair -> {
builder.putString("kind", "EncryptionKeyPair")
val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) }
builder.putString("wrappers", wrappers)
builder.putString("targetUser", kind.targetUser)
}
}
return builder.build()
}
class Factory : Job.Factory<ClosedGroupUpdateMessageSendJobV2> {
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
val destination = data.getString("destination")
val rawKind = data.getString("kind")
val sentTime = data.getLong("sentTime")
val kind: Kind
when (rawKind) {
"New" -> {
val publicKey = data.getByteArray("publicKey")
val name = data.getString("name")
val encryptionKeyPairPublicKey = data.getByteArray("encryptionKeyPairPublicKey")
val encryptionKeyPairPrivateKey = data.getByteArray("encryptionKeyPairPrivateKey")
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairPublicKey), DjbECPrivateKey(encryptionKeyPairPrivateKey))
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.New(publicKey, name, encryptionKeyPair, members, admins)
}
"Update" -> {
val name = data.getString("name")
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.Update(name, members)
}
"EncryptionKeyPair" -> {
val wrappers: Collection<KeyPairWrapper> = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) }
val targetUser = data.getString("targetUser")
kind = Kind.EncryptionKeyPair(wrappers, targetUser)
}
"RemoveMembers" -> {
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.RemoveMembers(members)
}
"AddMembers" -> {
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.AddMembers(members)
}
"NameChange" -> {
val name = data.getString("name")
kind = Kind.NameChange(name)
}
"Leave" -> {
kind = Kind.Leave
}
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
}
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime)
}
}
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 = DataMessage.newBuilder()
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
when (kind) {
is Kind.New -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
closedGroupUpdate.publicKey = ByteString.copyFrom(kind.publicKey)
closedGroupUpdate.name = kind.name
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) })
}
is Kind.Update -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.UPDATE
closedGroupUpdate.name = kind.name
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
}
is Kind.EncryptionKeyPair -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.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 = DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
}
is Kind.RemoveMembers -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
}
is Kind.AddMembers -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
}
is Kind.NameChange -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
closedGroupUpdate.name = kind.name
}
}
dataMessage.closedGroupControlMessage = closedGroupUpdate.build()
contentMessage.dataMessage = dataMessage.build()
val serializedContentMessage = contentMessage.build().toByteArray()
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(sendDestination)
val recipient = Recipient.from(context, Address.fromSerialized(sendDestination), false)
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
val ttl = when (kind) {
is Kind.EncryptionKeyPair -> 4 * 24 * 60 * 60 * 1000
else -> TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate)
}
try {
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
messageSender.sendMessage(0, address, udAccess,
sentTime, serializedContentMessage, false, ttl,
true, false, false, Optional.absent())
} catch (e: Exception) {
Log.d("Loki", "Failed to send closed group update message to: $sendDestination due to error: $e.")
}
}
public override fun onShouldRetry(e: Exception): Boolean {
return true
}
override fun onCanceled() { }
}

View File

@@ -3,9 +3,6 @@ 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.session.libsignal.libsignal.ecc.Curve
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
@@ -14,11 +11,8 @@ import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
@@ -28,7 +22,10 @@ import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
import org.session.libsignal.utilities.Hex
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair
import org.session.libsession.messaging.sending_receiving.pendingKeyPair
import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.GroupRecord
@@ -37,258 +34,8 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import java.util.*
import java.util.concurrent.ConcurrentHashMap
object ClosedGroupsProtocolV2 {
const val groupSizeLimit = 100
private val pendingKeyPair = ConcurrentHashMap<String,Optional<ECKeyPair>>()
sealed class Error(val description: String) : Exception() {
object NoThread : Error("Couldn't find a thread associated with the given group public key")
object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.")
object InvalidUpdate : Error("Invalid group update.")
}
fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>()
ThreadUtils.queue {
// Prepare
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val membersAsData = members.map { Hex.fromStringCondensed(it) }
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
// Generate the group's public key
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
val sentTime = System.currentTimeMillis()
// Generate the key pair that'll be used for encryption and decryption
val encryptionKeyPair = Curve.generateKeyPair()
// Create the group
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val admins = setOf( userPublicKey )
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }), System.currentTimeMillis())
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
// Send a closed group update message to all members individually
// Add the group to the user's set of public keys to poll for
apiDB.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
for (member in members) {
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately to make all of this sync
}
// Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Fulfill the promise
deferred.resolve(groupID)
}
// Return
return deferred.promise
}
/**
* @param notifyUser Inserts an outgoing info message for the user's leave message, useful to set `false` if
* you are exiting asynchronously and deleting the thread from [HomeActivity][org.thoughtcrime.securesms.loki.activities.HomeActivity.deleteConversation]
*/
@JvmStatic @JvmOverloads
fun explicitLeave(context: Context, groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
ThreadUtils.queue {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
val admins = group.admins.map { it.serialize() }
val name = group.title
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
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately
// Notify the user
val infoType = GroupContext.Type.QUIT
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
if (notifyUser) {
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
// Remove the group private key and unsubscribe from PNs
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
deferred.resolve(Unit)
}
return deferred.promise
}
@JvmStatic
fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List<String>) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.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() + 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[groupPublicKey]?.orNull() ?: 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
// 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)
}
}
@JvmStatic
fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List<String>) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.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)
}
}
@JvmStatic
fun explicitNameChange(context: Context, groupPublicKey: String, newName: String) {
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.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<String>) {
// Prepare
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Can't update nonexistent closed group.")
return
}
if (!group.admins.map { it.toString() }.contains(userPublicKey)) {
Log.d("Loki", "Can't distribute new encryption key pair as non-admin.")
return
}
// Generate the new encryption key pair
val newKeyPair = Curve.generateKeyPair()
// replace call will not succeed if no value already set
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
do {
// make sure we set the pendingKeyPair or wait until it is not null
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
// Distribute it
sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers)
// Store it * after * having sent out the message to the group
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
pendingKeyPair[groupPublicKey] = Optional.absent()
}
private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, 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())
val plaintext = proto.build().toByteArray()
val wrappers = targetMembers.mapNotNull { publicKey ->
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
}
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers, targetUser), System.currentTimeMillis())
if (force) {
job.setContext(context)
job.onRun() // Run the job immediately
} else {
ApplicationContext.getInstance(context).jobManager.add(job)
}
}
@JvmStatic
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
@@ -410,7 +157,7 @@ object ClosedGroupsProtocolV2 {
val isCurrentUserAdmin = admins.contains(userPublicKey)
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, newMembers)
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
}
}
val (contextType, signalType) =
@@ -460,7 +207,7 @@ object ClosedGroupsProtocolV2 {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
for (user in updateMembers) {
sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
}
}
}
@@ -523,7 +270,7 @@ object ClosedGroupsProtocolV2 {
val isCurrentUserAdmin = admins.contains(userPublicKey)
groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList)
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
}
}
// Notify user
@@ -685,7 +432,7 @@ object ClosedGroupsProtocolV2 {
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
if (wasAnyUserRemoved && isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members)
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
}
// Update the group
groupDB.updateTitle(groupID, name)