From 03ff1d1941068ff8ee62031933a2c336af2d52b5 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Fri, 5 Feb 2021 16:35:15 +1100 Subject: [PATCH 01/28] configuration message --- .../securesms/ApplicationContext.java | 4 +- .../securesms/database/GroupDatabase.java | 10 ++ .../securesms/database/Storage.kt | 4 + .../activities/CreateClosedGroupActivity.kt | 6 +- .../loki/protocol/ClosedGroupsProtocolV2.kt | 2 + .../loki/protocol/MultiDeviceProtocol.kt | 53 +++++++++ .../libsession/messaging/StorageProtocol.kt | 2 + .../messages/control/ConfigurationMessage.kt | 105 ++++++++++++++++++ .../utilities/TextSecurePreferences.kt | 23 ++++ 9 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt 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 { From 05da743ea273ae776640ef59bee210ba43c1a384 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Mon, 8 Feb 2021 16:44:26 +1100 Subject: [PATCH 02/28] configuration message handling --- .../securesms/ApplicationContext.java | 1 + .../securesms/database/GroupDatabase.java | 1 + .../securesms/database/Storage.kt | 9 +++++ .../securesms/jobs/PushDecryptJob.java | 6 +-- .../loki/protocol/MultiDeviceProtocol.kt | 40 ++++++++++++++++++- .../SingleRecipientNotificationBuilder.java | 2 +- .../libsession/messaging/StorageProtocol.kt | 3 ++ .../messages/control/ConfigurationMessage.kt | 9 +++-- .../api/SignalServiceMessageSender.java | 9 +---- .../api/crypto/SignalServiceCipher.java | 13 +++++- .../api/messages/SignalServiceContent.java | 27 +++++++++++-- 11 files changed, 97 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index e1563ebc89..a5c5697b3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -206,6 +206,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey); SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey); } + org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol.Companion.configureIfNeeded(apiDB); SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this); setUpP2PAPIIfNeeded(); PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG); 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 22f2a6c301..3603b68f51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -157,6 +157,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt while ((record = reader.getNext()) != null) { if (record.isActive()) { groups.add(record); } } + reader.close(); return groups; } 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 c8eb646915..3cc08abf81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -27,6 +27,7 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachment import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.loki.api.opengroups.PublicChat import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase @@ -371,6 +372,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseFactory.getLokiAPIDatabase(context).getLatestClosedGroupEncryptionKeyPair(groupPublicKey) } + override fun getAllClosedGroupPublicKeys(): Set { + return DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys() + } + + override fun getAllOpenGroups(): Map { + return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats() + } + override fun getAllGroups(): List { return DatabaseFactory.getGroupDatabase(context).allGroups } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 0d23b6af7f..e7f3c02a54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -80,6 +80,7 @@ import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2; +import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; @@ -264,8 +265,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType { SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content); - if (content.getDeviceLink().isPresent()) { - throw new UnsupportedOperationException("Device link operations are not supported!"); + if (content.configurationMessageProto.isPresent()) { + MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender()); } else if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); @@ -277,7 +278,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (message.getClosedGroupUpdateV2().isPresent()) { ClosedGroupsProtocolV2.handleMessage(context, message.getClosedGroupUpdateV2().get(), message.getTimestamp(), envelope.getSource(), content.getSender()); } - if (message.isEndSession()) { handleEndSessionMessage(content, smsMessageId); } else if (message.isGroupUpdate()) { 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 index e3cefce922..bb1ce53fca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -1,12 +1,18 @@ 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.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.* @@ -14,7 +20,7 @@ object MultiDeviceProtocol { @JvmStatic fun syncConfigurationIfNeeded(context: Context) { - val userPublicKey = TextSecurePreferences.getLocalNumber(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 @@ -35,7 +41,7 @@ object MultiDeviceProtocol { } fun forceSyncConfigurationNowIfNeeded(context: Context) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return val configurationMessage = ConfigurationMessage.getCurrent() val serializedMessage = configurationMessage.toProto()!!.toByteArray() val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() @@ -50,4 +56,34 @@ object MultiDeviceProtocol { Log.d("Loki", "Failed to send configuration message due to error: $e.") } } + + @JvmStatic + fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String) { + 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) + } + val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } + for (openGroup in configurationMessage.openGroups) { + if (allOpenGroups.contains(openGroup)) continue + OpenGroupUtilities.addGroup(context, openGroup, 1) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 62bfcc8b5e..680c9ac684 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -327,7 +327,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) { String publicKey = recipient.getAddress().serialize(); String hepk = (recipient.isLocalNumber() && publicKey != null) - ? TextSecurePreferences.getMasterHexEncodedPublicKey(context) + ? TextSecurePreferences.getLocalNumber(context) : publicKey; String displayName = recipient.getName(); return AvatarPlaceholderGenerator.generate(context, 128, hepk, displayName); 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 3749c731f3..2aa82f4971 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -21,6 +21,7 @@ import org.session.libsignal.libsignal.ecc.ECPrivateKey import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.loki.api.opengroups.PublicChat interface StorageProtocol { @@ -109,6 +110,8 @@ interface StorageProtocol { fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? // Groups + fun getAllClosedGroupPublicKeys(): Set + fun getAllOpenGroups(): Map fun getAllGroups(): List // Settings 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 index 818bbed9bd..aee6356740 100644 --- 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 @@ -10,6 +10,7 @@ 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, val openGroups: List): ControlMessage() { @@ -36,14 +37,14 @@ class ConfigurationMessage(val closedGroups: List, val openGroups: fun toProto(): SignalServiceProtos.ConfigurationMessage.ClosedGroup? { val result = SignalServiceProtos.ConfigurationMessage.ClosedGroup.newBuilder() - result.publicKey = ByteString.copyFrom(publicKey.toByteArray()) + 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(it.toByteArray()) }) - result.addAllAdmins(admins.map { ByteString.copyFrom(it.toByteArray()) }) + result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }) + result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }) return result.build() } } @@ -61,7 +62,7 @@ class ConfigurationMessage(val closedGroups: List, val openGroups: 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? + val groupPublicKey = GroupUtil.getDecodedGroupIDAsData(GroupUtil.getDecodedGroupID(groupRecord.encodedId)).toHexString() // 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() }) diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java index f0a9d2d0ec..bd23c1f91f 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java @@ -381,7 +381,6 @@ public class SignalServiceMessageSender { } else if (message.getStickerPackOperations().isPresent()) { content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get()); } else if (message.getVerified().isPresent()) { - sendMessage(message.getVerified().get(), unidentifiedAccess); return; } else { throw new IOException("Unsupported sync message!"); @@ -452,12 +451,6 @@ public class SignalServiceMessageSender { result.getUrl()); } - private void sendMessage(VerifiedMessage message, Optional unidentifiedAccess) - throws IOException, UntrustedIdentityException - { - - } - private byte[] createTypingContent(SignalServiceTypingMessage message) { Content.Builder container = Content.newBuilder(); TypingMessage.Builder builder = TypingMessage.newBuilder(); @@ -1155,7 +1148,7 @@ public class SignalServiceMessageSender { final boolean notifyPNServer) throws IOException, UntrustedIdentityException { - if (recipient.getNumber().equals(userPublicKey)) { return SendMessageResult.success(recipient, false, false); } +// if (recipient.getNumber().equals(userPublicKey)) { return SendMessageResult.success(recipient, false, false); } final SettableFuture[] future = { new SettableFuture() }; OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content); // Loki - Remove this when we have shared sender keys diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java index 8f9521793b..7c7f575b63 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java @@ -216,7 +216,18 @@ public class SignalServiceCipher { ); } - if (message.hasDeviceLinkMessage()) { + if (message.hasConfigurationMessage()) { + SignalServiceCipher.Metadata metadata = plaintext.getMetadata(); + SignalServiceContent content = new SignalServiceContent(message, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp()); + + if (message.hasDataMessage()) { + setProfile(message.getDataMessage(), content); + SignalServiceDataMessage signalServiceDataMessage = createSignalServiceMessage(metadata, message.getDataMessage()); + content.setDataMessage(signalServiceDataMessage); + } + + return content; + } else if (message.hasDeviceLinkMessage()) { SignalServiceProtos.DeviceLinkMessage protoDeviceLinkMessage = message.getDeviceLinkMessage(); String masterPublicKey = protoDeviceLinkMessage.getPrimaryPublicKey(); String slavePublicKey = protoDeviceLinkMessage.getSecondaryPublicKey(); diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceContent.java b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceContent.java index 5d5e02a9f9..0e978e5954 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceContent.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceContent.java @@ -12,7 +12,9 @@ import org.session.libsignal.service.api.messages.SignalServiceNullMessage; import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; import org.session.libsignal.service.api.messages.SignalServiceTypingMessage; import org.session.libsignal.service.api.messages.calls.SignalServiceCallMessage; +import org.session.libsignal.service.api.messages.multidevice.ConfigurationMessage; import org.session.libsignal.service.api.messages.multidevice.SignalServiceSyncMessage; +import org.session.libsignal.service.internal.push.SignalServiceProtos; import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink; import org.session.libsignal.service.loki.protocol.sessionmanagement.PreKeyBundleMessage; @@ -33,10 +35,11 @@ public class SignalServiceContent { private final Optional typingMessage; // Loki - private final Optional deviceLink; - public Optional preKeyBundleMessage = Optional.absent(); - public Optional senderDisplayName = Optional.absent(); - public Optional senderProfilePictureURL = Optional.absent(); + private final Optional deviceLink; + public Optional configurationMessageProto = Optional.absent(); + public Optional preKeyBundleMessage = Optional.absent(); + public Optional senderDisplayName = Optional.absent(); + public Optional senderProfilePictureURL = Optional.absent(); public SignalServiceContent(SignalServiceDataMessage message, String sender, int senderDevice, long timestamp, boolean needsReceipt, boolean isDeviceUnlinkingRequest) { this.sender = sender; @@ -128,6 +131,22 @@ public class SignalServiceContent { this.isDeviceUnlinkingRequest = false; } + public SignalServiceContent(SignalServiceProtos.Content configurationMessageProto, String sender, int senderDevice, long timestamp) { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.needsReceipt = false; + this.message = Optional.absent(); + this.synchronizeMessage = Optional.absent(); + this.callMessage = Optional.absent(); + this.nullMessage = Optional.absent(); + this.readMessage = Optional.absent(); + this.typingMessage = Optional.absent(); + this.deviceLink = Optional.absent(); + this.configurationMessageProto = Optional.fromNullable(configurationMessageProto); + this.isDeviceUnlinkingRequest = false; + } + public SignalServiceContent(SignalServiceNullMessage nullMessage, String sender, int senderDevice, long timestamp) { this.sender = sender; this.senderDevice = senderDevice; From 57d532f4b804768bb32e1ee772eab8a20c315425 Mon Sep 17 00:00:00 2001 From: jubb Date: Mon, 8 Feb 2021 16:57:12 +1100 Subject: [PATCH 03/28] feat: add self sending syncTarget messages --- .../securesms/database/SmsDatabase.java | 19 +- .../securesms/jobs/PushDecryptJob.java | 167 ++++++++------ .../securesms/jobs/PushGroupSendJob.java | 78 ++++--- .../securesms/jobs/PushMediaSendJob.java | 35 ++- .../securesms/jobs/PushTextSendJob.java | 39 +++- .../ClosedGroupUpdateMessageSendJob.kt | 3 +- .../ClosedGroupUpdateMessageSendJobV2.kt | 3 +- .../loki/protocol/NullMessageSendJob.kt | 3 +- .../sms/IncomingEncryptedMessage.java | 7 +- .../sms/IncomingEndSessionMessage.java | 7 +- .../securesms/sms/IncomingGroupMessage.java | 7 +- .../sms/IncomingPreKeyBundleMessage.java | 7 +- .../securesms/sms/IncomingTextMessage.java | 5 - .../libsession/messaging/threads/Address.kt | 1 + .../api/SignalServiceMessageSender.java | 207 ++++-------------- .../api/crypto/SignalServiceCipher.java | 4 +- .../messages/SignalServiceDataMessage.java | 18 +- 17 files changed, 314 insertions(+), 296 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 61a7af375e..aaa14151d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -686,6 +686,15 @@ public class SmsDatabase extends MessagingDatabase { return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp); } + public Optional insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp) { + long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null); + if (messageId == -1) { + return Optional.absent(); + } + markAsSent(messageId, true); + return Optional.fromNullable(new InsertResult(messageId, threadId)); + } + public long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener) { @@ -716,9 +725,17 @@ public class SmsDatabase extends MessagingDatabase { contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum()); contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum()); + SQLiteDatabase readDb = databaseHelper.getReadableDatabase(); + Cursor existingRecord = readDb.query(TABLE_NAME, null, String.format("%s = ? AND %s = ? AND %s = ?",ADDRESS, THREAD_ID, DATE_SENT), + new String[] { address.serialize(), Long.toString(threadId), Long.toString(date) }, null, null, null); + int existingRecordCount = existingRecord.getCount(); + if (existingRecordCount > 0) { + // return -1 because record exists from Address to ThreadID with the same date sent (probably sent from us) + return -1; + } + SQLiteDatabase db = databaseHelper.getWritableDatabase(); long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); - if (insertListener != null) { insertListener.onComplete(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 0d23b6af7f..081946041c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -28,6 +28,7 @@ import org.session.libsignal.metadata.ProtocolNoSessionException; import org.session.libsignal.metadata.ProtocolUntrustedIdentityException; import org.session.libsignal.metadata.SelfSendException; import org.session.libsignal.service.loki.api.crypto.SessionProtocol; +import org.session.libsignal.service.loki.utilities.HexEncodingKt; import org.session.libsignal.utilities.PromiseUtilities; import org.thoughtcrime.securesms.ApplicationContext; @@ -568,6 +569,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType { { Recipient originalRecipient = getMessageDestination(content, message); Recipient masterRecipient = getMessageMasterDestination(content.getSender()); + String syncTarget = message.getSyncTarget().orNull(); + notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice()); @@ -582,75 +585,79 @@ public class PushDecryptJob extends BaseJob implements InjectableType { masterAddress = getMessageMasterDestination(content.getSender()).getAddress(); } - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1, - message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), - quote, sharedContacts, linkPreviews, sticker); + if (syncTarget != null && !syncTarget.isEmpty()) { +// OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterAddress, message.getTimestamp(), -1, +// message.getExpiresInSeconds() * 1000L, false, ) + } else { + IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1, + message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), + quote, sharedContacts, linkPreviews, sticker); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + database.beginTransaction(); - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - database.beginTransaction(); + // Ignore message if it has no body and no attachments + if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) { + return; + } - // Ignore message if it has no body and no attachments - if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) { - return; - } + Optional insertResult; - Optional insertResult; + try { + if (message.isGroupMessage()) { + insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp()); + } else { + insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); + } - try { - if (message.isGroupMessage()) { - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp()); - } else { - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); + if (insertResult.isPresent()) { + List allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); + List stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); + List attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); + + forceStickerDownloadIfNecessary(stickerAttachments); + + for (DatabaseAttachment attachment : attachments) { + ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); + } + + if (smsMessageId.isPresent()) { + DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); + } + + database.setTransactionSuccessful(); + } + } catch (MmsException e) { + throw new StorageFailedException(e, content.getSender(), content.getSenderDevice()); + } finally { + database.endTransaction(); } if (insertResult.isPresent()) { - List allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); - List stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); - List attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); - - forceStickerDownloadIfNecessary(stickerAttachments); - - for (DatabaseAttachment attachment : attachments) { - ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); - } - - if (smsMessageId.isPresent()) { - DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); - } - - database.setTransactionSuccessful(); - } - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender(), content.getSenderDevice()); - } finally { - database.endTransaction(); - } - - if (insertResult.isPresent()) { - messageNotifier.updateNotification(context, insertResult.get().getThreadId()); - } - - if (insertResult.isPresent()) { - InsertResult result = insertResult.get(); - - // Loki - Cache the user hex encoded public key (for mentions) - MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context); - MentionsManager.shared.cache(content.getSender(), result.getThreadId()); - - // Loki - Store message open group server ID if needed - if (messageServerIDOrNull.isPresent()) { - long messageID = result.getMessageId(); - long messageServerID = messageServerIDOrNull.get(); - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - lokiMessageDatabase.setServerID(messageID, messageServerID); + messageNotifier.updateNotification(context, insertResult.get().getThreadId()); } - // Loki - Update mapping of message ID to original thread ID - if (result.getMessageId() > -1) { - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient); - lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); + if (insertResult.isPresent()) { + InsertResult result = insertResult.get(); + + // Loki - Cache the user hex encoded public key (for mentions) + MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context); + MentionsManager.shared.cache(content.getSender(), result.getThreadId()); + + // Loki - Store message open group server ID if needed + if (messageServerIDOrNull.isPresent()) { + long messageID = result.getMessageId(); + long messageServerID = messageServerIDOrNull.get(); + LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); + lokiMessageDatabase.setServerID(messageID, messageServerID); + } + + // Loki - Update mapping of message ID to original thread ID + if (result.getMessageId() > -1) { + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); + long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient); + lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); + } } } } @@ -769,6 +776,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { String body = message.getBody().isPresent() ? message.getBody().get() : ""; Recipient originalRecipient = getMessageDestination(content, message); Recipient masterRecipient = getMessageMasterDestination(content.getSender()); + String syncTarget = message.getSyncTarget().orNull(); if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) { handleExpirationUpdate(content, message, Optional.absent()); @@ -778,15 +786,46 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second; + } else if (syncTarget != null && !syncTarget.isEmpty()) { + Address targetAddress = Address.fromSerialized(syncTarget); + + OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false), + body, message.getExpiresInSeconds(), -1); + + // Ignore the message if it has no body + if (tm.getMessageBody().length() == 0) { return; } + + // Check if we have the thread already + long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(syncTarget); + + + // Insert the message into the database + Optional insertResult; + insertResult = database.insertMessageOutbox(threadID, tm, content.getTimestamp()); + + if (insertResult.isPresent()) { + threadId = insertResult.get().getThreadId(); + } + + if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get()); + + if (threadId != null) { + messageNotifier.updateNotification(context, threadId); + } + + if (insertResult.isPresent()) { + InsertResult result = insertResult.get(); + + // Loki - Cache the user hex encoded public key (for mentions) + MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context); + MentionsManager.shared.cache(content.getSender(), result.getThreadId()); + } + } else { notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice()); Address masterAddress = masterRecipient.getAddress(); - if (message.isGroupMessage()) { - masterAddress = getMessageMasterDestination(content.getSender()).getAddress(); - } - IncomingTextMessage tm = new IncomingTextMessage(masterAddress, content.getSenderDevice(), message.getTimestamp(), body, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 1c7df45f52..e4c8b588df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -15,6 +15,7 @@ import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.Address; import org.session.libsession.utilities.GroupUtil; +import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -143,6 +144,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { List existingNetworkFailures = message.getNetworkFailures(); List existingIdentityMismatches = message.getIdentityKeyMismatches(); + String userPublicKey = TextSecurePreferences.getLocalNumber(context); + SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey); + if (database.isSent(messageId)) { log(TAG, "Message " + messageId + " was already sent. Ignoring."); return; @@ -190,6 +194,22 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { } if (existingNetworkFailures.isEmpty() && networkFailures.isEmpty() && identityMismatches.isEmpty() && existingIdentityMismatches.isEmpty()) { + Address address = message.getRecipient().getAddress(); + if (!address.isOpenGroup()) { + try { + SignalServiceDataMessage selfSend = getDataMessage(address, message) + .withSyncTarget(address.toGroupString()) + .build(); + // send to ourselves to sync multi-device + Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); + SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, selfSend); + if (selfSendResult.getLokiAPIError() != null) { + throw selfSendResult.getLokiAPIError(); + } + } catch (Exception e) { + Log.e("Loki", "Error sending message to ourselves", e); + } + } database.markAsSent(messageId, true); markAttachmentsUploaded(messageId, message.getAttachments()); @@ -238,25 +258,18 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { // return results; // } - String groupId = address.toGroupString(); - Optional profileKey = getProfileKey(message.getRecipient()); - Optional quote = getQuoteFor(message); - Optional sticker = getStickerFor(message); - List sharedContacts = getSharedContactsFor(message); - List previews = getPreviewsFor(message); List addresses = Stream.of(destinations).map(this::getPushAddress).toList(); - List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); - List attachmentPointers = getAttachmentPointersFor(attachments); - List> unidentifiedAccess = Stream.of(addresses) .map(a -> Address.Companion.fromSerialized(a.getNumber())) .map(a -> Recipient.from(context, a, false)) .map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)) .toList(); - SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; - if (message.isGroup() && address.isClosedGroup()) { + SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; + String groupId = address.toGroupString(); + List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); + List attachmentPointers = getAttachmentPointersFor(attachments); // Loki - Only send GroupUpdate or GroupQuit messages to closed groups OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; GroupContext groupContext = groupMessage.getGroupContext(); @@ -271,25 +284,40 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage); } else { - SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType); - SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getSentTimeMillis()) - .asGroupMessage(group) - .withAttachments(attachmentPointers) - .withBody(message.getBody()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .asExpirationUpdate(message.isExpirationUpdate()) - .withProfileKey(profileKey.orNull()) - .withQuote(quote.orNull()) - .withSticker(sticker.orNull()) - .withSharedContacts(sharedContacts) - .withPreviews(previews) - .build(); + SignalServiceDataMessage groupMessage = getDataMessage(address, message).build(); return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupMessage); } } + public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) { + + SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; + + String groupId = address.toGroupString(); + Optional profileKey = getProfileKey(message.getRecipient()); + Optional quote = getQuoteFor(message); + Optional sticker = getStickerFor(message); + List sharedContacts = getSharedContactsFor(message); + List previews = getPreviewsFor(message); + List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); + List attachmentPointers = getAttachmentPointersFor(attachments); + + SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType); + return SignalServiceDataMessage.newBuilder() + .withTimestamp(message.getSentTimeMillis()) + .asGroupMessage(group) + .withAttachments(attachmentPointers) + .withBody(message.getBody()) + .withExpiration((int)(message.getExpiresIn() / 1000)) + .asExpirationUpdate(message.isExpirationUpdate()) + .withProfileKey(profileKey.orNull()) + .withQuote(quote.orNull()) + .withSticker(sticker.orNull()) + .withSharedContacts(sharedContacts) + .withPreviews(previews); + } + public static class Factory implements Job.Factory { @Override public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 19254a7d74..c89b44f131 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -245,7 +245,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { { try { Recipient recipient = Recipient.from(context, destination, false); + String userPublicKey = TextSecurePreferences.getLocalNumber(context); SignalServiceAddress address = getPushAddress(recipient.getAddress()); + SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey); List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); List serviceAttachments = getAttachmentPointersFor(attachments); Optional profileKey = getProfileKey(message.getRecipient()); @@ -254,6 +256,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { List sharedContacts = getSharedContactsFor(message); List previews = getPreviewsFor(message); + Optional unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(context, recipient); + SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() .withBody(message.getBody()) .withAttachments(serviceAttachments) @@ -267,6 +271,20 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { .asExpirationUpdate(message.isExpirationUpdate()) .build(); + SignalServiceDataMessage mediaSelfSendMessage = SignalServiceDataMessage.newBuilder() + .withBody(message.getBody()) + .withAttachments(serviceAttachments) + .withTimestamp(message.getSentTimeMillis()) + .withSyncTarget(destination.serialize()) + .withExpiration((int)(message.getExpiresIn() / 1000)) + .withProfileKey(profileKey.orNull()) + .withQuote(quote.orNull()) + .withSticker(sticker.orNull()) + .withSharedContacts(sharedContacts) + .withPreviews(previews) + .asExpirationUpdate(message.isExpirationUpdate()) + .build(); + if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) { // Loki - Device link messages don't go through here Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); @@ -275,11 +293,24 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { messageSender.sendMessage(syncMessage, syncAccess); return syncAccess.isPresent(); } else { - SendMessageResult result = messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage); + SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage); if (result.getLokiAPIError() != null) { throw result.getLokiAPIError(); } else { - return result.getSuccess().isUnidentified(); + boolean isUnidentified = result.getSuccess().isUnidentified(); + + try { + // send to ourselves to sync multi-device + Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); + SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, mediaSelfSendMessage); + if (selfSendResult.getLokiAPIError() != null) { + throw selfSendResult.getLokiAPIError(); + } + } catch (Exception e) { + Log.e("Loki", "Error sending message to ourselves", e); + } + + return isUnidentified; } } } catch (UnregisteredUserException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 00789a08ef..acac2e3d98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import org.session.libsession.messaging.jobs.Data; +import org.session.libsignal.service.api.crypto.UnidentifiedAccess; import org.session.libsignal.utilities.logging.Log; import org.session.libsession.messaging.threads.Address; @@ -192,8 +193,10 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException, SnodeAPI.Error { try { + String userPublicKey = TextSecurePreferences.getLocalNumber(context); Recipient recipient = Recipient.from(context, destination, false); SignalServiceAddress address = getPushAddress(recipient.getAddress()); + SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey); Optional profileKey = getProfileKey(recipient); Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); @@ -205,13 +208,21 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { // } SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getDateSent()) - .withBody(message.getBody()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .withProfileKey(profileKey.orNull()) -// .withPreKeyBundle(preKeyBundle) - .asEndSessionMessage(message.isEndSession()) - .build(); + .withTimestamp(message.getDateSent()) + .withBody(message.getBody()) + .withExpiration((int)(message.getExpiresIn() / 1000)) + .withProfileKey(profileKey.orNull()) + .asEndSessionMessage(message.isEndSession()) + .build(); + + SignalServiceDataMessage textSecureSelfSendMessage = SignalServiceDataMessage.newBuilder() + .withTimestamp(message.getDateSent()) + .withBody(message.getBody()) + .withSyncTarget(destination.serialize()) + .withExpiration((int)(message.getExpiresIn() / 1000)) + .withProfileKey(profileKey.orNull()) + .asEndSessionMessage(message.isEndSession()) + .build(); if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) { // Loki - Device link messages don't go through here @@ -225,7 +236,19 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { if (result.getLokiAPIError() != null) { throw result.getLokiAPIError(); } else { - return result.getSuccess().isUnidentified(); + boolean isUnidentified = result.getSuccess().isUnidentified(); + + try { + // send to ourselves to sync multi-device + Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); + SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, textSecureSelfSendMessage); + if (selfSendResult.getLokiAPIError() != null) { + throw selfSendResult.getLokiAPIError(); + } + } catch (Exception e) { + Log.e("Loki", "Error sending message to ourselves", e); + } + return isUnidentified; } } } catch (UnregisteredUserException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt index 7f24078034..28f4a5a864 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol import com.google.protobuf.ByteString import org.session.libsession.messaging.jobs.Data +import org.session.libsignal.libsignal.util.guava.Optional import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.jobmanager.Job @@ -128,7 +129,7 @@ class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters // 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, - useFallbackEncryption, false, false, false) + useFallbackEncryption, false, false, Optional.absent()) } catch (e: Exception) { Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt index 3fcb203e74..8ae19749e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt @@ -9,6 +9,7 @@ import org.session.libsession.messaging.jobs.Data 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.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.protocol.meta.TTLUtilities @@ -221,7 +222,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete // 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) + true, false, false, Optional.absent()) } catch (e: Exception) { Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt index 834c01ea23..9f0eff11c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.BaseJob import org.session.libsignal.utilities.logging.Log import org.session.libsession.messaging.threads.recipients.Recipient +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.protocol.meta.TTLUtilities @@ -56,7 +57,7 @@ class NullMessageSendJob private constructor(parameters: Parameters, private val try { messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, Date().time, serializedContentMessage, false, ttl, false, - false, false, false, false) + false, false, false, Optional.absent()) } catch (e: Exception) { Log.d("Loki", "Failed to send null message to: $publicKey due to error: $e.") throw e diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java index 63489bf7bc..684329fc77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java @@ -6,12 +6,7 @@ public class IncomingEncryptedMessage extends IncomingTextMessage { super(base, newBody); } - @Override - public IncomingTextMessage withMessageBody(String body) { - return new IncomingEncryptedMessage(this, body); - } - - @Override + @Override public boolean isSecureMessage() { return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEndSessionMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEndSessionMessage.java index 9277e989a7..f5b7b5e402 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEndSessionMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEndSessionMessage.java @@ -10,12 +10,7 @@ public class IncomingEndSessionMessage extends IncomingTextMessage { super(base, newBody); } - @Override - public IncomingEndSessionMessage withMessageBody(String messageBody) { - return new IncomingEndSessionMessage(this, messageBody); - } - - @Override + @Override public boolean isEndSession() { return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java index 546881ceba..af17d4b963 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java @@ -11,12 +11,7 @@ public class IncomingGroupMessage extends IncomingTextMessage { this.groupContext = groupContext; } - @Override - public IncomingGroupMessage withMessageBody(String body) { - return new IncomingGroupMessage(this, groupContext, body); - } - - @Override + @Override public boolean isGroup() { return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java index 1910c366f4..39f75a5da0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java @@ -9,12 +9,7 @@ public class IncomingPreKeyBundleMessage extends IncomingTextMessage { this.legacy = legacy; } - @Override - public IncomingPreKeyBundleMessage withMessageBody(String messageBody) { - return new IncomingPreKeyBundleMessage(this, messageBody, legacy); - } - - @Override + @Override public boolean isLegacyPreKeyBundle() { return legacy; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index 36f01071d9..eaf06f80d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -175,10 +175,6 @@ public class IncomingTextMessage implements Parcelable { return message; } - public IncomingTextMessage withMessageBody(String message) { - return new IncomingTextMessage(this, message); - } - public Address getSender() { return sender; } @@ -250,7 +246,6 @@ public class IncomingTextMessage implements Parcelable { public boolean isUnidentified() { return unidentified; } - @Override public int describeContents() { return 0; diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt b/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt index 72291a8a2c..3b2556272a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt @@ -152,6 +152,7 @@ class Address private constructor(address: String) : Parcelable, Comparable>() + @JvmStatic fun fromSerialized(serialized: String): Address { return Address(serialized) } diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java index f0a9d2d0ec..263b9276bf 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java @@ -26,10 +26,6 @@ import org.session.libsignal.service.api.messages.SignalServiceDataMessage; import org.session.libsignal.service.api.messages.SignalServiceGroup; import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; import org.session.libsignal.service.api.messages.SignalServiceTypingMessage; -import org.session.libsignal.service.api.messages.calls.AnswerMessage; -import org.session.libsignal.service.api.messages.calls.IceUpdateMessage; -import org.session.libsignal.service.api.messages.calls.OfferMessage; -import org.session.libsignal.service.api.messages.calls.SignalServiceCallMessage; import org.session.libsignal.service.api.messages.multidevice.BlockedListMessage; import org.session.libsignal.service.api.messages.multidevice.ConfigurationMessage; import org.session.libsignal.service.api.messages.multidevice.ReadMessage; @@ -51,7 +47,6 @@ import org.session.libsignal.service.internal.push.PushServiceSocket; import org.session.libsignal.service.internal.push.PushTransportDetails; import org.session.libsignal.service.internal.push.SignalServiceProtos; import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer; -import org.session.libsignal.service.internal.push.SignalServiceProtos.CallMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.Content; import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext; @@ -217,34 +212,14 @@ public class SignalServiceMessageSender { * @param recipient The sender of the received message you're acknowledging. * @param message The read receipt to deliver. * @throws IOException - * @throws UntrustedIdentityException */ public void sendReceipt(SignalServiceAddress recipient, Optional unidentifiedAccess, SignalServiceReceiptMessage message) - throws IOException, UntrustedIdentityException - { + throws IOException { byte[] content = createReceiptContent(message); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption, false); - } - - /** - * Send a typing indicator. - * - * @param recipient The destination - * @param message The typing indicator to deliver - * @throws IOException - * @throws UntrustedIdentityException - */ - public void sendTyping(SignalServiceAddress recipient, - Optional unidentifiedAccess, - SignalServiceTypingMessage message) - throws IOException, UntrustedIdentityException - { - byte[] content = createTypingContent(message); - boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), useFallbackEncryption, false); + sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption); } public void sendTyping(List recipients, @@ -256,42 +231,24 @@ public class SignalServiceMessageSender { sendMessage(0, recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), false, false); } - /** - * Send a call setup message to a single recipient. - * - * @param recipient The message's destination. - * @param message The call message. - * @throws IOException - */ - public void sendCallMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, - SignalServiceCallMessage message) - throws IOException, UntrustedIdentityException - { - byte[] content = createCallContent(message); - boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, message.getTTL(), useFallbackEncryption, false); - } - /** * Send a message to a single recipient. * * @param recipient The message's destination. * @param message The message. - * @throws UntrustedIdentityException * @throws IOException */ public SendMessageResult sendMessage(long messageID, SignalServiceAddress recipient, Optional unidentifiedAccess, SignalServiceDataMessage message) - throws UntrustedIdentityException, IOException + throws IOException { byte[] content = createMessageContent(message, recipient); long timestamp = message.getTimestamp(); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL; - SendMessageResult result = sendMessage(messageID, recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, message.getTTL(), message.getDeviceLink().isPresent(), useFallbackEncryption, isClosedGroup, false, message.hasVisibleContent()); + SendMessageResult result = sendMessage(messageID, recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, message.getTTL(), message.getDeviceLink().isPresent(), useFallbackEncryption, isClosedGroup, message.hasVisibleContent(), message.getSyncTarget()); // // Loki - This shouldn't get invoked for note to self // boolean wouldSignalSendSyncMessage = (result.getSuccess() != null && result.getSuccess().isNeedsSync()) || unidentifiedAccess.isPresent(); @@ -325,8 +282,7 @@ public class SignalServiceMessageSender { List recipients, List> unidentifiedAccess, SignalServiceDataMessage message) - throws IOException, UntrustedIdentityException - { + throws IOException { // Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine // whether an attachment is being sent to an open group or not. byte[] content = createMessageContent(message, recipients.get(0)); @@ -350,7 +306,7 @@ public class SignalServiceMessageSender { for (String device : linkedDevices) { SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(syncMessage, device, store); - sendMessage(deviceAsAddress, Optional.absent(), timestamp, syncMessage, false, message.getTTL(), useFallbackEncryption, true); + sendMessage(deviceAsAddress, Optional.absent(), timestamp, syncMessage, false, message.getTTL(), useFallbackEncryption); } } @@ -392,18 +348,10 @@ public class SignalServiceMessageSender { for (String device : linkedDevices) { SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, device, store); - sendMessageToPrivateChat(0, deviceAsAddress, Optional.absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false); + // sendMessageToPrivateChat(0, deviceAsAddress, Optional.absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false); } } - public void setSoTimeoutMillis(long soTimeoutMillis) { - socket.setSoTimeoutMillis(soTimeoutMillis); - } - - public void cancelInFlightRequests() { - socket.cancelInFlightRequests(); - } - public void setMessagePipe(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) { this.pipe.set(Optional.fromNullable(pipe)); this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe)); @@ -452,9 +400,7 @@ public class SignalServiceMessageSender { result.getUrl()); } - private void sendMessage(VerifiedMessage message, Optional unidentifiedAccess) - throws IOException, UntrustedIdentityException - { + private void sendMessage(VerifiedMessage message, Optional unidentifiedAccess) { } @@ -494,32 +440,6 @@ public class SignalServiceMessageSender { { Content.Builder container = Content.newBuilder(); -// if (message.getPreKeyBundle().isPresent()) { -// PreKeyBundle preKeyBundle = message.getPreKeyBundle().get(); -// PreKeyBundleMessage.Builder preKeyBundleMessageBuilder = PreKeyBundleMessage.newBuilder() -// .setDeviceId(preKeyBundle.getDeviceId()) -// .setIdentityKey(ByteString.copyFrom(preKeyBundle.getIdentityKey().serialize())) -// .setPreKeyId(preKeyBundle.getPreKeyId()) -// .setPreKey(ByteString.copyFrom(preKeyBundle.getPreKey().serialize())) -// .setSignedKeyId(preKeyBundle.getSignedPreKeyId()) -// .setSignedKey(ByteString.copyFrom(preKeyBundle.getSignedPreKey().serialize())) -// .setSignature(ByteString.copyFrom(preKeyBundle.getSignedPreKeySignature())) -// .setIdentityKey(ByteString.copyFrom(preKeyBundle.getIdentityKey().serialize())); -// container.setPreKeyBundleMessage(preKeyBundleMessageBuilder); -// } - -// if (message.getDeviceLink().isPresent()) { -// DeviceLink deviceLink = message.getDeviceLink().get(); -// SignalServiceProtos.DeviceLinkMessage.Builder deviceLinkMessageBuilder = SignalServiceProtos.DeviceLinkMessage.newBuilder() -// .setPrimaryPublicKey(deviceLink.getMasterPublicKey()) -// .setSecondaryPublicKey(deviceLink.getSlavePublicKey()) -// .setRequestSignature(ByteString.copyFrom(Objects.requireNonNull(deviceLink.getRequestSignature()))); -// if (deviceLink.getAuthorizationSignature() != null) { -// deviceLinkMessageBuilder.setAuthorizationSignature(ByteString.copyFrom(deviceLink.getAuthorizationSignature())); -// } -// container.setDeviceLinkMessage(deviceLinkMessageBuilder.build()); -// } - DataMessage.Builder builder = DataMessage.newBuilder(); List pointers = createAttachmentPointers(message.getAttachments(), recipient); @@ -559,6 +479,10 @@ public class SignalServiceMessageSender { builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get())); } + if (message.getSyncTarget().isPresent()) { + builder.setSyncTarget(message.getSyncTarget().get()); + } + if (message.getQuote().isPresent()) { DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder() .setId(message.getQuote().get().getId()) @@ -636,40 +560,6 @@ public class SignalServiceMessageSender { return container.build().toByteArray(); } - private byte[] createCallContent(SignalServiceCallMessage callMessage) { - Content.Builder container = Content.newBuilder(); - CallMessage.Builder builder = CallMessage.newBuilder(); - - if (callMessage.getOfferMessage().isPresent()) { - OfferMessage offer = callMessage.getOfferMessage().get(); - builder.setOffer(CallMessage.Offer.newBuilder() - .setId(offer.getId()) - .setDescription(offer.getDescription())); - } else if (callMessage.getAnswerMessage().isPresent()) { - AnswerMessage answer = callMessage.getAnswerMessage().get(); - builder.setAnswer(CallMessage.Answer.newBuilder() - .setId(answer.getId()) - .setDescription(answer.getDescription())); - } else if (callMessage.getIceUpdateMessages().isPresent()) { - List updates = callMessage.getIceUpdateMessages().get(); - - for (IceUpdateMessage update : updates) { - builder.addIceUpdate(CallMessage.IceUpdate.newBuilder() - .setId(update.getId()) - .setSdp(update.getSdp()) - .setSdpMid(update.getSdpMid()) - .setSdpMLineIndex(update.getSdpMLineIndex())); - } - } else if (callMessage.getHangupMessage().isPresent()) { - builder.setHangup(CallMessage.Hangup.newBuilder().setId(callMessage.getHangupMessage().get().getId())); - } else if (callMessage.getBusyMessage().isPresent()) { - builder.setBusy(CallMessage.Busy.newBuilder().setId(callMessage.getBusyMessage().get().getId())); - } - - container.setCallMessage(builder); - return container.build().toByteArray(); - } - private byte[] createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) throws IOException { @@ -987,6 +877,7 @@ public class SignalServiceMessageSender { throws IOException { List results = new LinkedList<>(); + SignalServiceAddress ownAddress = localAddress; Iterator recipientIterator = recipients.iterator(); Iterator> unidentifiedAccessIterator = unidentifiedAccess.iterator(); @@ -995,7 +886,7 @@ public class SignalServiceMessageSender { try { boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(content, recipient.getNumber(), store); - SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, false, useFallbackEncryption, isClosedGroup, false, notifyPNServer); + SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, false, useFallbackEncryption, isClosedGroup, notifyPNServer, Optional.absent()); results.add(result); } catch (UnregisteredUserException e) { Log.w(TAG, e); @@ -1009,41 +900,46 @@ public class SignalServiceMessageSender { return results; } - private SendMessageResult sendMessage(SignalServiceAddress recipient, + private SendMessageResult sendMessage(SignalServiceAddress recipient, Optional unidentifiedAccess, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean useFallbackEncryption, - boolean isSyncMessage) + long timestamp, + byte[] content, + boolean online, + int ttl, + boolean useFallbackEncryption) throws IOException { // Loki - This method is only invoked for various types of control messages - return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, false, useFallbackEncryption, isSyncMessage, false); + return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, false, useFallbackEncryption, false,Optional.absent()); } - public SendMessageResult sendMessage(final long messageID, - final SignalServiceAddress recipient, + public SendMessageResult sendMessage(final long messageID, + final SignalServiceAddress recipient, Optional unidentifiedAccess, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean isDeviceLinkMessage, - boolean useFallbackEncryption, - boolean isClosedGroup, - boolean isSyncMessage, - boolean notifyPNServer) + long timestamp, + byte[] content, + boolean online, + int ttl, + boolean isDeviceLinkMessage, + boolean useFallbackEncryption, + boolean isClosedGroup, + boolean notifyPNServer, + Optional syncTarget) throws IOException { - long threadID = threadDatabase.getThreadID(recipient.getNumber()); + boolean isSelfSend = syncTarget.isPresent() && !syncTarget.get().isEmpty(); + long threadID; + if (isSelfSend) { + threadID = threadDatabase.getThreadID(syncTarget.get()); + } else { + threadID = threadDatabase.getThreadID(recipient.getNumber()); + } PublicChat publicChat = threadDatabase.getPublicChat(threadID); try { if (publicChat != null) { return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat); } else { - return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer); + return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer, syncTarget); } } catch (PushNetworkException e) { return SendMessageResult.networkFailure(recipient); @@ -1152,10 +1048,11 @@ public class SignalServiceMessageSender { int ttl, boolean useFallbackEncryption, boolean isClosedGroup, - final boolean notifyPNServer) + final boolean notifyPNServer, + Optional syncTarget) throws IOException, UntrustedIdentityException { - if (recipient.getNumber().equals(userPublicKey)) { return SendMessageResult.success(recipient, false, false); } + if (recipient.getNumber().equals(userPublicKey) && !syncTarget.isPresent()) { return SendMessageResult.success(recipient, false, false); } final SettableFuture[] future = { new SettableFuture() }; OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content); // Loki - Remove this when we have shared sender keys @@ -1221,14 +1118,10 @@ public class SignalServiceMessageSender { } return Unit.INSTANCE; } - }).fail(new Function1() { - - @Override - public Unit invoke(Exception exception) { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - return Unit.INSTANCE; - } + }).fail(exception -> { + @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; + f.setException(exception); + return Unit.INSTANCE; }); @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; @@ -1304,12 +1197,6 @@ public class SignalServiceMessageSender { return builder.build(); } - private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment) - throws IOException - { - return createAttachmentPointer(attachment, false, null); - } - private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient) throws IOException { diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java index 8f9521793b..608dee89cd 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java @@ -389,6 +389,7 @@ public class SignalServiceCipher { ClosedGroupUpdate closedGroupUpdate = content.getClosedGroupUpdate(); ClosedGroupUpdateV2 closedGroupUpdateV2 = content.getClosedGroupUpdateV2(); boolean isDeviceUnlinkingRequest = ((content.getFlags() & DataMessage.Flags.DEVICE_UNLINKING_REQUEST_VALUE) != 0); + String syncTarget = content.getSyncTarget(); for (AttachmentPointer pointer : content.getAttachmentsList()) { attachments.add(createAttachmentPointer(pointer)); @@ -417,7 +418,8 @@ public class SignalServiceCipher { null, closedGroupUpdate, closedGroupUpdateV2, - isDeviceUnlinkingRequest); + isDeviceUnlinkingRequest, + syncTarget); } private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content) diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceDataMessage.java b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceDataMessage.java index 14853977c5..151eac5696 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceDataMessage.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceDataMessage.java @@ -41,6 +41,7 @@ public class SignalServiceDataMessage { private final Optional closedGroupUpdate; private final Optional closedGroupUpdateV2; private final boolean isDeviceUnlinkingRequest; + private final Optional syncTarget; /** * Construct a SignalServiceDataMessage with a body and no attachments. @@ -134,7 +135,7 @@ public class SignalServiceDataMessage { Quote quote, List sharedContacts, List previews, Sticker sticker) { - this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false); + this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false, null); } /** @@ -155,7 +156,7 @@ public class SignalServiceDataMessage { Quote quote, List sharedContacts, List previews, Sticker sticker, PreKeyBundle preKeyBundle, DeviceLink deviceLink, ClosedGroupUpdate closedGroupUpdate, ClosedGroupUpdateV2 closedGroupUpdateV2, - boolean isDeviceUnlinkingRequest) + boolean isDeviceUnlinkingRequest, String syncTarget) { this.timestamp = timestamp; this.body = Optional.fromNullable(body); @@ -172,6 +173,7 @@ public class SignalServiceDataMessage { this.closedGroupUpdate = Optional.fromNullable(closedGroupUpdate); this.closedGroupUpdateV2 = Optional.fromNullable(closedGroupUpdateV2); this.isDeviceUnlinkingRequest = isDeviceUnlinkingRequest; + this.syncTarget = Optional.fromNullable(syncTarget); if (attachments != null && !attachments.isEmpty()) { this.attachments = Optional.of(attachments); @@ -250,6 +252,10 @@ public class SignalServiceDataMessage { return profileKey; } + public Optional getSyncTarget() { + return syncTarget; + } + public Optional getQuote() { return quote; } @@ -307,6 +313,7 @@ public class SignalServiceDataMessage { private Sticker sticker; private PreKeyBundle preKeyBundle; private DeviceLink deviceLink; + private String syncTarget; private boolean isDeviceUnlinkingRequest; private Builder() {} @@ -336,6 +343,11 @@ public class SignalServiceDataMessage { return this; } + public Builder withSyncTarget(String syncTarget) { + this.syncTarget = syncTarget; + return this; + } + public Builder asEndSessionMessage() { return asEndSessionMessage(true); } @@ -417,7 +429,7 @@ public class SignalServiceDataMessage { profileKeyUpdate, quote, sharedContacts, previews, sticker, preKeyBundle, deviceLink, null, null, - isDeviceUnlinkingRequest); + isDeviceUnlinkingRequest, syncTarget); } } From 7c2b124ebc372151856cd2eb50b2dedd16ad4bf2 Mon Sep 17 00:00:00 2001 From: jubb Date: Mon, 8 Feb 2021 17:30:38 +1100 Subject: [PATCH 04/28] feat: adding the outbound attachment handling for handling media messages --- .../securesms/jobs/PushDecryptJob.java | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 081946041c..7d3b72a97a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -28,7 +28,6 @@ import org.session.libsignal.metadata.ProtocolNoSessionException; import org.session.libsignal.metadata.ProtocolUntrustedIdentityException; import org.session.libsignal.metadata.SelfSendException; import org.session.libsignal.service.loki.api.crypto.SessionProtocol; -import org.session.libsignal.service.loki.utilities.HexEncodingKt; import org.session.libsignal.utilities.PromiseUtilities; import org.thoughtcrime.securesms.ApplicationContext; @@ -567,17 +566,17 @@ public class PushDecryptJob extends BaseJob implements InjectableType { @NonNull Optional messageServerIDOrNull) throws StorageFailedException { - Recipient originalRecipient = getMessageDestination(content, message); - Recipient masterRecipient = getMessageMasterDestination(content.getSender()); + Recipient originalRecipient = getMessageDestination(content, message); + Recipient masterRecipient = getMessageMasterDestination(content.getSender()); String syncTarget = message.getSyncTarget().orNull(); notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice()); - Optional quote = getValidatedQuote(message.getQuote()); - Optional> sharedContacts = getContacts(message.getSharedContacts()); - Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); - Optional sticker = getStickerAttachment(message.getSticker()); + Optional quote = getValidatedQuote(message.getQuote()); + Optional> sharedContacts = getContacts(message.getSharedContacts()); + Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); + Optional sticker = getStickerAttachment(message.getSticker()); Address masterAddress = masterRecipient.getAddress(); @@ -586,8 +585,57 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } if (syncTarget != null && !syncTarget.isEmpty()) { -// OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterAddress, message.getTimestamp(), -1, -// message.getExpiresInSeconds() * 1000L, false, ) + List attachments = PointerAttachment.forPointers(message.getAttachments()); + + OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(), + attachments, + message.getTimestamp(), -1, + message.getExpiresInSeconds() * 1000, + ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(), + sharedContacts.or(Collections.emptyList()), + linkPreviews.or(Collections.emptyList()), + Collections.emptyList(), Collections.emptyList()); + + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + database.beginTransaction(); + + // Ignore message if it has no body and no attachments + if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) { + return; + } + + Optional insertResult; + + try { + if (message.isGroupMessage()) { + insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, content.getTimestamp()); + } else { + insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, -1); + } + + if (insertResult.isPresent()) { + List allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); + List stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); + List dbAttachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); + + forceStickerDownloadIfNecessary(stickerAttachments); + + for (DatabaseAttachment attachment : dbAttachments) { + ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); + } + + if (smsMessageId.isPresent()) { + DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); + } + + database.setTransactionSuccessful(); + } + } catch (MmsException e) { + throw new StorageFailedException(e, content.getSender(), content.getSenderDevice()); + } finally { + database.endTransaction(); + } + } else { IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1, message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), From 5ceaf87ba91d1978dc46b5ee4d9513e37ee6ebb0 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 9 Feb 2021 11:45:38 +1100 Subject: [PATCH 05/28] implement closed group v2 handling and configuration message handling in refactored message receiving pipeline --- .../securesms/database/Storage.kt | 16 + .../libsession/messaging/StorageProtocol.kt | 23 +- .../control/ClosedGroupControlMessage.kt | 31 +- .../messages/control/ConfigurationMessage.kt | 2 +- .../MessageReceiverHandler.kt | 414 ++++++++++++------ .../MessageSenderClosedGroup.kt | 40 +- .../notifications/PushNotificationAPI.kt | 1 + .../session/libsession/utilities/GroupUtil.kt | 16 + 8 files changed, 379 insertions(+), 164 deletions(-) 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 3cc08abf81..bb52fb13e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -376,6 +376,22 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys() } + override fun addClosedGroupPublicKey(groupPublicKey: String) { + DatabaseFactory.getLokiAPIDatabase(context).addClosedGroupPublicKey(groupPublicKey) + } + + override fun removeClosedGroupPublicKey(groupPublicKey: String) { + DatabaseFactory.getLokiAPIDatabase(context).removeClosedGroupPublicKey(groupPublicKey) + } + + override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) { + DatabaseFactory.getLokiAPIDatabase(context).addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) + } + + override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) { + DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) + } + override fun getAllOpenGroups(): Map { return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats() } 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 2aa82f4971..856c4154c8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -57,6 +57,7 @@ interface StorageProtocol { // Open Groups fun getOpenGroup(threadID: String): OpenGroup? fun getThreadID(openGroupID: String): String? + fun getAllOpenGroups(): Map // Open Group Public Keys fun getOpenGroupPublicKey(server: String): String? @@ -66,6 +67,13 @@ interface StorageProtocol { fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String) fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? + // Open Group Metadata + fun setUserCount(group: Long, server: String, newValue: Int) + fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) + fun getOpenGroupProfilePictureURL(group: Long, server: String): String? + fun updateTitle(groupID: String, newValue: String) + fun updateProfilePicture(groupID: String, newValue: ByteArray) + // Last Message Server ID fun getLastMessageServerID(group: Long, server: String): Long? fun setLastMessageServerID(group: Long, server: String, newValue: Long) @@ -76,13 +84,6 @@ interface StorageProtocol { fun setLastDeletionServerID(group: Long, server: String, newValue: Long) fun removeLastDeletionServerID(group: Long, server: String) - // Open Group Metadata - fun setUserCount(group: Long, server: String, newValue: Int) - fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) - fun getOpenGroupProfilePictureURL(group: Long, server: String): String? - fun updateTitle(groupID: String, newValue: String) - fun updateProfilePicture(groupID: String, newValue: ByteArray) - // Message Handling fun getReceivedMessageTimestamps(): Set fun addReceivedMessageTimestamp(timestamp: Long) @@ -102,6 +103,11 @@ interface StorageProtocol { fun removeMember(groupID: String, member: Address) fun updateMembers(groupID: String, members: List
) // Closed Group + fun getAllClosedGroupPublicKeys(): Set + fun addClosedGroupPublicKey(groupPublicKey: String) + fun removeClosedGroupPublicKey(groupPublicKey: String) + fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) + fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection) fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, @@ -109,9 +115,8 @@ interface StorageProtocol { fun isClosedGroup(publicKey: String): Boolean fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? + // Groups - fun getAllClosedGroupPublicKeys(): Set - fun getAllOpenGroups(): Map fun getAllGroups(): List // Settings diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt index 97b22ab0a7..176cd21182 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt @@ -6,6 +6,8 @@ import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.loki.utilities.toHexString +import org.session.libsignal.utilities.Hex class ClosedGroupControlMessage() : ControlMessage() { @@ -33,11 +35,23 @@ class ClosedGroupControlMessage() : ControlMessage() { class NameChange(val name: String) : Kind() class MembersAdded(val members: List) : Kind() class MembersRemoved( val members: List) : Kind() - class MemberLeft() : Kind() + object MemberLeft : Kind() + + val description: String = run { + when(this) { + is New -> "new" + is Update -> "update" + is EncryptionKeyPair -> "encryptionKeyPair" + is NameChange -> "nameChange" + is MembersAdded -> "membersAdded" + is MembersRemoved -> "membersRemoved" + MemberLeft -> "memberLeft" + } + } } companion object { - const val TAG = "ClosedGroupUpdateV2" + const val TAG = "ClosedGroupControlMessage" fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? { val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdateV2 ?: return null @@ -75,7 +89,7 @@ class ClosedGroupControlMessage() : ControlMessage() { kind = Kind.MembersRemoved(closedGroupUpdateProto.membersList) } SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> { - kind = Kind.MemberLeft() + kind = Kind.MemberLeft } } return ClosedGroupControlMessage(kind) @@ -168,10 +182,15 @@ class ClosedGroupControlMessage() : ControlMessage() { } } - final class KeyPairWrapper(val publicKey: String?, private val encryptedKeyPair: ByteString?) { + class KeyPairWrapper(val publicKey: String?, val encryptedKeyPair: ByteString?) { + + val isValid: Boolean = run { + this.publicKey != null && this.encryptedKeyPair != null + } + companion object { fun fromProto(proto: SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper): KeyPairWrapper { - return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair) + return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair) } } @@ -179,7 +198,7 @@ class ClosedGroupControlMessage() : ControlMessage() { val publicKey = publicKey ?: return null val encryptedKeyPair = encryptedKeyPair ?: return null val result = SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper.newBuilder() - result.publicKey = ByteString.copyFrom(publicKey.toByteArray()) + result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey)) result.encryptedKeyPair = encryptedKeyPair return try { 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 index aee6356740..ebf3236cab 100644 --- 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 @@ -62,7 +62,7 @@ class ConfigurationMessage(val closedGroups: List, val openGroups: for (groupRecord in groups) { if (groupRecord.isClosedGroup) { if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue - val groupPublicKey = GroupUtil.getDecodedGroupIDAsData(GroupUtil.getDecodedGroupID(groupRecord.encodedId)).toHexString() // Double decoded + 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() }) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index 49e40b3ffa..9157e3d918 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -4,12 +4,8 @@ import android.text.TextUtils import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message -import org.session.libsession.messaging.messages.control.ClosedGroupUpdate -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate -import org.session.libsession.messaging.messages.control.ReadReceipt -import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsession.messaging.messages.control.* import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment @@ -17,19 +13,19 @@ import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPrevie import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.GroupRecord import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.GroupUtil -import org.session.libsignal.utilities.Hex import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.TextSecurePreferences +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.utilities.logging.Log import org.session.libsignal.libsignal.util.guava.Optional import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.internal.push.SignalServiceProtos -import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet -import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType -import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey -import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString import java.security.MessageDigest import java.util.* @@ -45,8 +41,9 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, when (message) { is ReadReceipt -> handleReadReceipt(message) is TypingIndicator -> handleTypingIndicator(message) - is ClosedGroupUpdate -> handleClosedGroupUpdate(message) + is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message) is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message, proto) + is ConfigurationMessage -> handleConfigurationMessage(message) is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID) } } @@ -105,6 +102,21 @@ fun MessageReceiver.disableExpirationTimer(message: ExpirationTimerUpdate, proto SSKEnvironment.shared.messageExpirationManager.disableExpirationTimer(id, senderPublicKey, proto) } +private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMessage) { + val storage = MessagingConfiguration.shared.storage + if (message.sender != storage.getUserPublicKey()) return + val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys() + for (closeGroup in message.closedGroups) { + if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue + handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins) + } + val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } + for (openGroup in message.openGroups) { + if (allOpenGroups.contains(openGroup)) continue + // TODO + } +} + fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) { val storage = MessagingConfiguration.shared.storage val context = MessagingConfiguration.shared.context @@ -188,173 +200,293 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS SSKEnvironment.shared.notificationManager.updateNotification(context, threadID) } -private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate) { +private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroupControlMessage) { when (message.kind!!) { - is ClosedGroupUpdate.Kind.New -> handleNewGroup(message) - is ClosedGroupUpdate.Kind.Info -> handleGroupUpdate(message) - is ClosedGroupUpdate.Kind.SenderKeyRequest -> handleSenderKeyRequest(message) - is ClosedGroupUpdate.Kind.SenderKey -> handleSenderKey(message) + is ClosedGroupControlMessage.Kind.New -> handleNewClosedGroup(message) + is ClosedGroupControlMessage.Kind.Update -> handleClosedGroupUpdated(message) + is ClosedGroupControlMessage.Kind.EncryptionKeyPair -> handleClosedGroupEncryptionKeyPair(message) + is ClosedGroupControlMessage.Kind.NameChange -> handleClosedGroupNameChanged(message) + is ClosedGroupControlMessage.Kind.MembersAdded -> handleClosedGroupMembersAdded(message) + is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message) + ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message) } } -private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) { +private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) { + val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return + val groupPublicKey = kind.publicKey.toByteArray().toHexString() + val members = kind.members.map { it.toByteArray().toHexString() } + val admins = kind.admins.map { it.toByteArray().toHexString() } + handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins) +} + +// Parameter @sender:String is just for inserting incoming info message +private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List, admins: List) { val context = MessagingConfiguration.shared.context val storage = MessagingConfiguration.shared.storage - val sskDatabase = MessagingConfiguration.shared.sskDatabase - if (message.kind !is ClosedGroupUpdate.Kind.New) { return } - val kind = message.kind!! as ClosedGroupUpdate.Kind.New - val groupPublicKey = kind.groupPublicKey.toHexString() - val name = kind.name - val groupPrivateKey = kind.groupPrivateKey - val senderKeys = kind.senderKeys - val members = kind.members.map { it.toHexString() } - val admins = kind.admins.map { it.toHexString() } - // Persist the ratchets - senderKeys.forEach { senderKey -> - if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach } - val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) - sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current) - } - // Sort out any discrepancies between the provided sender keys and what's required - val missingSenderKeys = members.toSet().subtract(senderKeys.map { Hex.toStringCondensed(it.publicKey) }) - val userPublicKey = storage.getUserPublicKey()!! - if (missingSenderKeys.contains(userPublicKey)) { - val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) - val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) - members.forEach { member -> - if (member == userPublicKey) return@forEach - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(groupPublicKey.toByteArray(), userSenderKey) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey)) - } - } - missingSenderKeys.minus(userPublicKey).forEach { publicKey -> - MessageSender.requestSenderKey(groupPublicKey, publicKey) - } // Create the group - val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) if (storage.getGroup(groupID) != null) { // Update the group storage.updateTitle(groupID, name) storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) } else { storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it) })) + null, null, LinkedList(admins.map { Address.fromSerialized(it) })) } storage.setProfileSharing(Address.fromSerialized(groupID), true) // Add the group to the user's set of public keys to poll for - sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) - // Notify the PN server - PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) + storage.addClosedGroupPublicKey(groupPublicKey) + // Store the encryption key pair + storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) // Notify the user - storage.insertIncomingInfoMessage(context, message.sender!!, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + // Notify the PN server + PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!) } -private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) { +private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControlMessage) { + // Prepare val context = MessagingConfiguration.shared.context val storage = MessagingConfiguration.shared.storage - val sskDatabase = MessagingConfiguration.shared.sskDatabase - if (message.kind !is ClosedGroupUpdate.Kind.Info) { return } - val kind = message.kind!! as ClosedGroupUpdate.Kind.Info - val groupPublicKey = kind.groupPublicKey.toHexString() - val name = kind.name - val senderKeys = kind.senderKeys - val members = kind.members.map { it.toHexString() } - val admins = kind.admins.map { it.toHexString() } - // Get the group - val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded - val group = storage.getGroup(groupID) ?: return Log.d("Loki", "Ignoring closed group info message for nonexistent group.") - // Check that the sender is a member of the group (before the update) - if (!group.members.contains(Address.fromSerialized(message.sender!!))) { return Log.d("Loki", "Ignoring closed group info message from non-member.") } - // Store the ratchets for any new members (it's important that this happens before the code below) - senderKeys.forEach { senderKey -> - val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) - sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current) - } - // Delete all ratchets and either: - // • Send out the user's new ratchet using established channels if other members of the group left or were removed - // • Remove the group from the user's set of public keys to poll for if the current user was among the members that were removed - val oldMembers = group.members.map { it.serialize() }.toSet() + val senderPublicKey = message.sender ?: return + val kind = message.kind!! as? ClosedGroupControlMessage.Kind.Update ?: return + val groupPublicKey = message.groupPublicKey ?: return val userPublicKey = storage.getUserPublicKey()!! - val wasUserRemoved = !members.contains(userPublicKey) - val wasSenderRemoved = !members.contains(message.sender!!) - if (members.toSet().intersect(oldMembers) != oldMembers.toSet()) { - val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) - for (pair in allOldRatchets) { - val senderPublicKey = pair.first - val ratchet = pair.second - val collection = ClosedGroupRatchetCollectionType.Old - sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet, collection) - } - sskDatabase.removeAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) - if (wasUserRemoved) { - sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) - storage.setActive(groupID, false) - storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) - // Notify the PN server - PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) - } else { - val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) - val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) - members.forEach { member -> - if (member == userPublicKey) return@forEach - val address = Address.fromSerialized(member) - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, address) - } - } + // Unwrap the message + val name = kind.name + val members = kind.members.map { it.toByteArray().toHexString() } + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + val oldMembers = group.members.map { it.serialize() } + // Check common group update logic + if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { + return + } + // Check that the admin wasn't removed unless the group was destroyed entirely + if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) { + android.util.Log.d("Loki", "Ignoring invalid closed group update message.") + 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) { + disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, 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.toString() }.contains(userPublicKey) + if (wasAnyUserRemoved && isCurrentUserAdmin) { + MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members) } // Update the group storage.updateTitle(groupID, name) - storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) - // Notify the user if needed + if (!wasCurrentUserRemoved) { + // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead + storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + } + // Notify the user + val wasSenderRemoved = !members.contains(senderPublicKey) val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE - storage.insertIncomingInfoMessage(context, message.sender!!, groupID, type0, type1, name, members, admins) + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }) } -private fun MessageReceiver.handleSenderKeyRequest(message: ClosedGroupUpdate) { - if (message.kind !is ClosedGroupUpdate.Kind.SenderKeyRequest) { return } - val kind = message.kind!! as ClosedGroupUpdate.Kind.SenderKeyRequest +private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) { + // Prepare val storage = MessagingConfiguration.shared.storage - val sskDatabase = MessagingConfiguration.shared.sskDatabase + val senderPublicKey = message.sender ?: return + val kind = message.kind!! as? ClosedGroupControlMessage.Kind.EncryptionKeyPair ?: return + val groupPublicKey = message.groupPublicKey ?: return val userPublicKey = storage.getUserPublicKey()!! - val groupPublicKey = kind.groupPublicKey.toHexString() - val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded - val group = storage.getGroup(groupID) - if (group == null) { - Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.") + val userKeyPair = storage.getUserX25519KeyPair() + // Unwrap the message + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") return } - // Check that the requesting user is a member of the group - if (!group.members.map { it.serialize() }.contains(message.sender!!)) { - Log.d("Loki", "Ignoring closed group sender key request from non-member.") + if (!group.admins.map { it.toString() }.contains(senderPublicKey)) { + android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.") return } - // Respond to the request - Log.d("Loki", "Responding to sender key request from: ${message.sender!!}.") - val userRatchet = sskDatabase.getClosedGroupRatchet(groupPublicKey, userPublicKey, ClosedGroupRatchetCollectionType.Current) - ?: SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) - val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, Address.fromSerialized(groupID)) + // Find our wrapper and decrypt it if possible + val wrapper = kind.wrappers.firstOrNull { it.publicKey!!.toByteArray().toHexString() == userPublicKey } ?: return + val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray() + val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first + // Parse it + val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) + val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) + // Store it + storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey) + Log.d("Loki", "Received a new closed group encryption key pair") } -private fun MessageReceiver.handleSenderKey(message: ClosedGroupUpdate) { - if (message.kind !is ClosedGroupUpdate.Kind.SenderKey) { return } - val kind = message.kind!! as ClosedGroupUpdate.Kind.SenderKey - val groupPublicKey = kind.groupPublicKey.toHexString() - val senderKey = kind.senderKey - if (senderKey.publicKey.toHexString() != message.sender!!) { - Log.d("Loki", "Ignoring invalid closed group sender key.") +private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val senderPublicKey = message.sender ?: return + val kind = message.kind!! as? ClosedGroupControlMessage.Kind.NameChange ?: return + val groupPublicKey = message.groupPublicKey ?: return + // Check that the sender is a member of the group (before the update) + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") return } - Log.d("Loki", "Received a sender key from: ${message.sender!!}.") - val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) - MessagingConfiguration.shared.sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current) + // Check common group update logic + if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { + return + } + val members = group.members.map { it.serialize() } + val admins = group.admins.map { it.serialize() } + val name = kind.name + storage.updateTitle(groupID, name) + + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) +} + +private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val senderPublicKey = message.sender ?: return + val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return + val groupPublicKey = message.groupPublicKey ?: return + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { + return + } + val name = group.title + // Check common group update logic + val members = group.members.map { it.serialize() } + val admins = group.admins.map { it.serialize() } + + // Users that are part of this remove update + val updateMembers = kind.members.map { it.toByteArray().toHexString() } + // newMembers to save is old members minus removed members + val newMembers = members + updateMembers + storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) + + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) +} + +private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! + val senderPublicKey = message.sender ?: return + val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersRemoved ?: return + val groupPublicKey = message.groupPublicKey ?: return + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + val name = group.title + // Check common group update logic + val members = group.members.map { it.serialize() } + val admins = group.admins.map { it.toString() } + + // Users that are part of this remove update + val updateMembers = kind.members.map { it.toByteArray().toHexString() } + + if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } + // If admin leaves the group is disbanded + val didAdminLeave = admins.any { it in updateMembers } + // newMembers to save is old members minus removed members + val newMembers = members - updateMembers + // user should be posting MEMBERS_LEFT so this should not be encountered + val senderLeft = senderPublicKey in updateMembers + if (senderLeft) { + android.util.Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey") + } + val wasCurrentUserRemoved = userPublicKey in updateMembers + + // admin should send a MEMBERS_LEFT message but handled here in case + if (didAdminLeave || wasCurrentUserRemoved) { + disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) + } else { + val isCurrentUserAdmin = admins.contains(userPublicKey) + storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) + if (isCurrentUserAdmin) { + MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers) + } + } + val (contextType, signalType) = + if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT + else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE + + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins) +} + +private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val senderPublicKey = message.sender ?: return + val userPublicKey = storage.getUserPublicKey()!! + if (senderPublicKey == userPublicKey) { return } // Check the user leaving isn't us, will already be handled + val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return + val groupPublicKey = message.groupPublicKey ?: return + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + val name = group.title + // Check common group update logic + val members = group.members.map { it.serialize() } + val admins = group.admins.map { it.toString() } + if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { + return + } + // If admin leaves the group is disbanded + val didAdminLeave = admins.contains(senderPublicKey) + val updatedMemberList = members - senderPublicKey + + if (didAdminLeave) { + disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) + } else { + val isCurrentUserAdmin = admins.contains(userPublicKey) + storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) + if (isCurrentUserAdmin) { + MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList) + } + } + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins) +} + +private fun isValidGroupUpdate(group: GroupRecord, + sentTimestamp: Long, + senderPublicKey: String): Boolean { + val oldMembers = group.members.map { it.serialize() } + // Check that the message isn't from before the group was created + if (group.createdAt > sentTimestamp) { + android.util.Log.d("Loki", "Ignoring closed group update from before thread was created.") + return false + } + // Check that the sender is a member of the group (before the update) + if (senderPublicKey !in oldMembers) { + android.util.Log.d("Loki", "Ignoring closed group info message from non-member.") + return false + } + return true +} + +private fun disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String) { + val storage = MessagingConfiguration.shared.storage + storage.removeClosedGroupPublicKey(groupPublicKey) + // Remove the key pairs + storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) + // Mark the group as inactive + storage.setActive(groupID, false) + storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) + // Notify the PN server + PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt index 807d693fae..7f2bea8939 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt @@ -3,10 +3,12 @@ package org.session.libsession.messaging.sending_receiving import android.util.Log +import com.google.protobuf.ByteString import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.MessageSender.Error @@ -21,6 +23,7 @@ import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSende import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import java.util.* fun MessageSender.createClosedGroup(name: String, members: Collection): Promise { @@ -216,11 +219,34 @@ fun MessageSender.leave(groupPublicKey: String) { return update(groupPublicKey, newMembers, name).get() } -fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) { - Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.") - val address = Address.fromSerialized(senderPublicKey) - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey)) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, address) +fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, targetMembers: Collection) { + // Prepare + val storage = MessagingConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Can't update nonexistent closed group.") + throw Error.NoThread + } + if (!group.admins.map { it.toString() }.contains(userPublicKey)) { + Log.d("Loki", "Can't distribute new encryption key pair as non-admin.") + throw Error.InvalidClosedGroupUpdate + } + // Generate the new encryption key pair + val newKeyPair = Curve.generateKeyPair() + // Distribute it + val proto = SignalServiceProtos.KeyPair.newBuilder() + proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) + proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize()) + val plaintext = proto.build().toByteArray() + val wrappers = targetMembers.map { publicKey -> + val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) + ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext)) + } + val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(wrappers) + val closedGroupControlMessage = ClosedGroupControlMessage(kind) + sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success { + // Store it * after * having sent out the message to the group + storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index 61f4139efe..601bec9975 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.sending_receiving.notifications +import android.annotation.SuppressLint import nl.komponents.kovenant.functional.map import okhttp3.* import org.session.libsession.messaging.MessagingConfiguration diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt index 24fcb44862..ea9866822c 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt @@ -2,6 +2,8 @@ package org.session.libsession.utilities import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.utilities.Hex +import java.io.IOException +import kotlin.jvm.Throws object GroupUtil { const val CLOSED_GROUP_PREFIX = "__textsecure_group__!" @@ -65,4 +67,18 @@ object GroupUtil { fun isClosedGroup(groupId: String): Boolean { return groupId.startsWith(CLOSED_GROUP_PREFIX) } + + // NOTE: Signal group ID handling is weird. The ID is double encoded in the database, but not in a `GroupContext`. + + @JvmStatic + @Throws(IOException::class) + fun doubleEncodeGroupID(groupPublicKey: String): String { + return getEncodedClosedGroupID(getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) + } + + @JvmStatic + @Throws(IOException::class) + fun doubleDecodeGroupID(groupID: String): ByteArray { + return getDecodedGroupIDAsData(getDecodedGroupID(groupID)) + } } \ No newline at end of file From c32c58eee75ea5f5a5b0fc99eaa5bf7247ad0f11 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 9 Feb 2021 13:16:33 +1100 Subject: [PATCH 06/28] add open group through configuration message --- .../main/java/org/thoughtcrime/securesms/database/Storage.kt | 5 +++++ .../securesms/loki/utilities/OpenGroupUtilities.kt | 2 +- .../java/org/session/libsession/messaging/StorageProtocol.kt | 1 + .../messaging/sending_receiving/MessageReceiverHandler.kt | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) 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 bb52fb13e3..8fdcb3b100 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -31,6 +31,7 @@ import org.session.libsignal.service.loki.api.opengroups.PublicChat import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase +import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities import org.thoughtcrime.securesms.loki.utilities.get import org.thoughtcrime.securesms.loki.utilities.getString import org.thoughtcrime.securesms.mms.IncomingMediaMessage @@ -396,6 +397,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats() } + override fun addOpenGroup(server: String, channel: Long) { + OpenGroupUtilities.addGroup(context, server, channel) + } + override fun getAllGroups(): List { return DatabaseFactory.getGroupDatabase(context).allGroups } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt index f965f370c4..83d86f9f8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt @@ -27,7 +27,7 @@ object OpenGroupUtilities { val groupID = PublicChat.getId(channel, url) val threadID = GroupManager.getOpenGroupThreadID(groupID, context) val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) - if (openGroup != null) return openGroup + if (openGroup != null) { return openGroup } // Add the new group. val application = ApplicationContext.getInstance(context) 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 856c4154c8..164bf53088 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -58,6 +58,7 @@ interface StorageProtocol { fun getOpenGroup(threadID: String): OpenGroup? fun getThreadID(openGroupID: String): String? fun getAllOpenGroups(): Map + fun addOpenGroup(server: String, channel: Long) // Open Group Public Keys fun getOpenGroupPublicKey(server: String): String? diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index 9157e3d918..8df3e86da2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -113,7 +113,7 @@ private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMes val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } for (openGroup in message.openGroups) { if (allOpenGroups.contains(openGroup)) continue - // TODO + storage.addOpenGroup(openGroup, 1) } } From 77eb460ba74bbc7ad0096857870be86a166730f6 Mon Sep 17 00:00:00 2001 From: jubb Date: Tue, 9 Feb 2021 13:45:38 +1100 Subject: [PATCH 07/28] feat: add image handling across device self-send. close an unclosed resource. remove unnecessary checks and SmsDatabase way of checking for existing message from ourselves --- app/build.gradle | 4 +-- .../securesms/database/MmsDatabase.java | 11 ++++++++ .../securesms/database/SmsDatabase.java | 9 ------- .../securesms/jobs/PushDecryptJob.java | 24 ++++++++++++++---- .../SingleRecipientNotificationBuilder.java | 2 +- .../loki/utilities/DownloadUtilities.kt | 25 ++++++++++--------- 6 files changed, 46 insertions(+), 29 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 64b4078941..b83c5ae9c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,8 +158,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 121 -def canonicalVersionName = "1.6.4" +def canonicalVersionCode = 135 +def canonicalVersionName = "1.6.12" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 7b84e9975d..2e6e2a9b3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -902,6 +902,17 @@ public class MmsDatabase extends MessagingDatabase { return insertMessageInbox(retrieved, contentLocation, threadId, type, 0); } + public Optional insertSecureDecryptedMessageOutbox(OutgoingMediaMessage retrieved, long threadId, long serverTimestamp) + throws MmsException + { + long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp); + if (messageId == -1) { + return Optional.absent(); + } + markAsSent(messageId, true); + return Optional.fromNullable(new InsertResult(messageId, threadId)); + } + public Optional insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId, long serverTimestamp) throws MmsException { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index aaa14151d2..0234dfae92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -725,15 +725,6 @@ public class SmsDatabase extends MessagingDatabase { contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum()); contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum()); - SQLiteDatabase readDb = databaseHelper.getReadableDatabase(); - Cursor existingRecord = readDb.query(TABLE_NAME, null, String.format("%s = ? AND %s = ? AND %s = ?",ADDRESS, THREAD_ID, DATE_SENT), - new String[] { address.serialize(), Long.toString(threadId), Long.toString(date) }, null, null, null); - int existingRecordCount = existingRecord.getCount(); - if (existingRecordCount > 0) { - // return -1 because record exists from Address to ThreadID with the same date sent (probably sent from us) - return -1; - } - SQLiteDatabase db = databaseHelper.getWritableDatabase(); long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); if (insertListener != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 7d3b72a97a..1047085422 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -83,6 +83,7 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2; import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; +import org.thoughtcrime.securesms.loki.utilities.DatabaseUtilitiesKt; import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; @@ -584,9 +585,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType { masterAddress = getMessageMasterDestination(content.getSender()).getAddress(); } + // Handle sync message from ourselves if (syncTarget != null && !syncTarget.isEmpty()) { List attachments = PointerAttachment.forPointers(message.getAttachments()); + Address targetAddress = Address.fromSerialized(syncTarget); + OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(), attachments, message.getTimestamp(), -1, @@ -596,6 +600,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { linkPreviews.or(Collections.emptyList()), Collections.emptyList(), Collections.emptyList()); + if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) { + Log.d("Loki","Message already exists, don't insert again"); + return; + } + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); database.beginTransaction(); @@ -607,11 +616,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Optional insertResult; try { - if (message.isGroupMessage()) { - insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, content.getTimestamp()); - } else { - insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, -1); - } + + // Check if we have the thread already + long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(syncTarget); + + insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, threadID, content.getTimestamp()); if (insertResult.isPresent()) { List allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); @@ -837,6 +846,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } else if (syncTarget != null && !syncTarget.isEmpty()) { Address targetAddress = Address.fromSerialized(syncTarget); + if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) { + Log.d("Loki","Message already exists, don't insert again"); + return; + } + OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false), body, message.getExpiresInSeconds(), -1); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 62bfcc8b5e..1ea22350a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -326,7 +326,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) { String publicKey = recipient.getAddress().serialize(); - String hepk = (recipient.isLocalNumber() && publicKey != null) + String hepk = (recipient.isLocalNumber() && publicKey == null) ? TextSecurePreferences.getMasterHexEncodedPublicKey(context) : publicKey; String displayName = recipient.getName(); diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt index 1986d2c2db..a582c0fa31 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt @@ -65,19 +65,20 @@ object DownloadUtilities { Log.d("Loki", "Attachment size limit exceeded.") throw PushNetworkException("Max response size exceeded.") } - val input = body.inputStream() - val buffer = ByteArray(32768) - var count = 0 - var bytes = input.read(buffer) - while (bytes >= 0) { - outputStream.write(buffer, 0, bytes) - count += bytes - if (count > maxSize) { - Log.d("Loki", "Attachment size limit exceeded.") - throw PushNetworkException("Max response size exceeded.") + body.inputStream().use { input -> + val buffer = ByteArray(32768) + var count = 0 + var bytes = input.read(buffer) + while (bytes >= 0) { + outputStream.write(buffer, 0, bytes) + count += bytes + if (count > maxSize) { + Log.d("Loki", "Attachment size limit exceeded.") + throw PushNetworkException("Max response size exceeded.") + } + listener?.onAttachmentProgress(body.size.toLong(), count.toLong()) + bytes = input.read(buffer) } - listener?.onAttachmentProgress(body.size.toLong(), count.toLong()) - bytes = input.read(buffer) } } catch (e: Exception) { Log.d("Loki", "Couldn't download attachment due to error: $e.") From 4cfa5e9f3b4f2de0ebd1d98ae2a21847337ec473 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 9 Feb 2021 13:55:01 +1100 Subject: [PATCH 08/28] refactor: message sender + closed group --- .../MessageSenderClosedGroup.kt | 77 +++++++++---------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt index 7f2bea8939..c1f55ff346 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt @@ -24,52 +24,47 @@ import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeys import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded +import org.session.libsignal.utilities.ThreadUtils import java.util.* fun MessageSender.createClosedGroup(name: String, members: Collection): Promise { val deferred = deferred() - // Prepare - val context = MessagingConfiguration.shared.context - val storage = MessagingConfiguration.shared.storage - val members = members - val userPublicKey = storage.getUserPublicKey()!! - // Generate a key pair for the group - val groupKeyPair = Curve.generateKeyPair() - val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix - members.plus(userPublicKey) - val membersAsData = members.map { Hex.fromStringCondensed(it) } - // Create ratchets for all members - val senderKeys: List = members.map { publicKey -> - val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) - ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) + ThreadUtils.queue { + // Prepare + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! + val membersAsData = members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } + // 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 = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val admins = setOf( userPublicKey ) + val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } + storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), + null, null, LinkedList(admins.map { Address.fromSerialized(it) })) + storage.setProfileSharing(Address.fromSerialized(groupID), true) + // Send a closed group update message to all members individually + val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData) + for (member in members) { + if (member == userPublicKey) { continue } + val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind) + sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).get() + } + // Add the group to the user's set of public keys to poll for + storage.addClosedGroupPublicKey(groupPublicKey) + // Store the encryption key pair + storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) + // Notify the user + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID) + // Notify the PN server + PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) + // Fulfill the promise + deferred.resolve(groupID) } - // Create the group - val admins = setOf( userPublicKey ) - val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded - storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), null, null, LinkedList(admins.map { Address.fromSerialized(it) })) - storage.setProfileSharing(Address.fromSerialized(groupID), true) - // Send a closed group update message to all members using established channels - val promises = mutableListOf>() - for (member in members) { - if (member == userPublicKey) { continue } - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), - senderKeys, membersAsData, adminsAsData) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - val address = Address.fromSerialized(member) - val promise = MessageSender.sendNonDurably(closedGroupUpdate, address) - promises.add(promise) - } - // Add the group to the user's set of public keys to poll for - MessagingConfiguration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) - // Notify the PN server - PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) - // Notify the user - val threadID =storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) - storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID) - // Fulfill the promise - deferred.resolve(groupPublicKey) // Return return deferred.promise } From fb2757588da38111005ff2e4dff1206137d57e76 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 9 Feb 2021 14:45:22 +1100 Subject: [PATCH 09/28] refactor: message sender + closed group --- .../MessageReceiverHandler.kt | 2 +- .../sending_receiving/MessageSender.kt | 1 + .../MessageSenderClosedGroup.kt | 143 ++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index 8df3e86da2..9cac49d82d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -479,7 +479,7 @@ private fun isValidGroupUpdate(group: GroupRecord, return true } -private fun disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String) { +fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String) { val storage = MessagingConfiguration.shared.storage storage.removeClosedGroupPublicKey(groupPublicKey) // Remove the key pairs diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 3eff41157e..eb9155eb47 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -41,6 +41,7 @@ object MessageSender { // Closed groups object NoThread : Error("Couldn't find a thread associated with the given group public key.") + object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.") object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.") object InvalidClosedGroupUpdate : Error("Invalid group update.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt index c1f55ff346..1f23e1fa3c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt @@ -13,7 +13,9 @@ import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Hex import org.session.libsignal.libsignal.ecc.Curve @@ -69,6 +71,147 @@ fun MessageSender.createClosedGroup(name: String, members: Collection): return deferred.promise } +fun MessageSender.v2_update(groupPublicKey: String, members: List, name: String) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Can't update nonexistent closed group.") + throw Error.NoThread + } + // Update name if needed + if (name != group.title) { setName(groupPublicKey, name) } + // Add members if needed + val addedMembers = members - group.members.map { it.serialize() } + if (!addedMembers.isEmpty()) { addMembers(groupPublicKey, addedMembers) } + // Remove members if needed + val removedMembers = group.members.map { it.serialize() } - members + if (removedMembers.isEmpty()) { removeMembers(groupPublicKey, removedMembers) } +} + +fun MessageSender.setName(groupPublicKey: String, newName: String) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Can't change name for nonexistent closed group.") + throw Error.NoThread + } + val members = group.members.map { it.serialize() }.toSet() + val admins = group.admins.map { it.serialize() } + // Send the update to the group + val kind = ClosedGroupControlMessage.Kind.NameChange(newName) + val closedGroupControlMessage = ClosedGroupControlMessage(kind) + send(closedGroupControlMessage, Address.fromSerialized(groupID)) + // Update the group + storage.updateTitle(groupID, newName) + // Notify the user + val infoType = SignalServiceProtos.GroupContext.Type.UPDATE + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID) +} + +fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Can't add members to nonexistent closed group.") + throw Error.NoThread + } + if (membersToAdd.isEmpty()) { + Log.d("Loki", "Invalid closed group update.") + throw Error.InvalidClosedGroupUpdate + } + val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd + // Save the new group members + storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) + val membersAsData = updatedMembers.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } + val newMembersAsData = membersToAdd.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } + val admins = group.admins.map { it.serialize() } + val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } + val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: run { + Log.d("Loki", "Couldn't get encryption key pair for closed group.") + throw Error.NoKeyPair + } + val name = group.title + // Send the update to the group + val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData) + val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind) + send(closedGroupControlMessage, Address.fromSerialized(groupID)) + // Send closed group update messages to any new members individually + for (member in membersToAdd) { + val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData) + val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind) + send(closedGroupControlMessage, Address.fromSerialized(member)) + } + // Notify the user + val infoType = SignalServiceProtos.GroupContext.Type.UPDATE + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) +} + +fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Can't remove members from nonexistent closed group.") + throw Error.NoThread + } + if (membersToRemove.isEmpty()) { + Log.d("Loki", "Invalid closed group update.") + throw Error.InvalidClosedGroupUpdate + } + val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove + // Save the new group members + storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) + val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } + val admins = group.admins.map { it.serialize() } + if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) { + Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") + throw Error.InvalidClosedGroupUpdate + } + val name = group.title + // Send the update to the group + val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData) + val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind) + send(closedGroupControlMessage, Address.fromSerialized(groupID)) + val isCurrentUserAdmin = admins.contains(userPublicKey) + if (isCurrentUserAdmin) { + generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers) + } + // Notify the user + val infoType = SignalServiceProtos.GroupContext.Type.UPDATE + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) +} + +fun MessageSender.v2_leave(groupPublicKey: String) { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Can't leave nonexistent closed group.") + throw Error.NoThread + } + val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey + val admins = group.admins.map { it.serialize() } + val name = group.title + // Send the update to the group + val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft) + sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success { + // Remove the group private key and unsubscribe from PNs + MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) + } + // Notify the user + val infoType = SignalServiceProtos.GroupContext.Type.QUIT + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) +} + fun MessageSender.update(groupPublicKey: String, members: Collection, name: String): Promise { val deferred = deferred() val context = MessagingConfiguration.shared.context From 61a3cea895cad1ba65914ac96e59eff46f6d7092 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 9 Feb 2021 15:02:30 +1100 Subject: [PATCH 10/28] move `forceSyncConfigurationNowIfNeeded` --- .../securesms/loki/activities/CreateClosedGroupActivity.kt | 3 +++ .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) 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 772fc7aaae..8521502d81 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 @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.mms.GlideApp import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2 +import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol //TODO Refactor to avoid using kotlinx.android.synthetic class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks> { @@ -110,6 +111,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM isLoading = true loaderContainer.fadeIn() ClosedGroupsProtocolV2.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> + // Force sync configuration message + MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this) loaderContainer.fadeOut() isLoading = false val threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) 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 d08e3f91f4..1adc93d44d 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,8 +84,6 @@ 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) } From 998258fd73f03e78fbe960271e44c26c9c63ecae Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 9 Feb 2021 15:12:21 +1100 Subject: [PATCH 11/28] force sync configuration message when joining an open group --- .../securesms/loki/activities/JoinPublicChatActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index d78e1966ba..79c107c3ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.session.libsignal.utilities.logging.Log import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate +import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { @@ -73,6 +74,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode lifecycleScope.launch(Dispatchers.IO) { try { OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel) + MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity) } catch (e: Exception) { Log.e("JoinPublicChatActivity", "Fialed to join open group.", e) withContext(Dispatchers.Main) { From e8d007dce4823c23bdde850bfedc25d85abc9b6f Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 9 Feb 2021 15:42:07 +1100 Subject: [PATCH 12/28] force sync message when clearing data --- .../thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt index 3dbbfe2ebc..4a14d23ced 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt @@ -10,6 +10,7 @@ import android.view.LayoutInflater import kotlinx.android.synthetic.main.dialog_clear_all_data.view.* import network.loki.messenger.R import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities class ClearAllDataDialog : DialogFragment() { @@ -27,6 +28,7 @@ class ClearAllDataDialog : DialogFragment() { private fun clearAllData() { if (KeyPairUtilities.hasV2KeyPair(requireContext())) { + MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext()) ApplicationContext.getInstance(context).clearAllData(false) } else { val dialog = AlertDialog.Builder(requireContext()) From 6b45cc683ee48c7cb6e6b4fd10f13e4bb03e8827 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 9 Feb 2021 17:00:17 +1100 Subject: [PATCH 13/28] only handle the first configuration message --- .../loki/activities/CreateClosedGroupActivity.kt | 2 -- .../securesms/loki/protocol/MultiDeviceProtocol.kt | 2 ++ .../libsession/utilities/TextSecurePreferences.kt | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) 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 8521502d81..af0f5cc57b 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 @@ -111,8 +111,6 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM isLoading = true loaderContainer.fadeIn() ClosedGroupsProtocolV2.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> - // Force sync configuration message - MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this) loaderContainer.fadeOut() isLoading = false val threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) 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 index bb1ce53fca..a248d6647c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -59,6 +59,7 @@ object MultiDeviceProtocol { @JvmStatic fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String) { + if (TextSecurePreferences.getConfigurationMessageSynced(context)) return val configurationMessage = ConfigurationMessage.fromProto(content) ?: return val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return if (senderPublicKey != userPublicKey) return @@ -85,5 +86,6 @@ object MultiDeviceProtocol { if (allOpenGroups.contains(openGroup)) continue OpenGroupUtilities.addGroup(context, openGroup, 1) } + TextSecurePreferences.setConfigurationMessageSynced(context, true) } } \ 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 d697b06740..8d4581f18a 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -131,6 +131,7 @@ object TextSecurePreferences { // 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" + private const val CONFIGURATION_SYNCED = "pref_configuration_synced" @JvmStatic fun isUsingMultiDevice(context: Context): Boolean { @@ -152,6 +153,16 @@ object TextSecurePreferences { setLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, value) } + @JvmStatic + fun getConfigurationMessageSynced(context: Context): Boolean { + return getBooleanPreference(context, CONFIGURATION_SYNCED, false) + } + + @JvmStatic + fun setConfigurationMessageSynced(context: Context, value: Boolean) { + setBooleanPreference(context, CONFIGURATION_SYNCED, value) + } + @JvmStatic fun isUsingFCM(context: Context): Boolean { return getBooleanPreference(context, IS_USING_FCM, false) From e62eb819c917f1c63ece598dd65ef775e9634f2b Mon Sep 17 00:00:00 2001 From: jubb Date: Tue, 9 Feb 2021 17:04:56 +1100 Subject: [PATCH 14/28] refactor: GroupUtil functions to be JvmStatic --- .../src/main/java/org/session/libsession/utilities/GroupUtil.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt index 24fcb44862..30b3796011 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt @@ -58,10 +58,12 @@ object GroupUtil { return groupId.startsWith(MMS_GROUP_PREFIX) } + @JvmStatic fun isOpenGroup(groupId: String): Boolean { return groupId.startsWith(OPEN_GROUP_PREFIX) } + @JvmStatic fun isClosedGroup(groupId: String): Boolean { return groupId.startsWith(CLOSED_GROUP_PREFIX) } From fd0596f9eaab499055a223f06e0686c07b8d9c7c Mon Sep 17 00:00:00 2001 From: jubb Date: Wed, 10 Feb 2021 17:57:08 +1100 Subject: [PATCH 15/28] fix: closed groups now propagate properly without self-sends --- .../groups/GroupMessageProcessor.java | 9 ++++++++ .../securesms/jobs/PushDecryptJob.java | 23 +++++++++++++------ .../securesms/jobs/PushGroupSendJob.java | 18 +-------------- .../securesms/loki/api/SessionProtocolImpl.kt | 4 ++-- .../loki/protocol/ClosedGroupsProtocolV2.kt | 1 - .../loki/utilities/KeyPairUtilities.kt | 2 +- .../utilities/UnidentifiedAccessUtil.kt | 2 +- .../api/crypto/SignalServiceCipher.java | 1 - 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index ab5dede3ea..e6d9090021 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.session.libsignal.utilities.logging.Log; @@ -123,11 +124,19 @@ public class GroupMessageProcessor { { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); String id = GroupUtil.getEncodedId(group); + Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group)); + Recipient recipient = Recipient.from(context, address, false); String userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); if (userMasterDevice == null) { userMasterDevice = TextSecurePreferences.getLocalNumber(context); } + if (content.getSender().equals(userMasterDevice)) { + long threadId = threadDatabase.getThreadIdIfExistsFor(recipient); + return threadId == -1 ? null : threadId; + } + if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) { // Loki - Only update the group if the group admin sent the message String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 1047085422..40cfb1fdc2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -586,11 +586,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } // Handle sync message from ourselves - if (syncTarget != null && !syncTarget.isEmpty()) { + if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) { + Address targetAddress = masterRecipient.getAddress(); + if (message.getGroupInfo().isPresent()) { + targetAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())); + } else if (syncTarget != null && !syncTarget.isEmpty()) { + targetAddress = Address.fromSerialized(syncTarget); + } List attachments = PointerAttachment.forPointers(message.getAttachments()); - Address targetAddress = Address.fromSerialized(syncTarget); - OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(), attachments, message.getTimestamp(), -1, @@ -618,7 +622,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { try { // Check if we have the thread already - long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(syncTarget); + long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize()); insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, threadID, content.getTimestamp()); @@ -843,8 +847,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second; - } else if (syncTarget != null && !syncTarget.isEmpty()) { - Address targetAddress = Address.fromSerialized(syncTarget); + } else if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) { + Address targetAddress = masterRecipient.getAddress(); + if (message.getGroupInfo().isPresent()) { + targetAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())); + } else if (syncTarget != null && !syncTarget.isEmpty()) { + targetAddress = Address.fromSerialized(syncTarget); + } if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) { Log.d("Loki","Message already exists, don't insert again"); @@ -858,7 +867,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (tm.getMessageBody().length() == 0) { return; } // Check if we have the thread already - long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(syncTarget); + long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize()); // Insert the message into the database diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index e4c8b588df..e5ef36cb0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -194,22 +194,6 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { } if (existingNetworkFailures.isEmpty() && networkFailures.isEmpty() && identityMismatches.isEmpty() && existingIdentityMismatches.isEmpty()) { - Address address = message.getRecipient().getAddress(); - if (!address.isOpenGroup()) { - try { - SignalServiceDataMessage selfSend = getDataMessage(address, message) - .withSyncTarget(address.toGroupString()) - .build(); - // send to ourselves to sync multi-device - Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); - SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, selfSend); - if (selfSendResult.getLokiAPIError() != null) { - throw selfSendResult.getLokiAPIError(); - } - } catch (Exception e) { - Log.e("Loki", "Error sending message to ourselves", e); - } - } database.markAsSent(messageId, true); markAttachmentsUploaded(messageId, message.getAttachments()); @@ -290,7 +274,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { } } - public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) { + public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) throws IOException { SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt index 8a8c18565d..e3c5b0aa6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt @@ -18,10 +18,11 @@ import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities class SessionProtocolImpl(private val context: Context) : SessionProtocol { + private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } + override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray { val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded()) - val sodium = LazySodiumAndroid(SodiumAndroid()) val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey val signature = ByteArray(Sign.BYTES) @@ -47,7 +48,6 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol { val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize() val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded()) Log.d("Test", "recipientX25519PublicKey: $recipientX25519PublicKey") - val sodium = LazySodiumAndroid(SodiumAndroid()) val signatureSize = Sign.BYTES val ed25519PublicKeySize = Sign.PUBLICKEYBYTES 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..c97591b879 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 @@ -70,7 +70,6 @@ object ClosedGroupsProtocolV2 { // 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt index e965beeb11..8acd255597 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt @@ -14,7 +14,7 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair object KeyPairUtilities { - private val sodium = LazySodiumAndroid(SodiumAndroid()) + private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } fun generate(): KeyPairGenerationResult { val seed = sodium.randomBytesBuf(16) diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt index 671189778f..56427bfb62 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt @@ -13,7 +13,7 @@ import org.session.libsignal.service.api.crypto.UnidentifiedAccessPair object UnidentifiedAccessUtil { private val TAG = UnidentifiedAccessUtil::class.simpleName - private val sodium = LazySodiumAndroid(SodiumAndroid()) + private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } fun getAccessFor(recipientPublicKey: String): UnidentifiedAccessPair? { try { diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java index 608dee89cd..c5424d2cf0 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java @@ -331,7 +331,6 @@ public class SignalServiceCipher { kotlin.Pair plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl); paddedMessage = plaintextAndSenderPublicKey.getFirst(); String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); - if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isPreKeySignalMessage()) { From 34fab9681ceaa7d23a038bbf8b7c5c736fec8e26 Mon Sep 17 00:00:00 2001 From: jubb Date: Thu, 11 Feb 2021 14:01:31 +1100 Subject: [PATCH 16/28] fix: closed groups info messages work now --- .../securesms/database/GroupDatabase.java | 5 +- .../securesms/database/MmsSmsDatabase.java | 10 +- .../securesms/database/Storage.kt | 3 +- .../securesms/groups/GroupManager.java | 2 +- .../groups/GroupMessageProcessor.java | 12 +- .../securesms/jobs/PushDecryptJob.java | 113 +------------- .../loki/database/LokiMessageDatabase.kt | 2 +- .../ClosedGroupUpdateMessageSendJobV2.kt | 13 +- .../loki/protocol/ClosedGroupsProtocol.kt | 4 +- .../loki/protocol/ClosedGroupsProtocolV2.kt | 140 ++++++++++++------ .../mms/OutgoingGroupMediaMessage.java | 1 - .../api/SignalServiceMessageSender.java | 1 - 12 files changed, 130 insertions(+), 176 deletions(-) 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..164172aba1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -184,7 +184,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt } } - public void create(@NonNull String groupId, @Nullable String title, @NonNull List
members, + public long create(@NonNull String groupId, @Nullable String title, @NonNull List
members, @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List
admins) { Collections.sort(members); @@ -211,7 +211,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt contentValues.put(ADMINS, Address.Companion.toSerializedList(admins, ',')); } - databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); + long threadId = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); Recipient.applyCached(Address.Companion.fromSerialized(groupId), recipient -> { recipient.setName(title); @@ -220,6 +220,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt }); notifyConversationListListeners(); + return threadId; } public boolean delete(@NonNull String groupId) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 0ddf142866..159d961816 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -87,7 +87,7 @@ public class MmsSmsDatabase extends Database { } } - public @Nullable MessageRecord getMessageFor(long timestamp, Address author) { + public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) { MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) { @@ -96,8 +96,8 @@ public class MmsSmsDatabase extends Database { MessageRecord messageRecord; while ((messageRecord = reader.getNext()) != null) { - if ((Util.isOwnNumber(context, author.serialize()) && messageRecord.isOutgoing()) || - (!Util.isOwnNumber(context, author.serialize()) && messageRecord.getIndividualRecipient().getAddress().equals(author))) + if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) || + (!Util.isOwnNumber(context, serializedAuthor) && messageRecord.getIndividualRecipient().getAddress().equals(serializedAuthor))) { return messageRecord; } @@ -107,6 +107,10 @@ public class MmsSmsDatabase extends Database { return null; } + public @Nullable MessageRecord getMessageFor(long timestamp, Address author) { + return getMessageFor(timestamp, author.serialize()); + } + public Cursor getConversation(long threadId, long offset, long limit) { String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; 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..984f9fb82c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -8,7 +8,6 @@ import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageSendJob -import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.opengroups.OpenGroup @@ -351,7 +350,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) mmsDB.markAsSent(infoMessageID, true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index 6d9ac5e29a..af11e476c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -194,7 +194,7 @@ public class GroupManager { avatarAttachment = new UriAttachment(avatarUri, MediaTypes.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null); } - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList()); + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, 0, null, Collections.emptyList(), Collections.emptyList()); long threadId = MessageSender.send(context, outgoingMessage, -1, false, null); return new GroupActionResult(groupRecipient, threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index e6d9090021..e6474ce4b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.session.libsignal.utilities.logging.Log; @@ -269,9 +270,16 @@ public class GroupMessageProcessor { MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group)); Recipient recipient = Recipient.from(context, address, false); - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList()); + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, 0, null, Collections.emptyList(), Collections.emptyList()); long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); - long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); + Address senderAddress = Address.Companion.fromExternal(context, content.getSender()); + MessageRecord existingMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), senderAddress); + long messageId; + if (existingMessage != null) { + messageId = existingMessage.getId(); + } else { + messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); + } mmsDatabase.markAsSent(messageId, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 40cfb1fdc2..85b27d315e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -47,7 +47,6 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.contactshare.ContactModelMapper; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; -import org.thoughtcrime.securesms.crypto.SecurityEvent; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; import org.thoughtcrime.securesms.database.AttachmentDatabase; @@ -83,7 +82,6 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2; import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; -import org.thoughtcrime.securesms.loki.utilities.DatabaseUtilitiesKt; import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; @@ -97,7 +95,6 @@ import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; -import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.session.libsignal.utilities.Hex; import org.session.libsignal.libsignal.InvalidMessageException; @@ -399,33 +396,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message) - { - SmsDatabase database = DatabaseFactory.getSmsDatabase(context); - Recipient recipient = getSyncMessageDestination(message); - OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, "", -1); - OutgoingEndSessionMessage outgoingEndSessionMessage = new OutgoingEndSessionMessage(outgoingTextMessage); - - long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); - - if (!recipient.isGroupRecipient()) { - // TODO: Handle session reset on sync messages - /* - SessionStore sessionStore = new TextSecureSessionStore(context); - sessionStore.deleteAllSessions(recipient.getAddress().toPhoneString()); - */ - - SecurityEvent.broadcastSecurityUpdateEvent(context); - - long messageId = database.insertMessageOutbox(threadId, outgoingEndSessionMessage, - false, message.getTimestamp(), - null); - database.markAsSent(messageId, true); - } - - return threadId; - } - private void handleGroupMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId) @@ -484,83 +454,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private void handleSynchronizeStickerPackOperation(@NonNull List stickerPackOperations) { - JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - - for (StickerPackOperationMessage operation : stickerPackOperations) { - if (operation.getPackId().isPresent() && operation.getPackKey().isPresent() && operation.getType().isPresent()) { - String packId = Hex.toStringCondensed(operation.getPackId().get()); - String packKey = Hex.toStringCondensed(operation.getPackKey().get()); - - switch (operation.getType().get()) { - case INSTALL: - jobManager.add(new StickerPackDownloadJob(packId, packKey, false)); - break; - case REMOVE: - DatabaseFactory.getStickerDatabase(context).uninstallPack(packId); - break; - } - } else { - Log.w(TAG, "Received incomplete sticker pack operation sync."); - } - } - } - - private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content, - @NonNull SentTranscriptMessage message) - throws StorageFailedException - - { - try { - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - - Long threadId; - - if (message.getMessage().isEndSession()) { - threadId = handleSynchronizeSentEndSessionMessage(message); - } else if (message.getMessage().isGroupUpdate()) { - threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true); - } else if (message.getMessage().isExpirationUpdate()) { - threadId = handleSynchronizeSentExpirationUpdate(message); - } else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent()) { - threadId = handleSynchronizeSentMediaMessage(message); - } else { - threadId = handleSynchronizeSentTextMessage(message); - } - - if (threadId == -1L) { threadId = null; } - - if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get()))) { - handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); - } - - if (message.getMessage().getProfileKey().isPresent()) { - Recipient recipient = null; - - if (message.getDestination().isPresent()) recipient = Recipient.from(context, Address.Companion.fromSerialized(message.getDestination().get()), false); - else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false); - - - if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) { - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); - } - - SessionMetaProtocol.handleProfileKeyUpdate(context, content); - } - - SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content); - - if (threadId != null) { - DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); - messageNotifier.updateNotification(context); - } - - messageNotifier.setLastDesktopActivityTimestamp(message.getTimestamp()); - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender(), content.getSenderDevice()); - } - } - public void handleMediaMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId, @@ -1429,6 +1322,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType { return true; } + if (content.getSender().equals(TextSecurePreferences.getLocalNumber(context)) && + DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), content.getSender()) != null) { + Log.d("Loki", "Skipping message from self we already have"); + return true; + } + Recipient sender = Recipient.from(context, Address.Companion.fromSerialized(content.getSender()), false); if (content.getDeviceLink().isPresent()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt index 108783a51f..66c1907405 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt @@ -29,7 +29,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab } override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? { - val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, Address.fromSerialized(quoteePublicKey)) + val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey) return if (message != null) getServerID(message.getId()) else null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt index 8ae19749e5..6b4060aa17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt @@ -27,7 +27,7 @@ import org.session.libsignal.utilities.Hex import java.util.* import java.util.concurrent.TimeUnit -class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) { +class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind, private val sentTime: Long) : BaseJob(parameters) { sealed class Kind { class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection, val admins: Collection) : Kind() @@ -61,20 +61,22 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete } } - constructor(destination: String, kind: Kind) : this(Parameters.Builder() + constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setQueue(KEY) .setLifespan(TimeUnit.DAYS.toMillis(1)) .setMaxAttempts(20) .build(), destination, - kind) + kind, + sentTime) override fun getFactoryKey(): String { return KEY } override fun serialize(): Data { val builder = Data.Builder() builder.putString("destination", destination) + builder.putLong("sentTime", sentTime) when (kind) { is Kind.New -> { builder.putString("kind", "New") @@ -124,6 +126,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 { val destination = data.getString("destination") val rawKind = data.getString("kind") + val sentTime = data.getLong("sentTime") val kind: Kind when (rawKind) { "New" -> { @@ -162,7 +165,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete } else -> throw Exception("Invalid closed group update message kind: $rawKind.") } - return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind) + return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime) } } @@ -221,7 +224,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete 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, + sentTime, serializedContentMessage, false, ttl, false, true, false, false, Optional.absent()) } catch (e: Exception) { Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 3dd347472c..6c47269505 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -285,7 +285,7 @@ object ClosedGroupsProtocol { .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) mmsDB.markAsSent(infoMessageID, true) @@ -324,6 +324,6 @@ object ClosedGroupsProtocol { .setId(decodedGroupId) .setType(GroupContext.Type.QUIT) .build() - return OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, null, emptyList(), emptyList()) + return OutgoingGroupMediaMessage(groupRecipient, groupContext, null, 0, null, emptyList(), emptyList()) } } \ No newline at end of file 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 c97591b879..7395fd2b9c 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 @@ -58,6 +58,7 @@ object ClosedGroupsProtocolV2 { val apiDB = DatabaseFactory.getLokiAPIDatabase(context) // Generate the group's public key val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix + val sentTime = System.currentTimeMillis() // Generate the key pair that'll be used for encryption and decryption val encryptionKeyPair = Curve.generateKeyPair() // Create the group @@ -68,19 +69,20 @@ object ClosedGroupsProtocolV2 { 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) { - 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) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime) + + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) + for (member in members) { + val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime) + job.setContext(context) + job.onRun() // Run the job immediately to make all of this sync + } // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) // Fulfill the promise @@ -102,13 +104,14 @@ object ClosedGroupsProtocolV2 { val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey val admins = group.admins.map { it.serialize() } val name = group.title + val sentTime = System.currentTimeMillis() if (group == null) { Log.d("Loki", "Can't leave nonexistent closed group.") return@queue deferred.reject(Error.NoThread) } // Send the update to the group @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime) job.setContext(context) job.onRun() // Run the job immediately // Remove the group private key and unsubscribe from PNs @@ -116,7 +119,7 @@ object ClosedGroupsProtocolV2 { // Notify the user val infoType = GroupContext.Type.QUIT val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) deferred.resolve(Unit) } return deferred.promise @@ -140,6 +143,7 @@ object ClosedGroupsProtocolV2 { val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) } val admins = group.admins.map { it.serialize() } val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + val sentTime = System.currentTimeMillis() val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") @@ -148,7 +152,7 @@ object ClosedGroupsProtocolV2 { val name = group.title // Send the update to the group val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) job.setContext(context) job.onRun() // Run the job immediately // Send closed group update messages to any new members individually @@ -156,13 +160,13 @@ object ClosedGroupsProtocolV2 { @Suppress("NAME_SHADOWING") val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) @Suppress("NAME_SHADOWING") - val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind) + val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime) ApplicationContext.getInstance(context).jobManager.add(newMemberJob) } // Notify the user val infoType = GroupContext.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) } } @@ -183,6 +187,7 @@ object ClosedGroupsProtocolV2 { groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) } val admins = group.admins.map { it.serialize() } + val sentTime = System.currentTimeMillis() val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") @@ -195,7 +200,7 @@ object ClosedGroupsProtocolV2 { val name = group.title // Send the update to the group val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) job.setContext(context) job.onRun() // Run the job immediately val isCurrentUserAdmin = admins.contains(userPublicKey) @@ -205,7 +210,7 @@ object ClosedGroupsProtocolV2 { // Notify the user val infoType = GroupContext.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) } } @@ -218,13 +223,14 @@ object ClosedGroupsProtocolV2 { val group = groupDB.getGroup(groupID).orNull() val members = group.members.map { it.serialize() }.toSet() val admins = group.admins.map { it.serialize() } + val sentTime = System.currentTimeMillis() if (group == null) { Log.d("Loki", "Can't leave nonexistent closed group.") return@queue deferred.reject(Error.NoThread) } // Send the update to the group val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime) job.setContext(context) job.onRun() // Run the job immediately // Update the group @@ -232,7 +238,7 @@ object ClosedGroupsProtocolV2 { // Notify the user val infoType = GroupContext.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID) + insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime) deferred.resolve(Unit) } return deferred.promise @@ -272,6 +278,7 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Can't update nonexistent closed group.") return@queue deferred.reject(Error.NoThread) } + val sentTime = System.currentTimeMillis() val oldMembers = group.members.map { it.serialize() }.toSet() val newMembers = members.minus(oldMembers) val membersAsData = members.map { Hex.fromStringCondensed(it) } @@ -298,7 +305,7 @@ object ClosedGroupsProtocolV2 { @Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData) @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind, sentTime) job.setContext(context) job.onRun() // Run the job immediately if (isUserLeaving) { @@ -322,7 +329,7 @@ object ClosedGroupsProtocolV2 { @Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind) + val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime) ApplicationContext.getInstance(context).jobManager.add(job) } } @@ -335,7 +342,7 @@ object ClosedGroupsProtocolV2 { // 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) + insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID, sentTime) deferred.resolve(Unit) } return deferred.promise @@ -367,7 +374,7 @@ object ClosedGroupsProtocolV2 { val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey) ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext) } - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers)) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis()) job.setContext(context) job.onRun() // Run the job immediately // Store it * after * having sent out the message to the group @@ -376,9 +383,9 @@ object ClosedGroupsProtocolV2 { @JvmStatic fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - if (!isValid(closedGroupUpdate, senderPublicKey)) { return } + if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return } when (closedGroupUpdate.type) { - SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey, sentTimestamp) SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) @@ -391,7 +398,10 @@ object ClosedGroupsProtocolV2 { } } - private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String): Boolean { + private fun isValid(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String, sentTimestamp: Long): Boolean { + val record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, senderPublicKey) + if (record != null) return false + return when (closedGroupUpdate.type) { SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> { (!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty @@ -413,7 +423,7 @@ object ClosedGroupsProtocolV2 { } } - public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) { + public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String, sentTimestamp: Long) { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val apiDB = DatabaseFactory.getLokiAPIDatabase(context) @@ -426,7 +436,8 @@ object ClosedGroupsProtocolV2 { // Create the group val groupID = doubleEncodeGroupID(groupPublicKey) val groupDB = DatabaseFactory.getGroupDatabase(context) - if (groupDB.getGroup(groupID).orNull() != null) { + val prevGroup = groupDB.getGroup(groupID).orNull() + if (prevGroup != null) { // Update the group groupDB.updateTitle(groupID, name) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) @@ -440,8 +451,14 @@ object ClosedGroupsProtocolV2 { // Store the encryption key pair val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), 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 user (if we didn't make the group) + if (userPublicKey != senderPublicKey) { + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + } else if (prevGroup == null) { + // only notify if we created this group + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + } // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) } @@ -451,8 +468,8 @@ object ClosedGroupsProtocolV2 { 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.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! @@ -491,16 +508,21 @@ object ClosedGroupsProtocolV2 { val (contextType, signalType) = if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE - - insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins) + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp) + } else { + insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins) + } } fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: 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", "Ignoring closed group info message for nonexistent group.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { @@ -517,16 +539,22 @@ object ClosedGroupsProtocolV2 { val newMembers = members + updateMembers groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + } else { + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + } } fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { // Check that the sender is a member of the group (before the update) + 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", "Ignoring closed group info message for nonexistent group.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } // Check common group update logic @@ -538,21 +566,23 @@ object ClosedGroupsProtocolV2 { val name = closedGroupUpdate.name groupDB.updateTitle(groupID, name) - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + } else { + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + } } private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { // Check the user leaving isn't us, will already be handled val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - if (senderPublicKey == userPublicKey) { - return - } 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", "Ignoring closed group info message for nonexistent group.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } val name = group.title @@ -575,7 +605,12 @@ object ClosedGroupsProtocolV2 { generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList) } } - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins) + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + } else { + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + } } private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { @@ -588,8 +623,8 @@ object ClosedGroupsProtocolV2 { 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.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } val oldMembers = group.members.map { it.serialize() } @@ -623,7 +658,13 @@ object ClosedGroupsProtocolV2 { 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.toString() }) + val admins = group.admins.map { it.toString() } + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp) + } else { + insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins) + } } private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) { @@ -699,7 +740,8 @@ object ClosedGroupsProtocolV2 { } private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, - members: Collection, admins: Collection, threadID: Long) { + members: Collection, admins: Collection, threadID: Long, + sentTime: Long) { val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val groupContextBuilder = GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) @@ -707,9 +749,9 @@ object ClosedGroupsProtocolV2 { .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) - val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) + val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTime) mmsDB.markAsSent(infoMessageID, true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java index d34a14909c..4b1fbfb257 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java @@ -40,7 +40,6 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { public OutgoingGroupMediaMessage(@NonNull Recipient recipient, @NonNull GroupContext group, @Nullable final Attachment avatar, - long sentTimeMillis, long expireIn, @Nullable QuoteModel quote, @NonNull List contacts, diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java index 263b9276bf..be28948a3c 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java @@ -1052,7 +1052,6 @@ public class SignalServiceMessageSender { Optional syncTarget) throws IOException, UntrustedIdentityException { - if (recipient.getNumber().equals(userPublicKey) && !syncTarget.isPresent()) { return SendMessageResult.success(recipient, false, false); } final SettableFuture[] future = { new SettableFuture() }; OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content); // Loki - Remove this when we have shared sender keys From 05fef1188904531962b281d6343b240f8d1e9c37 Mon Sep 17 00:00:00 2001 From: jubb Date: Thu, 11 Feb 2021 14:20:12 +1100 Subject: [PATCH 17/28] fix: non-compatible handle messages requiring timestamp --- .../org/thoughtcrime/securesms/jobs/PushDecryptJob.java | 2 +- .../securesms/loki/protocol/MultiDeviceProtocol.kt | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index ca8bd7fdf3..4510b1eefb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -264,7 +264,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content); if (content.configurationMessageProto.isPresent()) { - MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender()); + MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender(), content.getTimestamp()); } else if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); 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 index a248d6647c..d0ba9c36e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -5,6 +5,7 @@ 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 @@ -33,7 +34,7 @@ object MultiDeviceProtocol { try { messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false, - true, false, true, 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.") @@ -51,14 +52,14 @@ object MultiDeviceProtocol { try { messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false, - true, false, true, 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) { + 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 @@ -79,7 +80,7 @@ object MultiDeviceProtocol { 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) + ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp) } val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } for (openGroup in configurationMessage.openGroups) { From 6a8e0ae195770689bf440b77c573d4df90751bb0 Mon Sep 17 00:00:00 2001 From: jubb Date: Thu, 11 Feb 2021 16:34:01 +1100 Subject: [PATCH 18/28] feat: use new explicit groups --- .../conversation/ConversationActivity.java | 2 +- .../activities/EditClosedGroupActivity.kt | 32 +++++++++---------- .../securesms/loki/activities/HomeActivity.kt | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 0789f443af..010eef08a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1109,7 +1109,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } try { if (isSSKBasedClosedGroup) { - ClosedGroupsProtocolV2.leave(this, groupPublicKey); + ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey); initializeEnabledCheck(); } else if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) { initializeEnabledCheck(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt index e1039c854e..80fd0281ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt @@ -277,24 +277,22 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { isLoading = true loaderContainer.fadeIn() val promise: Promise = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) { - ClosedGroupsProtocolV2.leave(this, groupPublicKey!!) + ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey!!) } else { -// TODO: uncomment when we switch to sending new explicit updates after clients update -// task { -// val name = -// if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name) -// else Promise.of(Unit) -// name.get() -// members.filterNot { it in originalMembers }.let { adds -> -// if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() }) -// else Promise.of(Unit) -// }.get() -// originalMembers.filterNot { it in members }.let { removes -> -// if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() }) -// else Promise.of(Unit) -// }.get() -// } - ClosedGroupsProtocolV2.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name) + task { + val name = + if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name) + else Promise.of(Unit) + name.get() + members.filterNot { it in originalMembers }.let { adds -> + if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() }) + else Promise.of(Unit) + }.get() + originalMembers.filterNot { it in members }.let { removes -> + if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() }) + else Promise.of(Unit) + }.get() + } } promise.successUi { loaderContainer.fadeOut() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 2da2a39ef5..88c5eea005 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -358,7 +358,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe isSSKBasedClosedGroup = false } if (isSSKBasedClosedGroup) { - ClosedGroupsProtocolV2.leave(context, groupPublicKey!!) + ClosedGroupsProtocolV2.explicitLeave(context, groupPublicKey!!) } else if (!ClosedGroupsProtocol.leaveLegacyGroup(context, recipient)) { Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show() return@launch From b6951f09b4a08a7df9832c41a05dd820c1de316c Mon Sep 17 00:00:00 2001 From: jubb Date: Thu, 11 Feb 2021 18:58:38 +1100 Subject: [PATCH 19/28] feat: use cached keypair --- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 7395fd2b9c..b35e9e8d98 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 @@ -38,11 +38,14 @@ import org.session.libsession.utilities.TextSecurePreferences import java.io.IOException import java.util.* +import java.util.concurrent.atomic.AtomicReference import kotlin.jvm.Throws object ClosedGroupsProtocolV2 { const val groupSizeLimit = 100 + private val pendingKeyPair = AtomicReference(null) + 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.") @@ -364,7 +367,7 @@ object ClosedGroupsProtocolV2 { return } // Generate the new encryption key pair - val newKeyPair = Curve.generateKeyPair() + val newKeyPair = pendingKeyPair.getAndSet(Curve.generateKeyPair()) ?: Curve.generateKeyPair() // Distribute it val proto = SignalServiceProtos.KeyPair.newBuilder() proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) From 2a50a09614fb22f8c7a767f9a872bc7c42b27b21 Mon Sep 17 00:00:00 2001 From: jubb Date: Fri, 12 Feb 2021 14:16:06 +1100 Subject: [PATCH 20/28] feat: share pending key pair between generate EC and add members --- .../loki/protocol/ClosedGroupsProtocolV2.kt | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) 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 b35e9e8d98..5874c1d88d 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 @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log import com.google.protobuf.ByteString +import kotlinx.coroutines.delay import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import nl.komponents.kovenant.task @@ -44,7 +45,7 @@ import kotlin.jvm.Throws object ClosedGroupsProtocolV2 { const val groupSizeLimit = 100 - private val pendingKeyPair = AtomicReference(null) + private val pendingKeyPair = AtomicReference(null) sealed class Error(val description: String) : Exception() { object NoThread : Error("Couldn't find a thread associated with the given group public key") @@ -104,9 +105,6 @@ object ClosedGroupsProtocolV2 { val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() - val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey - val admins = group.admins.map { it.serialize() } - val name = group.title val sentTime = System.currentTimeMillis() if (group == null) { Log.d("Loki", "Can't leave nonexistent closed group.") @@ -119,10 +117,6 @@ object ClosedGroupsProtocolV2 { job.onRun() // Run the job immediately // Remove the group private key and unsubscribe from PNs disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) - // Notify the user - val infoType = GroupContext.Type.QUIT - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) deferred.resolve(Unit) } return deferred.promise @@ -147,7 +141,7 @@ object ClosedGroupsProtocolV2 { val admins = group.admins.map { it.serialize() } val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val sentTime = System.currentTimeMillis() - val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + val encryptionKeyPair = pendingKeyPair.get() ?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") return@task Error.NoKeyPair @@ -166,10 +160,6 @@ object ClosedGroupsProtocolV2 { val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime) ApplicationContext.getInstance(context).jobManager.add(newMemberJob) } - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) } } @@ -210,10 +200,7 @@ object ClosedGroupsProtocolV2 { if (isCurrentUserAdmin) { generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers) } - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) + return@task Unit } } @@ -238,10 +225,6 @@ object ClosedGroupsProtocolV2 { job.onRun() // Run the job immediately // Update the group groupDB.updateTitle(groupID, newName) - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime) deferred.resolve(Unit) } return deferred.promise @@ -367,7 +350,11 @@ object ClosedGroupsProtocolV2 { return } // Generate the new encryption key pair - val newKeyPair = pendingKeyPair.getAndSet(Curve.generateKeyPair()) ?: Curve.generateKeyPair() + val newKeyPair = Curve.generateKeyPair() + do { + // make sure we set the pendingKeyPair or wait until it is not null + } while (!pendingKeyPair.compareAndSet(null, newKeyPair)) + // Distribute it val proto = SignalServiceProtos.KeyPair.newBuilder() proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) @@ -382,6 +369,7 @@ object ClosedGroupsProtocolV2 { job.onRun() // Run the job immediately // Store it * after * having sent out the message to the group apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) + pendingKeyPair.set(null) } @JvmStatic From 83d107cbce7bc29f44812f554ef39c6453c79e31 Mon Sep 17 00:00:00 2001 From: jubb Date: Fri, 12 Feb 2021 14:28:25 +1100 Subject: [PATCH 21/28] fix: compare serialized to serialized --- .../org/thoughtcrime/securesms/database/MmsSmsDatabase.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 159d961816..a27ae0c691 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -97,7 +97,9 @@ public class MmsSmsDatabase extends Database { while ((messageRecord = reader.getNext()) != null) { if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) || - (!Util.isOwnNumber(context, serializedAuthor) && messageRecord.getIndividualRecipient().getAddress().equals(serializedAuthor))) + (!Util.isOwnNumber(context, serializedAuthor) + && messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor) + )) { return messageRecord; } From 93f7d428cb3a5210c184a6022cbafd80431f2558 Mon Sep 17 00:00:00 2001 From: Jubb Date: Mon, 15 Feb 2021 12:05:04 +1100 Subject: [PATCH 22/28] fix: add in the encryption key send from current / pending for groupID in handleMembersAdded if admin and change pendingKeyPair implementation to keyed on groupPublicKey --- .../loki/protocol/ClosedGroupsProtocolV2.kt | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) 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 5874c1d88d..0e5d0a8c80 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 @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log import com.google.protobuf.ByteString -import kotlinx.coroutines.delay import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import nl.komponents.kovenant.task @@ -39,13 +38,13 @@ import org.session.libsession.utilities.TextSecurePreferences import java.io.IOException import java.util.* -import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.ConcurrentHashMap import kotlin.jvm.Throws object ClosedGroupsProtocolV2 { const val groupSizeLimit = 100 - private val pendingKeyPair = AtomicReference(null) + private val pendingKeyPair = ConcurrentHashMap>() sealed class Error(val description: String) : Exception() { object NoThread : Error("Couldn't find a thread associated with the given group public key") @@ -141,7 +140,9 @@ object ClosedGroupsProtocolV2 { val admins = group.admins.map { it.serialize() } val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val sentTime = System.currentTimeMillis() - val encryptionKeyPair = pendingKeyPair.get() ?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + val encryptionKeyPair = pendingKeyPair.getOrElse(groupPublicKey) { + Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)) + }.orNull() if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") return@task Error.NoKeyPair @@ -190,7 +191,6 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") return@task Error.InvalidUpdate } - val name = group.title // Send the update to the group val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) @@ -351,11 +351,19 @@ object ClosedGroupsProtocolV2 { } // Generate the new encryption key pair val newKeyPair = Curve.generateKeyPair() + // replace call will not succeed if no value already set + pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent()) do { // make sure we set the pendingKeyPair or wait until it is not null - } while (!pendingKeyPair.compareAndSet(null, newKeyPair)) - + } while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair))) // Distribute it + sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers) + // Store it * after * having sent out the message to the group + apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) + pendingKeyPair[groupPublicKey] = Optional.absent() + } + + private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection, force: Boolean = true) { val proto = SignalServiceProtos.KeyPair.newBuilder() proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize()) @@ -365,11 +373,12 @@ object ClosedGroupsProtocolV2 { ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext) } val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis()) - job.setContext(context) - job.onRun() // Run the job immediately - // Store it * after * having sent out the message to the group - apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) - pendingKeyPair.set(null) + if (force) { + job.setContext(context) + job.onRun() // Run the job immediately + } else { + ApplicationContext.getInstance(context).jobManager.add(job) + } } @JvmStatic @@ -509,6 +518,7 @@ object ClosedGroupsProtocolV2 { fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { 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() @@ -536,6 +546,17 @@ object ClosedGroupsProtocolV2 { } else { insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) } + if (userPublicKey in admins) { + // send current encryption key to the latest added members + val encryptionKeyPair = pendingKeyPair.getOrElse(groupPublicKey) { + Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)) + }.orNull() + if (encryptionKeyPair == null) { + Log.d("Loki", "Couldn't get encryption key pair for closed group.") + } else { + sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, newMembers, false) + } + } } fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { From 8476e090e2ddde9c1994bf8b32329cce3c7fdd89 Mon Sep 17 00:00:00 2001 From: Jubb Date: Mon, 15 Feb 2021 12:08:08 +1100 Subject: [PATCH 23/28] fix: handle pendingKeyPair.getOrElse nullable after checking if key exists --- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 0e5d0a8c80..ac66ec2163 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 @@ -548,9 +548,8 @@ object ClosedGroupsProtocolV2 { } if (userPublicKey in admins) { // send current encryption key to the latest added members - val encryptionKeyPair = pendingKeyPair.getOrElse(groupPublicKey) { - Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)) - }.orNull() + val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() + ?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") } else { From 069719d568aa2cf9bd1defa1a4cf5210dcb88aae Mon Sep 17 00:00:00 2001 From: Jubb Date: Mon, 15 Feb 2021 12:27:01 +1100 Subject: [PATCH 24/28] fix: re-add sync message send to self --- .../libsignal/service/api/SignalServiceMessageSender.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java index 1b854b97a4..ab402c93ca 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java @@ -347,7 +347,7 @@ public class SignalServiceMessageSender { for (String device : linkedDevices) { SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, device, store); - // sendMessageToPrivateChat(0, deviceAsAddress, Optional.absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false); + sendMessageToPrivateChat(0, deviceAsAddress, Optional.absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false, Optional.absent()); } } From a44a79e59f69c03eb54c837ab5dd5cd22ae1954b Mon Sep 17 00:00:00 2001 From: Jubb Date: Mon, 15 Feb 2021 16:38:50 +1100 Subject: [PATCH 25/28] fix: messages now filter properly for explicit group update messages --- .../loki/protocol/ClosedGroupsProtocolV2.kt | 25 ++++++++++++++++++- .../mms/OutgoingGroupMediaMessage.java | 19 +++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) 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 ac66ec2163..b5e3e4eb56 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 @@ -104,6 +104,9 @@ object ClosedGroupsProtocolV2 { val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() + val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey + val admins = group.admins.map { it.serialize() } + val name = group.title val sentTime = System.currentTimeMillis() if (group == null) { Log.d("Loki", "Can't leave nonexistent closed group.") @@ -114,6 +117,10 @@ object ClosedGroupsProtocolV2 { val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime) job.setContext(context) job.onRun() // Run the job immediately + // Notify the user + val infoType = GroupContext.Type.QUIT + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) // Remove the group private key and unsubscribe from PNs disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) deferred.resolve(Unit) @@ -153,6 +160,10 @@ object ClosedGroupsProtocolV2 { val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) job.setContext(context) job.onRun() // Run the job immediately + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) // Send closed group update messages to any new members individually for (member in membersToAdd) { @Suppress("NAME_SHADOWING") @@ -191,11 +202,16 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") return@task Error.InvalidUpdate } + val name = group.title // Send the update to the group val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) job.setContext(context) job.onRun() // Run the job immediately + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) val isCurrentUserAdmin = admins.contains(userPublicKey) if (isCurrentUserAdmin) { generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers) @@ -223,6 +239,10 @@ object ClosedGroupsProtocolV2 { val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime) job.setContext(context) job.onRun() // Run the job immediately + // Notify the user + val infoType = GroupContext.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime) // Update the group groupDB.updateTitle(groupID, newName) deferred.resolve(Unit) @@ -753,6 +773,7 @@ object ClosedGroupsProtocolV2 { private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTime: Long) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val groupContextBuilder = GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) @@ -760,8 +781,10 @@ object ClosedGroupsProtocolV2 { .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTime, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) + val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) + if (mmsSmsDB.getMessageFor(sentTime,userPublicKey) != null) return val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTime) mmsDB.markAsSent(infoMessageID, true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java index 4b1fbfb257..cf63d5a058 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java @@ -44,10 +44,27 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { @Nullable QuoteModel quote, @NonNull List contacts, @NonNull List previews) + { + super(recipient, Base64.encodeBytes(group.toByteArray()), + new LinkedList() {{if (avatar != null) add(avatar);}}, + System.currentTimeMillis(), + ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews); + + this.group = group; + } + + public OutgoingGroupMediaMessage(@NonNull Recipient recipient, + @NonNull GroupContext group, + @Nullable final Attachment avatar, + long sentTime, + long expireIn, + @Nullable QuoteModel quote, + @NonNull List contacts, + @NonNull List previews) { super(recipient, Base64.encodeBytes(group.toByteArray()), new LinkedList() {{if (avatar != null) add(avatar);}}, - System.currentTimeMillis(), + sentTime, ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews); this.group = group; From a63fce4ca66d7ac9ebe73c090bb616e124902d70 Mon Sep 17 00:00:00 2001 From: Jubb Date: Mon, 15 Feb 2021 17:35:56 +1100 Subject: [PATCH 26/28] fix: self-send messages now send to yourself only and aren't treated as sync messages --- .../thoughtcrime/securesms/jobs/PushMediaSendJob.java | 11 ++++++----- .../thoughtcrime/securesms/jobs/PushTextSendJob.java | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index c89b44f131..e9d729f29e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -287,11 +287,12 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) { // Loki - Device link messages don't go through here - Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); - SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess); - - messageSender.sendMessage(syncMessage, syncAccess); - return syncAccess.isPresent(); + SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage); + if (result.getLokiAPIError() != null) { + throw result.getLokiAPIError(); + } else { + return result.getSuccess().isUnidentified(); + } } else { SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage); if (result.getLokiAPIError() != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index acac2e3d98..abec311d1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -226,11 +226,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) { // Loki - Device link messages don't go through here - Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); - SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess); - - messageSender.sendMessage(syncMessage, syncAccess); - return syncAccess.isPresent(); + SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage); + if (result.getLokiAPIError() != null) { + throw result.getLokiAPIError(); + } else { + return result.getSuccess().isUnidentified(); + } } else { SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage); if (result.getLokiAPIError() != null) { From 9ccfb4702d3dff9a733b95018b40908f7e8d3879 Mon Sep 17 00:00:00 2001 From: Jubb Date: Mon, 15 Feb 2021 17:41:10 +1100 Subject: [PATCH 27/28] fix: self leave group is treated as QUIT instead of UPDATE --- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b5e3e4eb56..c654944633 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 @@ -638,7 +638,7 @@ object ClosedGroupsProtocolV2 { } if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp) } else { insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) } From 9f60a3ca365aab181bd6b925fd016732fb4d90cf Mon Sep 17 00:00:00 2001 From: Jubb Date: Mon, 15 Feb 2021 17:42:31 +1100 Subject: [PATCH 28/28] fix: other users treated as quit --- .../securesms/loki/protocol/ClosedGroupsProtocolV2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c654944633..bf12e073e0 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 @@ -640,7 +640,7 @@ object ClosedGroupsProtocolV2 { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp) } else { - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins) } }