From 6be509c6574197970fb468d0f63a9874c76a3b6b Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 7 Aug 2020 12:04:23 +1000 Subject: [PATCH] Implement ClosedGroupUpdateMessageSendJob --- .../migration/WorkManagerFactoryMappings.java | 2 + .../securesms/jobs/JobManagerFactories.java | 4 +- .../loki/database/SharedSenderKeysDatabase.kt | 7 +- .../ClosedGroupUpdateMessageSendJob.kt | 183 ++++++++++++++++++ 4 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java index 353dcd2149..04bb55eca4 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.jobs.SmsSentJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.jobs.UpdateApkJob; +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; import java.util.HashMap; @@ -76,6 +77,7 @@ public class WorkManagerFactoryMappings { put(PushNotificationReceiveJob.class.getName(), PushNotificationReceiveJob.KEY); put(PushTextSendJob.class.getName(), PushTextSendJob.KEY); put(NullMessageSendJob.class.getName(), NullMessageSendJob.KEY); + put(ClosedGroupUpdateMessageSendJob.class.getName(), ClosedGroupUpdateMessageSendJob.KEY); put(RefreshAttributesJob.class.getName(), RefreshAttributesJob.KEY); put(RefreshPreKeysJob.class.getName(), RefreshPreKeysJob.KEY); put(RefreshUnidentifiedDeliveryAbilityJob.class.getName(), RefreshUnidentifiedDeliveryAbilityJob.KEY); diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 66bf564ad9..41b31fd332 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; 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.protocol.ClosedGroupUpdateMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob; import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob; @@ -51,7 +52,8 @@ public final class JobManagerFactories { put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory()); put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory()); put(PushTextSendJob.KEY, new PushTextSendJob.Factory()); - put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory()); + put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory()); + put(ClosedGroupUpdateMessageSendJob.KEY, new ClosedGroupUpdateMessageSendJob.Factory()); put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory()); put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory()); put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory()); diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt index 749f4ba02f..1b39068b49 100644 --- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -22,7 +22,8 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : private val keyIndex = "key_index" private val messageKeys = "message_keys" @JvmStatic val createClosedGroupRatchetsTableCommand - = "CREATE TABLE $closedGroupRatchetsTable (PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey), $chainKey STRING, $keyIndex INTEGER DEFAULT 0, $messageKeys STRING);" + = "CREATE TABLE $closedGroupRatchetsTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " + + "$keyIndex INTEGER DEFAULT 0, $messageKeys STRING, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));" // Private keys private val closedGroupPrivateKeysTable = "closed_group_private_keys" private val closedGroupPrivateKey = "closed_group_private_key" @@ -37,7 +38,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : return database.get(closedGroupRatchetsTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> val chainKey = cursor.getString(Companion.chainKey) val keyIndex = cursor.getInt(Companion.keyIndex) - val messageKeys = cursor.getString(Companion.messageKeys).split("-") + val messageKeys = cursor.getString(Companion.messageKeys).split(" - ") ClosedGroupRatchet(chainKey, keyIndex, messageKeys) } } @@ -49,7 +50,7 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : values.put(Companion.senderPublicKey, senderPublicKey) values.put(Companion.chainKey, ratchet.chainKey) values.put(Companion.keyIndex, ratchet.keyIndex) - values.put(Companion.messageKeys, ratchet.messageKeys.joinToString("-")) + values.put(Companion.messageKeys, ratchet.messageKeys.joinToString(" - ")) val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" database.insertOrUpdate(closedGroupRatchetsTable, values, query, arrayOf( groupPublicKey, senderPublicKey )) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt new file mode 100644 index 0000000000..f45f701d17 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt @@ -0,0 +1,183 @@ +package org.thoughtcrime.securesms.loki.protocol + +import com.google.protobuf.ByteString +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil +import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobs.BaseJob +import org.thoughtcrime.securesms.logging.Log +import org.thoughtcrime.securesms.loki.utilities.recipient +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.Hex +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities +import org.whispersystems.signalservice.loki.utilities.toHexString +import java.io.IOException +import java.security.SecureRandom +import java.util.* +import java.util.concurrent.TimeUnit + +class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) { + + sealed class Kind { + class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class SenderKeyRequest(val groupPublicKey: ByteArray) : Kind() + class SenderKey(val groupPublicKey: ByteArray, val senderKey: ClosedGroupSenderKey) : Kind() + } + + companion object { + const val KEY = "ClosedGroupUpdateMessageSendJob" + } + + constructor(destination: String, kind: Kind) : this(Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue(KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(1) + .build(), + destination, + kind) + + override fun getFactoryKey(): String { return KEY } + + override fun serialize(): Data { + val builder = Data.Builder() + builder.putString("destination", destination) + when (kind) { + is Kind.New -> { + builder.putString("kind", "New") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("name", kind.name) + builder.putByteArray("groupPrivateKey", kind.groupPrivateKey) + val senderKeys = kind.senderKeys.joinToString(" - ") { it.toJSON() } + builder.putString("senderKeys", senderKeys) + val members = kind.members.joinToString(" - ") { it.toHexString() } + builder.putString("members", members) + val admins = kind.admins.joinToString(" - ") { it.toHexString() } + builder.putString("admins", admins) + } + is Kind.Info -> { + builder.putString("kind", "Info") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("name", kind.name) + val senderKeys = kind.senderKeys.joinToString(" - ") { it.toJSON() } + builder.putString("senderKeys", senderKeys) + val members = kind.members.joinToString(" - ") { it.toHexString() } + builder.putString("members", members) + val admins = kind.admins.joinToString(" - ") { it.toHexString() } + builder.putString("admins", admins) + } + is Kind.SenderKeyRequest -> { + builder.putString("kind", "SenderKeyRequest") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + } + is Kind.SenderKey -> { + builder.putString("kind", "SenderKey") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("senderKey", kind.senderKey.toJSON()) + } + } + return builder.build() + } + + public override fun onRun() { + val contentMessage = SignalServiceProtos.Content.newBuilder() + val dataMessage = SignalServiceProtos.DataMessage.newBuilder() + val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdate.newBuilder() + when (kind) { + is Kind.New -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.NEW + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.name = kind.name + closedGroupUpdate.groupPrivateKey = ByteString.copyFrom(kind.groupPrivateKey) + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.Info -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.INFO + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.name = kind.name + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.SenderKeyRequest -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + } + is Kind.SenderKey -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.addAllSenderKeys(listOf( kind.senderKey.toProto() )) + } + } + dataMessage.closedGroupUpdate = closedGroupUpdate.build() + 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 udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) + val ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate) + try { + // TODO: useFallbackEncryption + // TODO: isClosedGroup + messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, + Date().time, serializedContentMessage, false, ttl, false, + false, false, false) + } catch (e: Exception) { + Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") + throw e + } + } + + public override fun onShouldRetry(e: Exception): Boolean { + // Disable since we have our own retrying + return false + } + + override fun onCanceled() { } + + class Factory : Job.Factory { + + override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJob { + val destination = data.getString("destination") + val rawKind = data.getString("kind") + val groupPublicKey = data.getByteArray("groupPublicKey") + val kind: Kind + when (rawKind) { + "New" -> { + val name = data.getString("name") + val groupPrivateKey = data.getByteArray("groupPrivateKey") + val senderKeys = data.getString("senderKeys").split(" - ").map { ClosedGroupSenderKey.fromJSON(it)!! } + val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } + val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } + kind = Kind.New(groupPublicKey, name, groupPrivateKey, senderKeys, members, admins) + } + "Info" -> { + val name = data.getString("name") + val senderKeys = data.getString("senderKeys").split(" - ").map { ClosedGroupSenderKey.fromJSON(it)!! } + val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } + val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } + kind = Kind.Info(groupPublicKey, name, senderKeys, members, admins) + } + "SenderKeyRequest" -> { + kind = Kind.SenderKeyRequest(groupPublicKey) + } + "SenderKey" -> { + val senderKey = ClosedGroupSenderKey.fromJSON(data.getString("senderKey"))!! + kind = Kind.SenderKey(groupPublicKey, senderKey) + } + else -> throw Exception("Invalid closed group update message kind: $rawKind.") + } + return ClosedGroupUpdateMessageSendJob(parameters, destination, kind) + } + } +}