diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 5cee8acc10..e1563ebc89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; +import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; import org.thoughtcrime.securesms.loki.utilities.Broadcaster; import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities; @@ -115,7 +116,6 @@ import org.session.libsignal.service.loki.protocol.meta.SessionMetaProtocol; import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol; import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocolDelegate; import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink; -import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol; import org.session.libsignal.service.loki.protocol.shelved.syncmessages.SyncMessagesProtocol; import java.io.File; @@ -206,7 +206,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey); SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey); } - MultiDeviceProtocol.Companion.configureIfNeeded(apiDB); SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this); setUpP2PAPIIfNeeded(); PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG); @@ -249,6 +248,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc startPollingIfNeeded(); publicChatManager.markAllAsNotCaughtUp(); publicChatManager.startPollersIfNeeded(); + MultiDeviceProtocol.syncConfigurationIfNeeded(this); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index f17e6aca8c..22f2a6c301 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -150,6 +150,16 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt return new Reader(cursor); } + public List getAllGroups() { + Reader reader = getGroups(); + GroupRecord record; + List groups = new LinkedList<>(); + while ((record = reader.getNext()) != null) { + if (record.isActive()) { groups.add(record); } + } + return groups; + } + public @NonNull List getGroupMembers(String groupId, boolean includeSelf) { List
members = getCurrentMembers(groupId); List recipients = new LinkedList<>(); 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 3f34547fde..c8eb646915 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -371,6 +371,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseFactory.getLokiAPIDatabase(context).getLatestClosedGroupEncryptionKeyPair(groupPublicKey) } + override fun getAllGroups(): List { + return DatabaseFactory.getGroupDatabase(context).allGroups + } + override fun setProfileSharing(address: Address, value: Boolean) { val recipient = Recipient.from(context, address, false) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index 41e54fe055..772fc7aaae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -2,8 +2,6 @@ package org.thoughtcrime.securesms.loki.activities import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.os.AsyncTask import android.os.Bundle import androidx.loader.app.LoaderManager import androidx.loader.content.Loader @@ -20,15 +18,12 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity import org.session.libsession.messaging.threads.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.loki.utilities.fadeIn import org.thoughtcrime.securesms.loki.utilities.fadeOut import org.thoughtcrime.securesms.mms.GlideApp import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.libsignal.util.guava.Optional import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2 -import java.lang.ref.WeakReference //TODO Refactor to avoid using kotlinx.android.synthetic class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks> { @@ -122,6 +117,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) finish() } + } } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index 1adc93d44d..d08e3f91f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -84,6 +84,8 @@ object ClosedGroupsProtocolV2 { insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) + // Force sync configuration message + MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(context) // Fulfill the promise deferred.resolve(groupID) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt new file mode 100644 index 0000000000..e3cefce922 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -0,0 +1,53 @@ +package org.thoughtcrime.securesms.loki.protocol + +import android.content.Context +import org.session.libsession.messaging.messages.control.ConfigurationMessage +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.service.api.push.SignalServiceAddress +import org.session.libsignal.utilities.logging.Log +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil +import org.thoughtcrime.securesms.loki.utilities.recipient +import java.util.* + +object MultiDeviceProtocol { + + @JvmStatic + fun syncConfigurationIfNeeded(context: Context) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + 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, false) + 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)!! + 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, false) + } catch (e: Exception) { + Log.d("Loki", "Failed to send configuration message due to error: $e.") + } + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index 4a4b2fe82b..3749c731f3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -108,6 +108,8 @@ interface StorageProtocol { fun isClosedGroup(publicKey: String): Boolean fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? + // Groups + fun getAllGroups(): List // Settings fun setProfileSharing(address: Address, value: Boolean) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt new file mode 100644 index 0000000000..818bbed9bd --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt @@ -0,0 +1,105 @@ +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 + +class ConfigurationMessage(val closedGroups: List, val openGroups: List): ControlMessage() { + + class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List, val admins: List) { + 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(publicKey.toByteArray()) + 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(it.toByteArray()) }) + result.addAllAdmins(admins.map { ByteString.copyFrom(it.toByteArray()) }) + 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() + val openGroups = mutableListOf() + 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.getDecodedGroupID(groupRecord.encodedId) // TODO: Check if this is correct. Does it need to be double decoded? + 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() + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 0576265a24..d697b06740 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -128,6 +128,29 @@ object TextSecurePreferences { private const val FCM_TOKEN = "pref_fcm_token" private const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2" + // region Multi Device + private const val IS_USING_MULTI_DEVICE = "pref_is_using_multi_device" + private const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time" + + @JvmStatic + fun isUsingMultiDevice(context: Context): Boolean { + return getBooleanPreference(context, IS_USING_MULTI_DEVICE, false) + } + + @JvmStatic + fun setIsUsingMultiDevice(context: Context, value: Boolean) { + setBooleanPreference(context, IS_USING_MULTI_DEVICE, value) + } + + @JvmStatic + fun getLastConfigurationSyncTime(context: Context): Long { + return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0) + } + + @JvmStatic + fun setLastConfigurationSyncTime(context: Context, value: Long) { + setLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, value) + } @JvmStatic fun isUsingFCM(context: Context): Boolean {