mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-25 19:12:19 +00:00
Implement rough V2 session protocol
This commit is contained in:
@@ -12,6 +12,7 @@ buildscript {
|
|||||||
classpath "com.android.tools.build:gradle:4.0.1"
|
classpath "com.android.tools.build:gradle:4.0.1"
|
||||||
classpath files('libs/gradle-witness.jar')
|
classpath files('libs/gradle-witness.jar')
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
classpath "com.google.gms:google-services:4.3.3"
|
classpath "com.google.gms:google-services:4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,7 @@ apply plugin: 'kotlin-android-extensions'
|
|||||||
apply plugin: 'witness'
|
apply plugin: 'witness'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
@@ -154,6 +156,7 @@ dependencies {
|
|||||||
implementation "org.whispersystems:signal-service-android:2.13.2" // Run ./gradlew install from session-android-service to install
|
implementation "org.whispersystems:signal-service-android:2.13.2" // Run ./gradlew install from session-android-service to install
|
||||||
implementation "org.whispersystems:curve25519-java:0.5.0"
|
implementation "org.whispersystems:curve25519-java:0.5.0"
|
||||||
// Remote:
|
// Remote:
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
||||||
implementation "com.goterl.lazycode:lazysodium-android:4.2.0@aar"
|
implementation "com.goterl.lazycode:lazysodium-android:4.2.0@aar"
|
||||||
implementation "net.java.dev.jna:jna:5.5.0@aar"
|
implementation "net.java.dev.jna:jna:5.5.0@aar"
|
||||||
implementation "com.google.protobuf:protobuf-java:2.5.0"
|
implementation "com.google.protobuf:protobuf-java:2.5.0"
|
||||||
|
@@ -66,6 +66,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
|||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||||
|
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||||
@@ -126,6 +127,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOper
|
|||||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.loki.api.crypto.SessionProtocol;
|
||||||
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI;
|
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI;
|
||||||
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
|
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
|
||||||
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager;
|
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager;
|
||||||
@@ -260,7 +262,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
|||||||
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
|
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
|
||||||
SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context);
|
SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context);
|
||||||
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
|
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
|
||||||
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), new SessionProtocolImpl(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator());
|
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(context);
|
||||||
|
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), new SessionProtocolImpl(context), sessionResetProtocol, apiDB, UnidentifiedAccessUtil.getCertificateValidator());
|
||||||
|
|
||||||
SignalServiceContent content = cipher.decrypt(envelope);
|
SignalServiceContent content = cipher.decrypt(envelope);
|
||||||
|
|
||||||
@@ -380,6 +383,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
|||||||
Log.i(TAG, "Dropping UD message from self.");
|
Log.i(TAG, "Dropping UD message from self.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.i(TAG, "IOException during message decryption.");
|
Log.i(TAG, "IOException during message decryption.");
|
||||||
|
} catch (SessionProtocol.Exception e) {
|
||||||
|
Log.i(TAG, "Couldn't handle message due to error: " + e.getDescription());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,12 +11,15 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
|||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.whispersystems.libsignal.IdentityKeyPair
|
||||||
|
import org.whispersystems.libsignal.ecc.ECKeyPair
|
||||||
import org.whispersystems.libsignal.util.Hex
|
import org.whispersystems.libsignal.util.Hex
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE
|
||||||
import org.whispersystems.signalservice.loki.api.SnodeAPI
|
import org.whispersystems.signalservice.loki.api.SnodeAPI
|
||||||
import org.whispersystems.signalservice.loki.api.crypto.SessionProtocol
|
import org.whispersystems.signalservice.loki.api.crypto.SessionProtocol
|
||||||
|
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
|
||||||
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
|
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
|
||||||
import org.whispersystems.signalservice.loki.utilities.toHexString
|
import org.whispersystems.signalservice.loki.utilities.toHexString
|
||||||
|
|
||||||
@@ -47,25 +50,9 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
|||||||
return ciphertext
|
return ciphertext
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun decrypt(envelope: SignalServiceEnvelope): Pair<ByteArray, String> {
|
override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||||
val ciphertext = envelope.content ?: throw SessionProtocol.Exception.NoData
|
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||||
val recipientX25519PrivateKey: ByteArray
|
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||||
val recipientX25519PublicKey: ByteArray
|
|
||||||
when (envelope.type) {
|
|
||||||
UNIDENTIFIED_SENDER_VALUE -> {
|
|
||||||
recipientX25519PrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
|
||||||
recipientX25519PublicKey = Hex.fromStringCondensed(TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded())
|
|
||||||
}
|
|
||||||
CLOSED_GROUP_CIPHERTEXT_VALUE -> {
|
|
||||||
val hexEncodedGroupPublicKey = envelope.source
|
|
||||||
val sskDB = DatabaseFactory.getSSKDatabase(context)
|
|
||||||
if (!sskDB.isSSKBasedClosedGroup(hexEncodedGroupPublicKey)) { throw SessionProtocol.Exception.InvalidGroupPublicKey }
|
|
||||||
val hexEncodedGroupPrivateKey = sskDB.getClosedGroupPrivateKey(hexEncodedGroupPublicKey) ?: throw SessionProtocol.Exception.NoGroupPrivateKey
|
|
||||||
recipientX25519PrivateKey = Hex.fromStringCondensed(hexEncodedGroupPrivateKey)
|
|
||||||
recipientX25519PublicKey = Hex.fromStringCondensed(hexEncodedGroupPublicKey.removing05PrefixIfNeeded())
|
|
||||||
}
|
|
||||||
else -> throw AssertionError()
|
|
||||||
}
|
|
||||||
val sodium = LazySodiumAndroid(SodiumAndroid())
|
val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||||
val signatureSize = Sign.BYTES
|
val signatureSize = Sign.BYTES
|
||||||
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES
|
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES
|
||||||
|
@@ -10,6 +10,10 @@ import org.thoughtcrime.securesms.database.Database
|
|||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.loki.utilities.*
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.whispersystems.libsignal.IdentityKeyPair
|
||||||
|
import org.whispersystems.libsignal.ecc.DjbECPrivateKey
|
||||||
|
import org.whispersystems.libsignal.ecc.DjbECPublicKey
|
||||||
|
import org.whispersystems.libsignal.ecc.ECKeyPair
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
||||||
import org.whispersystems.signalservice.loki.api.Snode
|
import org.whispersystems.signalservice.loki.api.Snode
|
||||||
import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol
|
import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol
|
||||||
@@ -392,6 +396,31 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
TextSecurePreferences.setLastSnodePoolRefreshDate(context, date)
|
TextSecurePreferences.setLastSnodePoolRefreshDate(context, date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getUserX25519KeyPair(): ECKeyPair {
|
||||||
|
val keyPair = IdentityKeyUtil.getIdentityKeyPair(context)
|
||||||
|
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize()), DjbECPrivateKey(keyPair.privateKey.serialize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): List<ECKeyPair> {
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addClosedGroupPublicKey(groupPublicKey: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// region Deprecated
|
// region Deprecated
|
||||||
override fun getDeviceLinks(publicKey: String): Set<DeviceLink> {
|
override fun getDeviceLinks(publicKey: String): Set<DeviceLink> {
|
||||||
return setOf()
|
return setOf()
|
||||||
|
@@ -0,0 +1,187 @@
|
|||||||
|
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.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||||
|
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl
|
||||||
|
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.util.Hex
|
||||||
|
import org.whispersystems.libsignal.SignalProtocolAddress
|
||||||
|
import org.whispersystems.libsignal.ecc.DjbECPrivateKey
|
||||||
|
import org.whispersystems.libsignal.ecc.DjbECPublicKey
|
||||||
|
import org.whispersystems.libsignal.ecc.ECKeyPair
|
||||||
|
import org.whispersystems.libsignal.ecc.ECPublicKey
|
||||||
|
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.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : 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()
|
||||||
|
class EncryptionKeyPair(val wrappers: Collection<KeyPairWrapper>) : 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: SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper): KeyPairWrapper {
|
||||||
|
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toProto(): SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper {
|
||||||
|
val result = SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper.newBuilder()
|
||||||
|
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||||
|
result.encryptedKeyPair = ByteString.copyFrom(encryptedKeyPair)
|
||||||
|
return result.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(destination: String, kind: Kind) : this(Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setQueue(KEY)
|
||||||
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
|
.setMaxAttempts(20)
|
||||||
|
.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("publicKey", kind.publicKey)
|
||||||
|
builder.putString("name", kind.name)
|
||||||
|
builder.putByteArray("encryptionKeyPairPublicKey", kind.encryptionKeyPair.publicKey.serialize())
|
||||||
|
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.EncryptionKeyPair -> {
|
||||||
|
builder.putString("kind", "EncryptionKeyPair")
|
||||||
|
val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) }
|
||||||
|
builder.putString("wrappers", wrappers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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) }
|
||||||
|
kind = Kind.EncryptionKeyPair(wrappers)
|
||||||
|
}
|
||||||
|
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
|
||||||
|
}
|
||||||
|
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onRun() {
|
||||||
|
val contentMessage = SignalServiceProtos.Content.newBuilder()
|
||||||
|
val dataMessage = SignalServiceProtos.DataMessage.newBuilder()
|
||||||
|
val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdateV2.newBuilder()
|
||||||
|
when (kind) {
|
||||||
|
is Kind.New -> {
|
||||||
|
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW
|
||||||
|
closedGroupUpdate.publicKey = ByteString.copyFrom(kind.publicKey)
|
||||||
|
closedGroupUpdate.name = kind.name
|
||||||
|
val encryptionKeyPair = SignalServiceProtos.ClosedGroupUpdateV2.KeyPair.newBuilder()
|
||||||
|
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize())
|
||||||
|
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 = SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE
|
||||||
|
closedGroupUpdate.name = kind.name
|
||||||
|
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||||
|
}
|
||||||
|
is Kind.EncryptionKeyPair -> {
|
||||||
|
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR
|
||||||
|
closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataMessage.closedGroupUpdateV2 = 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: Int
|
||||||
|
when (kind) {
|
||||||
|
is Kind.EncryptionKeyPair -> ttl = 4 * 24 * 60 * 60 * 1000
|
||||||
|
else -> ttl = 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.get().targetUnidentifiedAccess,
|
||||||
|
Date().time, serializedContentMessage, false, ttl, false,
|
||||||
|
true, false, false, false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onShouldRetry(e: Exception): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCanceled() { }
|
||||||
|
}
|
@@ -0,0 +1,350 @@
|
|||||||
|
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.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.database.Address
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
||||||
|
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
||||||
|
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||||
|
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.sms.IncomingGroupMessage
|
||||||
|
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||||
|
import org.thoughtcrime.securesms.sms.MessageSender
|
||||||
|
import org.thoughtcrime.securesms.util.GroupUtil
|
||||||
|
import org.thoughtcrime.securesms.util.Hex
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.whispersystems.libsignal.ecc.Curve
|
||||||
|
import org.whispersystems.libsignal.ecc.DjbECPrivateKey
|
||||||
|
import org.whispersystems.libsignal.ecc.DjbECPublicKey
|
||||||
|
import org.whispersystems.libsignal.ecc.ECKeyPair
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext
|
||||||
|
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet
|
||||||
|
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType
|
||||||
|
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
|
||||||
|
import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation
|
||||||
|
import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey
|
||||||
|
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
|
||||||
|
import org.whispersystems.signalservice.loki.utilities.toHexString
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.jvm.Throws
|
||||||
|
|
||||||
|
object ClosedGroupsProtocolV2 {
|
||||||
|
val groupSizeLimit = 20
|
||||||
|
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||||
|
val deferred = deferred<String, Exception>()
|
||||||
|
Thread {
|
||||||
|
// 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
|
||||||
|
// Generate the key pair that'll be used for encryption and decryption
|
||||||
|
val encryptionKeyPair = Curve.generateKeyPair()
|
||||||
|
// Create the group
|
||||||
|
val groupID = 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) }))
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||||
|
// Send a closed group update message to all members individually
|
||||||
|
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||||
|
for (member in members) {
|
||||||
|
if (member == userPublicKey) { continue }
|
||||||
|
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind)
|
||||||
|
job.setContext(context)
|
||||||
|
job.onRun() // Run the job immediately to make all of this sync
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
// Notify the PN server
|
||||||
|
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||||
|
// Fulfill the promise
|
||||||
|
deferred.resolve(groupID)
|
||||||
|
}.start()
|
||||||
|
// Return
|
||||||
|
return deferred.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
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)
|
||||||
|
return update(context, groupPublicKey, newMembers, name).get()
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
|
||||||
|
val deferred = deferred<Unit, Exception>()
|
||||||
|
Thread {
|
||||||
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
|
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||||
|
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||||
|
val group = groupDB.getGroup(groupID).orNull()
|
||||||
|
if (group == null) {
|
||||||
|
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||||
|
return@Thread deferred.reject(Error.NoThread)
|
||||||
|
}
|
||||||
|
val oldMembers = group.members.map { it.serialize() }.toSet()
|
||||||
|
val newMembers = members.minus(oldMembers)
|
||||||
|
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||||
|
val admins = group.admins.map { it.serialize() }
|
||||||
|
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||||
|
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||||
|
if (encryptionKeyPair == null) {
|
||||||
|
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||||
|
return@Thread deferred.reject(Error.NoKeyPair)
|
||||||
|
}
|
||||||
|
val removedMembers = oldMembers.minus(members)
|
||||||
|
if (removedMembers.contains(admins.first())) {
|
||||||
|
Log.d("Loki", "Can't remove admin from closed group.")
|
||||||
|
return@Thread deferred.reject(Error.InvalidUpdate)
|
||||||
|
}
|
||||||
|
val isUserLeaving = removedMembers.contains(userPublicKey)
|
||||||
|
if (isUserLeaving && (removedMembers.count() != 1 || newMembers.isNotEmpty())) {
|
||||||
|
Log.d("Loki", "Can't remove self and add or remove others simultaneously.")
|
||||||
|
return@Thread deferred.reject(Error.InvalidUpdate)
|
||||||
|
}
|
||||||
|
// Send the update to the group
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData)
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind)
|
||||||
|
job.setContext(context)
|
||||||
|
job.onRun() // Run the job immediately
|
||||||
|
if (isUserLeaving) {
|
||||||
|
// Remove the group private key and unsubscribe from PNs
|
||||||
|
apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||||
|
// Mark the group as inactive
|
||||||
|
groupDB.setActive(groupID, false)
|
||||||
|
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||||
|
// Notify the PN server
|
||||||
|
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||||
|
} else {
|
||||||
|
// Generate and distribute a new encryption key pair if needed
|
||||||
|
val wasAnyUserRemoved = removedMembers.isNotEmpty()
|
||||||
|
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||||
|
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||||
|
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members.minus(newMembers))
|
||||||
|
}
|
||||||
|
// Send closed group update messages to any new members individually
|
||||||
|
for (member in newMembers) {
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
|
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind)
|
||||||
|
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update the group
|
||||||
|
groupDB.updateTitle(groupID, name)
|
||||||
|
if (!isUserLeaving) {
|
||||||
|
// 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 infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
|
||||||
|
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||||
|
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
|
||||||
|
deferred.resolve(Unit)
|
||||||
|
}.start()
|
||||||
|
return deferred.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection<String>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
public fun handleSharedSenderKeysUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) {
|
||||||
|
if (!isValid(closedGroupUpdate)) { return; }
|
||||||
|
when (closedGroupUpdate.type) {
|
||||||
|
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey)
|
||||||
|
SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> handleClosedGroupUpdate(context, closedGroupUpdate, senderPublicKey)
|
||||||
|
SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, senderPublicKey)
|
||||||
|
else -> {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2): Boolean {
|
||||||
|
if (closedGroupUpdate.publicKey.isEmpty) { return false }
|
||||||
|
when (closedGroupUpdate.type) {
|
||||||
|
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> {
|
||||||
|
return !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
|
||||||
|
&& !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0
|
||||||
|
}
|
||||||
|
SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> {
|
||||||
|
return !closedGroupUpdate.name.isNullOrEmpty() && closedGroupUpdate.membersCount > 0
|
||||||
|
}
|
||||||
|
SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> return true
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) {
|
||||||
|
// Prepare
|
||||||
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
|
// Unwrap the message
|
||||||
|
val groupPublicKey = closedGroupUpdate.publicKey.toByteArray().toHexString()
|
||||||
|
val name = closedGroupUpdate.name
|
||||||
|
val encryptionKeyPairAsProto = closedGroupUpdate.encryptionKeyPair
|
||||||
|
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||||
|
val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() }
|
||||||
|
// Create the group
|
||||||
|
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||||
|
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||||
|
if (groupDB.getGroup(groupID).orNull() != null) {
|
||||||
|
// Update the group
|
||||||
|
groupDB.updateTitle(groupID, name)
|
||||||
|
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||||
|
} else {
|
||||||
|
groupDB.create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||||
|
null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||||
|
}
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||||
|
// Add the group to the user's set of public keys to poll for
|
||||||
|
apiDB.addClosedGroupPublicKey(groupPublicKey)
|
||||||
|
// Store the encryption key pair
|
||||||
|
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||||
|
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||||
|
// Notify the user
|
||||||
|
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||||
|
// Notify the PN server
|
||||||
|
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) {
|
||||||
|
// Prepare
|
||||||
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
|
// Unwrap the message
|
||||||
|
val groupPublicKey = closedGroupUpdate.publicKey.toByteArray().toHexString()
|
||||||
|
val name = closedGroupUpdate.name
|
||||||
|
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||||
|
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||||
|
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||||
|
val group = groupDB.getGroup(groupID).orNull()
|
||||||
|
if (group == null) {
|
||||||
|
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val oldMembers = group.members.map { it.serialize() }
|
||||||
|
val newMembers = members.toSet().minus(oldMembers)
|
||||||
|
// Check that the sender is a member of the group (before the update)
|
||||||
|
if (!oldMembers.contains(senderPublicKey)) {
|
||||||
|
Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||||
|
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) {
|
||||||
|
apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||||
|
// Mark the group as inactive
|
||||||
|
groupDB.setActive(groupID, false)
|
||||||
|
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||||
|
// Notify the PN server
|
||||||
|
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, 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.toPhoneString() }.contains(userPublicKey)
|
||||||
|
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||||
|
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members.minus(newMembers))
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toPhoneString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleGroupEncryptionKeyPair(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||||
|
name: String, members: Collection<String>, admins: Collection<String>) {
|
||||||
|
val groupContextBuilder = GroupContext.newBuilder()
|
||||||
|
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID)))
|
||||||
|
.setType(type0)
|
||||||
|
.setName(name)
|
||||||
|
.addAllMembers(members)
|
||||||
|
.addAllAdmins(admins)
|
||||||
|
val group = SignalServiceGroup(type1, GroupUtil.getDecodedId(groupID), GroupType.SIGNAL, name, members.toList(), null, admins.toList())
|
||||||
|
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true)
|
||||||
|
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
|
||||||
|
val smsDB = DatabaseFactory.getSmsDatabase(context)
|
||||||
|
smsDB.insertMessageInbox(infoMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String,
|
||||||
|
members: Collection<String>, admins: Collection<String>, threadID: Long) {
|
||||||
|
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||||
|
val groupContextBuilder = GroupContext.newBuilder()
|
||||||
|
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID)))
|
||||||
|
.setType(type)
|
||||||
|
.setName(name)
|
||||||
|
.addAllMembers(members)
|
||||||
|
.addAllAdmins(admins)
|
||||||
|
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf())
|
||||||
|
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||||
|
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||||
|
mmsDB.markAsSent(infoMessageID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Signal group ID handling is weird. The ID is double encoded in the database, but not in a `GroupContext`.
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(IOException::class)
|
||||||
|
public fun doubleEncodeGroupID(groupPublicKey: String): String {
|
||||||
|
return GroupUtil.getEncodedId(GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false).toByteArray(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(IOException::class)
|
||||||
|
public fun doubleDecodeGroupID(groupID: String): ByteArray {
|
||||||
|
return GroupUtil.getDecodedId(GroupUtil.getDecodedStringId(groupID))
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user