commit
c72c87d698
@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import android.content.Context
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
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.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||
import java.util.*
|
||||
|
||||
object MultiDeviceProtocol {
|
||||
|
||||
@JvmStatic
|
||||
fun syncConfigurationIfNeeded(context: Context) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastSyncTime < 2 * 24 * 60 * 60 * 1000) return
|
||||
val configurationMessage = ConfigurationMessage.getCurrent()
|
||||
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(userPublicKey)
|
||||
val recipient = recipient(context, userPublicKey)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
try {
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
|
||||
true, false, true, Optional.absent())
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send configuration message due to error: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
fun forceSyncConfigurationNowIfNeeded(context: Context) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
val configurationMessage = ConfigurationMessage.getCurrent()
|
||||
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(userPublicKey)
|
||||
val recipient = recipient(context, userPublicKey)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
try {
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
|
||||
true, false, true, Optional.absent())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send configuration message due to error: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) {
|
||||
if (TextSecurePreferences.getConfigurationMessageSynced(context)) return
|
||||
val configurationMessage = ConfigurationMessage.fromProto(content) ?: return
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
if (senderPublicKey != userPublicKey) return
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closedGroup in configurationMessage.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
|
||||
|
||||
val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdateV2.newBuilder()
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey))
|
||||
closedGroupUpdate.name = closedGroup.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.privateKey.serialize())
|
||||
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
|
||||
ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
for (openGroup in configurationMessage.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup)) continue
|
||||
OpenGroupUtilities.addGroup(context, openGroup, 1)
|
||||
}
|
||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
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.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups: List<String>): ControlMessage() {
|
||||
|
||||
class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<String>, val admins: List<String>) {
|
||||
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.ClosedGroup): ClosedGroup? {
|
||||
if (!proto.hasPublicKey() || !proto.hasName() || !proto.hasEncryptionKeyPair()) return null
|
||||
val publicKey = proto.publicKey.toByteArray().toHexString()
|
||||
val name = proto.name
|
||||
val encryptionKeyPairAsProto = proto.encryptionKeyPair
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
val members = proto.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
|
||||
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
|
||||
}
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.ConfigurationMessage.ClosedGroup? {
|
||||
val result = SignalServiceProtos.ConfigurationMessage.ClosedGroup.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.name = name
|
||||
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
override val ttl: Long = 4 * 24 * 60 * 60 * 1000
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
companion object {
|
||||
|
||||
fun getCurrent(): ConfigurationMessage {
|
||||
val closedGroups = mutableListOf<ClosedGroup>()
|
||||
val openGroups = mutableListOf<String>()
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val groups = storage.getAllGroups()
|
||||
for (groupRecord in groups) {
|
||||
if (groupRecord.isClosedGroup) {
|
||||
if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||
val groupPublicKey = GroupUtil.getDecodedGroupIDAsData(groupRecord.encodedId).toHexString()
|
||||
if (!storage.isClosedGroup(groupPublicKey)) continue
|
||||
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
|
||||
val closedGroup = ClosedGroup(groupPublicKey, groupRecord.title, encryptionKeyPair, groupRecord.members.map { it.serialize() }, groupRecord.admins.map { it.serialize() })
|
||||
closedGroups.add(closedGroup)
|
||||
}
|
||||
if (groupRecord.isOpenGroup) {
|
||||
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
|
||||
val openGroup = storage.getOpenGroup(threadID) ?: continue
|
||||
openGroups.add(openGroup.server)
|
||||
}
|
||||
}
|
||||
return ConfigurationMessage(closedGroups, openGroups)
|
||||
}
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ConfigurationMessage? {
|
||||
if (!proto.hasConfigurationMessage()) return null
|
||||
val configurationProto = proto.configurationMessage
|
||||
val closedGroups = configurationProto.closedGroupsList.mapNotNull { ClosedGroup.fromProto(it) }
|
||||
val openGroups = configurationProto.openGroupsList
|
||||
return ConfigurationMessage(closedGroups, openGroups)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder()
|
||||
configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() })
|
||||
configurationProto.addAllOpenGroups(openGroups)
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
contentProto.configurationMessage = configurationProto.build()
|
||||
return contentProto.build()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return """
|
||||
ConfigurationMessage(
|
||||
closedGroups: ${(closedGroups)}
|
||||
openGroups: ${(openGroups)}
|
||||
)
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue