diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index ea37d3f49d..73aaa43177 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -959,7 +959,7 @@ open class Storage( groupCreationTimestamp ) val keysBatchInfo = SnodeAPI.buildAuthenticatedStoreBatchInfo( - GroupKeysConfig.namespace(), + groupKeys.namespace(), keysSnodeMessage, adminKey ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index 2ece21c0bc..3a86e37430 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -11,6 +11,7 @@ import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.GroupMembersConfig import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserProfile +import org.session.libsession.messaging.messages.Destination import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigFactoryUpdateListener @@ -358,4 +359,9 @@ class ConfigFactory( val timestamp = SnodeAPI.nowWithOffset configDatabase.storeGroupConfigs(pubKey, groupKeys.dump(), groupInfo.dump(), groupMembers.dump(), timestamp) } + + override fun scheduleUpdate(destination: Destination) { + // there's probably a better way to do this + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(destination) + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt index b0954f2327..8d068690d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistry.kt @@ -73,7 +73,7 @@ class PushRegistry @Inject constructor( token: String, publicKey: String, userEd25519Key: KeyPair, - namespaces: List = listOf(Namespace.DEFAULT) + namespaces: List = listOf(Namespace.DEFAULT()) ): Promise<*, Exception> { Log.d(TAG, "register() called") diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt index 4bef45ff97..d05eb2a0e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt @@ -53,7 +53,7 @@ class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) val requestParameters = SubscriptionRequest( pubkey = publicKey, session_ed25519 = userEd25519Key.publicKey.asHexString, - namespaces = listOf(Namespace.DEFAULT), + namespaces = listOf(Namespace.DEFAULT()), data = true, // only permit data subscription for now (?) service = device.service, sig_ts = timestamp, diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt index 37a54a4afc..b6e2f2f1aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt @@ -104,7 +104,8 @@ class ClearAllDataDialog : DialogFragment() { if (!deleteNetworkMessages) { try { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()).get() + // TODO: maybe convert this to a blocking config job + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()) } catch (e: Exception) { Log.e("Loki", "Failed to force sync", e) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt index 6d35d2f145..c60bc8f5c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.util import android.content.Context -import android.provider.Telephony.Sms.Conversations import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.ConversationVolatileConfig @@ -12,7 +11,6 @@ import network.loki.messenger.libsession_util.util.Contact import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.UserPic -import nl.komponents.kovenant.Promise import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.ConfigurationSyncJob import org.session.libsession.messaging.jobs.JobQueue @@ -27,6 +25,7 @@ import org.session.libsession.utilities.WindowDebouncer import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.SessionId import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.database.GroupDatabase @@ -38,17 +37,16 @@ object ConfigurationMessageUtilities { private val debouncer = WindowDebouncer(3000, Timer()) - private fun scheduleConfigSync(userPublicKey: String) { + private fun scheduleConfigSync(destination: Destination) { debouncer.publish { // don't schedule job if we already have one val storage = MessagingModuleConfiguration.shared.storage - val ourDestination = Destination.Contact(userPublicKey) - val currentStorageJob = storage.getConfigSyncJob(ourDestination) + val currentStorageJob = storage.getConfigSyncJob(destination) if (currentStorageJob != null) { (currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true) return@publish } - val newConfigSync = ConfigurationSyncJob(ourDestination) + val newConfigSync = ConfigurationSyncJob(destination) JobQueue.shared.add(newConfigSync) } } @@ -60,7 +58,7 @@ object ConfigurationMessageUtilities { val forcedConfig = TextSecurePreferences.hasForcedNewConfig(context) val currentTime = SnodeAPI.nowWithOffset if (ConfigBase.isNewConfigEnabled(forcedConfig, currentTime)) { - scheduleConfigSync(userPublicKey) + scheduleConfigSync(Destination.Contact(userPublicKey)) return } val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context) @@ -84,34 +82,21 @@ object ConfigurationMessageUtilities { TextSecurePreferences.setLastConfigurationSyncTime(context, now) } - fun forceSyncConfigurationNowIfNeeded(context: Context): Promise { + fun forceSyncConfigurationNowIfNeeded(destination: Destination) { + scheduleConfigSync(destination) + } + + + fun forceSyncConfigurationNowIfNeeded(context: Context) { // add if check here to schedule new config job process and return early - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofFail(NullPointerException("User Public Key is null")) + val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Log.e("Loki", NullPointerException("User Public Key is null")) val forcedConfig = TextSecurePreferences.hasForcedNewConfig(context) val currentTime = SnodeAPI.nowWithOffset if (ConfigBase.isNewConfigEnabled(forcedConfig, currentTime)) { // schedule job if none exist // don't schedule job if we already have one - scheduleConfigSync(userPublicKey) - return Promise.ofSuccess(Unit) + scheduleConfigSync(Destination.Contact(userPublicKey)) } - val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> - !recipient.isGroupRecipient && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty() - }.map { recipient -> - ConfigurationMessage.Contact( - publicKey = recipient.address.serialize(), - name = recipient.name!!, - profilePicture = recipient.profileAvatar, - profileKey = recipient.profileKey, - isApproved = recipient.isApproved, - isBlocked = recipient.isBlocked, - didApproveMe = recipient.hasApprovedMe() - ) - } - val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return Promise.ofSuccess(Unit) - val promise = MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)), isSyncMessage = true) - TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) - return promise } private fun maybeUserSecretKey() = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index 3c2c9e9c32..2a9ae764c8 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -292,7 +292,7 @@ class GroupMembersConfig(pointer: Long): ConfigBase(pointer), Closeable { } } -abstract class ConfigSig(pointer: Long) : Config(pointer) +sealed class ConfigSig(pointer: Long) : Config(pointer) class GroupKeysConfig(pointer: Long): ConfigSig(pointer) { companion object { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt index b5306a181e..6281fb49f6 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt @@ -2,15 +2,23 @@ package org.session.libsession.messaging.jobs import network.loki.messenger.libsession_util.Config import network.loki.messenger.libsession_util.ConfigBase +import network.loki.messenger.libsession_util.ConfigBase.Companion.protoKindFor +import network.loki.messenger.libsession_util.GroupKeysConfig +import network.loki.messenger.libsession_util.util.ConfigPush import nl.komponents.kovenant.functional.bind import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.control.SharedConfigurationMessage +import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI.SnodeBatchRequestInfo +import org.session.libsession.snode.SnodeMessage import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.SessionId import java.util.concurrent.atomic.AtomicBoolean class InvalidDestination: Exception("Trying to push configs somewhere other than our swarm or a closed group") @@ -30,8 +38,65 @@ data class ConfigurationSyncJob(val destination: Destination): Job { data class SyncInformation(val configs: List, val toDelete: List) - private fun destinationConfigs(delegate: JobDelegate, configFactoryProtocol: ConfigFactoryProtocol): SyncInformation { - TODO() + private fun destinationConfigs(delegate: JobDelegate, + dispatcherName: String, + configFactoryProtocol: ConfigFactoryProtocol): SyncInformation { + val toDelete = mutableListOf() + val configsRequiringPush = if (destination is Destination.ClosedGroup) { + val sentTimestamp = SnodeAPI.nowWithOffset + // destination is a closed group, get all configs requiring push here + val groupId = SessionId.from(destination.publicKey) + + val signingKey = configFactoryProtocol.userGroups!!.getClosedGroup(destination.publicKey)!!.signingKey() + + val keys = configFactoryProtocol.getGroupKeysConfig(groupId)!! + val info = configFactoryProtocol.getGroupInfoConfig(groupId)!! + val members = configFactoryProtocol.getGroupMemberConfig(groupId)!! + + val requiringPush = listOf(keys, info, members).filter { + when (it) { + is GroupKeysConfig -> it.pendingConfig()?.isNotEmpty() == true + is ConfigBase -> it.needsPush() + else -> false + } + } + + // free the objects that were created but won't be used after this point + // in case any of the configs don't need pushing, they won't be freed later + (listOf(keys,info,members) subtract requiringPush).forEach(Config::free) + + requiringPush.map { config -> + val (push, seqNo, obsoleteHashes) = if (config is GroupKeysConfig) { + ConfigPush(config.pendingConfig()!!, 0, emptyList()) // should not be null from filter step previous + } else if (config is ConfigBase) { + config.push() + } else throw IllegalArgumentException("Got a non group keys or config base object for config sync") + toDelete += obsoleteHashes + val message = SnodeMessage(destination.publicKey, Base64.encodeBytes(push), SnodeMessage.CONFIG_TTL, sentTimestamp) + + ConfigMessageInformation(SnodeAPI.buildAuthenticatedStoreBatchInfo(config.namespace(), message, signingKey), config, seqNo) + } + } else { + // assume our own user as check already takes place in `execute` for our own key if contact + configFactoryProtocol.getUserConfigs().filter { it.needsPush() }.map { config -> + val (bytes, seqNo, obsoleteHashes) = config.push() + toDelete += obsoleteHashes + val message = messageForConfig(config, bytes, seqNo) + ?: throw NullPointerException("SnodeBatchRequest message was null, check group keys exists") + ConfigMessageInformation(message, config, seqNo) + } + } + return SyncInformation(configsRequiringPush, toDelete) + } + + private fun messageForConfig( + config: ConfigBase, + bytes: ByteArray, + seqNo: Long + ): SnodeBatchRequestInfo? { + val message = SharedConfigurationMessage(config.protoKindFor(), bytes, seqNo) + val snodeMessage = MessageSender.buildWrappedMessageToSnode(destination, message, true) + return SnodeAPI.buildAuthenticatedStoreBatchInfo(config.namespace(), snodeMessage) } override suspend fun execute(dispatcherName: String) { @@ -39,26 +104,17 @@ data class ConfigurationSyncJob(val destination: Destination): Job { val userPublicKey = storage.getUserPublicKey() val delegate = delegate ?: return Log.e("ConfigurationSyncJob", "No Delegate") - if (destination is Destination.Contact && destination.publicKey != userPublicKey) { - return delegate.handleJobFailedPermanently(this, dispatcherName, InvalidContactDestination()) - } else if (destination !is Destination.ClosedGroup) { + if (destination !is Destination.ClosedGroup && (destination !is Destination.Contact || destination.publicKey != userPublicKey)) { return delegate.handleJobFailedPermanently(this, dispatcherName, InvalidDestination()) } // configFactory singleton instance will come in handy for modifying hashes and fetching configs for namespace etc val configFactory = MessagingModuleConfiguration.shared.configFactory - // **** start user **** - // get latest states, filter out configs that don't need push - val configsRequiringPush = configFactory.getUserConfigs().filter { config -> config.needsPush() } - - // don't run anything if we don't need to push anything - if (configsRequiringPush.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName) - - // **** end user **** // allow null results here so the list index matches configsRequiringPush - val sentTimestamp: Long = SnodeAPI.nowWithOffset - val (batchObjects, toDeleteHashes) = destinationConfigs(delegate, configFactory) + val (batchObjects, toDeleteHashes) = destinationConfigs(delegate, dispatcherName, configFactory) + + if (batchObjects.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName) val toDeleteRequest = toDeleteHashes.let { toDeleteFromAllNamespaces -> if (toDeleteFromAllNamespaces.isEmpty()) null @@ -86,22 +142,6 @@ data class ConfigurationSyncJob(val destination: Destination): Job { val rawResponses = batchResponse.get() @Suppress("UNCHECKED_CAST") val responseList = (rawResponses["results"] as List) - // we are always adding in deletions at the end - val deletionResponse = if (toDeleteRequest != null && responseList.isNotEmpty()) responseList.last() else null - val deletedHashes = deletionResponse?.let { - @Suppress("UNCHECKED_CAST") - // get the sub-request body - (deletionResponse["body"] as? RawResponse)?.let { body -> - // get the swarm dict - body["swarm"] as? RawResponse - }?.mapValues { (_, swarmDict) -> - // get the deleted values from dict - ((swarmDict as? RawResponse)?.get("deleted") as? List)?.toSet() ?: emptySet() - }?.values?.reduce { acc, strings -> - // create an intersection of all deleted hashes (common between all swarm nodes) - acc intersect strings - } - } ?: emptySet() // at this point responseList index should line up with configsRequiringPush index batchObjects.forEachIndexed { index, (message, config, seqNo) -> @@ -125,6 +165,10 @@ data class ConfigurationSyncJob(val destination: Destination): Job { if (config is ConfigBase && config.needsDump()) { // usually this will be true? configFactory.persist(config, (message.params["timestamp"] as String).toLong()) } + + if (destination is Destination.ClosedGroup) { + config.free() // after they are used, free the temporary group configs + } } } catch (e: Exception) { Log.e(TAG, "Error performing batch request", e) @@ -137,10 +181,6 @@ data class ConfigurationSyncJob(val destination: Destination): Job { } } - private fun getUserSyncInformation(delegate: JobDelegate) { - val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() - } - fun Destination.destinationPublicKey(): String = when (this) { is Destination.Contact -> publicKey is Destination.ClosedGroup -> publicKey diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt index 024f37065e..94ddb54fec 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt @@ -14,6 +14,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters +import org.session.libsession.messaging.messages.Destination import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.ConfigFactoryProtocol @@ -101,6 +102,8 @@ class ClosedGroupPoller(private val executor: CoroutineScope, val membersIndex = 2 val messageIndex = 3 + val requiresSync = info.needsPush() || members.needsPush() || keys.needsRekey() || keys.pendingConfig() != null + val messagePoll = SnodeAPI.buildAuthenticatedRetrieveBatchRequest( snode, closedGroupSessionId.hexString(), @@ -154,6 +157,9 @@ class ClosedGroupPoller(private val executor: CoroutineScope, info.free() members.free() + if (requiresSync) { + configFactoryProtocol.scheduleUpdate(Destination.ClosedGroup(closedGroupSessionId.hexString())) + } } catch (e: Exception) { if (ENABLE_LOGGING) Log.e("GroupPoller", "Polling failed for group", e) return POLL_INTERVAL diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt index a141de30ca..3dc137d686 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt @@ -30,4 +30,8 @@ data class SnodeMessage( "timestamp" to timestamp.toString(), ) } + + companion object { + const val CONFIG_TTL: Long = 30 * 24 * 60 * 60 * 1000L + } } diff --git a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index 6fa3dacf3d..7d9c33ff8d 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -9,6 +9,7 @@ import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.GroupMembersConfig import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserProfile +import org.session.libsession.messaging.messages.Destination import org.session.libsignal.utilities.SessionId interface ConfigFactoryProtocol { @@ -31,6 +32,8 @@ interface ConfigFactoryProtocol { groupInfo: GroupInfoConfig, groupMembers: GroupMembersConfig ) + + fun scheduleUpdate(destination: Destination) } interface ConfigFactoryUpdateListener {